From 8cd8e34b11a61cb3994a3c15cdd4ce74f71d3fc3 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 13 Aug 2026 18:24:04 +0530 Subject: [PATCH] updates on the ui changes and frully removed the mui and converted into astryx and color change --- .claude/launch.json | 11 + CLAUDE.md | 86 +- src/App.js | 11 +- src/assets/images/auth/AuthBackground.js | 42 - src/components/@extended/Avatar.js | 119 - src/components/@extended/Breadcrumbs.js | 222 - src/components/@extended/Dot.js | 39 - src/components/@extended/IconButton.js | 162 - src/components/@extended/LoadingButton.js | 231 - src/components/@extended/Snackbar.js | 128 - src/components/@extended/Tooltip.js | 67 - src/components/@extended/Transitions.js | 116 - .../@extended/progress/CircularWithLabel.js | 32 - .../@extended/progress/CircularWithPath.js | 65 - .../@extended/progress/LinearWithIcon.js | 22 - .../@extended/progress/LinearWithLabel.js | 23 - src/components/CircularLoader.js | 62 +- src/components/DateFilterDialog.js | 56 +- src/components/Loader.js | 24 +- src/components/MainCard.js | 159 +- src/components/ScrollTop.js | 7 +- src/components/ScrollX.js | 10 - src/components/SecondaryAction.js | 65 - src/components/cards/AuthFooter.js | 61 - src/components/cards/ComponentHeader.js | 49 - .../cards/e-commerce/FloatingCart.js | 52 - .../cards/e-commerce/ProductCard.js | 176 - .../cards/e-commerce/ProductReview.js | 52 - .../cards/skeleton/ProductPlaceholder.js | 44 - .../cards/statistics/AnalyticEcommerce.js | 66 - .../cards/statistics/AnalyticsDataCard.js | 56 - .../cards/statistics/HoverSocialCard.js | 96 +- src/components/logo/LogoIcon.js | 56 - src/components/logo/LogoMain.js | 68 - src/components/logo/index.js | 27 - .../nearle_components/AddressAutocomplete.js | 238 +- src/components/nearle_components/CLAUDE.md | 45 +- .../nearle_components/DateRangePicker.js | 113 +- .../nearle_components/DebounceSearchBar.js | 63 +- .../nearle_components/LoaderWithImage.js | 77 +- .../nearle_components/LocationAutocomplete.js | 231 +- .../nearle_components/MobileCard.js | 110 +- .../nearle_components/PageHeader.js | 54 +- src/components/nearle_components/StatCard.js | 100 +- .../nearle_components/TableLoader.js | 16 +- src/components/third-party/Notistack.js | 74 +- src/components/third-party/ReactTable.js | 605 +- src/components/third-party/SimpleBar.js | 62 - src/components/updateNetworkStatus.js | 15 +- src/firebase_notification/notification.js | 8 +- src/layout/CommonLayout/FooterBlock.js | 321 - src/layout/CommonLayout/Header.js | 251 - src/layout/CommonLayout/index.js | 78 +- src/layout/MainLayout/AppSideNav.js | 12 +- src/layout/MainLayout/AppTopNav.js | 25 +- src/layout/MainLayout/Footer.js | 23 - src/layout/MainLayout/index.js | 20 +- src/menu-items/other.js | 178 - src/pages/auth/check-mail.js | 57 - src/pages/auth/forgot-password.js | 41 - src/pages/auth/login.js | 41 - src/pages/auth/register.js | 41 - src/pages/auth/reset-password.js | 26 - src/pages/extra-pages/sample-page.js | 20 - src/pages/maintenance/404.js | 60 +- src/pages/maintenance/500.js | 47 +- src/pages/maintenance/coming-soon.js | 107 +- src/pages/maintenance/under-construction.js | 43 +- src/pages/nearle/accountsettings.js | 45 - src/pages/nearle/appUsers/appUsers.js | 34 +- .../nearle/clientPricing/clientPricing.js | 76 +- src/pages/nearle/clients/Tenants.js | 122 +- src/pages/nearle/clients/createCustomer.js | 28 +- src/pages/nearle/clients/createclient.js | 7 +- .../competitiveIntel/competitiveIntel.js | 16 +- src/pages/nearle/customers/customers.js | 57 +- src/pages/nearle/deliveries/deliveries.js | 6156 ++++++++--------- src/pages/nearle/dispatch/ActiveSection.js | 4 +- src/pages/nearle/dispatch/Dispatch.css | 344 +- src/pages/nearle/dispatch/Dispatch.js | 73 +- src/pages/nearle/exceptions/exceptions.js | 34 +- src/pages/nearle/hubs/hubs.js | 392 +- src/pages/nearle/login.js | 2 +- src/pages/nearle/orders/createorder1.js | 5067 +++++++------- src/pages/nearle/orders/multipleOrders.js | 3671 +++++----- .../nearle/orders/optimisedOrderPreview.js | 750 -- src/pages/nearle/orders/orders.js | 74 +- src/pages/nearle/reports/RidersRoutes.js | 123 +- src/pages/nearle/reports/mapWithRoute.js | 64 +- src/pages/nearle/reports/ordersDetails.js | 3460 +++++---- src/pages/nearle/reports/ordersSummary.js | 2637 ++++--- src/pages/nearle/reports/profitability.js | 58 +- src/pages/nearle/reports/ridersSummary.js | 2084 +++--- src/pages/nearle/riders/RiderSubstitution.js | 6 +- src/pages/nearle/riders/createrider.js | 6 +- src/pages/nearle/riders/editRider.js | 3 +- src/pages/nearle/riders/riders.js | 3719 +++++----- src/pages/nearle/tripsheets/tripsheets.js | 21 +- src/pages/nearle/vehicles/vehicles.js | 382 +- src/pages/nearle/viewProfile.js | 23 +- src/pages/titleCard.js | 31 - src/routes/LoginRoutes.js | 30 - src/routes/MainRoutes.js | 4 - src/sections/apps/chat/ChatDrawer.js | 275 - src/sections/apps/chat/ChatHistory.js | 119 - src/sections/apps/chat/UserAvatar.js | 33 - src/sections/apps/chat/UserDetails.js | 303 - src/sections/auth/AuthCard.js | 39 - src/sections/auth/AuthWrapper.js | 55 - .../auth/auth-forms/AuthForgotPassword.js | 126 - src/sections/auth/auth-forms/AuthLogin.js | 176 - src/sections/auth/auth-forms/AuthRegister.js | 282 - .../auth/auth-forms/AuthResetPassword.js | 201 - .../auth/auth-forms/FirebaseSocial.js | 82 - src/sections/widget/data/ApplicationSales.js | 68 - src/sections/widget/data/TasksCard.js | 175 - src/themes/astryx.js | 28 +- src/themes/dt/primitives.js | 83 + src/themes/dt/tokens.js | 76 +- src/themes/index.js | 10 +- src/themes/overrides/Autocomplete.js | 4 +- src/themes/theme/default.js | 20 +- 122 files changed, 15111 insertions(+), 22411 deletions(-) create mode 100644 .claude/launch.json delete mode 100644 src/assets/images/auth/AuthBackground.js delete mode 100644 src/components/@extended/Avatar.js delete mode 100644 src/components/@extended/Breadcrumbs.js delete mode 100644 src/components/@extended/Dot.js delete mode 100644 src/components/@extended/IconButton.js delete mode 100644 src/components/@extended/LoadingButton.js delete mode 100644 src/components/@extended/Snackbar.js delete mode 100644 src/components/@extended/Tooltip.js delete mode 100644 src/components/@extended/Transitions.js delete mode 100644 src/components/@extended/progress/CircularWithLabel.js delete mode 100644 src/components/@extended/progress/CircularWithPath.js delete mode 100644 src/components/@extended/progress/LinearWithIcon.js delete mode 100644 src/components/@extended/progress/LinearWithLabel.js delete mode 100644 src/components/ScrollX.js delete mode 100644 src/components/SecondaryAction.js delete mode 100644 src/components/cards/AuthFooter.js delete mode 100644 src/components/cards/ComponentHeader.js delete mode 100644 src/components/cards/e-commerce/FloatingCart.js delete mode 100644 src/components/cards/e-commerce/ProductCard.js delete mode 100644 src/components/cards/e-commerce/ProductReview.js delete mode 100644 src/components/cards/skeleton/ProductPlaceholder.js delete mode 100644 src/components/cards/statistics/AnalyticEcommerce.js delete mode 100644 src/components/cards/statistics/AnalyticsDataCard.js delete mode 100644 src/components/logo/LogoIcon.js delete mode 100644 src/components/logo/LogoMain.js delete mode 100644 src/components/logo/index.js delete mode 100644 src/components/third-party/SimpleBar.js delete mode 100644 src/layout/CommonLayout/FooterBlock.js delete mode 100644 src/layout/CommonLayout/Header.js delete mode 100644 src/layout/MainLayout/Footer.js delete mode 100644 src/menu-items/other.js delete mode 100644 src/pages/auth/check-mail.js delete mode 100644 src/pages/auth/forgot-password.js delete mode 100644 src/pages/auth/login.js delete mode 100644 src/pages/auth/register.js delete mode 100644 src/pages/auth/reset-password.js delete mode 100644 src/pages/extra-pages/sample-page.js delete mode 100644 src/pages/nearle/accountsettings.js delete mode 100644 src/pages/nearle/orders/optimisedOrderPreview.js delete mode 100644 src/pages/titleCard.js delete mode 100644 src/sections/apps/chat/ChatDrawer.js delete mode 100644 src/sections/apps/chat/ChatHistory.js delete mode 100644 src/sections/apps/chat/UserAvatar.js delete mode 100644 src/sections/apps/chat/UserDetails.js delete mode 100644 src/sections/auth/AuthCard.js delete mode 100644 src/sections/auth/AuthWrapper.js delete mode 100644 src/sections/auth/auth-forms/AuthForgotPassword.js delete mode 100644 src/sections/auth/auth-forms/AuthLogin.js delete mode 100644 src/sections/auth/auth-forms/AuthRegister.js delete mode 100644 src/sections/auth/auth-forms/AuthResetPassword.js delete mode 100644 src/sections/auth/auth-forms/FirebaseSocial.js delete mode 100644 src/sections/widget/data/ApplicationSales.js delete mode 100644 src/sections/widget/data/TasksCard.js create mode 100644 src/themes/dt/primitives.js diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..e3d678e --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "console", + "runtimeExecutable": "npm", + "runtimeArgs": ["start"], + "port": 3000 + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md index bb44abb..78df439 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,61 +117,31 @@ src/ ## 6. Design system (the `DT` token block) -The polished pages (`deliveries.js`) share a token block at the top of the file. Every new operator page must paste and reuse this block — do not invent fresh colours, spacings, or radius numbers. +**The design system lives in two shared modules. Import from them — never redeclare.** ```js -const DT = { - radiusPill: 999, - radiusCard: 16, - radiusInner: 12, - shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', - shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', - shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; - -// Alpha-suffix helpers — append hex transparency to any accent colour. -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); // very subtle surface tint -const soft = (c) => a(c, '18'); // soft chip / avatar bg -const ring = (c) => a(c, '26'); // focus ring -const edge = (c) => a(c, '55'); // resting border - -// Pill-style filter inputs. -const pillFieldSx = (color) => ({ - '& .MuiOutlinedInput-root': { - borderRadius: DT.radiusPill + 'px', - bgcolor: tint(color), - fontWeight: 600, - '& fieldset': { borderColor: edge(color), borderWidth: 1.5 }, - '&:hover fieldset': { borderColor: color }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 } - } -}); - -// Soft Paper used by all Autocomplete popups (matches the deliveries batch dropdown). -const SoftPaper = (props) => ( - -); - -// Colored avatar — flips between filled and soft based on `selected`. -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - {children} -); +import { DT, a, tint, soft, ring, edge, pillFieldSx, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens'; +import { SoftPaper, AccentAvatar } from 'themes/dt/primitives'; ``` +| Export | From | What it is | +|---|---|---| +| `DT` | `themes/dt/tokens` | Radii (`radiusPill/Card/Inner/Field`), the three shadows, text/border/surface colours, `DT.brand` (`#C01227`) | +| `a` `tint` `soft` `ring` `edge` | `themes/dt/tokens` | Alpha-suffix helpers — append hex transparency to any accent (`08` / `18` / `26` / `55`) | +| `pillFieldSx` | `themes/dt/tokens` | Filter `Autocomplete`/`TextField` styling. Takes no arguments — the field is neutral white with a brand focus ring on every page. Legacy call sites pass a colour; it's ignored. | +| `tableScrollSx` | `themes/dt/tokens` | Brand-red scrollbar for ``. Spread it: `sx={{ maxHeight: ..., ...tableScrollSx }}` | +| `tableHeadSx` | `themes/dt/tokens` | Uppercase muted header row. Use as `` | +| `tableRowSx` | `themes/dt/tokens` | Body row hairline divider + hover tint. Use as `` | +| `SoftPaper` | `themes/dt/primitives` | `PaperComponent` for every Autocomplete popup | +| `AccentAvatar` | `themes/dt/primitives` | Tinted icon avatar; fills solid when `selected` | + +> **Do not paste a local `const DT = {…}` into a page.** Seventeen pages used to +> carry their own copy and they drifted — `orders.js` sat on `radiusCard: 16` +> and the old heavy shadows while every other page had moved to `14` and the +> restrained set, which is exactly why pages stopped matching. They now all +> import. If a token needs to change, change it in `themes/dt/tokens.js` so +> every page moves together. + ### Page anatomy (every operator page should follow this order) > **Use brand red `#C01227` (with light variant `#D25463`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only. @@ -179,7 +149,7 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => ( 1. **Gradient header ``** — `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`, 48px filled `#C01227` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#C01227" paperComponent={SoftPaper}`). 2. **KPI tiles row** — `Grid` of 3–4 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand red `#C01227`; other tiles use semantic status colours. 3. **Filter bar ``** (optional) — pill-style `Autocomplete`s using `pillFieldSx(color)` + `SoftPaper`, each with an `AccentAvatar` start-adornment. -4. **Pill status tabs + pill search** — tabs as clickable `` pills (active = filled accent + glow ring, inactive = `tint` bg + `edge` border), each with an avatar icon and a count badge. Tab accents use the **semantic status colour** for each tab (pending → amber, delivered → emerald, etc.). Search via `DebounceSearchBar` styled with brand red `#C01227` (tint bg, edge border, focus ring). +4. **Pill status tabs + pill search** — tabs as clickable `` pills (inactive = `tint` bg + `edge` border, using each tab's **semantic status colour** — pending → amber, delivered → emerald, etc.). The **selected** tab always fills with **brand red `#C01227`** (border, bg, glow ring, hover), regardless of which status it represents — status colour is a resting-state accent only, not a selection colour. See `deliveries.js`'s `STATUS_TABS.map` (canonical) for the exact `active ? '#C01227' : ...` ternary shape; `orders.js`, `riders.js`, `Tenants.js`, and `reports/ordersDetails.js` all follow the same pattern. Search via `DebounceSearchBar` styled with brand red `#C01227` (tint bg, edge border, focus ring). 5. **Table ``** with `` + sticky `` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand red `edge('#C01227')`. 6. **Status badges in cells** — `Stack` with `AccentAvatar` + label inside a soft pill (tint bg, edge border). Status colours come from a per-page `STATUS_META` map keyed by lowercase status string — these are **semantic**, not brand. 7. **Edit / action icon buttons** — soft-pill `IconButton` using brand red `#C01227` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators). @@ -201,11 +171,17 @@ Variants (also from `theme/default.js`): **Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)` (subtle wash) or `linear-gradient(135deg, #C01227 0%, #D25463 100%)` (solid, for dialog titles). -> **Migration note:** `deliveries.js` currently uses `#6366f1` (indigo) as its brand accent — a holdover from the first design pass before brand red was canonicalised. It is scheduled to migrate to `#C01227`. The `createorder1` Saved-Address dialog has already been migrated. +> **Off-brand accents are gone.** `deliveries.js`'s old indigo `#6366f1` brand +> accent, `Tenants.js`'s purple `#662582`, and `multipleOrders.js`'s antd blue +> `#1890ff` / purple `#65387a` have all been retired — brand surfaces are +> `#C01227` and metric accents come from the status palette below. Indigo +> `#6366f1` now appears only as the "Accepted" status colour. ### Status palette (semantic — distinct from brand) -These colour-code lifecycle states. Do **not** swap them for brand red — operators rely on the colour to identify status at a glance. +These colour-code lifecycle states. Do **not** swap them for brand red on status **badges** (table-row status pills, `AccentAvatar` dropdown icons, KPI tiles) — operators rely on the colour to identify status at a glance in those read-only contexts. + +**Exception — selected status tabs:** the one deliberate place brand red *does* replace the status colour is a **selected** pill tab (§4 above) — clicking "Pending"/"Active"/etc. fills that tab red, not amber/green, so the clicked state always reads as "this is the active filter" in the one consistent brand colour. The status colour still shows on every *unselected* tab (as a tint) and on the row badges underneath, so at-a-glance status identification in the table itself is unaffected. | Meaning | Colour | |--------------------------|-----------| @@ -299,7 +275,7 @@ These colour-code lifecycle states. Do **not** swap them for brand red — opera ## 13. Cross-references - **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here. -- **For shared design patterns** between pages, the source of truth is the `DT` token block and helpers near the top of `src/pages/nearle/deliveries/deliveries.js` (search for `const DT = {`). Copy from there, don't redesign. +- **For shared design patterns** between pages, the source of truth is `src/themes/dt/tokens.js` (values, `pillFieldSx`, the three `table*Sx` helpers) and `src/themes/dt/primitives.js` (`SoftPaper`, `AccentAvatar`). Import from them, don't redesign and don't re-declare locally. `src/pages/nearle/deliveries/deliveries.js` remains the reference for how those tokens are *composed* into a page (filter bar → tabs → table), but it is no longer where the values live. Astryx v0.1.9 · 153 components diff --git a/src/App.js b/src/App.js index 19c8933..15d2f8a 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,6 @@ import ThemeCustomization from 'themes'; import Locales from 'components/Locales'; // import RTLLayout from 'components/RTLLayout'; import ScrollTop from 'components/ScrollTop'; -import Snackbar from 'components/@extended/Snackbar'; import Notistack from 'components/third-party/Notistack'; import { useNavigate } from 'react-router'; import { useEffect } from 'react'; @@ -29,12 +28,10 @@ const AppContent = () => { return () => unsubscribe?.(); }, []); - return ( - <> - - - - ); + // The old redux-driven MUI used to mount here. Nothing ever + // dispatched `openSnackbar`, so it was permanently closed — every real toast + // goes through notistack (OpenToast / GlobalToast). Removed with MUI. + return ; }; // ==============================|| APP - THEME, ROUTER, LOCAL ||============================== // diff --git a/src/assets/images/auth/AuthBackground.js b/src/assets/images/auth/AuthBackground.js deleted file mode 100644 index 361ffff..0000000 --- a/src/assets/images/auth/AuthBackground.js +++ /dev/null @@ -1,42 +0,0 @@ -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/material'; - -// project-imports -import { ThemeDirection, ThemeMode } from 'config'; - -// ==============================|| AUTH BLUR BACK SVG ||============================== // - -const AuthBackground = () => { - const theme = useTheme(); - return ( - - - - - - - - ); -}; - -export default AuthBackground; diff --git a/src/components/@extended/Avatar.js b/src/components/@extended/Avatar.js deleted file mode 100644 index 2450bfe..0000000 --- a/src/components/@extended/Avatar.js +++ /dev/null @@ -1,119 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { styled, useTheme } from '@mui/material/styles'; -import MuiAvatar from '@mui/material/Avatar'; - -// project import -import getColors from 'utils/getColors'; - -// ==============================|| AVATAR - COLOR STYLE ||============================== // - -function getColorStyle({ theme, color, type }) { - const colors = getColors(theme, color); - const { lighter, light, main, contrastText } = colors; - - switch (type) { - case 'filled': - return { - color: contrastText, - backgroundColor: main - }; - case 'outlined': - return { - color: main, - border: '1px solid', - borderColor: main, - backgroundColor: 'transparent' - }; - case 'combined': - return { - color: main, - border: '1px solid', - borderColor: light, - backgroundColor: lighter - }; - default: - return { - color: main, - backgroundColor: lighter - }; - } -} - -// ==============================|| AVATAR - SIZE STYLE ||============================== // - -function getSizeStyle(size) { - switch (size) { - case 'badge': - return { - border: '2px solid', - fontSize: '0.675rem', - width: 20, - height: 20 - }; - case 'xs': - return { - fontSize: '0.75rem', - width: 24, - height: 24 - }; - case 'sm': - return { - fontSize: '0.875rem', - width: 32, - height: 32 - }; - case 'lg': - return { - fontSize: '1.2rem', - width: 52, - height: 52 - }; - case 'xl': - return { - fontSize: '1.5rem', - width: 64, - height: 64 - }; - case 'md': - default: - return { - fontSize: '1rem', - width: 40, - height: 40 - }; - } -} - -// ==============================|| STYLED - AVATAR ||============================== // - -const AvatarStyle = styled(MuiAvatar, { shouldForwardProp: (prop) => prop !== 'color' && prop !== 'type' && prop !== 'size' })( - ({ theme, variant, color, type, size }) => ({ - ...getSizeStyle(size), - ...getColorStyle({ variant, theme, color, type }), - ...(size === 'badge' && { - borderColor: theme.palette.background.default - }) - }) -); - -// ==============================|| EXTENDED - AVATAR ||============================== // - -export default function Avatar({ variant = 'circular', children, color = 'primary', type, size = 'md', ...others }) { - const theme = useTheme(); - - return ( - - {children} - - ); -} - -Avatar.propTypes = { - children: PropTypes.node, - color: PropTypes.string, - type: PropTypes.string, - size: PropTypes.string, - variant: PropTypes.string -}; diff --git a/src/components/@extended/Breadcrumbs.js b/src/components/@extended/Breadcrumbs.js deleted file mode 100644 index 72d7cb1..0000000 --- a/src/components/@extended/Breadcrumbs.js +++ /dev/null @@ -1,222 +0,0 @@ -import PropTypes from 'prop-types'; -import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; - -// material-ui -import MuiBreadcrumbs from '@mui/material/Breadcrumbs'; -import { useTheme } from '@mui/material/styles'; -import { Divider, Grid, Typography } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// assets -import { ApartmentOutlined, HomeFilled, HomeOutlined } from '@ant-design/icons'; - -// ==============================|| BREADCRUMBS ||============================== // - -const Breadcrumbs = ({ - card, - divider = true, - icon, - icons, - maxItems, - navigation, - rightAlign, - separator, - title, - titleBottom, - sx, - ...others -}) => { - const theme = useTheme(); - const location = useLocation(); - const [main, setMain] = useState(); - const [item, setItem] = useState(); - - let currentPath = location.pathname; - - // only used for component demo breadcrumbs - if (currentPath.includes('/components-overview/breadcrumbs')) { - currentPath = '/apps/kanban/board'; - } - - if (currentPath.includes('/apps/kanban/backlogs')) { - currentPath = '/apps/kanban/board'; - } - - useEffect(() => { - if (currentPath.includes('/apps/profiles/user/payment')) { - setItem(undefined); - } - }, [item, currentPath]); - - const iconSX = { - marginRight: theme.spacing(0.75), - marginTop: `-${theme.spacing(0.25)}`, - width: '1rem', - height: '1rem', - color: theme.palette.secondary.main - }; - - // set active item state - const getCollapse = (menu) => { - if (menu.children) { - menu.children.filter((collapse) => { - if (collapse.type && collapse.type === 'collapse') { - getCollapse(collapse); - if (collapse.url === currentPath) { - setMain(collapse); - setItem(collapse); - } - } else if (collapse.type && collapse.type === 'item') { - if (currentPath.includes(collapse.url)) { - setMain(menu); - setItem(collapse); - } - } - return false; - }); - } - }; - - useEffect(() => { - navigation?.items?.map((menu) => { - if (menu.type && menu.type === 'group') { - getCollapse(menu); - } - return false; - }); - }); - - // item separator - const SeparatorIcon = separator; - const separatorIcon = separator ? : '/'; - - let mainContent; - let itemContent; - let breadcrumbContent = ; - let itemTitle = ''; - let CollapseIcon; - let ItemIcon; - - // collapse item - if (main && main.type === 'collapse' && main.breadcrumbs === true) { - CollapseIcon = main.icon ? main.icon : ApartmentOutlined; - mainContent = ( - - {icons && } - {main.title} - - ); - breadcrumbContent = ( - - - - - - {icons && } - {icon && !icons && } - {(!icon || icons) && 'Home'} - - {mainContent} - - - {title && titleBottom && ( - - {main.title} - - )} - - {card === false && divider !== false && } - - ); - } - - // items - if (item && item.type === 'item') { - itemTitle = item.title; - - ItemIcon = item.icon ? item.icon : ApartmentOutlined; - itemContent = ( - - {icons && } - {itemTitle} - - ); - - // main - if (item.breadcrumbs !== false) { - breadcrumbContent = ( - - - {title && !titleBottom && ( - - {item.title} - - )} - - - - {icons && } - {icon && !icons && } - {(!icon || icons) && 'Home'} - - {mainContent} - {itemContent} - - - {title && titleBottom && ( - - {item.title} - - )} - - {card === false && divider !== false && } - - ); - } - } - - return breadcrumbContent; -}; - -Breadcrumbs.propTypes = { - card: PropTypes.bool, - divider: PropTypes.bool, - icon: PropTypes.bool, - icons: PropTypes.bool, - maxItems: PropTypes.number, - navigation: PropTypes.object, - rightAlign: PropTypes.bool, - separator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - title: PropTypes.bool, - titleBottom: PropTypes.bool, - sx: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) -}; - -export default Breadcrumbs; diff --git a/src/components/@extended/Dot.js b/src/components/@extended/Dot.js deleted file mode 100644 index 33403b9..0000000 --- a/src/components/@extended/Dot.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/material'; - -// project import -import getColors from 'utils/getColors'; - -const Dot = ({ color, size, variant, sx }) => { - const theme = useTheme(); - const colors = getColors(theme, color || 'primary'); - const { main } = colors; - - return ( - - ); -}; - -Dot.propTypes = { - color: PropTypes.string, - size: PropTypes.number, - variant: PropTypes.string, - sx: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) -}; - -export default Dot; diff --git a/src/components/@extended/IconButton.js b/src/components/@extended/IconButton.js deleted file mode 100644 index 3790509..0000000 --- a/src/components/@extended/IconButton.js +++ /dev/null @@ -1,162 +0,0 @@ -import PropTypes from 'prop-types'; - -import { forwardRef } from 'react'; - -// material-ui -import MuiIconButton from '@mui/material/IconButton'; -import { alpha, styled, useTheme } from '@mui/material/styles'; - -// project imports -import getColors from 'utils/getColors'; -import getShadow from 'utils/getShadow'; - -// ==============================|| ICON BUTTON - COLOR STYLE ||============================== // - -function getColorStyle({ variant, theme, color }) { - const colors = getColors(theme, color); - const { lighter, light, dark, main, contrastText } = colors; - - const buttonShadow = `${color}Button`; - const shadows = getShadow(theme, buttonShadow); - - const commonShadow = { - '&::after': { - boxShadow: `0 0 6px 6px ${alpha(main, 0.9)}` - }, - '&:active::after': { - boxShadow: `0 0 0 0 ${alpha(main, 0.9)}` - }, - '&:focus-visible': { - outline: `2px solid ${dark}`, - outlineOffset: 2 - } - }; - - switch (variant) { - case 'contained': - return { - color: contrastText, - backgroundColor: main, - '&:hover': { - backgroundColor: dark - }, - ...commonShadow - }; - case 'light': - return { - color: main, - backgroundColor: lighter, - '&:hover': { - backgroundColor: light - }, - ...commonShadow - }; - case 'shadow': - return { - boxShadow: shadows, - color: contrastText, - backgroundColor: main, - '&:hover': { - boxShadow: 'none', - backgroundColor: dark - }, - ...commonShadow - }; - case 'outlined': - return { - '&:hover': { - backgroundColor: 'transparent', - color: dark, - borderColor: dark - }, - ...commonShadow - }; - case 'dashed': - return { - backgroundColor: lighter, - '&:hover': { - color: dark, - borderColor: dark - }, - ...commonShadow - }; - case 'text': - default: - return { - '&:hover': { - color: dark, - backgroundColor: lighter - }, - ...commonShadow - }; - } -} - -// ==============================|| STYLED - ICON BUTTON ||============================== // - -const IconButtonStyle = styled(MuiIconButton, { shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'shape' })( - ({ theme, variant, shape, color }) => ({ - position: 'relative', - '::after': { - content: '""', - display: 'block', - position: 'absolute', - left: 0, - top: 0, - width: '100%', - height: '100%', - borderRadius: shape === 'rounded' ? '50%' : 4, - opacity: 0, - transition: 'all 0.5s' - }, - - ':active::after': { - position: 'absolute', - borderRadius: shape === 'rounded' ? '50%' : 4, - left: 0, - top: 0, - opacity: 1, - transition: '0s' - }, - ...(shape === 'rounded' && { - borderRadius: '50%' - }), - ...(variant === 'outlined' && { - border: '1px solid', - borderColor: 'inherit' - }), - ...(variant === 'dashed' && { - border: '1px dashed', - borderColor: 'inherit' - }), - ...(variant !== 'text' && { - '&.Mui-disabled': { - backgroundColor: theme.palette.grey[200] - } - }), - ...getColorStyle({ variant, theme, color }) - }) -); - -// ==============================|| EXTENDED - ICON BUTTON ||============================== // - -const IconButton = forwardRef(({ variant = 'text', shape = 'square', children, color = 'primary', ...others }, ref) => { - const theme = useTheme(); - - return ( - - {children} - - ); -}); - -IconButton.propTypes = { - variant: PropTypes.string, - shape: PropTypes.string, - children: PropTypes.node, - color: PropTypes.string -}; - -IconButton.displayName = 'IconButton'; - -export default IconButton; diff --git a/src/components/@extended/LoadingButton.js b/src/components/@extended/LoadingButton.js deleted file mode 100644 index 3b0d711..0000000 --- a/src/components/@extended/LoadingButton.js +++ /dev/null @@ -1,231 +0,0 @@ -import PropTypes from 'prop-types'; -import { forwardRef } from 'react'; - -// material-ui -import MuiLoadingButton from '@mui/lab/LoadingButton'; -import { alpha, styled, useTheme } from '@mui/material/styles'; - -// project imports -import getColors from 'utils/getColors'; -import getShadow from 'utils/getShadow'; - -// ==============================|| LOADING BUTTON - COLOR STYLE ||============================== // - -function getColorStyle({ variant, theme, color, loadingPosition }) { - const colors = getColors(theme, color); - const { lighter, main, dark, contrastText } = colors; - - const buttonShadow = `${color}Button`; - const shadows = getShadow(theme, buttonShadow); - - const loadingIndicator = { - '& .MuiLoadingButton-loadingIndicator': { - color: main - } - }; - - const loadingColor = { - ...(loadingPosition && - loadingPosition !== 'center' && { - color: main - }) - }; - - const commonShadow = { - '&::after': { - boxShadow: `0 0 6px 6px ${alpha(main, 0.9)}` - }, - '&:active::after': { - boxShadow: `0 0 0 0 ${alpha(main, 0.9)}` - }, - '&:focus-visible': { - outline: `2px solid ${dark}`, - outlineOffset: 2 - } - }; - - switch (variant) { - case 'contained': - return { - backgroundColor: main, - ...(loadingPosition && - loadingPosition !== 'center' && { - color: contrastText - }), - '& .MuiLoadingButton-loadingIndicator': { - color: contrastText - }, - '&:hover': { - backgroundColor: dark, - color: contrastText - }, - ...commonShadow - }; - case 'light': - return { - backgroundColor: main, - ...(loadingPosition && - loadingPosition !== 'center' && { - color: contrastText - }), - '& .MuiLoadingButton-loadingIndicator': { - color: contrastText - }, - '&:hover': { - backgroundColor: dark, - color: contrastText - }, - ...commonShadow - }; - case 'shadow': - return { - boxShadow: shadows, - backgroundColor: main, - ...(loadingPosition && - loadingPosition !== 'center' && { - color: contrastText - }), - '& .MuiLoadingButton-loadingIndicator': { - color: contrastText - }, - '&:hover': { - boxShadow: 'none', - backgroundColor: dark, - color: contrastText - }, - ...commonShadow - }; - case 'outlined': - return { - backgroundColor: 'transparent', - borderColor: main, - ...loadingColor, - ...loadingIndicator - }; - case 'dashed': - return { - backgroundColor: lighter, - borderColor: main, - ...loadingColor, - ...loadingIndicator, - ...commonShadow - }; - case 'text': - default: - return { - color: main, - ...loadingIndicator, - ...commonShadow - }; - } -} - -// ==============================|| STYLED - LOADING BUTTON ||============================== // - -const LoadingButtonStyle = styled(MuiLoadingButton, { shouldForwardProp: (prop) => prop !== 'shape' && prop !== 'variant' })( - ({ theme, variant, shape, color, loading, loadingPosition }) => ({ - '::after': { - content: '""', - display: 'block', - position: 'absolute', - left: 0, - top: 0, - width: '100%', - height: '100%', - borderRadius: shape === 'rounded' ? '50%' : 4, - opacity: 0, - transition: 'all 0.5s' - }, - - ':active::after': { - position: 'absolute', - borderRadius: shape === 'rounded' ? '50%' : 4, - left: 0, - top: 0, - opacity: 1, - transition: '0s' - }, - ...(variant === 'text' && { - ...getColorStyle({ variant, theme, color, loadingPosition }), - '&.MuiButton-sizeMedium': { - height: 36 - }, - '&.MuiButton-sizeSmall': { - height: 30 - }, - '&.MuiButton-sizeLarge': { - height: 44 - } - }), - ...(shape && { - minWidth: 0, - '&.MuiButton-sizeMedium': { - width: 36, - height: 36 - }, - '&.MuiButton-sizeSmall': { - width: 30, - height: 30 - }, - '&.MuiButton-sizeLarge': { - width: 44, - height: 44 - }, - ...(shape === 'rounded' && { - borderRadius: '50%' - }) - }), - - ...(variant === 'outlined' && { - border: '1px solid' - }), - ...(variant === 'dashed' && { - border: '1px dashed' - }), - ...((variant === 'contained' || variant === 'shadow') && - !loading && { - color: '#fff' - }), - ...(variant !== 'text' && { - ...getColorStyle({ variant, theme, color, loadingPosition }) - }), - - '&.Mui-disabled': { - ...(variant !== 'text' && { - ...getColorStyle({ variant, theme, color, loadingPosition }) - }) - } - }) -); - -// ==============================|| EXTENDED - LOADING BUTTON ||============================== // - -const LoadingButton = forwardRef(({ variant = 'text', shape, children, color = 'primary', ...others }, ref) => { - const theme = useTheme(); - - return ( - - {children} - - ); -}); - -LoadingButton.propTypes = { - variant: PropTypes.string, - shape: PropTypes.string, - children: PropTypes.node, - color: PropTypes.string -}; - -LoadingButton.displayName = 'LoadingButton'; - -export default LoadingButton; diff --git a/src/components/@extended/Snackbar.js b/src/components/@extended/Snackbar.js deleted file mode 100644 index 1fa9b53..0000000 --- a/src/components/@extended/Snackbar.js +++ /dev/null @@ -1,128 +0,0 @@ -// material-ui -import { Alert, Button, Fade, Grow, Slide } from '@mui/material'; -import MuiSnackbar from '@mui/material/Snackbar'; - -// project-import -import IconButton from './IconButton'; -import { dispatch, useSelector } from 'store'; -import { closeSnackbar } from 'store/reducers/snackbar'; - -// assets -import { CloseOutlined } from '@ant-design/icons'; - -// animation function -function TransitionSlideLeft(props) { - return ; -} - -function TransitionSlideUp(props) { - return ; -} - -function TransitionSlideRight(props) { - return ; -} - -function TransitionSlideDown(props) { - return ; -} - -function GrowTransition(props) { - return ; -} - -// animation options -const animation = { - SlideLeft: TransitionSlideLeft, - SlideUp: TransitionSlideUp, - SlideRight: TransitionSlideRight, - SlideDown: TransitionSlideDown, - Grow: GrowTransition, - Fade -}; - -// ==============================|| SNACKBAR ||============================== // - -const Snackbar = () => { - const snackbar = useSelector((state) => state.snackbar); - const { actionButton, anchorOrigin, alert, close, message, open, transition, variant } = snackbar; - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return; - } - dispatch(closeSnackbar()); - }; - - return ( - <> - {/* default snackbar */} - {variant === 'default' && ( - - - - - - - } - /> - )} - - {/* alert snackbar */} - {variant === 'alert' && ( - - - {actionButton !== false && ( - - )} - {close !== false && ( - - - - )} - - } - sx={{ - ...(alert.variant === 'outlined' && { - bgcolor: 'grey.0' - }) - }} - > - {message} - - - )} - - ); -}; - -export default Snackbar; diff --git a/src/components/@extended/Tooltip.js b/src/components/@extended/Tooltip.js deleted file mode 100644 index 63f1f91..0000000 --- a/src/components/@extended/Tooltip.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { styled, useTheme } from '@mui/material/styles'; -import { Box, tooltipClasses, Tooltip as MuiTooltip } from '@mui/material'; - -// project import -import getColors from 'utils/getColors'; - -// ==============================|| TOOLTIP - VARIANT ||============================== // - -function getVariantStyle({ color, theme, labelColor }) { - const colors = getColors(theme, color); - const { main, contrastText } = colors; - let colorValue = color ? color : ''; - - if (['primary', 'secondary', 'info', 'success', 'warning', 'error'].includes(colorValue)) { - return { - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: main, - color: labelColor ? labelColor : contrastText - }, - [`& .${tooltipClasses.arrow}`]: { - color: main - } - }; - } else { - return { - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: colorValue, - color: labelColor ? labelColor : contrastText, - boxShadow: theme.shadows[1] - }, - [`& .${tooltipClasses.arrow}`]: { - color: colorValue - } - }; - } -} - -// ==============================|| STYLED - TOOLTIP COLOR ||============================== // - -const TooltipStyle = styled(({ className, ...props }) => , { - shouldForwardProp: (prop) => prop !== 'color' && prop !== 'labelColor' -})(({ theme, color, labelColor }) => ({ - ...(color && getVariantStyle({ color, theme, labelColor })) -})); - -// ==============================|| EXTENDED - TOOLTIP ||============================== // - -export default function CustomTooltip({ children, arrow, labelColor = '', ...rest }) { - const theme = useTheme(); - return ( - - - {children} - - - ); -} - -CustomTooltip.propTypes = { - children: PropTypes.element, - arrow: PropTypes.bool, - labelColor: PropTypes.string, - rest: PropTypes.array -}; diff --git a/src/components/@extended/Transitions.js b/src/components/@extended/Transitions.js deleted file mode 100644 index 1b4377b..0000000 --- a/src/components/@extended/Transitions.js +++ /dev/null @@ -1,116 +0,0 @@ -import PropTypes from 'prop-types'; -import { forwardRef } from 'react'; - -// material-ui -import { Collapse, Fade, Box, Grow, Slide, Zoom } from '@mui/material'; - -// ==============================|| TRANSITIONS ||============================== // - -const Transitions = forwardRef(({ children, position = 'top-left', type = 'grow', direction = 'up', ...others }, ref) => { - let positionSX = { - transformOrigin: '0 0 0' - }; - - switch (position) { - case 'top-right': - positionSX = { - transformOrigin: 'top right' - }; - break; - case 'top': - positionSX = { - transformOrigin: 'top' - }; - break; - case 'bottom-left': - positionSX = { - transformOrigin: 'bottom left' - }; - break; - case 'bottom-right': - positionSX = { - transformOrigin: 'bottom right' - }; - break; - case 'bottom': - positionSX = { - transformOrigin: 'bottom' - }; - break; - case 'top-left': - default: - positionSX = { - transformOrigin: '0 0 0' - }; - break; - } - - return ( - - {type === 'grow' && ( - - {children} - - )} - - {type === 'collapse' && ( - - {children} - - )} - - {type === 'fade' && ( - - {children} - - )} - - {type === 'slide' && ( - - {children} - - )} - - {type === 'zoom' && ( - - {children} - - )} - - ); -}); - -Transitions.propTypes = { - children: PropTypes.node, - type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']), - position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']), - direction: PropTypes.oneOf(['up', 'down', 'left', 'right']) -}; - -export default Transitions; - -export const PopupTransition = forwardRef(function Transition(props, ref) { - return ; -}); diff --git a/src/components/@extended/progress/CircularWithLabel.js b/src/components/@extended/progress/CircularWithLabel.js deleted file mode 100644 index 4348dfc..0000000 --- a/src/components/@extended/progress/CircularWithLabel.js +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, CircularProgress, Typography } from '@mui/material'; - -// ==============================|| PROGRESS - CIRCULAR LABEL ||============================== // - -export default function CircularWithLabel({ value, ...others }) { - return ( - - - - {`${Math.round(value)}%`} - - - ); -} - -CircularWithLabel.propTypes = { - value: PropTypes.number -}; diff --git a/src/components/@extended/progress/CircularWithPath.js b/src/components/@extended/progress/CircularWithPath.js deleted file mode 100644 index 2e85f3a..0000000 --- a/src/components/@extended/progress/CircularWithPath.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, CircularProgress, Typography, circularProgressClasses } from '@mui/material'; - -// ==============================|| PROGRESS - CIRCULAR PATH ||============================== // - -export default function CircularWithPath({ value, size, variant, thickness, showLabel, pathColor, sx, ...others }) { - return ( - - - {showLabel && ( - - - {value ? `${Math.round(value)}%` : '0%'} - - - )} - - - ); -} - -CircularWithPath.propTypes = { - value: PropTypes.number, - size: PropTypes.number, - variant: PropTypes.string, - thickness: PropTypes.number, - showLabel: PropTypes.bool, - pathColor: PropTypes.string, - sx: PropTypes.array, - others: PropTypes.array -}; diff --git a/src/components/@extended/progress/LinearWithIcon.js b/src/components/@extended/progress/LinearWithIcon.js deleted file mode 100644 index c1da65a..0000000 --- a/src/components/@extended/progress/LinearWithIcon.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, LinearProgress } from '@mui/material'; - -// ==============================|| PROGRESS - LINEAR ICON ||============================== // - -export default function LinearWithIcon({ icon, value, ...others }) { - return ( - - - - - {icon} - - ); -} - -LinearWithIcon.propTypes = { - icon: PropTypes.node, - value: PropTypes.number -}; diff --git a/src/components/@extended/progress/LinearWithLabel.js b/src/components/@extended/progress/LinearWithLabel.js deleted file mode 100644 index 46adb2d..0000000 --- a/src/components/@extended/progress/LinearWithLabel.js +++ /dev/null @@ -1,23 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, LinearProgress, Typography } from '@mui/material'; - -// ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== // - -export default function LinearWithLabel({ value, ...others }) { - return ( - - - - - - {`${Math.round(value)}%`} - - - ); -} - -LinearWithLabel.propTypes = { - value: PropTypes.number -}; diff --git a/src/components/CircularLoader.js b/src/components/CircularLoader.js index 5673005..e441520 100644 --- a/src/components/CircularLoader.js +++ b/src/components/CircularLoader.js @@ -1,47 +1,29 @@ -import { styled } from '@mui/material/styles'; -import CircularProgress from '@mui/material/CircularProgress'; -import nelogo from '../assets/images/doormile-mark.png'; +import { Center } from '@astryxdesign/core/Center'; -// Styled Loader Wrapper -const LoaderWrapper = styled('div')(() => ({ - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - zIndex: 2001, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - background: 'rgba(255, 255, 255, 0.7)', // Light overlay effect - padding: '20px', - borderRadius: '10px' // Soft rounded border -})); +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; +import { DT } from 'themes/dt/tokens'; -// Styled Logo Wrapper -const LogoWrapper = styled('div')(() => ({ - position: 'absolute', - width: '40px', // Adjust based on your logo size - height: '40px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center' -})); +// ==============================|| FULL-SCREEN BRANDED LOADER ||============================== // +// Centered branded ring on a translucent scrim. The ring itself is +// LoaderWithImage — this component only supplies the fixed-position overlay, +// so the two loaders can't drift apart visually. -// ==============================|| Custom Circular Loader with Logo ||============================== // const CircularLoader = () => ( - - {/* Circular Loader */} - - - {/* Logo Positioned at the Center */} - - Doormile - - +
+ +
); export default CircularLoader; diff --git a/src/components/DateFilterDialog.js b/src/components/DateFilterDialog.js index 6a870d5..50337c2 100644 --- a/src/components/DateFilterDialog.js +++ b/src/components/DateFilterDialog.js @@ -1,9 +1,15 @@ +import PropTypes from 'prop-types'; import React, { useState } from 'react'; -import { Dialog, DialogTitle, DialogContent, Button, Stack, Typography } from '@mui/material'; -import { DateRangePicker } from 'components/nearle_components/DateRangePicker'; import dayjs from 'dayjs'; import { addDays, addWeeks, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addMonths } from 'date-fns'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { VStack } from '@astryxdesign/core/VStack'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Button } from '@astryxdesign/core/Button'; + +import { DateRangePicker } from 'components/nearle_components/DateRangePicker'; + const DateFilterDialog = ({ open, onClose, onSelect }) => { const [selectedRange, setSelectedRange] = useState(null); @@ -16,12 +22,9 @@ const DateFilterDialog = ({ open, onClose, onSelect }) => { }; return ( - - - Select Filter Options - - - + !next && onClose()} width={640} purpose="form"> + + { { label: 'Last Month', startDate: startOfMonth(addMonths(new Date(), -1)), endDate: endOfMonth(addMonths(new Date(), -1)) } ]} /> - - - - + }} + /> + + ); }; +DateFilterDialog.propTypes = { + open: PropTypes.bool, + onClose: PropTypes.func, + onSelect: PropTypes.func +}; + export default DateFilterDialog; diff --git a/src/components/Loader.js b/src/components/Loader.js index d916a8a..269d5ac 100644 --- a/src/components/Loader.js +++ b/src/components/Loader.js @@ -1,25 +1,13 @@ -// material-ui -import { styled } from '@mui/material/styles'; -import LinearProgress from '@mui/material/LinearProgress'; - -// loader style -const LoaderWrapper = styled('div')(({ theme }) => ({ - position: 'fixed', - top: 0, - left: 0, - zIndex: 2001, - width: '100%', - '& > * + *': { - marginTop: theme.spacing(2) - } -})); +import { ProgressBar } from '@astryxdesign/core/ProgressBar'; // ==============================|| Loader ||============================== // +// Indeterminate bar pinned to the top of the viewport — the app-wide "work in +// flight" indicator (route suspense, page-level mutations). const Loader = () => ( - - - +
+ +
); export default Loader; diff --git a/src/components/MainCard.js b/src/components/MainCard.js index c2d5cca..1b065f5 100644 --- a/src/components/MainCard.js +++ b/src/components/MainCard.js @@ -1,141 +1,66 @@ import PropTypes from 'prop-types'; import { forwardRef } from 'react'; -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material'; - -// project-imports -import { ThemeMode } from 'config'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Divider } from '@astryxdesign/core/Divider'; // ==============================|| CUSTOM - MAIN CARD ||============================== // +// Bordered surface with an optional header slot (title + trailing action) and +// a content region. +// +// Astryx migration notes — the MUI-era prop surface was largely dead weight and +// has been trimmed rather than ported: +// • `sx` / `contentSX` → `style` and `padding` (Astryx spacing step). The +// three call sites that passed `borderRadius`/`boxShadow`/`borderColor` +// were restating what the Card already gets from the theme, so they simply +// dropped the prop. +// • `border` / `boxShadow` / `shadow` / `elevation` → the single `elevation` +// prop. Every call site used the defaults. +// • `darkTitle` / `modal` → removed; nothing passed either. +// `title`, `secondary`, `divider` and `content` keep their old meaning so the +// call sites read the same. const MainCard = forwardRef( - ( - { - border = true, - boxShadow, - children, - subheader, - content = true, - contentSX = {}, - headerSX, - darkTitle, - divider = true, - elevation, - secondary, - shadow, - sx = {}, - title, - modal = false, - ...others - }, - ref - ) => { - const theme = useTheme(); - boxShadow = theme.palette.mode === ThemeMode.DARK ? boxShadow || true : boxShadow; + ({ children, content = true, padding, divider = true, elevation = 'low', secondary, title, height, style, ...others }, ref) => { + const bodyPadding = content ? (padding ?? 4) : 0; return ( - - {/* card header and action */} - {/* {!darkTitle && title && ( */} - {!darkTitle && ( - + + {title && ( + <> + + {title} + {secondary} + + {divider && } + )} - {darkTitle && title && ( - {title || ''}
} - action={secondary} - /> - )} - - {/* content & header divider */} - {title && divider && } - - {/* card content */} - {content && ( - + {bodyPadding === 0 ? ( + children + ) : ( + {children} - + )} - {!content && children} ); } ); +MainCard.displayName = 'MainCard'; + MainCard.propTypes = { - border: PropTypes.bool, - boxShadow: PropTypes.bool, children: PropTypes.node, - subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), content: PropTypes.bool, - contentClass: PropTypes.string, - contentSX: PropTypes.object, - darkTitle: PropTypes.bool, + padding: PropTypes.number, divider: PropTypes.bool, - elevation: PropTypes.number, - secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]), - shadow: PropTypes.string, - sx: PropTypes.object, - title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]), - modal: PropTypes.bool + elevation: PropTypes.oneOf(['none', 'low', 'med', 'high']), + secondary: PropTypes.node, + title: PropTypes.node, + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + style: PropTypes.object }; export default MainCard; diff --git a/src/components/ScrollTop.js b/src/components/ScrollTop.js index 739b64f..3464f42 100644 --- a/src/components/ScrollTop.js +++ b/src/components/ScrollTop.js @@ -6,8 +6,11 @@ const ScrollTop = ({ children }) => { const { pathname } = useLocation(); useEffect(() => { - // Scroll to top - window.scrollTo({ + // MainLayout's AppShell (height="fill") scrolls `.astryx-layout-content` + // internally, not the window — window.scrollTo would be a no-op there. + // Fall back to window scroll for pages outside that shell (login, etc). + const scrollContainer = document.querySelector('.astryx-layout-content') || window; + scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'smooth' diff --git a/src/components/ScrollX.js b/src/components/ScrollX.js deleted file mode 100644 index eb4d863..0000000 --- a/src/components/ScrollX.js +++ /dev/null @@ -1,10 +0,0 @@ -// material-ui -import { styled } from '@mui/material/styles'; - -const ScrollX = styled('div')({ - width: '100%', - overflowX: 'auto', - display: 'block' -}); - -export default ScrollX; diff --git a/src/components/SecondaryAction.js b/src/components/SecondaryAction.js deleted file mode 100644 index d413aa6..0000000 --- a/src/components/SecondaryAction.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Avatar, ButtonBase, Link, Tooltip } from '@mui/material'; - -// ==============================|| CARD - SECONDARY ACTION ||============================== // - -const CardSecondaryAction = ({ title, link, icon }) => { - const theme = useTheme(); - - return ( - - - {!icon && ( - - - - - - - - - - - - - - - - )} - {icon && ( - - {icon} - - )} - - - ); -}; - -CardSecondaryAction.propTypes = { - title: PropTypes.string, - link: PropTypes.string, - icon: PropTypes.oneOf([PropTypes.node, PropTypes.string]) -}; - -export default CardSecondaryAction; diff --git a/src/components/cards/AuthFooter.js b/src/components/cards/AuthFooter.js deleted file mode 100644 index 75622ae..0000000 --- a/src/components/cards/AuthFooter.js +++ /dev/null @@ -1,61 +0,0 @@ -// material-ui -import { Container, Link, Stack, Typography, useMediaQuery } from '@mui/material'; - -// ==============================|| FOOTER - AUTHENTICATION ||============================== // - -const AuthFooter = () => { - const matchDownSM = useMediaQuery((theme) => theme.breakpoints.down('sm')); - - return ( - - - - This site is protected by{' '} - - Privacy Policy - - - - - - Terms and Conditions - - - Privacy Policy - - - CA Privacy Notice - - - - - ); -}; - -export default AuthFooter; diff --git a/src/components/cards/ComponentHeader.js b/src/components/cards/ComponentHeader.js deleted file mode 100644 index 20178bb..0000000 --- a/src/components/cards/ComponentHeader.js +++ /dev/null @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, Grid, Link, Stack, Typography } from '@mui/material'; - -// assets -import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons'; - -// ==============================|| COMPONENTS - BREADCRUMBS ||============================== // - -const ComponentHeader = ({ title, caption, directory, link }) => ( - - - {title} - {caption && ( - - {caption} - - )} - - - {directory && ( - - - - {directory} - - - )} - {link && ( - - - - {link} - - - )} - - -); - -ComponentHeader.propTypes = { - title: PropTypes.string, - caption: PropTypes.string, - directory: PropTypes.string, - link: PropTypes.string -}; - -export default ComponentHeader; diff --git a/src/components/cards/e-commerce/FloatingCart.js b/src/components/cards/e-commerce/FloatingCart.js deleted file mode 100644 index 75fe8d6..0000000 --- a/src/components/cards/e-commerce/FloatingCart.js +++ /dev/null @@ -1,52 +0,0 @@ -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Fab, Badge } from '@mui/material'; - -// project import - -// assets - -// ==============================|| CART ITEMS - FLOATING BUTTON ||============================== // - -const FloatingCart = ({ element, count = 0, onClick, sx }) => { - const theme = useTheme(); - - // const cart = useSelector((state) => state.cart); - // const totalQuantity = sum(cart.checkout.products.map((item) => item.quantity)); - - return ( - - - {element} - - - ); -}; - -export default FloatingCart; diff --git a/src/components/cards/e-commerce/ProductCard.js b/src/components/cards/e-commerce/ProductCard.js deleted file mode 100644 index d1e3b15..0000000 --- a/src/components/cards/e-commerce/ProductCard.js +++ /dev/null @@ -1,176 +0,0 @@ -import PropTypes from 'prop-types'; -import { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Box, Button, CardContent, CardMedia, Chip, Divider, Grid, Rating, Stack, Typography } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; -import IconButton from 'components/@extended/IconButton'; -import SkeletonProductPlaceholder from 'components/cards/skeleton/ProductPlaceholder'; -import { useDispatch, useSelector } from 'store'; -import { addProduct } from 'store/reducers/cart'; -import { openSnackbar } from 'store/reducers/snackbar'; - -// assets -import { HeartOutlined, HeartFilled } from '@ant-design/icons'; - -const prodImage = require.context('assets/images/e-commerce', true); - -// ==============================|| PRODUCT CARD ||============================== // - -const ProductCard = ({ id, color, name, brand, offer, isStock, image, description, offerPrice, salePrice, rating }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - - const prodProfile = image && prodImage(`./${image}`); - const [productRating] = useState(rating); - const [wishlisted, setWishlisted] = useState(false); - const cart = useSelector((state) => state.cart); - - const addCart = () => { - dispatch(addProduct({ id, name, image, salePrice, offerPrice, color, size: 8, quantity: 1, description }, cart.checkout.products)); - dispatch( - openSnackbar({ - open: true, - message: 'Add To Cart Success', - variant: 'alert', - alert: { - color: 'success' - }, - close: false - }) - ); - }; - - const addToFavourite = () => { - setWishlisted(!wishlisted); - dispatch( - openSnackbar({ - open: true, - message: 'Added to favourites', - variant: 'alert', - alert: { - color: 'success' - }, - close: false - }) - ); - }; - - const [isLoading, setLoading] = useState(true); - useEffect(() => { - setLoading(false); - }, []); - - return ( - <> - {isLoading ? ( - - ) : ( - - - - - - {!isStock && } - {offer && } - - {wishlisted ? ( - - ) : ( - - )} - - - - - - - - - {name} - - - {brand} - - - - - - - - ${offerPrice} - {salePrice && ( - - ${salePrice} - - )} - - - - ({productRating?.toFixed(1)}) - - - - - - - - - - )} - - ); -}; - -ProductCard.propTypes = { - id: PropTypes.number, - color: PropTypes.string, - name: PropTypes.string, - brand: PropTypes.string, - isStock: PropTypes.bool, - image: PropTypes.string, - description: PropTypes.string, - offerPrice: PropTypes.number, - salePrice: PropTypes.number, - offer: PropTypes.string, - rating: PropTypes.number -}; - -export default ProductCard; diff --git a/src/components/cards/e-commerce/ProductReview.js b/src/components/cards/e-commerce/ProductReview.js deleted file mode 100644 index bf45f2f..0000000 --- a/src/components/cards/e-commerce/ProductReview.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Grid, Rating, Stack, Typography } from '@mui/material'; - -// project imports -import Avatar from 'components/@extended/Avatar'; - -// assets -import { StarFilled, StarOutlined } from '@ant-design/icons'; - -const avatarImage = require.context('assets/images/users', true); - -// ==============================|| PRODUCT DETAILS - REVIEW ||============================== // - -const ProductReview = ({ avatar, date, name, rating, review }) => ( - - - - - - - {name} - - - {date} - - } - emptyIcon={} - precision={0.1} - readOnly - /> - - {review} - - - -); - -ProductReview.propTypes = { - avatar: PropTypes.string, - date: PropTypes.string, - name: PropTypes.string, - rating: PropTypes.number, - review: PropTypes.string -}; - -export default ProductReview; diff --git a/src/components/cards/skeleton/ProductPlaceholder.js b/src/components/cards/skeleton/ProductPlaceholder.js deleted file mode 100644 index 3002169..0000000 --- a/src/components/cards/skeleton/ProductPlaceholder.js +++ /dev/null @@ -1,44 +0,0 @@ -// material-ui -import { CardContent, Grid, Skeleton, Stack } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// ===========================|| SKELETON - PRODUCT CARD ||=========================== // - -const ProductPlaceholder = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default ProductPlaceholder; diff --git a/src/components/cards/statistics/AnalyticEcommerce.js b/src/components/cards/statistics/AnalyticEcommerce.js deleted file mode 100644 index 5610371..0000000 --- a/src/components/cards/statistics/AnalyticEcommerce.js +++ /dev/null @@ -1,66 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, Chip, Grid, Stack, Typography } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// assets -import { FallOutlined, RiseOutlined } from '@ant-design/icons'; - -// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== // - -const AnalyticEcommerce = ({ color = 'primary', title, count, percentage, isLoss, extra }) => ( - - - - {title} - - - - - {count} - - - {percentage && ( - - - {!isLoss && } - {isLoss && } - - } - label={`${percentage}%`} - sx={{ ml: 1.25, pl: 1 }} - size="small" - /> - - )} - - - - - You made an extra{' '} - - {extra} - {' '} - this year - - - -); - -AnalyticEcommerce.propTypes = { - title: PropTypes.string, - count: PropTypes.string, - percentage: PropTypes.number, - isLoss: PropTypes.bool, - color: PropTypes.string, - extra: PropTypes.string -}; - -export default AnalyticEcommerce; diff --git a/src/components/cards/statistics/AnalyticsDataCard.js b/src/components/cards/statistics/AnalyticsDataCard.js deleted file mode 100644 index 9d96fec..0000000 --- a/src/components/cards/statistics/AnalyticsDataCard.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, Chip, Stack, Typography } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// assets -import { RiseOutlined, FallOutlined } from '@ant-design/icons'; - -// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== // - -const AnalyticsDataCard = ({ color = 'primary', title, count, percentage, isLoss, children }) => ( - - - - - {title} - - - - {count} - - {percentage && ( - - {!isLoss && } - {isLoss && } - - } - label={`${percentage}%`} - sx={{ ml: 1.25, pl: 1 }} - size="small" - /> - )} - - - - {children} - -); - -AnalyticsDataCard.propTypes = { - title: PropTypes.string, - count: PropTypes.string, - percentage: PropTypes.number, - isLoss: PropTypes.bool, - color: PropTypes.string, - children: PropTypes.node -}; - -export default AnalyticsDataCard; diff --git a/src/components/cards/statistics/HoverSocialCard.js b/src/components/cards/statistics/HoverSocialCard.js index 192df37..44a5f2a 100644 --- a/src/components/cards/statistics/HoverSocialCard.js +++ b/src/components/cards/statistics/HoverSocialCard.js @@ -1,70 +1,40 @@ -// material-ui -import { Box, Card, CardContent, Grid, Typography } from '@mui/material'; +import PropTypes from 'prop-types'; + +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; // ===========================|| HOVER SOCIAL CARD ||=========================== // +// Solid-colour metric tile used on the order-preview summary row. `color` is a +// caller-supplied background hex, so it stays an inline style — Astryx's Card +// `variant` palette can't express an arbitrary colour. -const HoverSocialCard = ({ - primary, - secondary, - percentage, - // iconPrimary, - color, - sx -}) => { - // const IconPrimary = iconPrimary; - // const primaryIcon = iconPrimary ? : null; +const HoverSocialCard = ({ primary, secondary, percentage, color, style }) => ( + + + + + {secondary} + + + {primary} + + + + {typeof percentage === 'number' ? `${percentage} %` : percentage} + + + +); - return ( - - - - - {/* {percentage.toString()} % */} - {percentage && typeof percentage === 'number' ? `${percentage.toString()} %` : percentage} - - {/* {primaryIcon} */} - - - - - {secondary} - - - - - {primary} - - - - - - ); +HoverSocialCard.propTypes = { + primary: PropTypes.node, + secondary: PropTypes.node, + percentage: PropTypes.node, + color: PropTypes.string, + style: PropTypes.object }; export default HoverSocialCard; diff --git a/src/components/logo/LogoIcon.js b/src/components/logo/LogoIcon.js deleted file mode 100644 index cfc19e2..0000000 --- a/src/components/logo/LogoIcon.js +++ /dev/null @@ -1,56 +0,0 @@ -// material-ui -import { useTheme } from '@mui/material/styles'; - -/** - * if you want to use image instead of uncomment following. - * - * import logoIconDark from 'assets/images/logo-icon-dark.svg'; - * import logoIcon from 'assets/images/logo-icon.svg'; - * import { ThemeMode } from 'config'; - * - */ - -// ==============================|| LOGO ICON SVG ||============================== // - -const LogoIcon = () => { - const theme = useTheme(); - - return ( - /** - * if you want to use image instead of svg uncomment following, and comment out element. - * - * Mantis - * - */ - - - - - - - - - - - - - - - - - ); -}; - -export default LogoIcon; diff --git a/src/components/logo/LogoMain.js b/src/components/logo/LogoMain.js deleted file mode 100644 index 72adc22..0000000 --- a/src/components/logo/LogoMain.js +++ /dev/null @@ -1,68 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { ThemeMode } from 'config'; - -/** - * if you want to use image instead of uncomment following. - * - * import logoDark from 'assets/images/logo-dark.svg'; - * import logo from 'assets/images/logo.svg'; - * - */ - -// ==============================|| LOGO SVG ||============================== // - -const LogoMain = ({ reverse }) => { - const theme = useTheme(); - return ( - /** - * if you want to use image instead of svg uncomment following, and comment out element. - * - * Mantis - * - */ - <> - - - - - - - - - - - - - - - - - - - ); -}; - -LogoMain.propTypes = { - reverse: PropTypes.bool -}; - -export default LogoMain; diff --git a/src/components/logo/index.js b/src/components/logo/index.js deleted file mode 100644 index bb2e7af..0000000 --- a/src/components/logo/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -// material-ui -import { ButtonBase } from '@mui/material'; - -// project import -import LogoMain from './LogoMain'; -import LogoIcon from './LogoIcon'; -import { APP_DEFAULT_PATH } from 'config'; - -// ==============================|| MAIN LOGO ||============================== // - -const LogoSection = ({ reverse, isIcon, sx, to }) => ( - - {isIcon ? : } - -); - -LogoSection.propTypes = { - reverse: PropTypes.bool, - isIcon: PropTypes.bool, - sx: PropTypes.object, - to: PropTypes.string -}; - -export default LogoSection; diff --git a/src/components/nearle_components/AddressAutocomplete.js b/src/components/nearle_components/AddressAutocomplete.js index 3212de9..663e57c 100644 --- a/src/components/nearle_components/AddressAutocomplete.js +++ b/src/components/nearle_components/AddressAutocomplete.js @@ -1,7 +1,12 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; -import { Autocomplete, TextField, CircularProgress } from '@mui/material'; -import { debounce } from '@mui/material/utils'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Spinner } from '@astryxdesign/core/Spinner'; +import { Text } from '@astryxdesign/core/Text'; + +import { DT } from 'themes/dt/tokens'; // Free OSM Nominatim geocoder — replaces Google Places Autocomplete / // react-geocode across the console. No API key required. Nominatim's usage @@ -73,10 +78,15 @@ export async function geocodeAddress(address, { bias } = {}) { } } -// Address search-as-you-type — replaces Google Places Autocomplete -// (usePlacesWidget / window.google.maps.places.Autocomplete / -// AutocompleteService) across the console. Renders as an MUI Autocomplete -// with freeSolo so the operator can still submit free text. +// Address search-as-you-type — replaces Google Places Autocomplete across the +// console. +// +// Astryx note: this is a TextInput plus a hand-rolled suggestion list rather +// than Astryx's `Typeahead`. Typeahead is a *select-from-source* control — its +// value is a chosen item and arbitrary text can't be submitted. Every call +// site here needs free text (operators routinely type an address Nominatim +// can't match and submit it anyway), which is what the old MUI Autocomplete's +// `freeSolo` gave us. Keeping that behaviour means owning the dropdown. // // Props: // value / onChange(text) — controlled free-text input value @@ -91,103 +101,161 @@ const AddressAutocomplete = ({ onChange, onPlaceSelected, bias, - sx, - textFieldSx, fullWidth = true, disabled, - inputRef, - TextFieldProps = {} + status }) => { const [inputValue, setInputValue] = useState(value || ''); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const wrapRef = useRef(null); + const activeRef = useRef(0); useEffect(() => { if (value !== undefined && value !== inputValue) setInputValue(value || ''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); - const fetchPredictions = useMemo( - () => - debounce((query, bias1, callback) => { - if (!query) { - callback([]); - return; - } - const params = buildParams(query, bias1); - fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } }) - .then((res) => (res.ok ? res.json() : [])) - .then((results) => callback(results || [])) - .catch(() => callback([])); - }, 500), + const biasKey = `${bias?.lat ?? ''},${bias?.lng ?? ''}`; + + const fetchPredictions = useDebouncedCallback((query) => { + const seq = ++activeRef.current; + const params = buildParams(query, bias); + fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } }) + .then((res) => (res.ok ? res.json() : [])) + .then((results) => { + if (seq !== activeRef.current) return; // a newer keystroke won + setLoading(false); + setOptions(results || []); + setIsOpen((results || []).length > 0); + }) + .catch(() => { + if (seq !== activeRef.current) return; + setLoading(false); + setOptions([]); + }); + }, 500); + + useEffect(() => { + if (!inputValue) { + activeRef.current++; // invalidate any in-flight request + setOptions([]); + setLoading(false); + setIsOpen(false); + return; + } + setLoading(true); + fetchPredictions(inputValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputValue, biasKey]); + + // Close the list on an outside click. + useEffect(() => { + const onDocDown = (e) => { + if (wrapRef.current && !wrapRef.current.contains(e.target)) setIsOpen(false); + }; + document.addEventListener('mousedown', onDocDown); + return () => document.removeEventListener('mousedown', onDocDown); + }, []); + + const listStyle = useMemo( + () => ({ + position: 'absolute', + zIndex: 1300, + insetInlineStart: 0, + insetInlineEnd: 0, + marginTop: 4, + background: DT.surface, + border: `1px solid ${DT.borderSubtle}`, + borderRadius: DT.radiusInner + 'px', + boxShadow: DT.shadowPop, + overflow: 'hidden', + maxHeight: 260, + overflowY: 'auto' + }), [] ); - useEffect(() => { - let active = true; - if (!inputValue) { - setOptions([]); - setLoading(false); - return undefined; - } - setLoading(true); - fetchPredictions(inputValue, bias, (results) => { - if (!active) return; - setLoading(false); - setOptions(results); - }); - return () => { - active = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [inputValue, bias?.lat, bias?.lng, fetchPredictions]); + const pick = (option) => { + setInputValue(option.display_name || ''); + onChange?.(option.display_name || ''); + onPlaceSelected?.(toPlace(option)); + setIsOpen(false); + }; return ( - x} - getOptionLabel={(option) => (typeof option === 'string' ? option : option.display_name || '')} - isOptionEqualToValue={(option, val) => option.place_id === val?.place_id} - inputValue={inputValue} - onInputChange={(event, newInputValue) => { - setInputValue(newInputValue); - onChange?.(newInputValue); - }} - onChange={(event, selected) => { - if (selected && typeof selected !== 'string') { - setInputValue(selected.display_name || ''); - onPlaceSelected?.(toPlace(selected)); - } - }} - renderInput={(params) => ( - - {loading ? : null} - {TextFieldProps.InputProps?.endAdornment ?? params.InputProps.endAdornment} - - ) - }} - /> +
+ { + setInputValue(next); + onChange?.(next); + }} + onKeyDown={(e) => { + if (e.key === 'Escape') setIsOpen(false); + }} + /> + + {loading && ( + + + )} - /> + + {isOpen && options.length > 0 && ( +
    + {options.map((option) => ( +
  • + +
  • + ))} +
+ )} +
); }; +AddressAutocomplete.propTypes = { + id: PropTypes.string, + label: PropTypes.string, + placeholder: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + onPlaceSelected: PropTypes.func, + bias: PropTypes.object, + fullWidth: PropTypes.bool, + disabled: PropTypes.bool, + status: PropTypes.object +}; + export default AddressAutocomplete; diff --git a/src/components/nearle_components/CLAUDE.md b/src/components/nearle_components/CLAUDE.md index 9ceafb8..6575a6f 100644 --- a/src/components/nearle_components/CLAUDE.md +++ b/src/components/nearle_components/CLAUDE.md @@ -4,19 +4,28 @@ Rules for editing the shared Doormile Express UI primitives. These components are imported across every page. Changes here have **fan-out impact** — measure twice, cut once. +> **Astryx migration in progress.** This folder is being converted from MUI to +> Astryx (`@astryxdesign/core`) ahead of the pages that consume it, so a +> component here may already be Astryx while its call sites are still MUI — +> that mix is expected, not a bug. `src/pages/nearle/login.js` is the reference +> for Astryx component usage. §2–§4 below still describe the MUI-era `DT`/`sx` +> conventions and apply only to the components not yet marked ✅ Astryx. + --- ## 1. What lives here | Component | Use | Status | |---|---|---| -| `LocationAutocomplete.js` | Zone picker on every operator page | ✅ Canonical — has `pill` variant matching DT system | -| `DebounceSearchBar.js` | 500ms-debounced search with ⌘/Ctrl+K focus | ✅ Canonical | -| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | ✅ Canonical | +| `PageHeader.js` | Page title + live subtitle + action slot | ✅ **Astryx** — `Heading`/`Text`/`HStack`/`StatusDot` | +| `StatCard.js` | KPI / metric tile | ✅ **Astryx** — `Card`/`Text`/`Center`/`Skeleton` | +| `DebounceSearchBar.js` | 500ms-debounced search with ⌘/Ctrl+K focus | ✅ **Astryx** — `TextInput`; **no `sx` prop** (see §4) | +| `LocationAutocomplete.js` | Zone picker on every operator page | MUI — has `pill` variant matching DT system | +| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | MUI | | `GlobalToast.js` | Global toast wrapper | ✅ Used at root | | `SearchBar.js` | Non-debounced search input | Legacy — prefer `DebounceSearchBar` | | `TableLoader.js` | Inline table loading state | Legacy — prefer `OrdersTableSkeleton` | -| `TitleCard.js` | Old page header | ⛔ **Legacy** — do not use on new pages. The replacement is the gradient `` header pattern documented in root `CLAUDE.md` §6, implemented in `deliveries.js` (search for the comment `Header` near the first `` in the JSX `return`). | +| `TitleCard.js` | Old page header | ⛔ **Legacy** — do not use on new pages. The replacement is `PageHeader.js`. | --- @@ -62,7 +71,7 @@ The component supports two visual modes, controlled by the `pill` prop: --- -## 4. `DebounceSearchBar` — debounce + ⌘/Ctrl+K shortcut +## 4. `DebounceSearchBar` — debounce + ⌘/Ctrl+K shortcut (Astryx) ```jsx ``` +- **There is no `sx` prop.** It was removed with the Astryx conversion and stripped from all 12 call sites in the same pass. The field's appearance now comes from Astryx's `TextInput` theme, so search looks identical on every page instead of each page carrying its own pill styling. Don't reintroduce a styling prop — restyle via the theme (`src/themes/astryx.js`) so every page moves together. - **Don't lower the debounce below 500ms.** TanStack Query's cache keys include `debouncedSearch`, so each keystroke would cause a new server hit if the debounce drops. -- ⌘/Ctrl+K to focus and `Esc` to blur are wired globally inside this component. Don't add competing keyboard shortcuts at the page level using the same keys. -- The pill aesthetic (rounded `999`, tinted bg, accent border) is applied via the `sx` prop at the call site — see the ``, so the shortcut survived the conversion). Don't add competing keyboard shortcuts at the page level using the same keys. +- Width comes from the parent — the component sets `width="100%"`, the old `fullWidth`. --- @@ -91,15 +100,23 @@ Don't add a wrapper that just renames an MUI primitive (e.g. ``). --- -## 6. When to inline instead +## 6. Tokens are shared — do not inline them -The `DT` token block at the top of each page is repeated intentionally. Don't extract it into a shared module here. Reasons: +> **This section previously said the opposite** ("repeat the `DT` block per +> page, don't extract"). That was tried and it failed: 17 pages each grew their +> own copy and the copies drifted apart — `orders.js` was still on +> `radiusCard: 16` and the old heavy shadows while every other page had moved +> to `14` and the restrained set. That drift is what made pages stop looking +> alike. -- It's a 30-line block of constants — repetition is fine. -- Pages occasionally tweak a token for their own use (e.g. picking different KPI colours). A shared module would lock everyone into the same defaults. -- Importing from a shared file creates a cross-page coupling that's painful when one page wants to evolve its visual language. +`DT`, the alpha helpers (`a`/`tint`/`soft`/`ring`/`edge`), `pillFieldSx`, and +the table chrome (`tableScrollSx`/`tableHeadSx`/`tableRowSx`) live in +`src/themes/dt/tokens.js`. `SoftPaper` and `AccentAvatar` live in +`src/themes/dt/primitives.js`. **Import them. Never re-declare them in a page.** -Same logic for `SoftPaper`, `AccentAvatar`, `pillFieldSx` — these are 5–20 line helpers that live inside each page file. Don't extract. +If a page genuinely needs a different value, it should be a new named export in +the shared module, not a local override — that way the divergence is visible in +one place instead of hidden at the top of a 2000-line page. --- diff --git a/src/components/nearle_components/DateRangePicker.js b/src/components/nearle_components/DateRangePicker.js index ed68d54..10c468c 100644 --- a/src/components/nearle_components/DateRangePicker.js +++ b/src/components/nearle_components/DateRangePicker.js @@ -1,9 +1,13 @@ +import PropTypes from 'prop-types'; import { useState } from 'react'; -import { Box, Button, Divider, Stack, Typography } from '@mui/material'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { DT, soft, edge } from 'themes/dt/tokens'; +import dayjs from 'dayjs'; + +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { Divider } from '@astryxdesign/core/Divider'; +import { DateInput } from '@astryxdesign/core/DateInput'; // Drop-in replacement for the unmaintained `mui-daterange-picker` package // (pinned to React 17, never updated — see the React 19 upgrade plan). @@ -11,9 +15,13 @@ import { DT, soft, edge } from 'themes/dt/tokens'; // on every selection (preset or custom), `definedRanges` renders the quick // picks. `open`/`toggle` are accepted for API compatibility but unused — // visibility is already gated by the parent Dialog. +// +// Astryx note: DateInput speaks ISO `YYYY-MM-DD` strings, where the MUI +// DatePicker spoke dayjs objects. The conversion happens here so the +// `onChange` contract (native Date objects) is unchanged for callers. export const DateRangePicker = ({ onChange, definedRanges = [] }) => { - const [customStart, setCustomStart] = useState(null); - const [customEnd, setCustomEnd] = useState(null); + const [customStart, setCustomStart] = useState(undefined); + const [customEnd, setCustomEnd] = useState(undefined); const pickPreset = (range) => { onChange({ startDate: range.startDate, endDate: range.endDate, label: range.label }); @@ -21,70 +29,53 @@ export const DateRangePicker = ({ onChange, definedRanges = [] }) => { const pickCustom = (nextStart, nextEnd) => { if (nextStart && nextEnd) { - onChange({ startDate: nextStart.toDate(), endDate: nextEnd.toDate(), label: undefined }); + onChange({ startDate: dayjs(nextStart).toDate(), endDate: dayjs(nextEnd).toDate(), label: undefined }); } }; return ( - - + + {definedRanges.map((range) => ( - + - - - ); -}; - -export default CSVExport; +export const CSVExport = ({ data, filename, headers, label, style, btnLoading, onClick }) => ( + + - -
- - - - - -
- )} - - - - - - - - - - - - - Since 2017, More than 50K+ Developers trust the CodedThemes Digital Product. Mantis React is Manage under their - Experienced Team Players. - - - - - - - - - - - Help - - - - Blog - - - Documentation - - - Change Log - - - Support - - - - - - - - Store Help - - - - License - - - Refund Policy - - - Submit a Request - - - - - - - - Mantis Eco-System - - - {frameworks.map((item, index) => ( - - {item.title} - {/* {item.isUpcoming && } */} - - ))} - - - - - - - More Products - - - - Berry React Material - - - Free Berry React - - - Free Mantis React - - - - - - - - - - - - - - - - © Made with love by Team CodedThemes - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -FooterBlock.propTypes = { - isFull: PropTypes.bool -}; - -export default FooterBlock; diff --git a/src/layout/CommonLayout/Header.js b/src/layout/CommonLayout/Header.js deleted file mode 100644 index 2396bef..0000000 --- a/src/layout/CommonLayout/Header.js +++ /dev/null @@ -1,251 +0,0 @@ -import PropTypes from 'prop-types'; -import * as React from 'react'; -import { useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import AppBar from '@mui/material/AppBar'; -import { useTheme } from '@mui/material/styles'; -import { - useMediaQuery, - Box, - Button, - Chip, - Container, - Drawer, - Link, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Stack, - Toolbar, - Typography, - useScrollTrigger -} from '@mui/material'; - -// project import -import { APP_DEFAULT_PATH, ThemeMode } from 'config'; -import IconButton from 'components/@extended/IconButton'; -import AnimateButton from 'components/@extended/AnimateButton'; -import Logo from 'components/logo'; - -// assets -import { MenuOutlined, LineOutlined } from '@ant-design/icons'; - -// ==============================|| COMPONENTS - APP BAR ||============================== // - -// elevation scroll -function ElevationScroll({ layout, children, window }) { - const theme = useTheme(); - // const theme = useTheme(); - - const trigger = useScrollTrigger({ - disableHysteresis: true, - threshold: 10, - target: window ? window() : undefined - }); - - const backColorScroll = theme.palette.mode === ThemeMode.DARK ? theme.palette.grey[50] : theme.palette.grey[800]; - const backColor = layout !== 'landing' ? backColorScroll : 'transparent'; - - return React.cloneElement(children, { - style: { - backgroundColor: trigger ? backColorScroll : backColor - } - }); -} - -const Header = ({ handleDrawerOpen, layout = 'landing', ...others }) => { - const theme = useTheme(); - const matchDownMd = useMediaQuery(theme.breakpoints.down('md')); - const [drawerToggle, setDrawerToggle] = useState(false); - - /** Method called on multiple components with different event types */ - const drawerToggler = (open) => (event) => { - if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) { - return; - } - setDrawerToggle(open); - }; - - return ( - - - - - - - - - - - - - Dashboard - - - Components - - - Documentation - - - - - - - - - - - - - {layout === 'component' && ( - - )} - {layout !== 'component' && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -Header.propTypes = { - handleDrawerOpen: PropTypes.func, - layout: PropTypes.string -}; - -export default Header; diff --git a/src/layout/CommonLayout/index.js b/src/layout/CommonLayout/index.js index f304f8e..791de22 100644 --- a/src/layout/CommonLayout/index.js +++ b/src/layout/CommonLayout/index.js @@ -1,74 +1,16 @@ -import PropTypes from 'prop-types'; -import { lazy, Suspense } from 'react'; import { Outlet } from 'react-router-dom'; -// material-ui -import { Container, Toolbar } from '@mui/material'; - -// project import -import { dispatch, useSelector } from 'store'; -import { openComponentDrawer } from 'store/reducers/menu'; - -// material-ui -import { styled } from '@mui/material/styles'; -import LinearProgress from '@mui/material/LinearProgress'; - -const Header = lazy(() => import('./Header')); -const FooterBlock = lazy(() => import('./FooterBlock')); - -// ==============================|| Loader ||============================== // - -const LoaderWrapper = styled('div')(({ theme }) => ({ - position: 'fixed', - top: 0, - left: 0, - zIndex: 2001, - width: '100%', - '& > * + *': { - marginTop: theme.spacing(2) - } -})); - -const Loader = () => ( - - - -); - // ==============================|| MINIMAL LAYOUT ||============================== // +// Bare frame for the login and maintenance routes — the page itself supplies +// its own chrome (login.js mounts its own Astryx AppShell). +// +// This used to branch on a `layout` prop ('landing' | 'simple' | 'component' | +// 'blank') and lazily mount a Mantis marketing Header/FooterBlock. Both call +// sites (LoginRoutes, MainRoutes' /maintenance) always used the default +// 'blank', so those branches never rendered — they and their dependency tree +// (Header, FooterBlock, components/logo/*, @extended/IconButton) have been +// removed rather than migrated to Astryx. -const CommonLayout = ({ layout = 'blank' }) => { - const menu = useSelector((state) => state.menu); - const { componentDrawerOpen } = menu; - - const handleDrawerOpen = () => { - dispatch(openComponentDrawer({ componentDrawerOpen: !componentDrawerOpen })); - }; - - return ( - <> - {(layout === 'landing' || layout === 'simple') && ( - }> -
- - - - )} - {layout === 'component' && ( - }> - -
- - - - )} - {layout === 'blank' && } - - ); -}; - -CommonLayout.propTypes = { - layout: PropTypes.string -}; +const CommonLayout = () => ; export default CommonLayout; diff --git a/src/layout/MainLayout/AppSideNav.js b/src/layout/MainLayout/AppSideNav.js index 0931042..898f58a 100644 --- a/src/layout/MainLayout/AppSideNav.js +++ b/src/layout/MainLayout/AppSideNav.js @@ -60,13 +60,11 @@ const AppSideNav = ({ isCollapsed, onCollapsedChange }) => { paddingInline: '8px', boxSizing: 'border-box', '--spacing-12': '72px' - // Sticky positioning + height/scroll are already handled by - // AppShell's own auto-mode side nav wrapper (position: sticky, - // top/height driven by --appshell-header-height, see - // AppTopNav.js). A second nested position: sticky here — inside - // that wrapper's own scrollable LayoutPanel — was redundant and - // gave the offset two independent calculations to drift apart on, - // which is what caused the visible jump/scroll-away glitch. + // AppShell's height="fill" mode (see MainLayout/index.js) bounds + // the whole shell to the viewport height and scrolls only the + // main content panel internally — the side nav sits in a + // fixed-height flex column and never needs position: sticky or + // a JS-measured offset to stay in place. }} > {nearle.children.map(renderMenuItem)} diff --git a/src/layout/MainLayout/AppTopNav.js b/src/layout/MainLayout/AppTopNav.js index 914dd0c..6ed6a4c 100644 --- a/src/layout/MainLayout/AppTopNav.js +++ b/src/layout/MainLayout/AppTopNav.js @@ -88,16 +88,12 @@ const AppTopNav = () => { performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser }); }; - // AppShell (height="auto") pins the sticky side nav below the header using - // a --appshell-header-height CSS var it measures itself via ResizeObserver - // (AppShell.tsx), inside a useEffect that only runs (and paints) *after* - // the first frame. That one-frame gap is what made the side nav's sticky - // offset settle wrong and visibly jump/drift on scroll. Re-measure the - // header independently here in useLayoutEffect (synchronous, before the - // browser paints) and force the same variable with !important on the - // shell root — an author-stylesheet !important rule beats AppShell's own - // non-important inline style.setProperty() call on that same element, so - // this always wins regardless of what the internal measurement produced. + // MainLayout's AppShell no longer needs this var itself (height="fill" + // bounds the nav areas structurally, no sticky-offset math involved) — + // but Dispatch.css, Preview.js, and createorder1.js independently read + // --appshell-header-height to size their own "fill the viewport below + // the top nav" panels via calc(100vh - var(...)). Keep publishing it so + // those pages don't silently fall back to the hardcoded 64px default. useLayoutEffect(() => { const el = topNavRef.current; if (!el) return undefined; @@ -157,14 +153,7 @@ const AppTopNav = () => { {headerHeight != null && ( )} diff --git a/src/layout/MainLayout/Footer.js b/src/layout/MainLayout/Footer.js deleted file mode 100644 index bc2295c..0000000 --- a/src/layout/MainLayout/Footer.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import { Link, Stack, Typography } from '@mui/material'; - -const Footer = () => ( - - © All rights reserved - - - About us - - - Privacy - - - Terms - - - -); - -export default Footer; diff --git a/src/layout/MainLayout/index.js b/src/layout/MainLayout/index.js index 8d07cd6..5292275 100644 --- a/src/layout/MainLayout/index.js +++ b/src/layout/MainLayout/index.js @@ -25,14 +25,22 @@ const MainLayout = () => { return ( - {/* height="auto" (not doormile_crm's "fill"): "fill" scrolls the - content area in its own container, and CLAUDE.md's canonical - useInfiniteQuery pattern observes a sentinel against the window's - viewport (IntersectionObserver root=null), which only fires - correctly with normal document-level scroll. */} + {/* height="fill" (matches doormile_crm's validated shell): sticky nav + under height="auto" depends on AppShell measuring the header's + height in JS before it can position the side nav correctly, and + that measurement race caused the nav to visibly scroll with the + page for a frame after every reload (see the AppTopNav/AppSideNav + history before this comment for the two "auto" patches that tried + and failed to fully close that race). "fill" avoids the race + structurally — content scrolls inside AppShell's own container + instead of the window, so nav positioning never depends on a + runtime measurement. The one page whose infinite-scroll sentinel + watched the window (`orders.js`, `root: null`) now targets + `.astryx-layout-content` instead — every other infinite-scroll + page already scoped its observer to its own MUI table container. */} } sideNav={} mobileNav={{ breakpoint: 'lg' }} diff --git a/src/menu-items/other.js b/src/menu-items/other.js deleted file mode 100644 index 337cc53..0000000 --- a/src/menu-items/other.js +++ /dev/null @@ -1,178 +0,0 @@ -// third-party -import { FormattedMessage } from 'react-intl'; -// import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -// import SportsMotorsportsOutlinedIcon from '@mui/icons-material/SportsMotorsportsOutlined'; -import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined'; -import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined'; -import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; -import { TbListDetails } from 'react-icons/tb'; - -// assets -import { - BorderOutlined, - BoxPlotOutlined, - ChromeOutlined, - DeploymentUnitOutlined, - GatewayOutlined, - MenuUnfoldOutlined, - QuestionOutlined, - SmileOutlined, - StopOutlined, - DashboardOutlined, - ClockCircleOutlined, - UserOutlined, - SettingOutlined, - TeamOutlined, - MailOutlined, - ImportOutlined, - BarChartOutlined, - MoneyCollectOutlined -} from '@ant-design/icons'; - -// icons -const icons = { - ChromeOutlined, - MenuUnfoldOutlined, - BoxPlotOutlined, - StopOutlined, - BorderOutlined, - SmileOutlined, - GatewayOutlined, - QuestionOutlined, - DeploymentUnitOutlined, - DashboardOutlined, - ClockCircleOutlined, - UserOutlined, - SettingOutlined, - TeamOutlined, - MailOutlined, - ImportOutlined, - BarChartOutlined, - ReceiptOutlinedIcon -}; - -// ==============================|| MENU ITEMS - SUPPORT ||============================== // - -const other = { - id: 'other', - title: , - type: 'group', - children: [ - { - id: 'orders', - title: , - type: 'item', - url: '/orders', - icon: icons.DashboardOutlined - }, - - { - // id: 'deliveries', - id: 'dashboard', - title: , - type: 'item', - url: '/deliveries', - icon: MopedOutlinedIcon - }, - { - id: 'tenants', - title: , - type: 'item', - url: '/tenants', - icon: icons.UserOutlined - }, - { - id: 'pricing', - title: , - type: 'item', - url: '/clients_pricing', - icon: MoneyCollectOutlined - }, - { - id: 'customers', - title: , - type: 'item', - url: '/customers', - icon: icons.TeamOutlined - }, - { - id: 'riders', - title: , - type: 'item', - url: '/riders', - icon: DirectionsBikeOutlinedIcon - }, - { - id: 'reports', - title: , - type: 'collapse', - icon: icons.BarChartOutlined, - children: [ - { - id: 'orderssummary', - title: , - type: 'item', - url: '/reports/orderssummary', - icon: TbListDetails - }, - { - id: 'ordersdetails', - title: , - type: 'item', - url: '/reports/ordersdetails', - icon: icons.DashboardOutlined - // target: true - }, - { - id: 'riderssummary', - title: , - type: 'item', - url: '/reports/riderssummary', - icon: DirectionsBikeOutlinedIcon - // target: true - }, - { - id: 'riderslogs', - title: , - type: 'item', - url: '/reports/riderslogs', - icon: DirectionsBikeOutlinedIcon - // target: true - } - ] - }, - { - id: 'invoice', - title: , - type: 'item', - url: '/invoice', - icon: icons.ReceiptOutlinedIcon - } - - // { - // id: 'documentation', - // title: , - // type: 'item', - // url: 'https://links.codedthemes.com/BQFrl', - // icon: icons.QuestionOutlined, - // external: true, - // target: true, - // chip: { - // label: 'gitbook', - // color: 'secondary', - // size: 'small' - // } - // }, - // { - // id: 'roadmap', - // title: , - // type: 'item', - // url: 'https://links.codedthemes.com/RXnKQ', - // icon: icons.DeploymentUnitOutlined, - // external: true, - // target: true - // } - ] -}; - -export default other; diff --git a/src/pages/auth/check-mail.js b/src/pages/auth/check-mail.js deleted file mode 100644 index cce339d..0000000 --- a/src/pages/auth/check-mail.js +++ /dev/null @@ -1,57 +0,0 @@ -import { Link } from 'react-router-dom'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Box, Button, Grid, Divider, Typography, useMediaQuery } from '@mui/material'; - -// project import -import useAuth from 'hooks/useAuth'; -import AnimateButton from 'components/@extended/AnimateButton'; -import AuthWrapper from 'sections/auth/AuthWrapper'; - -// ================================|| CHECK MAIL ||================================ // - -const CheckMail = () => { - const theme = useTheme(); - const matchDownSM = useMediaQuery(theme.breakpoints.down('sm')); - - const { isLoggedIn } = useAuth(); - - return ( - - - - - Hi, Check Your Mail - - We have sent a password recover instructions to your email. - - - - - - - - - - - Sign up with - - - - - ); -}; - -export default CheckMail; diff --git a/src/pages/auth/forgot-password.js b/src/pages/auth/forgot-password.js deleted file mode 100644 index 79a8543..0000000 --- a/src/pages/auth/forgot-password.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Link } from 'react-router-dom'; - -// material-ui -import { Grid, Stack, Typography } from '@mui/material'; - -// project import -import useAuth from 'hooks/useAuth'; -import AuthWrapper from 'sections/auth/AuthWrapper'; -import AuthForgotPassword from 'sections/auth/auth-forms/AuthForgotPassword'; - -// ================================|| FORGOT PASSWORD ||================================ // - -const ForgotPassword = () => { - const { isLoggedIn } = useAuth(); - - return ( - - - - - Forgot Password - - Back to Login - - - - - - - - - ); -}; - -export default ForgotPassword; diff --git a/src/pages/auth/login.js b/src/pages/auth/login.js deleted file mode 100644 index 0742fa4..0000000 --- a/src/pages/auth/login.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Link } from 'react-router-dom'; - -// material-ui -import { Grid, Stack, Typography } from '@mui/material'; - -// project import -import useAuth from 'hooks/useAuth'; -import AuthWrapper from 'sections/auth/AuthWrapper'; -import AuthLogin from 'sections/auth/auth-forms/AuthLogin'; - -// ================================|| LOGIN ||================================ // - -const Login = () => { - const { isLoggedIn } = useAuth(); - - return ( - - - - - Login - - Don't have an account? - - - - - - - - - ); -}; - -export default Login; diff --git a/src/pages/auth/register.js b/src/pages/auth/register.js deleted file mode 100644 index c9d0cad..0000000 --- a/src/pages/auth/register.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Link } from 'react-router-dom'; - -// material-ui -import { Grid, Stack, Typography } from '@mui/material'; - -// project import -import useAuth from 'hooks/useAuth'; -import AuthWrapper from 'sections/auth/AuthWrapper'; -import FirebaseRegister from 'sections/auth/auth-forms/AuthRegister'; - -// ================================|| REGISTER ||================================ // - -const Register = () => { - const { isLoggedIn } = useAuth(); - - return ( - - - - - Sign up - - Already have an account? - - - - - - - - - ); -}; - -export default Register; diff --git a/src/pages/auth/reset-password.js b/src/pages/auth/reset-password.js deleted file mode 100644 index aa4c286..0000000 --- a/src/pages/auth/reset-password.js +++ /dev/null @@ -1,26 +0,0 @@ -// material-ui -import { Grid, Stack, Typography } from '@mui/material'; - -// project import -import AuthWrapper from 'sections/auth/AuthWrapper'; -import AuthResetPassword from 'sections/auth/auth-forms/AuthResetPassword'; - -// ================================|| RESET PASSWORD ||================================ // - -const ResetPassword = () => ( - - - - - Reset Password - Please choose your new password - - - - - - - -); - -export default ResetPassword; diff --git a/src/pages/extra-pages/sample-page.js b/src/pages/extra-pages/sample-page.js deleted file mode 100644 index 5ed2c60..0000000 --- a/src/pages/extra-pages/sample-page.js +++ /dev/null @@ -1,20 +0,0 @@ -// material-ui -import { Typography } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// ==============================|| SAMPLE PAGE ||============================== // - -const SamplePage = () => ( - - - Lorem ipsum dolor sit amen, consenter nipissing eli, sed do elusion tempos incident ut laborers et doolie magna alissa. Ut enif ad - minim venice, quin nostrum exercitation illampu laborings nisi ut liquid ex ea commons construal. Duos aube grue dolor in reprehended - in voltage veil esse colum doolie eu fujian bulla parian. Exceptive sin ocean cuspidate non president, sunk in culpa qui officiate - descent molls anim id est labours. - - -); - -export default SamplePage; diff --git a/src/pages/maintenance/404.js b/src/pages/maintenance/404.js index 3e9b0e8..e025bba 100644 --- a/src/pages/maintenance/404.js +++ b/src/pages/maintenance/404.js @@ -1,55 +1,33 @@ -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; + +import { Center } from '@astryxdesign/core/Center'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; // project import import { APP_DEFAULT_PATH } from 'config'; -// material-ui -import { Box, Button, Grid, Stack, Typography } from '@mui/material'; - // assets import error404 from 'assets/images/maintenance/Error404.png'; -import TwoCone from 'assets/images/maintenance/TwoCone.png'; // ==============================|| ERROR 404 - MAIN ||============================== // function Error404() { + const navigate = useNavigate(); + return ( - <> - - - - - - mantis - - - - - mantis - - - - - - - Page Not Found - - The page you are looking was moved, removed, renamed, or might never exist! - - - - - - +
+ + + Page Not Found + + The page you are looking for was moved, removed, renamed, or might never have existed. + +
); } diff --git a/src/pages/maintenance/500.js b/src/pages/maintenance/500.js index da019bb..64324ea 100644 --- a/src/pages/maintenance/500.js +++ b/src/pages/maintenance/500.js @@ -1,44 +1,33 @@ -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; + +import { Center } from '@astryxdesign/core/Center'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; // project import import { APP_DEFAULT_PATH } from 'config'; -// material-ui -import { useTheme } from '@mui/material/styles'; -import { useMediaQuery, Box, Button, Grid, Stack, Typography } from '@mui/material'; - // assets import error500 from 'assets/images/maintenance/Error500.png'; // ==============================|| ERROR 500 - MAIN ||============================== // function Error500() { - const theme = useTheme(); - const matchDownSM = useMediaQuery(theme.breakpoints.down('sm')); + const navigate = useNavigate(); return ( - <> - - - - mantis - - - - - - Internal Server Error - - - Server error 500. we fixing the problem. please try again at a later stage. - - - - - - +
+ + + Internal Server Error + + Server error 500. We are fixing the problem — please try again shortly. + +
); } diff --git a/src/pages/maintenance/coming-soon.js b/src/pages/maintenance/coming-soon.js index a8c5bdb..addc789 100644 --- a/src/pages/maintenance/coming-soon.js +++ b/src/pages/maintenance/coming-soon.js @@ -1,37 +1,38 @@ import PropTypes from 'prop-types'; -// material-ui -import { useTheme } from '@mui/material/styles'; -import { useMediaQuery, Box, Button, Grid, Stack, TextField, Typography } from '@mui/material'; +import { Card } from '@astryxdesign/core/Card'; +import { Center } from '@astryxdesign/core/Center'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; // third party const { useTimer } = require('react-timer-hook'); // assets import coming from 'assets/images/maintenance/coming-soon.png'; -import MainCard from 'components/MainCard'; +import { DT } from 'themes/dt/tokens'; // ==============================|| COMING SOON - MAIN ||============================== // +// The old page carried a Mantis-branded "be the first to be notified" email +// capture whose input and button were wired to nothing. It was template cruft, +// not a feature, so it is gone rather than ported to Astryx. -const TimerBox = ({ count, label }) => { - const theme = useTheme(); - const matchDownSM = useMediaQuery(theme.breakpoints.down('sm')); - - return ( - - - - {count} - - - - {label} - - - - - ); -}; +const TimerBox = ({ count, label }) => ( + + +
+ {String(count).padStart(2, '0')} +
+
+ + {label} + +
+
+
+); TimerBox.propTypes = { count: PropTypes.number, @@ -45,49 +46,23 @@ function ComingSoon() { const { seconds, minutes, hours, days } = useTimer({ expiryTimestamp: time }); return ( - <> - - - - mantis - - - - - - Coming Soon - - - Something new is on its way - - - - - - - : - - : - - : - - - - - - - Be the first to be notified when Mantis launches. - - - - - - - - - +
+ + + + + Coming Soon + Something new is on its way + + + + + + + + + +
); } diff --git a/src/pages/maintenance/under-construction.js b/src/pages/maintenance/under-construction.js index 9829e84..a96bdaa 100644 --- a/src/pages/maintenance/under-construction.js +++ b/src/pages/maintenance/under-construction.js @@ -1,38 +1,33 @@ -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; + +import { Center } from '@astryxdesign/core/Center'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; // project import import { APP_DEFAULT_PATH } from 'config'; -// material-ui -import { Box, Button, Grid, Stack, Typography } from '@mui/material'; - // assets import construction from 'assets/images/maintenance/under-construction.svg'; // ==============================|| UNDER CONSTRUCTION - MAIN ||============================== // function UnderConstruction() { + const navigate = useNavigate(); + return ( - - - - mantis - - - - - - Under Construction - - - Hey! Please check out this site later. We are doing some maintenance on it right now. - - - - - +
+ + + Under Construction + + Please check back later — we are doing some maintenance on this page right now. + +
); } diff --git a/src/pages/nearle/accountsettings.js b/src/pages/nearle/accountsettings.js deleted file mode 100644 index 5ba6cdb..0000000 --- a/src/pages/nearle/accountsettings.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Grid, useMediaQuery, useTheme } from '@mui/material'; -import { useQuery } from '@tanstack/react-query'; -import CircularLoader from 'components/CircularLoader'; -import Loader from 'components/Loader'; -import MainCard from 'components/MainCard'; -import { getusers } from 'pages/api/api'; - -const ViewProfile = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const { - data: userdata, - isLoading, - isError, - error - } = useQuery({ - queryKey: ['getuser'], - queryFn: getusers - }); - return ( - <> - {isLoading && ( - <> - - - - )} - - - - {userdata?.firstname} - - - {' '} - - - {' '} - - - - - ); -}; - -export default ViewProfile; diff --git a/src/pages/nearle/appUsers/appUsers.js b/src/pages/nearle/appUsers/appUsers.js index 413e574..1da8994 100644 --- a/src/pages/nearle/appUsers/appUsers.js +++ b/src/pages/nearle/appUsers/appUsers.js @@ -36,6 +36,7 @@ import StatCard from 'components/nearle_components/StatCard'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { getAppUsers, createAppUser, updateAppUser, deleteAppUser } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens'; // ============================================================================ // GET /admin/users confirmed live: { id, email, first_name, phone, role, @@ -47,22 +48,7 @@ import { enqueueSnackbar } from 'notistack'; // to match the GET shape. // ============================================================================ -const DT = { - radiusCard: 14, - radiusField: 10, - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - borderHover: '#cbd5e1', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const BRAND = '#C01227'; +const BRAND = '#000000'; const ROLES = ['admin', 'manager', 'executive', 'rep']; const roleLabel = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '—'); @@ -191,26 +177,16 @@ const AppUsers = () => { onChange={setSearchword} onDebouncedChange={setDebouncedSearch} placeholder="Search staff (ctrl+k)" - sx={{ - m: 0, - width: '100%', - borderRadius: DT.radiusField + 'px', - bgcolor: DT.surface, - '& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 }, - '&:hover fieldset': { borderColor: DT.borderHover }, - '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 1.5 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } - }} /> - + - + # Name Email @@ -237,7 +213,7 @@ const AppUsers = () => { ) : ( rows.map((row, index) => ( - + {index + 1} diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js index d08fc69..4bd6185 100644 --- a/src/pages/nearle/clientPricing/clientPricing.js +++ b/src/pages/nearle/clientPricing/clientPricing.js @@ -29,7 +29,6 @@ import { } from '@mui/material'; import { MdLocalOffer, - MdMyLocation, MdGroups, MdPlace, MdSpeed, @@ -56,66 +55,12 @@ import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { getallpricing, getTenants, fetchAppLocations } from 'pages/api/api'; import { createAdminPricing, updateAdminPricing, deleteAdminPricing, simulatePricing } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, tint, soft, edge } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; -// ============================================================================ -// Design tokens — shared with the deliveries / tenants / customers pages so every -// surface (header, KPI tiles, table, badges) speaks the same visual language. -// Keep this block in sync with customers.js / deliveries.js. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - radiusField: 10, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - borderHover: '#cbd5e1', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc', - brand: '#662582' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; +const BRAND = '#000000'; const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van']; -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - const formatRupees = (value) => new Intl.NumberFormat('en-IN', { style: 'currency', @@ -378,12 +323,7 @@ const ClientsPricing = () => { locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} - pill - accentColor={BRAND} - icon={} placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 220 }, zIndex: 100 }} />
{tab === 'branches' ? ( diff --git a/src/pages/nearle/customers/customers.js b/src/pages/nearle/customers/customers.js index 41a01bf..f24893b 100644 --- a/src/pages/nearle/customers/customers.js +++ b/src/pages/nearle/customers/customers.js @@ -23,7 +23,6 @@ import { useMediaQuery, useTheme } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; import { MdPeopleAlt, MdOutlinePeopleAlt, @@ -48,6 +47,8 @@ import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'compon import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { getAdminCustomers, updateAdminCustomer } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, soft, edge } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; // ============================================================================ // GET /admin/customers confirmed live: { appcustomerid, createdat, email, @@ -64,31 +65,8 @@ import { enqueueSnackbar } from 'notistack'; // not persisted" pattern as `pages/nearle/clients/createCustomer.js`). // ============================================================================ -const DT = { - radiusCard: 14, - radiusField: 10, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - borderHover: '#cbd5e1', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; -const tint = (c) => a(c, '08'); - -const AccentAvatar = ({ color, size = 24, children }) => ( - {children} -); +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; const custId = (row) => row.appcustomerid; const custName = (row) => row.name || '—'; @@ -265,16 +243,6 @@ const Customers = () => { onChange={setSearchword} onDebouncedChange={setDebouncedSearch} placeholder="Search customers (ctrl+k)" - sx={{ - m: 0, - width: '100%', - borderRadius: 999, - bgcolor: tint(BRAND), - '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 }, - '&:hover fieldset': { borderColor: BRAND }, - '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } - }} /> @@ -528,23 +496,6 @@ const Customers = () => { value={addressInput} onChange={(text) => setAddressInput(text)} onPlaceSelected={handleAddressPlaceSelected} - TextFieldProps={{ - variant: 'outlined', - InputProps: { - endAdornment: ( - { - setAddressInput(''); - setPickAddress((p) => ({ ...p, suburb: '', city: '', state: '', postcode: '' })); - setLatLng({ latitude: '', longitude: '' }); - }} - size="small" - > - - - ) - } - }} /> diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index cd32846..40e2536 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -1,3088 +1,3068 @@ -import logger from '../../../utils/logger'; -import { enqueueSnackbar } from 'notistack'; -import { DeleteFilled, EditOutlined } from '@ant-design/icons'; -import { useState, useEffect, Fragment, useRef, useMemo } from 'react'; -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); -import { notifyMiler, getConsignmentLogs } from 'pages/api/doormileApi'; -import { kalmanSmoothGps, polylineLengthKm } from '../dispatch/dispatchShared'; -import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; -import { useTheme } from '@mui/material/styles'; -import { - MdOutlineDateRange, - MdAccessTime, - MdAllInclusive, - MdLightMode, - MdWbSunny, - MdNightsStay, - MdCheck, - MdStorefront, - MdLocationOn, - MdDirectionsBike, - MdLocalShipping, - MdNotificationsActive, - MdPersonPin, - MdHistoryToggleOff, - MdCheckCircle, - MdCancel, - MdInventory2, - MdHourglassEmpty, - MdRoute, - MdSkipNext, - MdTune, - MdMyLocation, - MdOutlineLocalShipping, - MdOutlinePendingActions, - MdOutlineCheckCircle, - MdOutlineCancel -} from 'react-icons/md'; -import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; - -import { - Avatar, - Box, - Button, - Chip, - Grid, - IconButton, - Stack, - Typography, - Table, - TableCell, - TableBody, - TableHead, - Collapse, - Dialog, - TableRow, - DialogContent, - DialogTitle, - Tooltip, - DialogActions, - Checkbox, - Autocomplete, - TextField, - TableContainer, - Backdrop, - MenuItem, - Menu, - Paper, - useMediaQuery -} from '@mui/material'; - -import { PopupTransition } from 'components/@extended/Transitions'; -import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns'; -import { DateRangePicker } from 'components/nearle_components/DateRangePicker'; -import * as React from 'react'; -import Loader from 'components/Loader'; -import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material'; - -import { - cancelDeliveryAPI, - changeRiderAPI, - fetchCountAPI, - fetchDeliveries, - fetchRidersList, - notifyRider, - updateDeliveryAPI, - getorderdetails, - gettenantlocations, - getTenants -} from 'pages/api/api'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import { OpenToast } from 'components/third-party/OpenToast'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; - -// ============================================================================ -// Design tokens — shared across every DT-styled operator page so filters, -// KPIs, tabs, status badges, and dialogs stay visually consistent. -// See src/themes/dt/tokens.js for the canonical source. -// ============================================================================ -import { DT, a, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens'; - -// Status palette — drives tab pills, row status badges, dialogs. -const STATUS_META = { - pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, - arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, - picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, - active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, - skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, - delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, - cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } -}; - -// Ordered status list driving the tabs row (left → right). Each entry binds -// the visual meta above to the `currentStatus` key the queries use AND to the -// `batchCounts` key (so the chip count for the tab is one lookup). -const STATUS_TABS = [ - { status: 'pending', countKey: 'uncoveredLength' }, - { status: 'accepted', countKey: 'assignedLength' }, - { status: 'arrived', countKey: 'arrivedLength' }, - { status: 'picked', countKey: 'pickedLength' }, - { status: 'active', countKey: 'activeLength' }, - { status: 'skipped', countKey: 'skippedLength' }, - { status: 'delivered', countKey: 'coveredLength' }, - { status: 'cancelled', countKey: 'cancelLength' } -]; - -// KPI palette + icons — mirrors the four cards across the top of the page. -const KPI_META = [ - { key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping }, - { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions }, - { key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle }, - { key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel } -]; - -// Batches mirror the dispatch page's slot definitions so an operator who -// segments the day there sees the same buckets here. Hours are 24h, half-open -// [startHour, endHour) — a delivery at exactly endHour falls into the *next* -// batch (or none, if the gap isn't covered). -const BATCH_OPTIONS = [ - { id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' }, - { id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 }, - { - id: 'afternoon', - label: 'Afternoon Batch', - range: '9 AM to 12:30 PM', - color: '#f59e0b', - iconKey: 'afternoon', - startHour: 9, - endHour: 12.5 - }, - { id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 } -]; - -// Per-batch icon components — kept separate from BATCH_OPTIONS so the option -// objects stay serializable / printable. Looked up at render time by iconKey. -const BATCH_ICONS = { - all: MdAllInclusive, - morning: MdLightMode, - afternoon: MdWbSunny, - evening: MdNightsStay -}; - -// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the -// page lands them on the slot they're most likely curious about. Falls back -// to 'all' when the current hour lies in a configured gap (8–9 AM, 12 PM–4 -// PM, after 7 PM). Local time, not UTC, to match the dispatch page's -// bucketing — both pages must agree on which batch a given row belongs to. -const detectInitialBatchId = () => { - const now = dayjs(); - const h = now.hour() + now.minute() / 60; - for (const b of BATCH_OPTIONS) { - if (b.id === 'all') continue; - if (h >= b.startHour && h < b.endHour) return b.id; - } - return 'all'; -}; - -// Bucket by `assigntime` only — matches the spec ("bucketed by assigntime") -// and the dispatch page's default time-field selection (`Dispatch.js:763` -// initialises `selectedTimeField` to `'assigned'`, whose `keys` is just -// `['assigntime']`). The previous priority cascade started with -// `deliverytime`, which mis-bucketed delivered orders into the wave they -// were *completed* in rather than the wave they were *assigned to* — so an -// order assigned at 07:00 (Morning) but delivered at 11:00 fell into the -// 8–9 AM / 12 PM–4 PM gap and silently disappeared from every batch on this -// page while still showing up on dispatch's Morning Batch. Rows without an -// `assigntime` (unassigned pending orders) return null and intentionally -// don't bucket — matches dispatch. -const BATCH_TIME_KEYS = ['assigntime']; - -// Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null -// when the row has no usable timestamp / falls in a gap between batches. -const getRowBatchId = (row) => { - let t = null; - for (const k of BATCH_TIME_KEYS) { - if (row?.[k]) { - t = row[k]; - break; - } - } - if (!t) return null; - const str = String(t).trim(); - // Bare YYYY-MM-DD with no time component would always parse to midnight and - // get mis-bucketed into Morning — skip those. - if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null; - // Parse in LOCAL time to match the dispatch page's bucketing exactly - // (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment, - // so a row that lands in Evening Batch on the dispatch page must also land - // in Evening Batch here — otherwise an operator running in IST/PST/etc. - // sees different totals on the two pages for the same underlying rows. - // parseDoormileTimestamp (not bare dayjs()) strips a false trailing Z some - // Doormile timestamps carry (see utils/doormileTimestamp.js) — without - // this, a booking assigned "just now" could get shifted +5:30 (IST) past - // every batch window into a gap, silently vanishing from the table even - // though it was correctly assigned server-side. - const d = parseDoormileTimestamp(t); - if (!d.isValid()) return null; - const h = d.hour() + d.minute() / 60; - for (const b of BATCH_OPTIONS) { - if (b.id === 'all') continue; - if (h >= b.startHour && h < b.endHour) return b.id; - } - return null; -}; - -// Shared Paper used by every Autocomplete popup on the page. Matches the -// batch dropdown's soft elevated look. -const SoftPaper = (props) => ( - -); - -// Renders a colored avatar with an icon — used in pill input adornments and -// in dropdown rows. Tracks `selected` so it can flip between filled and -// soft variants identically to the batch dropdown. -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// ================================================= || deliveries (initial point)|| ================================================= -const Deliveries = () => { - const userid = localStorage.getItem('userid'); - const theme = useTheme(); - // Below `md` we swap the wide data table for an app-style card list. Desktop - // (md and up) keeps the exact same table — no behaviour change either way. - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const loadMoreRef = useRef(); - const containerRef = useRef(); - const [deliverylist, setDeliverylist] = useState([]); - const [dialogopen, setDialogopen] = useState(false); - const [locaName, setLocoName] = useState('All'); - const [appId, setAppId] = useState(0); - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [tabstatus, setTabstatus] = useState('Pending'); - const [tabvalue, setTabvalue] = useState(0); - const [open, setOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [kms, setKms] = useState(''); - const [cumulativekms, setCumulativeKms] = useState(); - const [deliveryamount, setDeliveryamount] = useState(); - const [notes, setNotes] = useState(''); - const [currentorder, setCurrentorder] = useState({}); - - // Real Actual KMs for the Update Status dialog, computed from real GPS - // ping history instead of leaving the field permanently blank (there is - // no backend-stored actual-distance field — see fetchDeliveries in - // api.js). Same sort -> Kalman-smooth -> polyline-length pipeline - // Dispatch.js's Compare feature already uses to show a real "actual km" - // figure, just reused here (see dispatchShared.js). Keyed off - // consignmentid, NOT deliveryid/bookingid — GET /admin/consignments/:id/logs - // is consignment-scoped (the same id-class mix-up already fixed in - // updateDeliveryAPI this session). - const { data: computedActualKm } = useQuery({ - queryKey: ['deliveryActualKm', currentorder?.consignmentid], - queryFn: async () => { - let rows = []; - try { - rows = (await getConsignmentLogs(currentorder.consignmentid)) || []; - } catch { - return null; - } - const sorted = rows - .map((r) => { - const ts = r?.logdate ? dayjs(r.logdate) : null; - return { - lat: parseFloat(r?.latitude ?? r?.lat), - lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), - logdate: r?.logdate, - _ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER - }; - }) - .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) - .sort((a, b) => a._ts - b._ts); - // Fewer than 2 usable pings (order not yet delivered, or no telemetry - // recorded) — a real, expected case, not an error. Leave the field - // to its existing blank/manual-entry behavior. - if (sorted.length < 2) return null; - const smoothed = kalmanSmoothGps(sorted); - const km = polylineLengthKm(smoothed.map((p) => [p.lat, p.lng])); - return km > 0 ? km : null; - }, - enabled: dialogopen && !!currentorder?.consignmentid, - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false - }); - - // Only pre-fill while the field is still empty — never overwrite an - // operator's own edit (or an earlier pre-fill) if this query happens to - // re-run while the dialog is already open. - useEffect(() => { - if (computedActualKm != null && !cumulativekms) { - setCumulativeKms(Number(computedActualKm.toFixed(2))); - } - }, [computedActualKm]); - const [deliverylat, setDeliverylat] = useState(''); - const [deliverylong, setDeliverylong] = useState(''); - const [currentStatus, setCurrentStatus] = useState('pending'); - const [updateStatus, setUpdateStatus] = useState('delivered'); - const locationRef = useRef(null); - const tenantRef = useRef(null); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(50); - const [totalCount, setTotalCount] = React.useState(); - const [productCollapse, setProductCollapse] = useState(null); - const [orderHeaderid, setOrderHeaderId] = useState(null); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); - const [selectedRow, setSelectedRow] = useState(null); - const [loading1, setLoading1] = useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - const [open2, setOpen2] = useState(''); - const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false); - const [changeDialogOpen, setChangeDialogOpen] = useState(false); - const [cancelFeed, setCancelFeed] = useState(''); - const [selectedRider, setSelectedRider] = useState(null); - const [tenantid, setTenantid] = useState(0); - const [locationid, setLocationid] = useState(0); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [riderid, setRiderid] = useState(0); - // Selected batch id — drives the client-side row filter. Defaults to the - // batch matching the current UTC hour (the operator is most likely curious - // about "now"); never `null` since there's no longer an "All" option. - const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId); - - useEffect(() => { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - }, [appId]); - // to clear the location autocomplete - useEffect(() => { - setLocationid(0); - setLocationValue(null); - }, [tenantid]); - - const menuOpen = Boolean(menuAnchorEl); - - const handleMenuOpen = (event, row) => { - setSelectedRow(row); - - setMenuAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setMenuAnchorEl(null); - }; - - // =========================================== || cancelDelivery || =========================================== - - const { mutate: cancelDelivery } = useMutation({ - mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed), - onSuccess: () => { - opentoast('Delivery Cancelled Successfully', 'success'); - setCancelDeliveryOpen(false); - fetchCountRefetch(); // Refresh count data - countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips - }, - onError: (error) => { - opentoast(error.message, 'error'); - } - }); - - // ==============================|| cancelridernotification ||============================== // - - const cancelridernotification = async () => { - try { - // /admin/milers/:id/notify keys off milerprofileid, not the userid - // stored on the booking (confirmed live) — see fetchDeliveries in api.js. - return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`); - } catch (err) { - opentoast(err.message, 'error', 2000); - } - }; - - // ==============================|| getTenants ||============================== // - - const { - data: tenantlist, - isLoading: fetchtenantsIsLoading, - isError: fetchtenantsIsError, - error: fetchtenantsError - } = useQuery({ - queryKey: ['tenantlist', appId], - queryFn: () => getTenants(appId), // Ensure appId is passed - // appId 0 is a real, deliberate choice — the synthetic "All" zone option - // (fetchAppLocations in api.js) — not "nothing picked yet"; only - // null/undefined means that now. See LocationAutocomplete.js's onChange. - enabled: appId != null - }); - // ==============================|| gettenantlocations ||============================== // - - const { - data: locationlist, - isLoading: fetchlocationsIsLoading, - isError: fetchlocationsIsError, - error: fetchlocationsError - } = useQuery({ - queryKey: ['gettenantlocations', tenantid], - queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed - enabled: tenantid !== 0 // Ensures query runs only when appId is valid - }); - // =========================================== || notifyrider || =========================================== - - const notifyRiderMutation = useMutation({ - mutationFn: notifyRider, // Using the separate function - onSuccess: () => { - enqueueSnackbar('Notification sent Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }, - onError: (error) => { - enqueueSnackbar(error.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } - }); - - // =========================================== || changerider || =========================================== - const changeRiderMutation = useMutation({ - mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow), - onSuccess: (res, { selectedRider, selectedRow }) => { - setLoading1(false); - setChangeDialogOpen(false); - if (res.success) { - logger.info( - `Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.displayname || selectedRider?.authname}` - ); - opentoast('Rider Changed Successfully', 'success'); - } - fetchCountRefetch(); // Refresh count data - countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips - // notifyMiler wants a miler profile id, not an FCM token — the new API - // looks the device token up server-side. selectedRider comes straight - // from getMilers(), which has milerprofileid directly (confirmed live; - // .userid there is the underlying app-user id, a different resource). - notifyRiderMutation.mutate(selectedRider.milerprofileid); - }, - onError: (err, { selectedRider, selectedRow }) => { - logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err); - opentoast(err.message, 'error'); - setLoading1(false); - } - }); - - /* ============================================= || opentoast || ============================================= */ - - const opentoast = (message, variant) => { - enqueueSnackbar(message, { - variant, - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }; - - // ==============================|| getorderdetails ||============================== // - - const { data: orderdetails } = useQuery({ - queryKey: ['orderdetails', orderHeaderid], - queryFn: () => getorderdetails(orderHeaderid), - enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined - }); - - const dialogclose = () => { - setDialogopen(false); - }; - - const handleChangetab = (e, i) => { - setPage(0); - setTabvalue(i); - setRowsPerPage(50); - if (i === 0) { - setTabstatus('Pending'); - setCurrentStatus('pending'); - setTotalCount(countData?.uncoveredLength); - } - if (i === 1) { - setTabstatus('Assigned'); - setCurrentStatus('accepted'); - setTotalCount(countData?.assignedLength); - } - if (i === 2) { - setTabstatus('Arrived'); - setCurrentStatus('arrived'); - setTotalCount(countData?.arrivedLength); - } - if (i === 3) { - setTabstatus('Picked'); - setCurrentStatus('picked'); - setTotalCount(countData?.pickedLength); - } - if (i === 4) { - setTabstatus('Active'); - setCurrentStatus('active'); - setTotalCount(countData?.activeLength); - } - if (i === 5) { - setTabstatus('Skipped'); - setCurrentStatus('skipped'); - setTotalCount(countData?.skippedLength); - } - if (i === 6) { - setTabstatus('Delivered'); - setCurrentStatus('delivered'); - setTotalCount(countData?.coveredLength); - } - - if (i === 7) { - setTabstatus('Cancelled'); - setCurrentStatus('cancelled'); - setTotalCount(countData?.cancelLength); - } - setSearchword(''); - }; - - const okclicked = () => { - setOpen(false); - }; - - // (filteredRows is defined below, after countSourceRows / countSourceLoading - // are in scope — they're the single source of truth for both the table and - // the per-status chip counts.) - - // Parallel "all-statuses" query used ONLY to derive batch-aware tab counts. - // The primary `fetchdeliveries` query above is keyed by `currentStatus`, so - // its rows are scoped to the active tab — they can't tell us how many - // delivered/cancelled/etc. exist in the same batch. We fetch the full day - // once (status='all'), keep it cached, and re-derive the counts per status - // whenever the operator switches batches. Search keyword is intentionally - // dropped from this key so the chip counts reflect the batch totals rather - // than the search-filtered subset. - const { - data: countSourceData, - fetchNextPage: countFetchNext, - hasNextPage: countHasNext, - isFetchingNextPage: countIsFetchingNext, - isLoading: countSourceIsLoading, - refetch: countSourceRefetch - } = useInfiniteQuery({ - queryKey: ['fetchdeliveries-batchcounts', appId, userid, 'all', startdate, enddate, 200, '', tenantid, locationid, riderid], - queryFn: fetchDeliveries, - getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined - }); - - // Newest activity first — an order just created and assigned should be the - // very first row an operator sees, not buried wherever GET /admin/bookings - // happens to return it (unconfirmed/unspecified server-side order). Every - // row here has been dispatched at least once (fetchDeliveries filters to - // that), so assigntime (booking.updatedat) is the more recent of the two - // timestamps on a fresh assignment; orderdate (createdat) covers the rare - // row missing it. parseDoormileTimestamp (not bare dayjs()) avoids the - // false-Z parsing bug documented in utils/doormileTimestamp.js. - const countSourceRows = useMemo(() => { - const rows = (countSourceData?.pages || []).flatMap((p) => p.rows || []); - return [...rows].sort( - (a, b) => - parseDoormileTimestamp(b.assigntime || b.orderdate).valueOf() - parseDoormileTimestamp(a.assigntime || a.orderdate).valueOf() - ); - }, [countSourceData]); - - useEffect(() => { - if (countHasNext && !countIsFetchingNext) countFetchNext(); - }, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]); - - // Loading flag for the table empty state — true while we're still streaming - // pages, so we don't show "No Orders" prematurely before the full - // day's rows have arrived. Goes false once auto-pagination drains. - const countSourceLoading = countHasNext || countIsFetchingNext; - - // The table now reads from the same source as the chip counts. That way the - // "74" in the Delivered chip and the rows shown in the table can never - // disagree (they used to, because the table consumed a separate per-status - // paginated query while the chips read from the full all-statuses dataset). - // Three filter steps: - // 1. Batch — selected via the dropdown. 'all' bypasses this filter. - // 2. Tab status — currentStatus comes from handleChangetab. - // 3. Search — client-side substring match across customer/address/order - // fields. Server-side search isn't needed since every row is loaded. - const filteredRows = useMemo(() => { - const wantStatus = String(currentStatus || '').toLowerCase(); - const q = String(debouncedSearch || '') - .trim() - .toLowerCase(); - return countSourceRows.filter((r) => { - if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false; - const s = String(r.orderstatus || '').toLowerCase(); - if (wantStatus && s !== wantStatus) return false; - if (q) { - const hay = [ - r.deliverycustomer, - r.deliveryaddress, - r.deliverysuburb, - r.pickupcustomer, - r.pickupaddress, - r.pickupsuburb, - r.orderid, - r.tenantname, - r.ridername, - r.username - ] - .map((v) => String(v || '').toLowerCase()) - .join(' '); - if (!hay.includes(q)) return false; - } - return true; - }); - }, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]); - - // Counts per status, scoped to the selected batch. Keys mirror the legacy - // *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical - // (countData?.uncoveredLength → batchCounts.uncoveredLength). - const batchCounts = useMemo(() => { - const c = { - uncoveredLength: 0, - assignedLength: 0, - arrivedLength: 0, - pickedLength: 0, - activeLength: 0, - skippedLength: 0, - coveredLength: 0, - cancelLength: 0 - }; - countSourceRows.forEach((r) => { - if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return; - const s = String(r.orderstatus || '').toLowerCase(); - switch (s) { - case 'pending': - c.uncoveredLength += 1; - break; - case 'accepted': - case 'assigned': - c.assignedLength += 1; - break; - case 'arrived': - c.arrivedLength += 1; - break; - case 'picked': - c.pickedLength += 1; - break; - case 'active': - c.activeLength += 1; - break; - case 'skipped': - c.skippedLength += 1; - break; - case 'delivered': - c.coveredLength += 1; - break; - case 'cancelled': - case 'canceled': - c.cancelLength += 1; - break; - default: - break; - } - }); - return c; - }, [countSourceRows, selectedBatch]); - - // Per-batch total (any status) for the dropdown badges. Computed once for - // every batch + 'all' so each option in the menu can show its own count - // without re-walking the list. Mirrors dispatch's batchCounts shape - // (`Dispatch.js:1198–1206`) — totals here should match the per-batch numbers - // visible on the dispatch page for the same date. - const batchTotals = useMemo(() => { - // `all` is the SUM of the three batch buckets — not the raw row count. - // Rows that fall in a gap (8–9 AM, 12:30–4 PM, after 7 PM) or have no - // `assigntime` belong to no batch, so they're excluded here too. This keeps - // the "All Batches" badge equal to Morning + Afternoon + Evening - // (e.g. 36 + 89 + 53 = 178) instead of over-reporting the un-bucketed total. - const totals = { all: 0 }; - BATCH_OPTIONS.forEach((b) => { - if (b.id === 'all') return; - totals[b.id] = 0; - }); - countSourceRows.forEach((r) => { - const id = getRowBatchId(r); - if (id) { - totals[id] = (totals[id] || 0) + 1; - totals.all += 1; - } - }); - return totals; - }, [countSourceRows]); - - // IntersectionObserver now watches the count-source query — that's the - // single source of truth for both the table and the chip counts. When the - // sentinel scrolls into view it nudges the next page of all-statuses - // deliveries to keep the dataset complete. - useEffect(() => { - if (!countHasNext) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - countFetchNext(); - } - }, - { - root: document.querySelector('.MuiTableContainer-root'), - rootMargin: '0px', - threshold: 1.0 - } - ); - if (loadMoreRef.current) observer.observe(loadMoreRef.current); - return () => { - if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); - }; - }, [countHasNext, countFetchNext]); - - const handleScroll = (event) => { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; - if (scrollTop + clientHeight >= scrollHeight - 50) { - if (countHasNext && !countIsFetchingNext) { - countFetchNext(); - } - } - }; - - /* ============================================= || fetchPercentageAPI | ============================================= */ - - /* ============================================= || fetchcount | ============================================= */ - - const { - data: countData = {}, // Default to empty object - isLoading: fetchCountIsLoading, - isError: fetchCountIsError, - error: fetchCountError, - refetch: fetchCountRefetch - } = useQuery({ - queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus], - queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) - }); - useEffect(() => { - if (tabvalue === 0 && countData) { - setTotalCount(countData.uncoveredLength); - } - }, [countData]); - - // ==============================|| fetchRidersList ||============================== // - - const { - data: ridersList = [], - isLoading: riderListIsLoading, - isError: ridersListIsError, - error: ridersListError - } = useQuery({ - queryKey: ['ridersList', appId], // Unique key for caching & re-fetching - // fetchRidersList now toasts + returns [] internally on failure (was - // throwing after already toasting there too, which showed this same - // error twice) — no onError needed here anymore. - queryFn: fetchRidersList, - enabled: appId != null - }); - - /* ============================================= || updatedelivery | ============================================= */ - - const updateDeliveryMutation = useMutation({ - mutationFn: (orderData) => updateDeliveryAPI(orderData), - onSuccess: (res) => { - if (res.success) { - opentoast('Updated Successfully', 'success'); - setDeliveryamount(''); - setNotes(''); - setDialogopen(false); - fetchCountRefetch(); - countSourceRefetch(); - } else { - // A 200 with success:false (the Doormile API's validation-failure - // shape, same pattern as customers.js's saveMutation) had no else - // branch here before — the dialog just sat open with zero feedback, - // looking exactly like the button didn't work. - opentoast(res.message || 'Failed to update delivery', 'error'); - } - }, - onError: (err) => { - opentoast(err.response?.data?.message || err.message || 'Failed to update delivery', 'error'); - } - }); - - const errorMessage = fetchCountIsError - ? `Error fetching count summary: ${fetchCountError?.message}` - : ridersListIsError - ? `Error fetching riders: ${ridersListError?.message}` - : fetchtenantsIsError - ? `Error tenant list: ${fetchtenantsError?.message}` - : fetchlocationsIsError - ? `Error location list: ${fetchlocationsError?.message}` - : null; - - if (errorMessage) { - OpenToast(errorMessage, 'error', 2000); - return null; // or return <> if inside a component - } - - return ( - <> - {(fetchCountIsLoading || countSourceIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading) && ( - <> - - {/* */} - - )} - { - theme.zIndex.drawer + 1 - }} - open={fetchCountIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading} - > - {/* */} - - } - {/* ============================================= || Header | ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards | ============================================= */} - - {[ - { ...KPI_META[0], value: batchTotals.all, percentage: null }, - { - ...KPI_META[1], - value: countData?.uncoveredLength, - percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 - }, - { - ...KPI_META[2], - value: countData?.coveredLength, - percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 - }, - { - ...KPI_META[3], - value: countData?.cancelLength, - percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 - } - ].map((item) => { - const Icon = item.icon; - const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage); - const hasPct = !Number.isNaN(pct) && item.percentage !== undefined && item.percentage !== null; - return ( - - } - color={item.color} - loading={fetchCountIsLoading} - caption={hasPct ? `${Math.abs(pct)}% of total` : undefined} - /> - - ); - })} - - - {/* ============================================= || Filter Bar | ============================================= */} - - - - - - - Filters - - - - - {/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and - "Orders-All" pills. Filters the loaded rows by the slot the - operator picks; defaults to 'all'. Mirrors the dispatch page's - batch model. UI mirrors the polished Absent Riders dropdown from - the orders page — custom Paper, per-batch color/icon, selected - indicator. */} - {(() => { - const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0]; - const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime; - return ( - opts} - options={BATCH_OPTIONS} - value={activeBatch} - onChange={(e, val) => val && setSelectedBatch(val.id)} - getOptionLabel={(option) => option.label} - isOptionEqualToValue={(option, value) => option.id === value.id} - PaperComponent={SoftPaper} - ListboxProps={{ sx: { py: 0, maxHeight: 360 } }} - renderOption={(props, option, { selected }) => { - const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime; - const total = batchTotals[option.id] ?? 0; - return ( -
  • - - - - - - {option.label} - - - {option.range} - - - {/* Per-batch total — same number you see next to each - batch on the dispatch page. Lets the operator - eyeball-compare without summing the per-status - chip counts on the tabs. */} - 0 ? `${option.color}18` : '#f1f5f9', - color: total > 0 ? option.color : '#94a3b8', - border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`, - '& .MuiChip-label': { px: 0.75 } - }} - /> - {selected && ( - - - - )} -
  • - ); - }} - renderInput={(params) => ( - - - - -
    - ) - }} - sx={{ - minWidth: { xs: 180, sm: 220, md: 240 }, - cursor: 'pointer', - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - color: '#0f172a', - paddingRight: '8px', - cursor: 'pointer', - transition: 'border-color 0.15s, box-shadow 0.15s', - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - }, - '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' } - }} - /> - )} - /> - ); - })()} - - {/* Date range pill — opens the date picker. Stays visible only when - a date range is set. Uses the same pill aesthetic as Batch. */} - {startdate && enddate && ( - setOpen(true)} - sx={{ - pl: 0.5, - pr: 1.25, - py: 0.5, - borderRadius: '10px', - cursor: 'pointer', - bgcolor: '#ffffff', - border: '1px solid #e2e8f0', - transition: 'border-color 0.15s', - '&:hover': { borderColor: '#cbd5e1' } - }} - > - - - - - {dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')} - - - )} -
    - - {/* Tenant */} - { - if (appId == null) { - event.preventDefault(); - OpenToast('Please select your zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(0); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - sx={pillFieldSx('#0ea5e9')} - /> - )} - /> - {/* Location */} - (option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '')} - value={locationValue} - PaperComponent={SoftPaper} - sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} - onOpen={(event) => { - if (appId == null && !tenantid) { - event.preventDefault(); - OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select your Tenant first!', 'warning', 3000); - setTimeout(() => tenantRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - sx={pillFieldSx('#14b8a6')} - /> - )} - /> - {/* Rider */} - { - const name = option?.displayname || option?.authname || ''; - return option?.contactno ? `${name} (${option.contactno})` : name; - }} - sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} - onChange={(e, value, reason) => { - if (reason === 'clear') setRiderid(0); - else setRiderid(value.userid); - }} - onOpen={(event) => { - if (appId == null) { - event.preventDefault(); - OpenToast('Please select your zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - sx={pillFieldSx('#8b5cf6')} - /> - )} - /> - -
    - {/* ============================================= || Status Tabs + Search || ============================================= */} - - - - {STATUS_TABS.map((t, idx) => { - const meta = STATUS_META[t.status]; - const Icon = meta.icon; - const active = tabvalue === idx; - const count = batchCounts[t.countKey] ?? 0; - return ( - handleChangetab(e, idx)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: { xs: 0.625, md: 0.875 }, - pl: 0.5, - pr: { xs: 1, md: 1.25 }, - py: 0.5, - flexShrink: 0, - cursor: 'pointer', - borderRadius: DT.radiusField + 'px', - border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`, - bgcolor: active ? '#C01227' : DT.surface, - color: active ? '#fff' : DT.textSecondary, - fontWeight: 600, - boxShadow: 'none', - transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', - '&:hover': { - borderColor: active ? '#C01227' : DT.borderHover, - bgcolor: active ? '#C01227' : DT.surfaceAlt - } - }} - > - - - - - {meta.label} - - - {count} - - - ); - })} - - - {/* ============================================= || Search | ============================================= */} - - - - - - {/* ============================================= || Table || ============================================= */} - - - {(() => { - const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered'; - const showSelect = tabstatus == 'Created'; - const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0); - return isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {filteredRows.length === 0 && !loading1 && !countSourceLoading && ( - - - - - - No deliveries to show - - - {selectedBatch === 'all' - ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` - : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ - BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' - }.`} - - - )} - {filteredRows.length === 0 && countSourceLoading && ( - - - - Loading deliveries… - - - )} - {filteredRows.map((row, index) => { - const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { - label: row.orderstatus || '—', - color: '#94a3b8', - icon: MdHistoryToggleOff - }; - const RowStatusIcon = rowStatusMeta.icon; - const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - const isOpen = productCollapse?.orderid === row?.orderid; - const chipSx = (c) => ({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - color: c, - fontWeight: 700, - fontSize: 12, - whiteSpace: 'nowrap' - }); - return ( - - - - {showSelect && ( - { - if (e.target.checked) { - let arr = deliverylist; - arr.push({ ...row, sno: deliverylist.length + 1 }); - setDeliverylist([...arr]); - } else { - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - if (res) { - let arr = deliverylist; - arr.splice(res.sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - setDeliverylist([...arr]); - } - } - }} - checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} - /> - )} - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - - - - {rowStatusMeta.label} - - - - - {row.deliverytype == 'C' && ( - { - if (productCollapse?.orderid === row.orderid) { - setProductCollapse(null); - setOrderHeaderId(null); - } else { - setProductCollapse(row); - setOrderHeaderId(row.orderheaderid); - } - }} - sx={{ - borderRadius: 999, - bgcolor: tint('#06b6d4'), - color: '#06b6d4', - border: `1px solid ${edge('#06b6d4')}`, - '&:hover': { bgcolor: soft('#06b6d4') } - }} - > - {isOpen ? : } - - )} - {showAction && ( - handleMenuOpen(e, row)} - sx={{ - borderRadius: 999, - bgcolor: tint('#C01227'), - color: '#C01227', - border: `1px solid ${edge('#C01227')}`, - '&:hover': { bgcolor: soft('#C01227') } - }} - > - - - )} - - - - - {row.tenantname} - - - {[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'} - - - - } - > - - - - {row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname} - - - {row.orderid} · {row.deliveryid} - - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - - {row.ridername ? ( - - - - - - - {row.ridername} - - - ID #{row.userid} · {row.ridercontact || '—'} - - - - ) : ( - - Unassigned - - )} - - - - {row.transitminutes || 0}m - - - - {Number(row.kms || 0).toFixed(2)} km - {Number(row.cumulativekms || 0).toFixed(2)} km - - - - - ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} - ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} - - - - - - {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} - - - - {row.step ? ( - {row.step} - ) : ( - - — - - )} - - {row.notes && ( - - - {row.notes} - - - )} - - {isOpen && ( - - - - - - - Product Details - - - - {/* GET /admin/bookings has no product image/qty/tax breakdown — this - renders the real "parcels" array (itemcategory/itemdescription/ - declaredvalue) instead of the old product-catalog shape. */} - {orderdetails?.parcels?.map((parcel, idx2) => ( - - - - - - - {parcel?.itemdescription || parcel?.itemcategory || 'Item'} - - - {parcel?.itemcategory || ''} {parcel?.weight ? `· ${parcel.weight}kg` : ''} - - - - ₹ {(parcel?.declaredvalue || 0).toFixed(2)} - - - ))} - - - Delivery Charge - - - ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} - - - - - )} - - ); - })} - {countSourceRows?.length != 0 && ( -
    - {countIsFetchingNext || countHasNext ? ( - - ) : ( - - · End of list · - - )} -
    - )} -
    - ) : ( -
    - - - {showSelect && ( - - 0 && deliverylist.length != filteredRows.length} - onChange={(e) => { - if (e.target.checked) { - setDeliverylist([...filteredRows]); - } else { - setDeliverylist([]); - } - }} - checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} - /> - - )} - # - Status - Tenant - Order / Location - Pickup - Drop - Rider - ETA - Transit - Kms - Amount - Notes - Step - Qty - COD - {showAction && Action} - - - - {(loading1 || countSourceIsLoading) && } - {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( - - - - - - - - No deliveries to show - - - {selectedBatch === 'all' - ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` - : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ - BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' - }.`} - - - - - )} - {filteredRows.length == 0 && countSourceLoading && ( - - - - - Loading deliveries… - - - - )} - {filteredRows.map((row, index) => { - const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { - label: row.orderstatus || '—', - color: '#94a3b8', - icon: MdHistoryToggleOff - }; - const RowStatusIcon = rowStatusMeta.icon; - const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - return ( - - - {tabstatus == 'Created' && ( - - { - if (e.target.checked) { - let arr = deliverylist; - arr.push({ ...row, sno: deliverylist.length + 1 }); - setDeliverylist([...arr]); - } else { - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - if (res) { - let arr = deliverylist; - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - arr.splice(res.sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - setDeliverylist([...arr]); - } - } - }} - checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} - /> - - )} - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - {/* Status badge */} - - - - - - - {rowStatusMeta.label} - - - - {/* Tenants */} - - - - - {row.tenantname} - - - {row.tenantsuburb} - - - {row.applocation} - - - - - {/* order details */} - - - - {row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname} - - - - - - - - {row.orderid} - - - {/* express-console-api.md: Doormile timestamps are IST - wall-clock with no timezone marker — bare dayjs() was - still being used here despite this comment saying - otherwise (the fix was never actually applied below, - only claimed) — a Z-suffixed response reinterprets as - local time and shifts 5:30, showing the wrong time - (and sometimes the wrong day). parseDoormileTimestamp - strips that, matching getRowBatchId above. */} - - - {parseDoormileTimestamp(row.orderdate).format('DD/MM/YYYY')} - - - {parseDoormileTimestamp(row.orderdate).format('hh:mm A')} - - - - - - - - - {row.deliveryid} - - - - - {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY')} - - - - {parseDoormileTimestamp(row.deliverydate).format('hh:mm A')} - - - - - - {/* pickup — capped to the same maxWidth as drop below, with CSS - ellipsis truncation, so one long address can't stretch this - column wider than its neighbour. row.pickuplocation is just - the full raw address (see fetchDeliveries in api.js), not a - pre-shortened name, so the old `.slice(0, 14)` fallback never - actually ran — it was always overridden by the untruncated - value on its left. Full text still available via the tooltip. */} - - - - {row.pickupcustomer} - - - {row.pickupcontactno} - - - - {row.pickuplocation || row.Pickupaddress || '—'} - - - - - {/* drop — same maxWidth/truncation treatment as pickup above. */} - - - - {row.deliverycustomer} - - - {row.deliverycontactno} - - - - {row.deliverylocation || row.deliveryaddress || '—'} - - - - - {/* rider */} - - {row.ridername ? ( - - - - - - - {row.ridername} - - - ID #{row.userid} · {row.ridercontact || '—'} - - - - ) : ( - - Unassigned - - )} - - {/* Estimated Delivery Time */} - - - {row.expecteddeliverytime ? parseDoormileTimestamp(row.expecteddeliverytime).format('hh:mm A') : '—'} - - - {/* Transit Minutes */} - - - {row.transitminutes || 0}m - - - {/* kms */} - - - - - {Number(row.kms || 0).toFixed(2)} km - - - - - {Number(row.cumulativekms || 0).toFixed(2)} km - - - - - {/* amount */} - - - - - ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} - - - - - ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} - - - - - {/* notes */} - - {row.notes ? ( - - - {row.notes} - - - ) : ( - - — - - )} - - {/* step */} - - {row.step ? ( - - {row.step} - - ) : ( - - — - - )} - - {/* qty */} - - - {row.Quantity || '—'} - - - {/* COD */} - - - {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} - - - {/* Action */} - {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( - - - {row.deliverytype == 'C' && ( - - { - if (productCollapse?.orderid === row.orderid) { - setProductCollapse(null); - setOrderHeaderId(null); - } else { - setProductCollapse(row); - setOrderHeaderId(row.orderheaderid); - } - }} - sx={{ - borderRadius: 999, - bgcolor: tint('#06b6d4'), - color: '#06b6d4', - border: `1px solid ${edge('#06b6d4')}`, - '&:hover': { bgcolor: soft('#06b6d4') } - }} - > - {productCollapse?.orderid === row.orderid ? ( - - ) : ( - - )} - - - )} - - - handleMenuOpen(e, row)} - sx={{ - borderRadius: 999, - bgcolor: tint('#C01227'), - color: '#C01227', - border: `1px solid ${edge('#C01227')}`, - '&:hover': { bgcolor: soft('#C01227') } - }} - > - - - - - - )} - - {productCollapse?.orderid === row?.orderid && ( - - - - - - - - - - - Product Details - - -
    - - - # - Item - Category - Weight - Declared Value - - - - {/* GET /admin/bookings has no product image/qty/tax breakdown — - this renders the real "parcels" array instead of the old - product-catalog shape. */} - {orderdetails?.parcels?.map((parcel, idx2) => ( - - - {String(idx2 + 1).padStart(2, '0')} - - - - - - - - {parcel?.itemdescription || 'Item'} - - - - {parcel?.itemcategory || '-'} - - {parcel?.weight ? `${parcel.weight}kg` : '-'} - - - ₹ {(parcel?.declaredvalue || 0).toFixed(2)} - - - ))} - - - Delivery Charge - - - ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} - - - -
    -
    - - - - - )} - - ); - })} - {countSourceRows?.length != 0 && ( - - -
    - {countIsFetchingNext ? ( - - ) : countHasNext ? ( - - ) : ( - - · End of list · - - )} -
    -
    -
    - )} - - - ); - })()} - - - - {/* Shared row-action menu — single instance reused by both the desktop - table rows and the mobile cards (both trigger handleMenuOpen). */} - - {selectedRow?.orderstatus !== 'delivered' && ( - { - notifyRiderMutation.mutate(selectedRow.milerprofileid); - handleMenuClose(); - }} - > - - - - Notify Rider - - )} - {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && ( - { - if (appId == null) { - opentoast('Please select a location first!', 'warning'); - locationRef.current?.focus(); - return; - } - setChangeDialogOpen(true); - handleMenuClose(); - }} - > - - - - Change Rider - - )} - {/* Was gated to roleid 1/2 (staff-only), matching the reference app's - identical gate — opened to every login per explicit decision, so - tenant operators can reach the same Update Status fields staff - already could. */} - { - // PUT /admin/consignments/:id/status is consignment-scoped — it - // only exists once the rider marks pickup-complete (doormile-flow.md). - // Without a real consignmentid, updateDeliveryAPI's `?? deliveryid` - // fallback was sending the BOOKING id where a consignment id is - // required — a different id space, so the call failed against - // the wrong (or nonexistent) resource. Combined with the mutation's - // onSuccess only handling `res.success === true` with no else - // branch, that failure was completely silent — dialog just sat - // there looking like nothing happened. Block it here instead, - // with a message that explains why, rather than letting the - // submit silently no-op. - if (!selectedRow.consignmentid) { - opentoast("This order hasn't been picked up yet — status can only be updated once it becomes a consignment.", 'warning'); - handleMenuClose(); - return; - } - setKms(selectedRow.kms); - setCumulativeKms(selectedRow.cumulativekms); - setDeliverylat(selectedRow.droplat); - setDeliverylong(selectedRow.droplon); - setNotes(selectedRow.notes); - setDeliveryamount(selectedRow.deliveryamt ?? selectedRow.deliveryamount); - setUpdateStatus(selectedRow.orderstatus || 'delivered'); - setCurrentorder(selectedRow); - setDialogopen(true); - handleMenuClose(); - }} - > - - - - Update Status - - {selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && ( - { - setCancelDeliveryOpen(true); - handleMenuClose(); - }} - > - - - - Cancel Delivery - - )} - - - {/* =============================== || cancel dialog || =============================== */} - setCancelDeliveryOpen(false)} - maxWidth="xs" - fullWidth - PaperProps={{ - elevation: 0, - sx: { - borderRadius: 3, - border: '1px solid', - borderColor: DT.borderSubtle, - boxShadow: DT.shadowPop, - overflow: 'hidden' - } - }} - > - - - - - - - - - Cancel Delivery? - - - This action will cancel the delivery and notify the rider. Please share a brief reason. - - - setCancelFeed(e.target.value)} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: 2, - bgcolor: DT.surfaceAlt, - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: '#ef4444' }, - '&.Mui-focused fieldset': { borderColor: '#ef4444', borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#ef4444')}` } - } - }} - /> - - - - - - - - {/* =============================== || change rider dialog || =============================== */} - setChangeDialogOpen(false)} - maxWidth="sm" - fullWidth - TransitionComponent={PopupTransition} - PaperProps={{ - elevation: 0, - sx: { - borderRadius: 3, - border: '1px solid', - borderColor: DT.borderSubtle, - boxShadow: DT.shadowPop, - overflow: 'hidden' - } - }} - > - - - - - - - - - Change Rider - - - Assign this delivery to a different rider - - - - - - { - const name = option?.displayname || option?.authname || ''; - return option?.contactno ? `${name} (${option.contactno})` : name; - }} - onChange={(e, value) => { - setSelectedRider(value); - logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); - }} - renderInput={(params) => ( - - - - - - ) - }} - sx={pillFieldSx('#8b5cf6')} - /> - )} - /> - - - - - - - - - - {/* =============================== || Date filter Dialog || =============================== */} - setOpen(false)} - fullScreen={isMobile} - PaperProps={{ - elevation: 0, - sx: { - borderRadius: { xs: 0, sm: 3 }, - border: '1px solid', - borderColor: DT.borderSubtle, - boxShadow: DT.shadowPop, - overflow: 'hidden' - } - }} - > - - - - - - - - - Date Range - - - Pick a window to filter the deliveries - - - - - - setOpen(!open)} - id="daterange1" - onChange={(range) => { - if (range.label === 'All') { - setStartdate(''); - setEnddate(''); - - setOpen(false); - } else { - setStartdate(dayjs(range.startDate).format('YYYY-MM-DD')); - setEnddate(dayjs(range.endDate).format('YYYY-MM-DD')); - if (range.label) { - setDatestatus(range.label); - } else { - setDatestatus(''); - } - } - }} - definedRanges={[ - { - label: 'Today', - startDate: new Date(), - endDate: new Date() - }, - { - label: 'Yesterday', - startDate: addDays(new Date(), -1), - endDate: addDays(new Date(), -1) - }, - { - label: 'Tomorrow', - startDate: addDays(new Date(), +1), - endDate: addDays(new Date(), +1) - }, - { - label: 'This Week', - startDate: startOfWeek(new Date()), - endDate: endOfWeek(new Date()) - }, - { - label: 'Last Week', - startDate: startOfWeek(addWeeks(new Date(), -1)), - endDate: endOfWeek(addWeeks(new Date(), -1)) - }, - { - label: 'Last 7 Days', - startDate: addWeeks(new Date(), -1), - endDate: new Date() - }, - { - label: 'This Month', - startDate: startOfMonth(new Date()), - endDate: endOfMonth(new Date()) - }, - { - label: 'Last Month', - startDate: startOfMonth(addMonths(new Date(), -1)), - endDate: endOfMonth(addMonths(new Date(), -1)) - } - // { - // label: 'All', - // startDate: new Date(), - // endDate: addDays(new Date(), -1), - // }, - ]} - /> - - - - - - - {/* =============================== || Update Delivery Dialog || =============================== */} - - - - - - - - - - Update Delivery Status - - - {currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'} - - - - - - - {[ - { label: 'KMs', value: kms, set: (v) => setKms(v), color: '#ef4444' }, - { label: 'Actual KMs', value: cumulativekms, set: (v) => setCumulativeKms(+v), color: '#10b981' }, - { label: 'Delivery Latitude', value: deliverylat, set: (v) => setDeliverylat(v), color: '#06b6d4' }, - { label: 'Delivery Longitude', value: deliverylong, set: (v) => setDeliverylong(v), color: '#06b6d4' } - ].map((f) => ( - - - {f.label} - - f.set(e.target.value)} - sx={{ - mt: 0.5, - '& .MuiOutlinedInput-root': { - borderRadius: 2, - bgcolor: '#fff', - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: f.color }, - '&.Mui-focused fieldset': { borderColor: f.color, borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(f.color)}` } - } - }} - /> - - ))} - - - Amount - - setDeliveryamount(+e.target.value)} - sx={{ - mt: 0.5, - '& .MuiOutlinedInput-root': { - borderRadius: 2, - bgcolor: '#fff', - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: '#10b981' }, - '&.Mui-focused fieldset': { borderColor: '#10b981', borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#10b981')}` } - } - }} - /> - - - - Status - - setUpdateStatus(e.target.value)} - SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 2, mt: 0.5, boxShadow: DT.shadowPop } } } }} - sx={{ - mt: 0.5, - '& .MuiOutlinedInput-root': { - borderRadius: 2, - bgcolor: '#fff', - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: '#C01227' }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` } - } - }} - > - {['pending', 'accepted', 'started', 'arrived', 'delivered', 'cancelled'].map((s) => { - const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff }; - const Ic = m.icon; - return ( - - - - - {m.label} - - ); - })} - - - - - Notes - - setNotes(e.target.value)} - sx={{ - mt: 0.5, - '& .MuiOutlinedInput-root': { - borderRadius: 2, - bgcolor: '#fff', - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: '#C01227' }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` } - } - }} - /> - - - - - - - - - - - - ); -}; - -export default Deliveries; +import logger from '../../../utils/logger'; +import { enqueueSnackbar } from 'notistack'; +import { DeleteFilled, EditOutlined } from '@ant-design/icons'; +import { useState, useEffect, Fragment, useRef, useMemo } from 'react'; +import dayjs from 'dayjs'; +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); +import { notifyMiler, getConsignmentLogs } from 'pages/api/doormileApi'; +import { kalmanSmoothGps, polylineLengthKm } from '../dispatch/dispatchShared'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; +import { useTheme } from '@mui/material/styles'; +import { + MdOutlineDateRange, + MdAccessTime, + MdAllInclusive, + MdLightMode, + MdWbSunny, + MdNightsStay, + MdCheck, + MdStorefront, + MdLocationOn, + MdDirectionsBike, + MdNotificationsActive, + MdPersonPin, + MdHistoryToggleOff, + MdCheckCircle, + MdCancel, + MdInventory2, + MdHourglassEmpty, + MdRoute, + MdSkipNext, + MdTune, + MdOutlineLocalShipping, + MdOutlinePendingActions, + MdOutlineCheckCircle, + MdOutlineCancel +} from 'react-icons/md'; +import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; + +import { + Avatar, + Box, + Button, + Chip, + Grid, + IconButton, + Stack, + Typography, + Table, + TableCell, + TableBody, + TableHead, + Collapse, + Dialog, + TableRow, + DialogContent, + DialogTitle, + Tooltip, + DialogActions, + Checkbox, + Autocomplete, + TextField, + TableContainer, + Backdrop, + MenuItem, + Menu, + Paper, + useMediaQuery +} from '@mui/material'; + +import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns'; +import { DateRangePicker } from 'components/nearle_components/DateRangePicker'; +import * as React from 'react'; +import Loader from 'components/Loader'; +import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material'; + +import { + cancelDeliveryAPI, + changeRiderAPI, + fetchCountAPI, + fetchDeliveries, + fetchRidersList, + notifyRider, + updateDeliveryAPI, + getorderdetails, + gettenantlocations, + getTenants +} from 'pages/api/api'; +import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; +import { OpenToast } from 'components/third-party/OpenToast'; +import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; +import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; +import PageHeader from 'components/nearle_components/PageHeader'; +import StatCard from 'components/nearle_components/StatCard'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; + +// ============================================================================ +// Design tokens — shared across every DT-styled operator page so filters, +// KPIs, tabs, status badges, and dialogs stay visually consistent. +// See src/themes/dt/tokens.js for the canonical source. +// ============================================================================ +import { DT, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens'; + +// Status palette — drives tab pills, row status badges, dialogs. +const STATUS_META = { + pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, + accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, + arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, + picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, + active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, + skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, + delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } +}; + +// Ordered status list driving the tabs row (left → right). Each entry binds +// the visual meta above to the `currentStatus` key the queries use AND to the +// `batchCounts` key (so the chip count for the tab is one lookup). +const STATUS_TABS = [ + { status: 'pending', countKey: 'uncoveredLength' }, + { status: 'accepted', countKey: 'assignedLength' }, + { status: 'arrived', countKey: 'arrivedLength' }, + { status: 'picked', countKey: 'pickedLength' }, + { status: 'active', countKey: 'activeLength' }, + { status: 'skipped', countKey: 'skippedLength' }, + { status: 'delivered', countKey: 'coveredLength' }, + { status: 'cancelled', countKey: 'cancelLength' } +]; + +// KPI palette + icons — mirrors the four cards across the top of the page. +const KPI_META = [ + { key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping }, + { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions }, + { key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle }, + { key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel } +]; + +// Batches mirror the dispatch page's slot definitions so an operator who +// segments the day there sees the same buckets here. Hours are 24h, half-open +// [startHour, endHour) — a delivery at exactly endHour falls into the *next* +// batch (or none, if the gap isn't covered). +const BATCH_OPTIONS = [ + { id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' }, + { id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 }, + { + id: 'afternoon', + label: 'Afternoon Batch', + range: '9 AM to 12:30 PM', + color: '#f59e0b', + iconKey: 'afternoon', + startHour: 9, + endHour: 12.5 + }, + { id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 } +]; + +// Per-batch icon components — kept separate from BATCH_OPTIONS so the option +// objects stay serializable / printable. Looked up at render time by iconKey. +const BATCH_ICONS = { + all: MdAllInclusive, + morning: MdLightMode, + afternoon: MdWbSunny, + evening: MdNightsStay +}; + +// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the +// page lands them on the slot they're most likely curious about. Falls back +// to 'all' when the current hour lies in a configured gap (8–9 AM, 12 PM–4 +// PM, after 7 PM). Local time, not UTC, to match the dispatch page's +// bucketing — both pages must agree on which batch a given row belongs to. +const detectInitialBatchId = () => { + const now = dayjs(); + const h = now.hour() + now.minute() / 60; + for (const b of BATCH_OPTIONS) { + if (b.id === 'all') continue; + if (h >= b.startHour && h < b.endHour) return b.id; + } + return 'all'; +}; + +// Bucket by `assigntime` only — matches the spec ("bucketed by assigntime") +// and the dispatch page's default time-field selection (`Dispatch.js:763` +// initialises `selectedTimeField` to `'assigned'`, whose `keys` is just +// `['assigntime']`). The previous priority cascade started with +// `deliverytime`, which mis-bucketed delivered orders into the wave they +// were *completed* in rather than the wave they were *assigned to* — so an +// order assigned at 07:00 (Morning) but delivered at 11:00 fell into the +// 8–9 AM / 12 PM–4 PM gap and silently disappeared from every batch on this +// page while still showing up on dispatch's Morning Batch. Rows without an +// `assigntime` (unassigned pending orders) return null and intentionally +// don't bucket — matches dispatch. +const BATCH_TIME_KEYS = ['assigntime']; + +// Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null +// when the row has no usable timestamp / falls in a gap between batches. +const getRowBatchId = (row) => { + let t = null; + for (const k of BATCH_TIME_KEYS) { + if (row?.[k]) { + t = row[k]; + break; + } + } + if (!t) return null; + const str = String(t).trim(); + // Bare YYYY-MM-DD with no time component would always parse to midnight and + // get mis-bucketed into Morning — skip those. + if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null; + // Parse in LOCAL time to match the dispatch page's bucketing exactly + // (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment, + // so a row that lands in Evening Batch on the dispatch page must also land + // in Evening Batch here — otherwise an operator running in IST/PST/etc. + // sees different totals on the two pages for the same underlying rows. + // parseDoormileTimestamp (not bare dayjs()) strips a false trailing Z some + // Doormile timestamps carry (see utils/doormileTimestamp.js) — without + // this, a booking assigned "just now" could get shifted +5:30 (IST) past + // every batch window into a gap, silently vanishing from the table even + // though it was correctly assigned server-side. + const d = parseDoormileTimestamp(t); + if (!d.isValid()) return null; + const h = d.hour() + d.minute() / 60; + for (const b of BATCH_OPTIONS) { + if (b.id === 'all') continue; + if (h >= b.startHour && h < b.endHour) return b.id; + } + return null; +}; + +// Shared Paper used by every Autocomplete popup on the page. Matches the +// batch dropdown's soft elevated look. +const SoftPaper = (props) => ( + +); + +// Renders a colored avatar with an icon — used in pill input adornments and +// in dropdown rows. Tracks `selected` so it can flip between filled and +// soft variants identically to the batch dropdown. +const AccentAvatar = ({ color, selected, size = 24, children }) => ( + + {children} + +); + +// ================================================= || deliveries (initial point)|| ================================================= +const Deliveries = () => { + const userid = localStorage.getItem('userid'); + const theme = useTheme(); + // Below `md` we swap the wide data table for an app-style card list. Desktop + // (md and up) keeps the exact same table — no behaviour change either way. + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const loadMoreRef = useRef(); + const containerRef = useRef(); + const [deliverylist, setDeliverylist] = useState([]); + const [dialogopen, setDialogopen] = useState(false); + const [locaName, setLocoName] = useState('All'); + const [appId, setAppId] = useState(0); + const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); + const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); + const [tabstatus, setTabstatus] = useState('Pending'); + const [tabvalue, setTabvalue] = useState(0); + const [open, setOpen] = useState(false); + const [datestatus, setDatestatus] = useState('Today'); + const [kms, setKms] = useState(''); + const [cumulativekms, setCumulativeKms] = useState(); + const [deliveryamount, setDeliveryamount] = useState(); + const [notes, setNotes] = useState(''); + const [currentorder, setCurrentorder] = useState({}); + + // Real Actual KMs for the Update Status dialog, computed from real GPS + // ping history instead of leaving the field permanently blank (there is + // no backend-stored actual-distance field — see fetchDeliveries in + // api.js). Same sort -> Kalman-smooth -> polyline-length pipeline + // Dispatch.js's Compare feature already uses to show a real "actual km" + // figure, just reused here (see dispatchShared.js). Keyed off + // consignmentid, NOT deliveryid/bookingid — GET /admin/consignments/:id/logs + // is consignment-scoped (the same id-class mix-up already fixed in + // updateDeliveryAPI this session). + const { data: computedActualKm } = useQuery({ + queryKey: ['deliveryActualKm', currentorder?.consignmentid], + queryFn: async () => { + let rows = []; + try { + rows = (await getConsignmentLogs(currentorder.consignmentid)) || []; + } catch { + return null; + } + const sorted = rows + .map((r) => { + const ts = r?.logdate ? dayjs(r.logdate) : null; + return { + lat: parseFloat(r?.latitude ?? r?.lat), + lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), + logdate: r?.logdate, + _ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER + }; + }) + .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) + .sort((a, b) => a._ts - b._ts); + // Fewer than 2 usable pings (order not yet delivered, or no telemetry + // recorded) — a real, expected case, not an error. Leave the field + // to its existing blank/manual-entry behavior. + if (sorted.length < 2) return null; + const smoothed = kalmanSmoothGps(sorted); + const km = polylineLengthKm(smoothed.map((p) => [p.lat, p.lng])); + return km > 0 ? km : null; + }, + enabled: dialogopen && !!currentorder?.consignmentid, + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false + }); + + // Only pre-fill while the field is still empty — never overwrite an + // operator's own edit (or an earlier pre-fill) if this query happens to + // re-run while the dialog is already open. + useEffect(() => { + if (computedActualKm != null && !cumulativekms) { + setCumulativeKms(Number(computedActualKm.toFixed(2))); + } + }, [computedActualKm]); + const [deliverylat, setDeliverylat] = useState(''); + const [deliverylong, setDeliverylong] = useState(''); + const [currentStatus, setCurrentStatus] = useState('pending'); + const [updateStatus, setUpdateStatus] = useState('delivered'); + const locationRef = useRef(null); + const tenantRef = useRef(null); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(50); + const [totalCount, setTotalCount] = React.useState(); + const [productCollapse, setProductCollapse] = useState(null); + const [orderHeaderid, setOrderHeaderId] = useState(null); + const [searchword, setSearchword] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); + const [selectedRow, setSelectedRow] = useState(null); + const [loading1, setLoading1] = useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const [open2, setOpen2] = useState(''); + const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false); + const [changeDialogOpen, setChangeDialogOpen] = useState(false); + const [cancelFeed, setCancelFeed] = useState(''); + const [selectedRider, setSelectedRider] = useState(null); + const [tenantid, setTenantid] = useState(0); + const [locationid, setLocationid] = useState(0); + const [tenantValue, setTenantValue] = useState(null); + const [locationValue, setLocationValue] = useState(null); + const [riderid, setRiderid] = useState(0); + // Selected batch id — drives the client-side row filter. Defaults to the + // batch matching the current UTC hour (the operator is most likely curious + // about "now"); never `null` since there's no longer an "All" option. + const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId); + + useEffect(() => { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + }, [appId]); + // to clear the location autocomplete + useEffect(() => { + setLocationid(0); + setLocationValue(null); + }, [tenantid]); + + const menuOpen = Boolean(menuAnchorEl); + + const handleMenuOpen = (event, row) => { + setSelectedRow(row); + + setMenuAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; + + // =========================================== || cancelDelivery || =========================================== + + const { mutate: cancelDelivery } = useMutation({ + mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed), + onSuccess: () => { + opentoast('Delivery Cancelled Successfully', 'success'); + setCancelDeliveryOpen(false); + fetchCountRefetch(); // Refresh count data + countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips + }, + onError: (error) => { + opentoast(error.message, 'error'); + } + }); + + // ==============================|| cancelridernotification ||============================== // + + const cancelridernotification = async () => { + try { + // /admin/milers/:id/notify keys off milerprofileid, not the userid + // stored on the booking (confirmed live) — see fetchDeliveries in api.js. + return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`); + } catch (err) { + opentoast(err.message, 'error', 2000); + } + }; + + // ==============================|| getTenants ||============================== // + + const { + data: tenantlist, + isLoading: fetchtenantsIsLoading, + isError: fetchtenantsIsError, + error: fetchtenantsError + } = useQuery({ + queryKey: ['tenantlist', appId], + queryFn: () => getTenants(appId), // Ensure appId is passed + // appId 0 is a real, deliberate choice — the synthetic "All" zone option + // (fetchAppLocations in api.js) — not "nothing picked yet"; only + // null/undefined means that now. See LocationAutocomplete.js's onChange. + enabled: appId != null + }); + // ==============================|| gettenantlocations ||============================== // + + const { + data: locationlist, + isLoading: fetchlocationsIsLoading, + isError: fetchlocationsIsError, + error: fetchlocationsError + } = useQuery({ + queryKey: ['gettenantlocations', tenantid], + queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed + enabled: tenantid !== 0 // Ensures query runs only when appId is valid + }); + // =========================================== || notifyrider || =========================================== + + const notifyRiderMutation = useMutation({ + mutationFn: notifyRider, // Using the separate function + onSuccess: () => { + enqueueSnackbar('Notification sent Successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + }, + onError: (error) => { + enqueueSnackbar(error.message, { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }); + + // =========================================== || changerider || =========================================== + const changeRiderMutation = useMutation({ + mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow), + onSuccess: (res, { selectedRider, selectedRow }) => { + setLoading1(false); + setChangeDialogOpen(false); + if (res.success) { + logger.info( + `Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.displayname || selectedRider?.authname}` + ); + opentoast('Rider Changed Successfully', 'success'); + } + fetchCountRefetch(); // Refresh count data + countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips + // notifyMiler wants a miler profile id, not an FCM token — the new API + // looks the device token up server-side. selectedRider comes straight + // from getMilers(), which has milerprofileid directly (confirmed live; + // .userid there is the underlying app-user id, a different resource). + notifyRiderMutation.mutate(selectedRider.milerprofileid); + }, + onError: (err, { selectedRider, selectedRow }) => { + logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err); + opentoast(err.message, 'error'); + setLoading1(false); + } + }); + + /* ============================================= || opentoast || ============================================= */ + + const opentoast = (message, variant) => { + enqueueSnackbar(message, { + variant, + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + }; + + // ==============================|| getorderdetails ||============================== // + + const { data: orderdetails } = useQuery({ + queryKey: ['orderdetails', orderHeaderid], + queryFn: () => getorderdetails(orderHeaderid), + enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined + }); + + const dialogclose = () => { + setDialogopen(false); + }; + + const handleChangetab = (e, i) => { + setPage(0); + setTabvalue(i); + setRowsPerPage(50); + if (i === 0) { + setTabstatus('Pending'); + setCurrentStatus('pending'); + setTotalCount(countData?.uncoveredLength); + } + if (i === 1) { + setTabstatus('Assigned'); + setCurrentStatus('accepted'); + setTotalCount(countData?.assignedLength); + } + if (i === 2) { + setTabstatus('Arrived'); + setCurrentStatus('arrived'); + setTotalCount(countData?.arrivedLength); + } + if (i === 3) { + setTabstatus('Picked'); + setCurrentStatus('picked'); + setTotalCount(countData?.pickedLength); + } + if (i === 4) { + setTabstatus('Active'); + setCurrentStatus('active'); + setTotalCount(countData?.activeLength); + } + if (i === 5) { + setTabstatus('Skipped'); + setCurrentStatus('skipped'); + setTotalCount(countData?.skippedLength); + } + if (i === 6) { + setTabstatus('Delivered'); + setCurrentStatus('delivered'); + setTotalCount(countData?.coveredLength); + } + + if (i === 7) { + setTabstatus('Cancelled'); + setCurrentStatus('cancelled'); + setTotalCount(countData?.cancelLength); + } + setSearchword(''); + }; + + const okclicked = () => { + setOpen(false); + }; + + // (filteredRows is defined below, after countSourceRows / countSourceLoading + // are in scope — they're the single source of truth for both the table and + // the per-status chip counts.) + + // Parallel "all-statuses" query used ONLY to derive batch-aware tab counts. + // The primary `fetchdeliveries` query above is keyed by `currentStatus`, so + // its rows are scoped to the active tab — they can't tell us how many + // delivered/cancelled/etc. exist in the same batch. We fetch the full day + // once (status='all'), keep it cached, and re-derive the counts per status + // whenever the operator switches batches. Search keyword is intentionally + // dropped from this key so the chip counts reflect the batch totals rather + // than the search-filtered subset. + const { + data: countSourceData, + fetchNextPage: countFetchNext, + hasNextPage: countHasNext, + isFetchingNextPage: countIsFetchingNext, + isLoading: countSourceIsLoading, + refetch: countSourceRefetch + } = useInfiniteQuery({ + queryKey: ['fetchdeliveries-batchcounts', appId, userid, 'all', startdate, enddate, 200, '', tenantid, locationid, riderid], + queryFn: fetchDeliveries, + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined + }); + + // Newest activity first — an order just created and assigned should be the + // very first row an operator sees, not buried wherever GET /admin/bookings + // happens to return it (unconfirmed/unspecified server-side order). Every + // row here has been dispatched at least once (fetchDeliveries filters to + // that), so assigntime (booking.updatedat) is the more recent of the two + // timestamps on a fresh assignment; orderdate (createdat) covers the rare + // row missing it. parseDoormileTimestamp (not bare dayjs()) avoids the + // false-Z parsing bug documented in utils/doormileTimestamp.js. + const countSourceRows = useMemo(() => { + const rows = (countSourceData?.pages || []).flatMap((p) => p.rows || []); + return [...rows].sort( + (a, b) => + parseDoormileTimestamp(b.assigntime || b.orderdate).valueOf() - parseDoormileTimestamp(a.assigntime || a.orderdate).valueOf() + ); + }, [countSourceData]); + + useEffect(() => { + if (countHasNext && !countIsFetchingNext) countFetchNext(); + }, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]); + + // Loading flag for the table empty state — true while we're still streaming + // pages, so we don't show "No Orders" prematurely before the full + // day's rows have arrived. Goes false once auto-pagination drains. + const countSourceLoading = countHasNext || countIsFetchingNext; + + // The table now reads from the same source as the chip counts. That way the + // "74" in the Delivered chip and the rows shown in the table can never + // disagree (they used to, because the table consumed a separate per-status + // paginated query while the chips read from the full all-statuses dataset). + // Three filter steps: + // 1. Batch — selected via the dropdown. 'all' bypasses this filter. + // 2. Tab status — currentStatus comes from handleChangetab. + // 3. Search — client-side substring match across customer/address/order + // fields. Server-side search isn't needed since every row is loaded. + const filteredRows = useMemo(() => { + const wantStatus = String(currentStatus || '').toLowerCase(); + const q = String(debouncedSearch || '') + .trim() + .toLowerCase(); + return countSourceRows.filter((r) => { + if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false; + const s = String(r.orderstatus || '').toLowerCase(); + if (wantStatus && s !== wantStatus) return false; + if (q) { + const hay = [ + r.deliverycustomer, + r.deliveryaddress, + r.deliverysuburb, + r.pickupcustomer, + r.pickupaddress, + r.pickupsuburb, + r.orderid, + r.tenantname, + r.ridername, + r.username + ] + .map((v) => String(v || '').toLowerCase()) + .join(' '); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]); + + // Counts per status, scoped to the selected batch. Keys mirror the legacy + // *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical + // (countData?.uncoveredLength → batchCounts.uncoveredLength). + const batchCounts = useMemo(() => { + const c = { + uncoveredLength: 0, + assignedLength: 0, + arrivedLength: 0, + pickedLength: 0, + activeLength: 0, + skippedLength: 0, + coveredLength: 0, + cancelLength: 0 + }; + countSourceRows.forEach((r) => { + if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return; + const s = String(r.orderstatus || '').toLowerCase(); + switch (s) { + case 'pending': + c.uncoveredLength += 1; + break; + case 'accepted': + case 'assigned': + c.assignedLength += 1; + break; + case 'arrived': + c.arrivedLength += 1; + break; + case 'picked': + c.pickedLength += 1; + break; + case 'active': + c.activeLength += 1; + break; + case 'skipped': + c.skippedLength += 1; + break; + case 'delivered': + c.coveredLength += 1; + break; + case 'cancelled': + case 'canceled': + c.cancelLength += 1; + break; + default: + break; + } + }); + return c; + }, [countSourceRows, selectedBatch]); + + // Per-batch total (any status) for the dropdown badges. Computed once for + // every batch + 'all' so each option in the menu can show its own count + // without re-walking the list. Mirrors dispatch's batchCounts shape + // (`Dispatch.js:1198–1206`) — totals here should match the per-batch numbers + // visible on the dispatch page for the same date. + const batchTotals = useMemo(() => { + // `all` is the SUM of the three batch buckets — not the raw row count. + // Rows that fall in a gap (8–9 AM, 12:30–4 PM, after 7 PM) or have no + // `assigntime` belong to no batch, so they're excluded here too. This keeps + // the "All Batches" badge equal to Morning + Afternoon + Evening + // (e.g. 36 + 89 + 53 = 178) instead of over-reporting the un-bucketed total. + const totals = { all: 0 }; + BATCH_OPTIONS.forEach((b) => { + if (b.id === 'all') return; + totals[b.id] = 0; + }); + countSourceRows.forEach((r) => { + const id = getRowBatchId(r); + if (id) { + totals[id] = (totals[id] || 0) + 1; + totals.all += 1; + } + }); + return totals; + }, [countSourceRows]); + + // IntersectionObserver now watches the count-source query — that's the + // single source of truth for both the table and the chip counts. When the + // sentinel scrolls into view it nudges the next page of all-statuses + // deliveries to keep the dataset complete. + useEffect(() => { + if (!countHasNext) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + countFetchNext(); + } + }, + { + root: document.querySelector('.MuiTableContainer-root'), + rootMargin: '0px', + threshold: 1.0 + } + ); + if (loadMoreRef.current) observer.observe(loadMoreRef.current); + return () => { + if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); + }; + }, [countHasNext, countFetchNext]); + + const handleScroll = (event) => { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; + if (scrollTop + clientHeight >= scrollHeight - 50) { + if (countHasNext && !countIsFetchingNext) { + countFetchNext(); + } + } + }; + + /* ============================================= || fetchPercentageAPI | ============================================= */ + + /* ============================================= || fetchcount | ============================================= */ + + const { + data: countData = {}, // Default to empty object + isLoading: fetchCountIsLoading, + isError: fetchCountIsError, + error: fetchCountError, + refetch: fetchCountRefetch + } = useQuery({ + queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus], + queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) + }); + useEffect(() => { + if (tabvalue === 0 && countData) { + setTotalCount(countData.uncoveredLength); + } + }, [countData]); + + // ==============================|| fetchRidersList ||============================== // + + const { + data: ridersList = [], + isLoading: riderListIsLoading, + isError: ridersListIsError, + error: ridersListError + } = useQuery({ + queryKey: ['ridersList', appId], // Unique key for caching & re-fetching + // fetchRidersList now toasts + returns [] internally on failure (was + // throwing after already toasting there too, which showed this same + // error twice) — no onError needed here anymore. + queryFn: fetchRidersList, + enabled: appId != null + }); + + /* ============================================= || updatedelivery | ============================================= */ + + const updateDeliveryMutation = useMutation({ + mutationFn: (orderData) => updateDeliveryAPI(orderData), + onSuccess: (res) => { + if (res.success) { + opentoast('Updated Successfully', 'success'); + setDeliveryamount(''); + setNotes(''); + setDialogopen(false); + fetchCountRefetch(); + countSourceRefetch(); + } else { + // A 200 with success:false (the Doormile API's validation-failure + // shape, same pattern as customers.js's saveMutation) had no else + // branch here before — the dialog just sat open with zero feedback, + // looking exactly like the button didn't work. + opentoast(res.message || 'Failed to update delivery', 'error'); + } + }, + onError: (err) => { + opentoast(err.response?.data?.message || err.message || 'Failed to update delivery', 'error'); + } + }); + + const errorMessage = fetchCountIsError + ? `Error fetching count summary: ${fetchCountError?.message}` + : ridersListIsError + ? `Error fetching riders: ${ridersListError?.message}` + : fetchtenantsIsError + ? `Error tenant list: ${fetchtenantsError?.message}` + : fetchlocationsIsError + ? `Error location list: ${fetchlocationsError?.message}` + : null; + + if (errorMessage) { + OpenToast(errorMessage, 'error', 2000); + return null; // or return <> if inside a component + } + + return ( + <> + {(fetchCountIsLoading || countSourceIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading) && ( + <> + + {/* */} + + )} + { + theme.zIndex.drawer + 1 + }} + open={fetchCountIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading} + > + {/* */} + + } + {/* ============================================= || Header | ============================================= */} + + } + /> + + {/* ============================================= || KPI Cards | ============================================= */} + + {[ + { ...KPI_META[0], value: batchTotals.all, percentage: null }, + { + ...KPI_META[1], + value: countData?.uncoveredLength, + percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 + }, + { + ...KPI_META[2], + value: countData?.coveredLength, + percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 + }, + { + ...KPI_META[3], + value: countData?.cancelLength, + percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 + } + ].map((item) => { + const Icon = item.icon; + const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage); + const hasPct = !Number.isNaN(pct) && item.percentage !== undefined && item.percentage !== null; + return ( + + } + color={item.color} + loading={fetchCountIsLoading} + caption={hasPct ? `${Math.abs(pct)}% of total` : undefined} + /> + + ); + })} + + + {/* ============================================= || Filter Bar | ============================================= */} + + + + + + + Filters + + + + + {/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and + "Orders-All" pills. Filters the loaded rows by the slot the + operator picks; defaults to 'all'. Mirrors the dispatch page's + batch model. UI mirrors the polished Absent Riders dropdown from + the orders page — custom Paper, per-batch color/icon, selected + indicator. */} + {(() => { + const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0]; + const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime; + return ( + opts} + options={BATCH_OPTIONS} + value={activeBatch} + onChange={(e, val) => val && setSelectedBatch(val.id)} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + PaperComponent={SoftPaper} + ListboxProps={{ sx: { py: 0, maxHeight: 360 } }} + renderOption={(props, option, { selected }) => { + const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime; + const total = batchTotals[option.id] ?? 0; + return ( +
  • + + + + + + {option.label} + + + {option.range} + + + {/* Per-batch total — same number you see next to each + batch on the dispatch page. Lets the operator + eyeball-compare without summing the per-status + chip counts on the tabs. */} + 0 ? `${option.color}18` : '#f1f5f9', + color: total > 0 ? option.color : '#94a3b8', + border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`, + '& .MuiChip-label': { px: 0.75 } + }} + /> + {selected && ( + + + + )} +
  • + ); + }} + renderInput={(params) => ( + + + + +
    + ) + }} + sx={{ + minWidth: { xs: 180, sm: 220, md: 240 }, + cursor: 'pointer', + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + bgcolor: '#ffffff', + fontWeight: 600, + color: '#0f172a', + paddingRight: '8px', + cursor: 'pointer', + transition: 'border-color 0.15s, box-shadow 0.15s', + '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, + '&:hover fieldset': { borderColor: '#cbd5e1' }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#000000')}` }, + '&.Mui-focused fieldset': { borderColor: '#000000', borderWidth: 1.5 } + }, + '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' } + }} + /> + )} + /> + ); + })()} + + {/* Date range pill — opens the date picker. Stays visible only when + a date range is set. Uses the same pill aesthetic as Batch. */} + {startdate && enddate && ( + setOpen(true)} + sx={{ + pl: 0.5, + pr: 1.25, + py: 0.5, + borderRadius: '10px', + cursor: 'pointer', + bgcolor: '#ffffff', + border: '1px solid #e2e8f0', + transition: 'border-color 0.15s', + '&:hover': { borderColor: '#cbd5e1' } + }} + > + + + + + {dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')} + + + )} +
    + + {/* Tenant */} + { + if (appId == null) { + event.preventDefault(); + OpenToast('Please select your zone first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + } else { + setTenantid(val?.tenantid || 0); + setTenantValue(val); + setLocationid(0); + setLocationValue(null); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#0ea5e9')} + /> + )} + /> + {/* Location */} + (option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '')} + value={locationValue} + PaperComponent={SoftPaper} + sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} + onOpen={(event) => { + if (appId == null && !tenantid) { + event.preventDefault(); + OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } else if (!tenantid) { + event.preventDefault(); + OpenToast('Please select your Tenant first!', 'warning', 3000); + setTimeout(() => tenantRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setLocationid(0); + setLocationValue(null); + } else { + setLocationid(val.locationid || 0); + setLocationValue(val); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#14b8a6')} + /> + )} + /> + {/* Rider */} + { + const name = option?.displayname || option?.authname || ''; + return option?.contactno ? `${name} (${option.contactno})` : name; + }} + sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} + onChange={(e, value, reason) => { + if (reason === 'clear') setRiderid(0); + else setRiderid(value.userid); + }} + onOpen={(event) => { + if (appId == null) { + event.preventDefault(); + OpenToast('Please select your zone first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#8b5cf6')} + /> + )} + /> + +
    + {/* ============================================= || Status Tabs + Search || ============================================= */} + + + + {STATUS_TABS.map((t, idx) => { + const meta = STATUS_META[t.status]; + const Icon = meta.icon; + const active = tabvalue === idx; + const count = batchCounts[t.countKey] ?? 0; + return ( + handleChangetab(e, idx)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: { xs: 0.625, md: 0.875 }, + pl: 0.5, + pr: { xs: 1, md: 1.25 }, + py: 0.5, + flexShrink: 0, + cursor: 'pointer', + borderRadius: DT.radiusField + 'px', + border: `1px solid ${active ? '#000000' : DT.borderSubtle}`, + bgcolor: active ? '#000000' : DT.surface, + color: active ? '#fff' : DT.textSecondary, + fontWeight: 600, + boxShadow: 'none', + transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', + '&:hover': { + borderColor: active ? '#000000' : DT.borderHover, + bgcolor: active ? '#000000' : DT.surfaceAlt + } + }} + > + + + + + {meta.label} + + + {count} + + + ); + })} + + + {/* ============================================= || Search | ============================================= */} + + + + + + {/* ============================================= || Table || ============================================= */} + + + {(() => { + const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered'; + const showSelect = tabstatus == 'Created'; + const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0); + return isMobile ? ( + /* ===================== MOBILE: card list ===================== */ + + {filteredRows.length === 0 && !loading1 && !countSourceLoading && ( + + + + + + No deliveries to show + + + {selectedBatch === 'all' + ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` + : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ + BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' + }.`} + + + )} + {filteredRows.length === 0 && countSourceLoading && ( + + + + Loading deliveries… + + + )} + {filteredRows.map((row, index) => { + const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { + label: row.orderstatus || '—', + color: '#94a3b8', + icon: MdHistoryToggleOff + }; + const RowStatusIcon = rowStatusMeta.icon; + const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + const isOpen = productCollapse?.orderid === row?.orderid; + const chipSx = (c) => ({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + color: c, + fontWeight: 700, + fontSize: 12, + whiteSpace: 'nowrap' + }); + return ( + + + + {showSelect && ( + { + if (e.target.checked) { + let arr = deliverylist; + arr.push({ ...row, sno: deliverylist.length + 1 }); + setDeliverylist([...arr]); + } else { + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + if (res) { + let arr = deliverylist; + arr.splice(res.sno - 1, 1); + arr.map((val, i) => { + val.sno = i + 1; + }); + setDeliverylist([...arr]); + } + } + }} + checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} + /> + )} + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + + + + + {rowStatusMeta.label} + + + + + {row.deliverytype == 'C' && ( + { + if (productCollapse?.orderid === row.orderid) { + setProductCollapse(null); + setOrderHeaderId(null); + } else { + setProductCollapse(row); + setOrderHeaderId(row.orderheaderid); + } + }} + sx={{ + borderRadius: 999, + bgcolor: tint('#06b6d4'), + color: '#06b6d4', + border: `1px solid ${edge('#06b6d4')}`, + '&:hover': { bgcolor: soft('#06b6d4') } + }} + > + {isOpen ? : } + + )} + {showAction && ( + handleMenuOpen(e, row)} + sx={{ + borderRadius: 999, + bgcolor: tint('#000000'), + color: '#000000', + border: `1px solid ${edge('#000000')}`, + '&:hover': { bgcolor: soft('#000000') } + }} + > + + + )} + + + + + {row.tenantname} + + + {[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'} + + + + } + > + + + + {row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname} + + + {row.orderid} · {row.deliveryid} + + + + + {row.pickupcustomer || '—'} + + + {row.pickupcontactno} + + + + + {row.deliverycustomer || '—'} + + + {row.deliverycontactno} + + + + {row.ridername ? ( + + + + + + + {row.ridername} + + + ID #{row.userid} · {row.ridercontact || '—'} + + + + ) : ( + + Unassigned + + )} + + + + {row.transitminutes || 0}m + + + + {Number(row.kms || 0).toFixed(2)} km + {Number(row.cumulativekms || 0).toFixed(2)} km + + + + + ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} + ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} + + + + + + {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} + + + + {row.step ? ( + {row.step} + ) : ( + + — + + )} + + {row.notes && ( + + + {row.notes} + + + )} + + {isOpen && ( + + + + + + + Product Details + + + + {/* GET /admin/bookings has no product image/qty/tax breakdown — this + renders the real "parcels" array (itemcategory/itemdescription/ + declaredvalue) instead of the old product-catalog shape. */} + {orderdetails?.parcels?.map((parcel, idx2) => ( + + + + + + + {parcel?.itemdescription || parcel?.itemcategory || 'Item'} + + + {parcel?.itemcategory || ''} {parcel?.weight ? `· ${parcel.weight}kg` : ''} + + + + ₹ {(parcel?.declaredvalue || 0).toFixed(2)} + + + ))} + + + Delivery Charge + + + ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} + + + + + )} + + ); + })} + {countSourceRows?.length != 0 && ( +
    + {countIsFetchingNext || countHasNext ? ( + + ) : ( + + · End of list · + + )} +
    + )} +
    + ) : ( + + + + {showSelect && ( + + 0 && deliverylist.length != filteredRows.length} + onChange={(e) => { + if (e.target.checked) { + setDeliverylist([...filteredRows]); + } else { + setDeliverylist([]); + } + }} + checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} + /> + + )} + # + Status + Tenant + Order / Location + Pickup + Drop + Rider + ETA + Transit + Kms + Amount + Notes + Step + Qty + COD + {showAction && Action} + + + + {(loading1 || countSourceIsLoading) && } + {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( + + + + + + + + No deliveries to show + + + {selectedBatch === 'all' + ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` + : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ + BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' + }.`} + + + + + )} + {filteredRows.length == 0 && countSourceLoading && ( + + + + + Loading deliveries… + + + + )} + {filteredRows.map((row, index) => { + const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { + label: row.orderstatus || '—', + color: '#94a3b8', + icon: MdHistoryToggleOff + }; + const RowStatusIcon = rowStatusMeta.icon; + const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + return ( + + + {tabstatus == 'Created' && ( + + { + if (e.target.checked) { + let arr = deliverylist; + arr.push({ ...row, sno: deliverylist.length + 1 }); + setDeliverylist([...arr]); + } else { + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + if (res) { + let arr = deliverylist; + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + arr.splice(res.sno - 1, 1); + arr.map((val, i) => { + val.sno = i + 1; + }); + setDeliverylist([...arr]); + } + } + }} + checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} + /> + + )} + + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + {/* Status badge */} + + + + + + + {rowStatusMeta.label} + + + + {/* Tenants */} + + + + + {row.tenantname} + + + {row.tenantsuburb} + + + {row.applocation} + + + + + {/* order details */} + + + + {row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname} + + + + + + + + {row.orderid} + + + {/* express-console-api.md: Doormile timestamps are IST + wall-clock with no timezone marker — bare dayjs() was + still being used here despite this comment saying + otherwise (the fix was never actually applied below, + only claimed) — a Z-suffixed response reinterprets as + local time and shifts 5:30, showing the wrong time + (and sometimes the wrong day). parseDoormileTimestamp + strips that, matching getRowBatchId above. */} + + + {parseDoormileTimestamp(row.orderdate).format('DD/MM/YYYY')} + + + {parseDoormileTimestamp(row.orderdate).format('hh:mm A')} + + + + - + + + + {row.deliveryid} + + + + + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY')} + + + + {parseDoormileTimestamp(row.deliverydate).format('hh:mm A')} + + + + + + {/* pickup — capped to the same maxWidth as drop below, with CSS + ellipsis truncation, so one long address can't stretch this + column wider than its neighbour. row.pickuplocation is just + the full raw address (see fetchDeliveries in api.js), not a + pre-shortened name, so the old `.slice(0, 14)` fallback never + actually ran — it was always overridden by the untruncated + value on its left. Full text still available via the tooltip. */} + + + + {row.pickupcustomer} + + + {row.pickupcontactno} + + + + {row.pickuplocation || row.Pickupaddress || '—'} + + + + + {/* drop — same maxWidth/truncation treatment as pickup above. */} + + + + {row.deliverycustomer} + + + {row.deliverycontactno} + + + + {row.deliverylocation || row.deliveryaddress || '—'} + + + + + {/* rider */} + + {row.ridername ? ( + + + + + + + {row.ridername} + + + ID #{row.userid} · {row.ridercontact || '—'} + + + + ) : ( + + Unassigned + + )} + + {/* Estimated Delivery Time */} + + + {row.expecteddeliverytime ? parseDoormileTimestamp(row.expecteddeliverytime).format('hh:mm A') : '—'} + + + {/* Transit Minutes */} + + + {row.transitminutes || 0}m + + + {/* kms */} + + + + + {Number(row.kms || 0).toFixed(2)} km + + + + + {Number(row.cumulativekms || 0).toFixed(2)} km + + + + + {/* amount */} + + + + + ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} + + + + + ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} + + + + + {/* notes */} + + {row.notes ? ( + + + {row.notes} + + + ) : ( + + — + + )} + + {/* step */} + + {row.step ? ( + + {row.step} + + ) : ( + + — + + )} + + {/* qty */} + + + {row.Quantity || '—'} + + + {/* COD */} + + + {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} + + + {/* Action */} + {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( + + + {row.deliverytype == 'C' && ( + + { + if (productCollapse?.orderid === row.orderid) { + setProductCollapse(null); + setOrderHeaderId(null); + } else { + setProductCollapse(row); + setOrderHeaderId(row.orderheaderid); + } + }} + sx={{ + borderRadius: 999, + bgcolor: tint('#06b6d4'), + color: '#06b6d4', + border: `1px solid ${edge('#06b6d4')}`, + '&:hover': { bgcolor: soft('#06b6d4') } + }} + > + {productCollapse?.orderid === row.orderid ? ( + + ) : ( + + )} + + + )} + + + handleMenuOpen(e, row)} + sx={{ + borderRadius: 999, + bgcolor: tint('#000000'), + color: '#000000', + border: `1px solid ${edge('#000000')}`, + '&:hover': { bgcolor: soft('#000000') } + }} + > + + + + + + )} + + {productCollapse?.orderid === row?.orderid && ( + + + + + + + + + + + Product Details + + +
    + + + # + Item + Category + Weight + Declared Value + + + + {/* GET /admin/bookings has no product image/qty/tax breakdown — + this renders the real "parcels" array instead of the old + product-catalog shape. */} + {orderdetails?.parcels?.map((parcel, idx2) => ( + + + {String(idx2 + 1).padStart(2, '0')} + + + + + + + + {parcel?.itemdescription || 'Item'} + + + + {parcel?.itemcategory || '-'} + + {parcel?.weight ? `${parcel.weight}kg` : '-'} + + + ₹ {(parcel?.declaredvalue || 0).toFixed(2)} + + + ))} + + + Delivery Charge + + + ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} + + + +
    +
    + + + + + )} + + ); + })} + {countSourceRows?.length != 0 && ( + + +
    + {countIsFetchingNext ? ( + + ) : countHasNext ? ( + + ) : ( + + · End of list · + + )} +
    +
    +
    + )} + + + ); + })()} + +
    + + {/* Shared row-action menu — single instance reused by both the desktop + table rows and the mobile cards (both trigger handleMenuOpen). */} + + {selectedRow?.orderstatus !== 'delivered' && ( + { + notifyRiderMutation.mutate(selectedRow.milerprofileid); + handleMenuClose(); + }} + > + + + + Notify Rider + + )} + {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && ( + { + if (appId == null) { + opentoast('Please select a location first!', 'warning'); + locationRef.current?.focus(); + return; + } + setChangeDialogOpen(true); + handleMenuClose(); + }} + > + + + + Change Rider + + )} + {/* Was gated to roleid 1/2 (staff-only), matching the reference app's + identical gate — opened to every login per explicit decision, so + tenant operators can reach the same Update Status fields staff + already could. */} + { + // PUT /admin/consignments/:id/status is consignment-scoped — it + // only exists once the rider marks pickup-complete (doormile-flow.md). + // Without a real consignmentid, updateDeliveryAPI's `?? deliveryid` + // fallback was sending the BOOKING id where a consignment id is + // required — a different id space, so the call failed against + // the wrong (or nonexistent) resource. Combined with the mutation's + // onSuccess only handling `res.success === true` with no else + // branch, that failure was completely silent — dialog just sat + // there looking like nothing happened. Block it here instead, + // with a message that explains why, rather than letting the + // submit silently no-op. + if (!selectedRow.consignmentid) { + opentoast("This order hasn't been picked up yet — status can only be updated once it becomes a consignment.", 'warning'); + handleMenuClose(); + return; + } + setKms(selectedRow.kms); + setCumulativeKms(selectedRow.cumulativekms); + setDeliverylat(selectedRow.droplat); + setDeliverylong(selectedRow.droplon); + setNotes(selectedRow.notes); + setDeliveryamount(selectedRow.deliveryamt ?? selectedRow.deliveryamount); + setUpdateStatus(selectedRow.orderstatus || 'delivered'); + setCurrentorder(selectedRow); + setDialogopen(true); + handleMenuClose(); + }} + > + + + + Update Status + + {selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && ( + { + setCancelDeliveryOpen(true); + handleMenuClose(); + }} + > + + + + Cancel Delivery + + )} + + + {/* =============================== || cancel dialog || =============================== */} + setCancelDeliveryOpen(false)} + maxWidth="xs" + fullWidth + PaperProps={{ + elevation: 0, + sx: { + borderRadius: 3, + border: '1px solid', + borderColor: DT.borderSubtle, + boxShadow: DT.shadowPop, + overflow: 'hidden' + } + }} + > + + + + + + + + + Cancel Delivery? + + + This action will cancel the delivery and notify the rider. Please share a brief reason. + + + setCancelFeed(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: DT.surfaceAlt, + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: '#ef4444' }, + '&.Mui-focused fieldset': { borderColor: '#ef4444', borderWidth: 2 }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#ef4444')}` } + } + }} + /> + + + + + + + + {/* =============================== || change rider dialog || =============================== */} + setChangeDialogOpen(false)} + maxWidth="sm" + fullWidth + PaperProps={{ + elevation: 0, + sx: { + borderRadius: 3, + border: '1px solid', + borderColor: DT.borderSubtle, + boxShadow: DT.shadowPop, + overflow: 'hidden' + } + }} + > + + + + + + + + + Change Rider + + + Assign this delivery to a different rider + + + + + + { + const name = option?.displayname || option?.authname || ''; + return option?.contactno ? `${name} (${option.contactno})` : name; + }} + onChange={(e, value) => { + setSelectedRider(value); + logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#8b5cf6')} + /> + )} + /> + + + + + + + + + + {/* =============================== || Date filter Dialog || =============================== */} + setOpen(false)} + fullScreen={isMobile} + PaperProps={{ + elevation: 0, + sx: { + borderRadius: { xs: 0, sm: 3 }, + border: '1px solid', + borderColor: DT.borderSubtle, + boxShadow: DT.shadowPop, + overflow: 'hidden' + } + }} + > + + + + + + + + + Date Range + + + Pick a window to filter the deliveries + + + + + + setOpen(!open)} + id="daterange1" + onChange={(range) => { + if (range.label === 'All') { + setStartdate(''); + setEnddate(''); + + setOpen(false); + } else { + setStartdate(dayjs(range.startDate).format('YYYY-MM-DD')); + setEnddate(dayjs(range.endDate).format('YYYY-MM-DD')); + if (range.label) { + setDatestatus(range.label); + } else { + setDatestatus(''); + } + } + }} + definedRanges={[ + { + label: 'Today', + startDate: new Date(), + endDate: new Date() + }, + { + label: 'Yesterday', + startDate: addDays(new Date(), -1), + endDate: addDays(new Date(), -1) + }, + { + label: 'Tomorrow', + startDate: addDays(new Date(), +1), + endDate: addDays(new Date(), +1) + }, + { + label: 'This Week', + startDate: startOfWeek(new Date()), + endDate: endOfWeek(new Date()) + }, + { + label: 'Last Week', + startDate: startOfWeek(addWeeks(new Date(), -1)), + endDate: endOfWeek(addWeeks(new Date(), -1)) + }, + { + label: 'Last 7 Days', + startDate: addWeeks(new Date(), -1), + endDate: new Date() + }, + { + label: 'This Month', + startDate: startOfMonth(new Date()), + endDate: endOfMonth(new Date()) + }, + { + label: 'Last Month', + startDate: startOfMonth(addMonths(new Date(), -1)), + endDate: endOfMonth(addMonths(new Date(), -1)) + } + // { + // label: 'All', + // startDate: new Date(), + // endDate: addDays(new Date(), -1), + // }, + ]} + /> + + + + + + + {/* =============================== || Update Delivery Dialog || =============================== */} + + + + + + + + + + Update Delivery Status + + + {currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'} + + + + + + + {[ + { label: 'KMs', value: kms, set: (v) => setKms(v), color: '#ef4444' }, + { label: 'Actual KMs', value: cumulativekms, set: (v) => setCumulativeKms(+v), color: '#10b981' }, + { label: 'Delivery Latitude', value: deliverylat, set: (v) => setDeliverylat(v), color: '#06b6d4' }, + { label: 'Delivery Longitude', value: deliverylong, set: (v) => setDeliverylong(v), color: '#06b6d4' } + ].map((f) => ( + + + {f.label} + + f.set(e.target.value)} + sx={{ + mt: 0.5, + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: '#fff', + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: f.color }, + '&.Mui-focused fieldset': { borderColor: f.color, borderWidth: 2 }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(f.color)}` } + } + }} + /> + + ))} + + + Amount + + setDeliveryamount(+e.target.value)} + sx={{ + mt: 0.5, + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: '#fff', + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: '#10b981' }, + '&.Mui-focused fieldset': { borderColor: '#10b981', borderWidth: 2 }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#10b981')}` } + } + }} + /> + + + + Status + + setUpdateStatus(e.target.value)} + SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 2, mt: 0.5, boxShadow: DT.shadowPop } } } }} + sx={{ + mt: 0.5, + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: '#fff', + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: '#000000' }, + '&.Mui-focused fieldset': { borderColor: '#000000', borderWidth: 2 }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#000000')}` } + } + }} + > + {['pending', 'accepted', 'started', 'arrived', 'delivered', 'cancelled'].map((s) => { + const m = STATUS_META[s] || { label: s, color: '#000000', icon: MdHistoryToggleOff }; + const Ic = m.icon; + return ( + + + + + {m.label} + + ); + })} + + + + + Notes + + setNotes(e.target.value)} + sx={{ + mt: 0.5, + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: '#fff', + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: '#000000' }, + '&.Mui-focused fieldset': { borderColor: '#000000', borderWidth: 2 }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#000000')}` } + } + }} + /> + + + + + + + + + + + + ); +}; + +export default Deliveries; diff --git a/src/pages/nearle/dispatch/ActiveSection.js b/src/pages/nearle/dispatch/ActiveSection.js index 9284fd2..d75580f 100644 --- a/src/pages/nearle/dispatch/ActiveSection.js +++ b/src/pages/nearle/dispatch/ActiveSection.js @@ -8,7 +8,7 @@ import { MdInventory2 } from 'react-icons/md'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; -import { CircularProgress } from '@mui/material'; +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; import { getStatusStyle, getActiveOrder } from './dispatchShared'; import { OpenToast } from 'components/third-party/OpenToast'; @@ -76,7 +76,7 @@ const ActiveSection = ({ if (isLoading) { return (
    - +
    Loading active deliveries...
    ); diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index fcb8486..c2ca58a 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -3,11 +3,11 @@ --bg-sub: #f8fafc; --bg-card: #ffffff; --border: #e2e8f0; - --border-active: #D25463; + --border-active: #3A3A3A; --text: #1e293b; --text-muted: #64748b; - --accent: #D25463; - --accent-soft: rgba(210, 84, 99, 0.08); + --accent: #3A3A3A; + --accent-soft: rgba(58, 58, 58, 0.08); --kitchen: #f59e0b; --kitchen-soft: rgba(245, 158, 11, 0.1); --success: #22c55e; @@ -78,7 +78,7 @@ width: 32px; height: 32px; border-radius: 8px; - background: linear-gradient(135deg, #D25463, #2563eb); + background: linear-gradient(135deg, #3A3A3A, #2563eb); display: flex; align-items: center; justify-content: center; @@ -318,7 +318,7 @@ background: var(--accent); border-color: var(--accent); color: #fff; - box-shadow: 0 4px 12px rgba(210, 84, 99, 0.25); + box-shadow: 0 4px 12px rgba(58, 58, 58, 0.25); } /* SVG icon slot inside each tab button — fixed square, color inherits from button @@ -399,7 +399,7 @@ .dispatch-container .strat-stat-orders { background: var(--accent-soft); - border-color: rgba(210, 84, 99, 0.25); + border-color: rgba(58, 58, 58, 0.25); } .dispatch-container .strat-stat-orders .strat-stat-value { @@ -521,22 +521,22 @@ } .dispatch-container .date-chip.is-open { - border-color: #C01227; - box-shadow: 0 0 0 3px rgba(192, 18, 39, 0.2), - 0 12px 30px rgba(192, 18, 39, 0.22); + border-color: #000000; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.2), + 0 12px 30px rgba(0, 0, 0, 0.22); } .dispatch-container .date-chip:hover { - border-color: rgba(192, 18, 39, 0.45); + border-color: rgba(0, 0, 0, 0.45); box-shadow: 0 2px 4px rgba(15, 23, 42, 0.06), - 0 8px 22px rgba(192, 18, 39, 0.15); + 0 8px 22px rgba(0, 0, 0, 0.15); transform: translateY(-1px); } .dispatch-container .date-chip:focus-within { - border-color: #C01227; - box-shadow: 0 0 0 3px rgba(192, 18, 39, 0.2), - 0 8px 22px rgba(192, 18, 39, 0.22); + border-color: #000000; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.2), + 0 8px 22px rgba(0, 0, 0, 0.22); } /* Center card — visible chrome the operator reads. Renders as a - - + + + + + + set('latitude')(v ?? '')} + isOptional + /> + set('longitude')(v ?? '')} + isOptional + /> + + + + + + + + - } - /> - - - - - {/* ====================================== ||Contact Name (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Contact Name" - value={pickCust.firstname} - onChange={(e) => { - setPickCust({ ...pickCust, firstname: e.target.value }); - }} - /> - - {/* ====================================== ||Contact Number(pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Contact Number" - value={pickCust.contactno} - onChange={(e) => { - if (e.target.value.length <= 10) { - setPickCust({ ...pickCust, contactno: e.target.value }); - } - if (pickNum == e.target.value) { - setShowCheck1(0); - } else { - setShowCheck1(1); - } - if (e.target.value.length < 10) { - setNumErr1(true); - } else { - setNumErr1(false); - } - }} - /> - - {/* ====================================== || Address (pick) || ====================================== */} - - - {addId1 == 0 ? ( -
    - { - setInputValue2(''); - setPickCust({ - ...pickCust, - doorno: '', - suburb: '', - city: '', - postcode: '', - landmark: '' - }); - setShowDistance(false); - setStartPoint({ latitude: 0, longitude: 0 }); - }} - size="small" - > - - - ) - } - }} - /> -
    - ) : ( - { - setAddId1(0); - setPickCust({ - ...pickCust, - // firstname: '', - // contactno: '', - doorno: '', - suburb: '', - city: '', - postcode: '', - landmark: '' - }); - setShowDistance(false); - setStartPoint({ latitude: 0, longitude: 0 }); - }} - > - - - ) - }} - variant="outlined" - placeholder="Select" - value={pickCust.address} - onChange={(e) => { - setPickCust({ ...pickCust, address: e.target.value }); - if (e.target.value == '') { - setAddId1(0); - setShowDistance(false); - setStartPoint({ latitude: 0, longitude: 0 }); - } - }} - /> - )} -
    -
    - - {/* ====================================== ||Door No (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Door No / Street" - value={pickCust.doorno} - onChange={(e) => { - setPickCust({ ...pickCust, doorno: e.target.value }); - }} - /> - - {/* ====================================== || Suburb (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Location" - value={pickCust.suburb} - onChange={(e) => { - setPickCust({ ...pickCust, suburb: e.target.value }); - }} - /> - - {/* ====================================== || City (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="City" - value={pickCust.city} - onChange={(e) => { - setPickCust({ ...pickCust, city: e.target.value }); - }} - /> - - {/* ====================================== || postcode (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Postcode" - value={pickCust.postcode} - onChange={(e) => { - setPickCust({ ...pickCust, postcode: e.target.value }); - }} - /> - - {/* ====================================== || Landmark (pick) || ====================================== */} - - - - - ) - }} - variant="outlined" - label="Landmark" - value={pickCust.landmark} - onChange={(e) => { - setPickCust({ ...pickCust, landmark: e.target.value }); - }} - /> - - {/* ====================================== ||Checkbox save for later (pick) || ====================================== */} - {showCheck1 == 1 && ( - - - { - setIsNumChange1(e.target.checked ? 1 : 0); - }} - /> - } - label="Save For Later" - /> - - - )} -
    -
    -
    -
    - - - {/* ================================================= || Drop || ================================================= */} - - - } - title="Drop Details" - subtitle="Where the order is delivered" - action={ - - } - /> - - - - - - Drop Details - - - {/* Customer */} - {/* { - if (val) { - setDropswitch(true); - setClientinfo({}); - } else { - setDropswitch(false); - setClientinfo({}); - } - }} - size="small" - /> - Business */} - - - - {/* new2 */} - {/* `${option.firstname} (${option.contactno})`} - sx={{ mt: 0, mb: 1 }} - renderInput={(params) => ( - { - setSearchCustList(e.target.value); - }} - InputProps={{ - ...params.InputProps, - inputProps: { - ...params.inputProps, - maxLength: 10 - } - }} - /> - )} - onChange={(e, val, reason) => { - if (val) { - if (pickCust.customerid === val.customerid) { - opentoast('Select Another Customer', 'error'); - setSearchCustList(''); - } else { - setDropCust(val); - setEndPoint({ latitude: val.latitude, longitude: val.longitude }); - setAddId2(1); - } - } - if (reason == 'clear') { - setSearchCustList(''); - } - }} - noOptionsText={ - /^[0-9]{10}$/.test(searchCustList) ? ( - - { - setDropCust({ ...dropCust, contactno: searchCustList }); - handleOkClick2(); - setSearchCustList(''); - }} - /> - - ) : null - } - /> */} - - - - - {/* ====================================== ||Contact Name (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.firstname} - onChange={(e) => { - setDropCust({ ...dropCust, firstname: e.target.value }); - }} - /> - - {/* ====================================== ||Contact Number (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.contactno} - onChange={(e) => { - if (e.target.value.length <= 10) { - setDropCust({ ...dropCust, contactno: e.target.value }); - } - if (dropNum == e.target.value) { - setShowCheck2(0); - } else { - setShowCheck2(1); - } - if (e.target.value.length < 10) { - setNumErr2(true); - } else { - setNumErr2(false); - } - }} - /> - - - - {addId2 == 0 ? ( -
    - { - setInputValue3(''); - setDropCust({ - ...dropCust, - doorno: '', - suburb: '', - city: '', - postcode: '', - landmark: '' - }); - setShowDistance(false); - setEndPoint({ latitude: 0, longitude: 0 }); - }} - size="small" - > - - - ) - } - }} - /> -
    - ) : ( - { - setAddId2(0); - setDropCust({ - ...dropCust, - firstname: '', - contactno: '', - doorno: '', - suburb: '', - city: '', - postcode: '', - landmark: '' - }); - setShowDistance(false); - setEndPoint({ latitude: 0, longitude: 0 }); - }} - > - - - ) - }} - variant="outlined" - placeholder="Select" - value={dropCust.address} - onChange={(e) => { - setPickCust({ ...dropCust, address: e.target.value }); - if (e.target.value == '') { - setAddId2(0); - setShowDistance(false); - setEndPoint({ latitude: 0, longitude: 0 }); - } - }} - /> - )} -
    -
    - - {/* ====================================== ||Door No (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.doorno} - onChange={(e) => { - setDropCust({ ...dropCust, doorno: e.target.value }); - }} - /> - - {/* ====================================== ||Suburb (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.suburb} - onChange={(e) => { - setDropCust({ ...dropCust, suburb: e.target.value }); - }} - /> - - {/* ====================================== ||City (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.city} - onChange={(e) => { - setDropCust({ ...dropCust, city: e.target.value }); - }} - /> - - - {/* ====================================== ||Postcode (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.postcode} - onChange={(e) => { - setDropCust({ ...dropCust, postcode: e.target.value }); - }} - /> - - {/* ====================================== ||Landmark (drop) || ====================================== */} - - - - - ) - }} - value={dropCust.landmark} - onChange={(e) => { - setDropCust({ ...dropCust, landmark: e.target.value }); - }} - /> - - {/* ====================================== ||Checkbox save for later (drop) || ====================================== */} - {showCheck2 == 1 && ( - - - { - setIsNumChange2(e.target.checked ? 1 : 0); - }} - /> - } - label="Save For Later" - /> - - - )} -
    -
    - {/* - - */} -
    -
    -
    -
    - {/* ================================================= || Time || ================================================= */} - - - } title="Schedule" subtitle="Pickup date & time slot" /> - - - - - Date - - - { - let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - setSelectedtime(''); - if (dateres11 <= 0) { - setStartdate(e); - setEnddate(e); - - let arr = []; - timeslotarr.map((val) => { - if ( - dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - ) { - arr.push(val); - } - }); - if (arr[0]) { - setOrderarr([ - { - sno: 1, - address: '', - customerid: '', - deliverytime: dayjs(arr[0]) || '', - deliverylocationid: '', - clientname: '', - contactno: '', - latitude: '', - longitude: '' - } - ]); - } else { - setOrderarr([]); - } - } else { - setAlertmessage('choose Upcoming Date'); - opentoast('choose Upcoming Date', 'warning'); - setStartdate(NaN); - } - }} - value={dayjs(startdate)} - sx={{ width: '100%', mt: 2 }} - disablePast - /> - - - Time - - - - {timeslotarr.map((val, index) => { - if ( - dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= - 0 - ) { - const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm'); - return ( - { - if (distance > appLocaRadius) { - setOpen4(true); - } else if (showDistance) { - setSelectedtime(val); - } else { - opentoast('Select Pickup and Drop', 'error'); - } - }} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: 0.5, - px: 1.25, - py: 0.5, - cursor: 'pointer', - borderRadius: 999, - border: `1.5px solid ${active ? '#f59e0b' : edge('#f59e0b')}`, - bgcolor: active ? '#f59e0b' : tint('#f59e0b'), - color: active ? '#fff' : '#f59e0b', - fontSize: 12, - fontWeight: 800, - transition: 'all 0.15s', - boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : 'none', - '&:hover': { - borderColor: '#f59e0b', - boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : `0 0 0 3px ${ring('#f59e0b')}` - } - }} - > - {dayjs(val).format('hh:mm A')} - - ); - } - })} - - - - - - - - - - } - title="Distance & Pricing" - subtitle="Auto-calculated from pickup → drop" - /> - - {showDistance && ( - - - - - - - - - )} - - Category - } - onChange={(event, value) => setCategory(value || null)} - onInputChange={(event, value, reason) => { - if (reason === 'input') setCategory(value || null); - }} - /> - - Weight - - { - handleChipClick('1-10kgs'); - setWeight('1-10kgs'); - }} - /> - { - handleChipClick('11-20kgs'); - setWeight('11-20kgs'); - }} - /> - { - handleChipClick('21-30kgs'); - setWeight('21-30kgs'); - }} - /> - - - SMS Delivery - { - setIsSms(e.target.checked ? 1 : 0); - }} - /> - - - - - - - - - {/* ============================== || Cash Collect & Quantity || ============================== */} - - } - title="Collection & Quantity" - subtitle="Cash to collect on delivery, total items" - /> - - - - setCollectionamt(e.target.value)} - inputProps={{ min: 0 }} - /> - - - setQuantity(e.target.value)} - inputProps={{ min: 1 }} - /> - - - - - - {/* ================================================= || Notes || ================================================= */} - - } - title="Notes" - subtitle="Add anything the rider should know" - /> - - setOtherinstructions(e.target.value)} - /> - - - {!showDistance && ( - - Set pickup & drop to enable - - )} - - - - - - - - {/* - - - Select Time - - - - - - {timeslotarr.map((val) => { - if ( - dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - // && currentsno - ) { - return ( - <> - - { - setSelectedtime(dayjs(val).format('hh:mm A')); - }} - /> - - - ); - } - })} - - - - - - - - */} - - {/* ============================================= || saved address Dialog || ============================================= */} - setIsCustomerOpen(false)} - fullWidth - maxWidth="sm" - PaperProps={{ sx: { borderRadius: 3 } }} - > - - - - - - - - Saved Contacts - - - Pick a saved contact's name & phone for {pickordrop === 1 ? 'Pickup' : 'Drop'} — address isn't stored, search it below - - - - - - setSearchCustList(e.target.value)} - sx={{ - flex: 1, - '& .MuiOutlinedInput-input': { p: '6px 4px', fontSize: 13, fontWeight: 600 }, - '& fieldset': { border: 'none' } - }} - endAdornment={ - searchCustList && ( - setSearchCustList('')} sx={{ p: 0.25, color: BRAND }}> - - - ) - } - autoComplete="off" - /> - - - - {customerlist.length === 0 ? ( - - - - - - No saved contacts - - - {searchCustList ? 'Try a different keyword.' : 'Place an order with a new contact — it will show up here next time.'} - - - ) : ( - - {customerlist.map((address, index) => { - // GET /admin/customers (B2C directory) — { appcustomerid, - // createdat, email, name, phone, totalbookings }. No - // address/coordinates at all (that was a jupiter-only field - // on the old customer record). Picking a saved contact here - // only pre-fills the name & phone; the operator still has - // to search the address below like a brand-new contact. - const contactPhone = address.phone; - const isUsed = !!contactPhone && pickCust.phone === contactPhone; - return ( - { - if (isUsed) return; - setIsCustomerOpen(false); - const contact = { firstname: address.name, contactno: contactPhone, phone: contactPhone }; - if (pickordrop === 1) { - setAddId1(1); - setPickCust((prev) => ({ ...prev, ...contact })); - setPickNum(contactPhone); - } else { - setAddId2(1); - setDropCust((prev) => ({ ...prev, ...contact })); - setdropNum(contactPhone); - } - }} - sx={{ - display: 'flex', - alignItems: 'flex-start', - gap: 1.25, - p: 1.5, - borderRadius: 2, - border: '1px solid', - borderColor: isUsed ? edge('#94a3b8') : DT.borderSubtle, - bgcolor: isUsed ? DT.surfaceAlt : '#fff', - cursor: isUsed ? 'not-allowed' : 'pointer', - opacity: isUsed ? 0.6 : 1, - transition: 'all 0.15s', - '&:hover': isUsed - ? {} - : { - borderColor: edge(BRAND), - bgcolor: tint(BRAND), - boxShadow: DT.shadowSoft - } - }} - > - - {String(address.name || '?') - .charAt(0) - .toUpperCase()} - - - - - {address.name || 'Unnamed'} - - - {contactPhone || '—'} - - {isUsed && ( - - Already used - - )} - - - {address.email || 'No address on file — search it below'} - - - - ); - })} - - )} - - - - - - - setOpen4(false)} PaperProps={{ sx: { borderRadius: 3 } }}> - - - - - - - - Out of Service Area - - - This drop point is outside the supported radius - - - - - - - Service is not available at this location. Try a different drop point within the coverage zone. - - - - - - - - - ); -}; - -export default Createorder1; +import { + MdAddShoppingCart, + MdMyLocation, + MdLocationOn, + MdSchedule, + MdLocalShipping, + MdAttachMoney, + MdNotes, + MdCheckCircle, + MdReceiptLong, + MdInventory2, + MdStraighten, + MdPersonPin, + MdPerson, + MdHistoryToggleOff +} from 'react-icons/md'; + +const dtA = (c, suffix) => `${c}${suffix}`; +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; + +// Soft card section header — coloured banner above each form section. +const SectionHeader = ({ color, icon, title, subtitle, action }) => ( + + + {icon} + + {title} + {subtitle && ( + + {subtitle} + + )} + + + {action} + +); + +// Section wrapper Paper used as the outer card for each form block. +const SectionCard = ({ children, sx = {} }) => ( + + {children} + +); + +// Some saved locations come back from GET /admin/tenants/:id/locations with +// a blank locationname (live-confirmed, e.g. tenant 1's only location) — fall +// back to the address rather than rendering a bare icon with no text. +const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location'; + +// Tenants with many locations in the same city (DailyGrubs has 23, all in +// Coimbatore) made the old "(city)" suffix useless for telling them apart — +// sort primary-first then alphabetically so the picker is at least ordered +// the same way every time; the full address is shown per-option instead +// (see renderOption on the Business Location Autocomplete below). +const sortLocations = (locations) => + [...(locations || [])].sort((a, b) => { + if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1; + return locationDisplayName(a).localeCompare(locationDisplayName(b)); + }); + +// POST /admin/expressbooking's parcels[].itemcategory is a free-text string +// (see express-console-api.md's sample body) — the old jupiter backend had a +// real /utils/getsubcategories lookup table behind this dropdown, but that +// endpoint has no equivalent on the new API (confirmed — see +// src/pages/api/CLAUDE.md's list of retired-with-no-replacement resources). +// This is a fixed, curated list rather than a live lookup so the dropdown +// actually has options; `freeSolo` lets an operator type a category that +// isn't on the list instead of being blocked. +const PARCEL_CATEGORIES = ['Food', 'Groceries', 'Documents', 'Electronics', 'Clothing & Apparel', 'Medicines', 'Furniture', 'Others']; + +const Createorder1 = () => { + // ================================================= || GoogleMaps (Drawer) || ================================================= + const [inputValue, setInputValue] = React.useState(''); + const [inputValue1, setInputValue1] = React.useState(''); + const [inputValue2, setInputValue2] = React.useState(''); + const [inputValue3, setInputValue3] = React.useState(''); + const [options, setOptions] = React.useState([]); + const [options1, setOptions1] = React.useState([]); + const loaded1 = React.useRef(false); + const [mobilenumber, setMobilenumber] = useState(''); + const [emailaddress, setEmailaddress] = useState(''); + const [city, setCity] = useState(''); + const [city1, setCity1] = useState(''); + const [zipcode, setZipcode] = useState(''); + const [zipcode1, setZipcode1] = useState(''); + const [state, setState] = useState(''); + const [state1, setState1] = useState(''); + const [suburb, setSuburb] = useState(''); + const [suburb1, setSuburb1] = useState(''); + const [pickContactName, setPickContactName] = useState(''); + const [dropContactName, setDropContactName] = useState(''); + const [pickdoorno, setPickDoorno] = useState(''); + const [dropDoorno, setDropDoorno] = useState(''); + const [pickLandmark, setPickLandmark] = useState(''); + const [dropLandmark, setDropLandmark] = useState(''); + const [latlong, setLatlong] = useState({}); + const [latlong1, setLatlong1] = useState({}); + const autocompleteService = useRef(null); + const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid')); + const [isLocation, setIsLocation] = useState(false); + // Drives what shows in place of the Business Location picker. A toast alone + // wasn't enough — it's easy to miss, and an empty Autocomplete with no + // toast visible reads as "broken" with zero explanation. This makes the + // reason a persistent, visible message instead. + const [locationsStatus, setLocationsStatus] = useState('loading'); + const textFieldRef1 = useRef(null); + const textFieldRef2 = useRef(null); + const [collectionamt, setCollectionamt] = useState(0); + const [quantity, setQuantity] = useState(1); + + const handleOkClick1 = () => { + // Set focus back to the text field after clicking the "OK" chip + if (textFieldRef1.current) { + textFieldRef1.current.focus(); + } + }; + + const handleOkClick2 = () => { + // Set focus back to the text field after clicking the "OK" chip + if (textFieldRef2.current) { + textFieldRef2.current.focus(); + } + }; + + const top100Films = [ + { label: 'The Shawshank Redemption', year: 1994 }, + { label: 'The Godfather', year: 1972 }, + { label: 'The Godfather: Part II', year: 1974 }, + { label: 'The Dark Knight', year: 2008 }, + { label: '12 Angry Men', year: 1957 } + ]; + + const appId = localStorage.getItem('applocationid'); + const navigate = useNavigate(); + const [open, setOpen] = useState({}); + const [open1, setOpen1] = useState(''); + const [open2, setOpen2] = useState(false); + const [open3, setOpen3] = useState(false); + const [open4, setOpen4] = useState(false); + const [shift, setShift] = useState(1); + const [clientlist, setClientlist] = useState([]); + const [clientdetail, setClientdetail] = useState([]); + const [eventname, setEventname] = useState(''); + const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); + const [enddate, setEnddate] = useState(dayjs().add(1, 'day').format('MM-DD-YYYY')); + // const [starttime, setStatrttime] = useState(`${dayjs().format('MM-DD-YYYY')} 08:00:00`); + const [starttime, setStatrttime] = useState(); + // const [endtime, setEndtime] = useState(`${dayjs().format('MM-DD-YYYY')} 20:00:00`); + const [endtime, setEndtime] = useState(); + const [timeslotarr, setTimeslotarr] = useState([]); + const [currentsno, setCurrentsno] = useState(''); + const [roleoptions, setRoleoptions] = useState([]); + const theme = useTheme(); + const [otherinstructions, setOtherinstructions] = useState(''); + const [attireslist, setAttireslist] = useState([]); + const [serviceaddonslist, setServiceaddonslist] = useState([]); + const [orderaddonobj, setOrderaddonobj] = useState([]); + const [stafflist, setStafflist] = useState([]); + const [loading2, setLoading2] = useState(false); + const [loading, setLoading] = useState(false); + const [shiftarr, setShiftarr] = useState([]); + const [shiftarr1, setShiftarr1] = useState([]); + const [orderarr, setOrderarr] = useState([]); + const [alertmessage, setAlertmessage] = useState(''); + const [tabstatus, setTabstatus] = useState(''); + const [tenantinfo, setTenantinfo] = useState({}); + const [searchword, setSearchword] = useState(''); + const [clientdetailarr, setClientdetailarr] = useState([]); + const [admintoken, setAdmintoken] = useState(); + const [tenantlocationlist, setTenantlocationlist] = useState([]); + const [tenant, setTenant] = useState({}); + const [clientinfo, setClientinfo] = useState({}); + const [selectedtime, setSelectedtime] = useState(''); + const [tenantlist, setTenantlist] = useState([]); + const [tenantid, setTenantid] = useState(); + const [tenantlocation, setTenantlocation] = useState(''); + const [pickupswitch, setPickupswitch] = useState(true); + const [deliverytype, setDeliverytype] = useState('B'); + const [dropswitch, setDropswitch] = useState(false); + const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 }); + const [endPoint, setEndPoint] = useState({ latitude: 0, longitude: 0 }); + const [showDistance, setShowDistance] = useState(false); + const [distance, setDistance] = useState(0); + const [basePrice, setBasePrice] = useState(0); + const [pricePerKm, setPricePerKm] = useState(0); + const [minKm, setMinKm] = useState(0); + const [totalCharge, setTotalCharge] = useState(0); + const [category, setCategory] = useState(null); + const [weight, setWeight] = useState(''); + // Per express-console-api.md's "Tenant scoping" section: token tenantid + // 0/null means Doormile's own staff, who can see/act on every tenant — + // NOT an error state. A client (tenant) login carries a real tenantid and + // never needs this picker at all. loggedInTenantId is what's actually on + // the account; selectedTenantId is what the page operates as right now + // (auto-filled for a client login, chosen from the picker for staff). + const loggedInTenantId = localStorage.getItem('tenantid'); + const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; + const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId); + const [staffTenantList, setStaffTenantList] = useState([]); + const [staffTenantListLoading, setStaffTenantListLoading] = useState(false); + const tid = selectedTenantId; + const [selectedCatChip, setSelectedCatChip] = useState(null); + const [isCustomerOpen, setIsCustomerOpen] = useState(false); + const [searchCustList, setSearchCustList] = useState(''); + const [customerlist, setCustomerlist] = useState([]); + const [pickCust, setPickCust] = useState({}); + const [dropCust, setDropCust] = useState({}); + const [pickordrop, setpickordrop] = useState(0); // 1 ->pick 2 -> drop + const [addId1, setAddId1] = useState(0); + const [addId2, setAddId2] = useState(0); + const [tenantLocations, setTenantlocations] = useState([]); + const [appLocaLat, setAppLocaLat] = useState(); + const [appLocaLng, setAppLocaLng] = useState(); + const [appLocaRadius, setAppLocaRadius] = useState(); + const [isNumChange1, setIsNumChange1] = useState(0); + const [isNumChange2, setIsNumChange2] = useState(0); + const [showCheck1, setShowCheck1] = useState(0); + const [showCheck2, setShowCheck2] = useState(0); + const [pickNum, setPickNum] = useState(); + const [dropNum, setdropNum] = useState(); + const [numErr1, setNumErr1] = useState(false); + const [numErr2, setNumErr2] = useState(false); + const [isSms, setIsSms] = useState(0); + + const handleChipClick = (chipLabel) => { + setSelectedCatChip(chipLabel); + }; + + const chipStyle = (chipLabel) => ({ + cursor: 'pointer', + backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : 'default', + color: selectedCatChip === chipLabel ? '#fff' : '', + '&:hover': { + backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : theme.palette.primary.light, + color: '#fff' + } + }); + + const fetchTenantPricing = async () => { + try { + const pricing = (await getAdminPricing()) || []; + // tid comes from localStorage (always a string) but pricing.tenantid is + // numeric — a strict === here never matched, so basePrice/pricePerKm/ + // minKm stayed at their 0 defaults and every order priced at ₹0. + const match = pricing.find((p) => String(p.tenantid) === String(tid)); + if (match) { + setBasePrice(match.baseprice); + setPricePerKm(match.priceperkm); + setMinKm(match.basedistance); + } + } catch (error) { + // swallow — pricing defaults to 0 and stays there, handled upstream + } + }; + useEffect(() => { + if (tid) fetchTenantPricing(); + }, [tid]); + + useEffect(() => { + if (startPoint.latitude != 0 && startPoint.longitude != 0 && endPoint.latitude != 0 && endPoint.longitude != 0) { + // getDistance(); + calculateDistance(startPoint, endPoint); + } + }, [startPoint, endPoint]); + + const getDistance = () => { + const dist = geolib.getPreciseDistance(startPoint, endPoint, 1); + const distanceInKm = (dist / 1000).toFixed(2); + const roundedDistance = Math.round(distanceInKm); + setDistance(roundedDistance.toFixed(2)); + if (roundedDistance < minKm) { + setTotalCharge(basePrice); + } else { + const total = (roundedDistance - minKm) * pricePerKm + basePrice; + setTotalCharge(total); + } + setShowDistance(true); + if (roundedDistance > appLocaRadius) { + setShowDistance(true); + setOpen4(true); + } + }; + + const calculateDistance = async (pickup, drop) => { + try { + const roundedDistance = await calculateDrivingDistance(pickup, drop); + setDistance(roundedDistance); + + const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm); + setTotalCharge(total); + + setShowDistance(true); + if (roundedDistance > appLocaRadius) { + setShowDistance(true); + setOpen4(true); + } + } catch (error) { + console.error('Error calculating distance:', error); + } + }; + + useEffect(() => { + if (timeslotarr[0]) { + let arr = []; + timeslotarr.map((val) => { + if (dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) { + arr.push(val); + } + }); + + if (arr[0]) { + setOrderarr([ + { + sno: 1, + address: '', + customerid: '', + deliverytime: dayjs(arr[0]) || '', + deliverylocationid: '', + clientname: '', + contactno: '', + latitude: '', + longitude: '' + } + ]); + } + } + }, [timeslotarr]); + + useEffect(() => { + if (searchword) { + let arr = clientdetail.filter((val) => { + return ( + val.address.toLowerCase().includes(searchword.toLowerCase()) || + val.firstname.toLowerCase().includes(searchword.toLowerCase()) || + val.contactno.toLowerCase().includes(searchword.toLowerCase()) + ); + }); + setClientdetailarr([...arr]); + } else { + setClientdetailarr([...clientdetail]); + } + }, [searchword]); + + // ==================================================== || fetchtenantinfo || ==================================================== + const fetchtenantinfo = async () => { + setLoading(true); + try { + const info = await getAdminTenant(tid); + if (info) { + setTenant(info); + setTenantid(info.tenantid); + fetchAppAdminTokens(); + } + } catch (err) { + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here. + opentoast(err?.message || 'Could not load tenant info', 'error', 3000); + } finally { + setLoading(false); + } + }; + useEffect(() => { + if (tid) fetchtenantinfo(); + }, [tid]); + + // ==================================================== || fetchTiming || ==================================================== + // No per-zone open/close-hours or geofence-radius endpoint in the new API + // (applocationid/"zones" aren't a resource there at all — see fetchAppLocations + // in pages/api/api.js). Falls back to a fixed 09:00–21:00 delivery window so + // the order-row / time-slot picker below still has slots to initialise from, + // and a generous default radius so the "too far" warning doesn't misfire. + const fetchTiming = async () => { + setLoading(true); + const opentime = '09:00:00'; + const closetime = '21:00:00'; + setAppLocaLat(0); + setAppLocaLng(0); + setAppLocaRadius(100); + setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`); + setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`); + let arr = []; + for ( + let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`; + dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0; + i = dayjs(i).add(30, 'm') + ) { + arr.push(i); + } + setTimeslotarr(arr); + setLoading(false); + }; + useEffect(() => { + fetchTiming(); + }, []); + + // =============================================== || fetchAppAdminTokens (via appId) || =============================================== + // No zone-admin-token lookup endpoint in the new API. + const fetchAppAdminTokens = async () => setAdmintoken([]); + + useEffect(() => { + if (starttime && endtime) { + fetchAppAdminTokens(); + } + }, [starttime, endtime]); + + // =============================================== || opentoast || =============================================== + + const opentoast = (message, variant, time) => { + enqueueSnackbar(message, { + variant: variant, + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: time ? time : 2000 + }); + }; + + function closeAddressModal() { + setOpen2(false); + } + function closetimemodal() { + setOpen3(false); + setCurrentsno(''); + } + + // =============================================== || createsubmitobj1 (create orders) || =============================================== + + const createsubmitobj2 = async () => { + // POST /admin/expressbooking's schema is a single flat booking object + // (tenantid, pickup* fields, customer_phone/name, delivery* fields, + // service_option, finalprice, notes, parcels[]) — nothing like the old + // pickup/drop/orders triple. The drop contact becomes the booking's + // customer (new API models the delivery recipient as "the customer"). + // + // Always send raw pickupaddress/pickuppincode/pickupcity/pickuplatitude/ + // pickuplongitude, even when a saved location was picked (the Business + // Location Autocomplete + gettenantlocations()'s single-location + // auto-select both copy the chosen location's fields into pickCust) — + // live-confirmed the pickuplocationid shortcut currently 500s on + // POST /admin/expressbooking (backend bug, reported; not something a + // frontend fix can work around other than avoiding the field). + const arr = { + tenantid: Number(tid) || tenant.tenantid, + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.postcode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0, + customer_phone: dropCust.contactno || '', + customer_name: dropCust.firstname || '', + deliveryaddress: dropCust.address || '', + deliverypincode: dropCust.postcode || '', + deliverycity: dropCust.city || '', + deliverylatitude: Number(dropCust.latitude) || 0, + deliverylongitude: Number(dropCust.longitude) || 0, + service_option: 'Normal', + finalprice: +totalCharge.toFixed(2) || 0, + notes: otherinstructions || '', + // The parcel object has no numeric quantity field (confirmed against + // express-console-api.md's schema) — "N items" is expressed by sending + // N parcel entries, which is also how the Deliveries page already reads + // it back (fetchDeliveries in api.js: `Quantity: b.parcels?.length`). + // This previously always sent exactly one parcel regardless of what the + // operator typed into the Quantity field, so it silently got dropped — + // matching "I entered 5, deliveries page shows 1". + parcels: Array.from({ length: Math.max(1, Number(quantity) || 1) }, () => ({ + itemcategory: category || 'General', + itemdescription: 'Order', + declaredvalue: +totalCharge.toFixed(2) || 0 + })) + }; + if (!arr.tenantid) { + opentoast('Pick a tenant before creating an order', 'warning', 2000); + } else if (!pickCust.firstname) { + opentoast('Enter Pickup Contact Name ', 'warning', 2000); + } else if (!pickCust.contactno) { + opentoast('Enter Pickup Contact Number ', 'warning', 2000); + } else if (pickCust.contactno.length != 10) { + opentoast('Check Pickup Contact Number ', 'error', 2000); + } else if (!pickCust.suburb) { + opentoast('Enter Pickup Location ', 'warning', 2000); + } else if (!pickCust.city) { + opentoast('Enter Pickup City ', 'warning', 2000); + } else if (!pickCust.postcode) { + opentoast('Enter Pickup Postcode ', 'warning', 2000); + } else if (!pickCust.landmark) { + opentoast('Enter Pickup Landmark ', 'warning', 2000); + } else if (!dropCust.firstname) { + opentoast('Enter Drop Contact Name ', 'warning', 2000); + } else if (!dropCust.contactno) { + opentoast('Enter Drop Contact Number', 'warning', 2000); + } else if (dropCust.contactno.length !== 10) { + opentoast('Check Drop Contact Number ', 'error', 2000); + } else if (!dropCust.suburb) { + opentoast('Enter Drop Suburb ', 'warning', 2000); + } else if (!dropCust.city) { + opentoast('Enter Drop City ', 'warning', 2000); + } else if (!dropCust.postcode) { + opentoast('Enter Drop postcode ', 'warning', 2000); + } else if (!dropCust.landmark) { + opentoast('Enter Drop Landmark ', 'warning', 2000); + } else if (!selectedtime) { + opentoast('Choose deliverytime ', 'warning', 2000); + } else { + setLoading(true); + try { + const createRes = await createExpressBooking(arr); + if (createRes.success) { + enqueueSnackbar('Order Created Successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 1000 + }); + navigate('/doormile/orders'); + } else { + opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning'); + } + } catch (error) { + // Previously silent — a thrown error here (network failure, CityGate + // rejection, validation error) left the operator with no feedback at + // all and a permanently stuck loading spinner (setLoading(false) was + // only reachable on the success path). doormileAxios's response + // interceptor already unwraps to error.response.data before + // rejecting, so the message lives at error.message, not + // error.response.data.message. + opentoast(error?.message || 'Cannot create order', 'error', 3000); + } finally { + setLoading(false); + } + } + }; + + const clicked1 = (e) => { + setShift(e); + }; + + const dialogopen = (i, result) => { + setOpen({ shiftsno: result.sno, sno: i.sno }); + }; + const dialogclose = () => { + setOpen(''); + }; + // ========================================================= || clientdetails || ========================================================= + // GET /admin/customers (B2C directory), not /admin/tenantcustomers — that + // resource is a manually-added contact book with nothing in it unless + // someone explicitly creates an entry. This one is confirmed to auto-grow: + // POST /admin/expressbooking's customer_phone/customer_name "creates a + // Guest customer if unknown" (see express-console-api.md), so every order + // placed lands a reusable contact here — matching the old jupiter flow + // where placing an order (customerid: 0) auto-saved the customer too. + // Field shape is { appcustomerid, createdat, email, name, phone, + // totalbookings } — a single `name`, not firstname/lastname, and still no + // address at all (see the Saved Contacts dialog below). + const clientdetails = async () => { + setLoading2(true); + try { + const customers = (await getAdminCustomers()) || []; + const filtered = searchCustList + ? customers.filter((val) => val.name?.toLowerCase().includes(searchCustList.toLowerCase()) || val.phone?.includes(searchCustList)) + : customers; + setClientdetail(filtered); + setCustomerlist(filtered); + setClientdetailarr(filtered.map((val) => ({ label: `${val.name} | ${val.phone}`, ...val }))); + } catch (err) { + opentoast('server error', 'warning'); + } finally { + setLoading2(false); + } + }; + useEffect(() => { + clientdetails(); + }, [searchCustList.length > 3, searchCustList == '']); + // ================================================== || sendnotifications || ================================================== + // No bulk-notify endpoint in the new API — never actually called (dead in + // the original code too, only referenced from a commented-out call site). + const sendnotifications = async () => {}; + // ============================================= || Address Autocomplete (pick) || ============================================= + const handlePickPlaceSelected = (place) => { + setInputValue2(`${place.name}, ${place.formatted_address}`); + // to trigger getDistance + setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() }); + setPickCust({ ...pickCust, address: `${place.name} ${place.formatted_address}` }); + const address = { + address: `${place.name} ${place.formatted_address}`, + street_number: '', + route: '', + locality: '', + sublocality_level_1: '', + administrative_area_level_3: '', + administrative_area_level_1: '', + country: '', + postal_code: '' + }; + place.address_components.forEach((component) => { + component.types.forEach((type) => { + switch (type) { + case 'street_number': + address.street_number = component.long_name; + break; + case 'route': + address.route = component.long_name; + break; + case 'locality': + address.locality = component.long_name; + break; + case 'sublocality_level_1': + address.sublocality_level_1 = component.long_name; + break; + case 'administrative_area_level_3': + address.administrative_area_level_3 = component.long_name; + break; + case 'administrative_area_level_1': + address.administrative_area_level_1 = component.long_name; + break; + case 'country': + address.country = component.long_name; + break; + case 'postal_code': + address.postal_code = component.long_name; + break; + // Add more cases as needed for other types + } + }); + }); + + // Use address object as per your requirements + setPickCust({ + ...pickCust, + address: address.address, + doorno: `${address.street_number} ${address.route}`, + suburb: address.sublocality_level_1, + city: address.locality, + postcode: address.postal_code, + latitude: place.geometry.location.lat(), + longitude: place.geometry.location.lng() + }); + }; + // ============================================= || Address Autocomplete (Drop) || ============================================= + + const handleDropPlaceSelected = (place) => { + setInputValue3(`${place.name}, ${place.formatted_address}`); + setEndPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() }); + setDropCust({ ...dropCust, address: `${place.name} ${place.formatted_address}` }); + const address = { + address: `${place.name} ${place.formatted_address}`, + street_number: '', + route: '', + locality: '', + sublocality_level_1: '', + administrative_area_level_3: '', + administrative_area_level_1: '', + country: '', + postal_code: '' + }; + place.address_components.forEach((component) => { + component.types.forEach((type) => { + switch (type) { + case 'street_number': + address.street_number = component.long_name; + break; + case 'route': + address.route = component.long_name; + break; + case 'locality': + address.locality = component.long_name; + break; + case 'sublocality_level_1': + address.sublocality_level_1 = component.long_name; + break; + case 'administrative_area_level_3': + address.administrative_area_level_3 = component.long_name; + break; + case 'administrative_area_level_1': + address.administrative_area_level_1 = component.long_name; + break; + case 'country': + address.country = component.long_name; + break; + case 'postal_code': + address.postal_code = component.long_name; + break; + // Add more cases as needed for other types + } + }); + }); + + // Use address object as per your requirements + setDropCust({ + ...dropCust, + address: address.address, + doorno: `${address.street_number} ${address.route}`, + suburb: address.sublocality_level_1, + city: address.locality, + postcode: address.postal_code, + latitude: place.geometry.location.lat(), + longitude: place.geometry.location.lng() + }); + }; + + // ============================================= || gettenantlocations (branches) || ============================================= + const gettenantlocations = async () => { + if (!tid) { + // Staff login (express-console-api.md: token tenantid 0/null = Doormile + // staff, who can act on any tenant) with no tenant picked yet — the + // picker UI covers this, not an error. A genuinely tenant-less, + // non-staff account can't get here at all (isStaffLogin would be true). + setLocationsStatus('no-tenant'); + return; + } + setLocationsStatus('loading'); + try { + const locations = (await getTenantLocations(tid)) || []; + setTenantlocations(locations); + if (locations.length === 1) { + setLocationsStatus('ready'); + setIsLocation(true); + setTenanatLocoId(locations[0].tenantlocationid); + const loc = locations[0]; + setPickCust((prev) => ({ + ...prev, + address: loc.address || '', + suburb: loc.locationname || loc.city || '', + city: loc.city || '', + postcode: loc.pincode || '', + landmark: loc.address || '', + latitude: loc.latitude, + longitude: loc.longitude + })); + } else if (locations.length === 0) { + // No saved locations for this tenant — every pickup field below is a + // plain manual TextField anyway, so let the operator fill it in by + // hand instead of leaving the form permanently disabled. + setLocationsStatus('empty'); + setIsLocation(true); + opentoast('No saved business locations — enter pickup details manually below', 'info', 3000); + } else { + setLocationsStatus('ready'); + } + } catch (err) { + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error) — so the message is err.message here, not + // err.response.data.message (that was reading through a layer that no + // longer exists at this point and always fell back to the generic text). + setTenantlocations([]); + setLocationsStatus('error'); + opentoast(err?.message || 'Could not load business locations', 'error', 4000); + } + }; + useEffect(() => { + gettenantlocations(); + }, [tid]); + + // ============================================= || fetchStaffTenantList (Doormile staff picker) || ============================================= + // Only staff logins (loggedInTenantId blank/0) need this — a client login's + // tenant is already fixed by their token and this never fetches for them. + useEffect(() => { + if (!isStaffLogin) return; + setStaffTenantListLoading(true); + getAdminTenants() + .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active'))) + .catch((err) => { + opentoast(err?.message || 'Could not load tenant list', 'error', 4000); + }) + .finally(() => setStaffTenantListLoading(false)); + }, []); + + return ( + <> + {loading && } + + + + + + + + + + + + Create Order + + + + + Fill pickup, drop, schedule & pricing details + + + + + + {locationsStatus === 'loading' ? ( + + Loading business locations… + + ) : locationsStatus === 'no-tenant' && isStaffLogin ? ( + // Staff login (express-console-api.md: token tenantid 0/null + // = "Doormile's own staff... sees everything, all tenants") + // — pick which tenant to create this order for. Everything + // downstream (pricing, saved locations, the booking's own + // tenantid) is keyed off `tid`, which equals selectedTenantId. + option?.tenantname || ''} + isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid} + PaperComponent={SoftPaper} + sx={{ width: { xs: '100%', sm: 300 } }} + size="small" + onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')} + renderInput={(params) => ( + + + + + + ) + }} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: DT.radiusPill + 'px', + bgcolor: '#fff', + fontWeight: 600, + '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 }, + '&:hover fieldset': { borderColor: BRAND }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }, + '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 } + } + }} + /> + )} + /> + ) : locationsStatus === 'no-tenant' ? ( + + This login has no tenant assigned — sign in as a tenant user + + ) : locationsStatus === 'error' ? ( + + Could not load business locations — tap to retry + + ) : locationsStatus === 'empty' ? ( + + No saved locations — enter pickup details manually below + + ) : tenantLocations.length === 1 ? ( + + {locationDisplayName(tenantLocations[0])} + + ) : ( + locationDisplayName(option)} + isOptionEqualToValue={(option, value) => option?.tenantlocationid === value?.tenantlocationid} + renderOption={(props, option) => ( + + + + + {locationDisplayName(option)} + + {option.isprimary && ( + + PRIMARY + + )} + + + {option.address || option.city || ''} + + + + )} + PaperComponent={SoftPaper} + sx={{ width: { xs: '100%', sm: 320 } }} + size="small" + renderInput={(params) => ( + + + + + + ) + }} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: DT.radiusPill + 'px', + bgcolor: '#fff', + fontWeight: 600, + '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 }, + '&:hover fieldset': { borderColor: BRAND }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }, + '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 } + } + }} + /> + )} + onChange={(event, value, reason) => { + if (value) { + setTenanatLocoId(value.tenantlocationid); + setIsLocation(true); + // Also fill pickCust's raw address fields from the + // picked location — POST /admin/expressbooking's + // pickuplocationid currently 500s server-side (live- + // confirmed), so createsubmitobj2() always sends raw + // pickup* fields instead; this keeps that path working + // when a saved location is chosen here. + setPickCust((prev) => ({ + ...prev, + address: value.address || '', + suburb: value.locationname || value.city || '', + city: value.city || '', + postcode: value.pincode || '', + landmark: value.address || '', + latitude: value.latitude, + longitude: value.longitude + })); + } + if (reason === 'clear') { + setIsLocation(false); + } + }} + /> + )} + + + + + + + + + + {/* ================================================= || Pickup || ================================================= */} + + + } + title="Pickup Details" + subtitle="Where the order is picked up" + action={ + + } + /> + + + + + {/* ====================================== ||Contact Name (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Contact Name" + value={pickCust.firstname} + onChange={(e) => { + setPickCust({ ...pickCust, firstname: e.target.value }); + }} + /> + + {/* ====================================== ||Contact Number(pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Contact Number" + value={pickCust.contactno} + onChange={(e) => { + if (e.target.value.length <= 10) { + setPickCust({ ...pickCust, contactno: e.target.value }); + } + if (pickNum == e.target.value) { + setShowCheck1(0); + } else { + setShowCheck1(1); + } + if (e.target.value.length < 10) { + setNumErr1(true); + } else { + setNumErr1(false); + } + }} + /> + + {/* ====================================== || Address (pick) || ====================================== */} + + + {addId1 == 0 ? ( +
    + +
    + ) : ( + { + setAddId1(0); + setPickCust({ + ...pickCust, + // firstname: '', + // contactno: '', + doorno: '', + suburb: '', + city: '', + postcode: '', + landmark: '' + }); + setShowDistance(false); + setStartPoint({ latitude: 0, longitude: 0 }); + }} + > + + + ) + }} + variant="outlined" + placeholder="Select" + value={pickCust.address} + onChange={(e) => { + setPickCust({ ...pickCust, address: e.target.value }); + if (e.target.value == '') { + setAddId1(0); + setShowDistance(false); + setStartPoint({ latitude: 0, longitude: 0 }); + } + }} + /> + )} +
    +
    + + {/* ====================================== ||Door No (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Door No / Street" + value={pickCust.doorno} + onChange={(e) => { + setPickCust({ ...pickCust, doorno: e.target.value }); + }} + /> + + {/* ====================================== || Suburb (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Location" + value={pickCust.suburb} + onChange={(e) => { + setPickCust({ ...pickCust, suburb: e.target.value }); + }} + /> + + {/* ====================================== || City (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="City" + value={pickCust.city} + onChange={(e) => { + setPickCust({ ...pickCust, city: e.target.value }); + }} + /> + + {/* ====================================== || postcode (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Postcode" + value={pickCust.postcode} + onChange={(e) => { + setPickCust({ ...pickCust, postcode: e.target.value }); + }} + /> + + {/* ====================================== || Landmark (pick) || ====================================== */} + + + + + ) + }} + variant="outlined" + label="Landmark" + value={pickCust.landmark} + onChange={(e) => { + setPickCust({ ...pickCust, landmark: e.target.value }); + }} + /> + + {/* ====================================== ||Checkbox save for later (pick) || ====================================== */} + {showCheck1 == 1 && ( + + + { + setIsNumChange1(e.target.checked ? 1 : 0); + }} + /> + } + label="Save For Later" + /> + + + )} +
    +
    +
    +
    +
    +
    + {/* ================================================= || Drop || ================================================= */} + + + } + title="Drop Details" + subtitle="Where the order is delivered" + action={ + + } + /> + + + + + + Drop Details + + + {/* Customer */} + {/* { + if (val) { + setDropswitch(true); + setClientinfo({}); + } else { + setDropswitch(false); + setClientinfo({}); + } + }} + size="small" + /> + Business */} + + + + {/* new2 */} + {/* `${option.firstname} (${option.contactno})`} + sx={{ mt: 0, mb: 1 }} + renderInput={(params) => ( + { + setSearchCustList(e.target.value); + }} + InputProps={{ + ...params.InputProps, + inputProps: { + ...params.inputProps, + maxLength: 10 + } + }} + /> + )} + onChange={(e, val, reason) => { + if (val) { + if (pickCust.customerid === val.customerid) { + opentoast('Select Another Customer', 'error'); + setSearchCustList(''); + } else { + setDropCust(val); + setEndPoint({ latitude: val.latitude, longitude: val.longitude }); + setAddId2(1); + } + } + if (reason == 'clear') { + setSearchCustList(''); + } + }} + noOptionsText={ + /^[0-9]{10}$/.test(searchCustList) ? ( + + { + setDropCust({ ...dropCust, contactno: searchCustList }); + handleOkClick2(); + setSearchCustList(''); + }} + /> + + ) : null + } + /> */} + + + + + {/* ====================================== ||Contact Name (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.firstname} + onChange={(e) => { + setDropCust({ ...dropCust, firstname: e.target.value }); + }} + /> + + {/* ====================================== ||Contact Number (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.contactno} + onChange={(e) => { + if (e.target.value.length <= 10) { + setDropCust({ ...dropCust, contactno: e.target.value }); + } + if (dropNum == e.target.value) { + setShowCheck2(0); + } else { + setShowCheck2(1); + } + if (e.target.value.length < 10) { + setNumErr2(true); + } else { + setNumErr2(false); + } + }} + /> + + + + {addId2 == 0 ? ( +
    + +
    + ) : ( + { + setAddId2(0); + setDropCust({ + ...dropCust, + firstname: '', + contactno: '', + doorno: '', + suburb: '', + city: '', + postcode: '', + landmark: '' + }); + setShowDistance(false); + setEndPoint({ latitude: 0, longitude: 0 }); + }} + > + + + ) + }} + variant="outlined" + placeholder="Select" + value={dropCust.address} + onChange={(e) => { + setPickCust({ ...dropCust, address: e.target.value }); + if (e.target.value == '') { + setAddId2(0); + setShowDistance(false); + setEndPoint({ latitude: 0, longitude: 0 }); + } + }} + /> + )} +
    +
    + + {/* ====================================== ||Door No (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.doorno} + onChange={(e) => { + setDropCust({ ...dropCust, doorno: e.target.value }); + }} + /> + + {/* ====================================== ||Suburb (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.suburb} + onChange={(e) => { + setDropCust({ ...dropCust, suburb: e.target.value }); + }} + /> + + {/* ====================================== ||City (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.city} + onChange={(e) => { + setDropCust({ ...dropCust, city: e.target.value }); + }} + /> + + + {/* ====================================== ||Postcode (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.postcode} + onChange={(e) => { + setDropCust({ ...dropCust, postcode: e.target.value }); + }} + /> + + {/* ====================================== ||Landmark (drop) || ====================================== */} + + + + + ) + }} + value={dropCust.landmark} + onChange={(e) => { + setDropCust({ ...dropCust, landmark: e.target.value }); + }} + /> + + {/* ====================================== ||Checkbox save for later (drop) || ====================================== */} + {showCheck2 == 1 && ( + + + { + setIsNumChange2(e.target.checked ? 1 : 0); + }} + /> + } + label="Save For Later" + /> + + + )} +
    +
    + {/* + + */} +
    +
    +
    +
    + {/* ================================================= || Time || ================================================= */} + + + } title="Schedule" subtitle="Pickup date & time slot" /> + + + + + Date + + + { + let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); + setSelectedtime(''); + if (dateres11 <= 0) { + setStartdate(e); + setEnddate(e); + + let arr = []; + timeslotarr.map((val) => { + if ( + dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 + ) { + arr.push(val); + } + }); + if (arr[0]) { + setOrderarr([ + { + sno: 1, + address: '', + customerid: '', + deliverytime: dayjs(arr[0]) || '', + deliverylocationid: '', + clientname: '', + contactno: '', + latitude: '', + longitude: '' + } + ]); + } else { + setOrderarr([]); + } + } else { + setAlertmessage('choose Upcoming Date'); + opentoast('choose Upcoming Date', 'warning'); + setStartdate(NaN); + } + }} + value={dayjs(startdate)} + sx={{ width: '100%', mt: 2 }} + disablePast + /> + + + Time + + + + {timeslotarr.map((val, index) => { + if ( + dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= + 0 + ) { + const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm'); + return ( + { + if (distance > appLocaRadius) { + setOpen4(true); + } else if (showDistance) { + setSelectedtime(val); + } else { + opentoast('Select Pickup and Drop', 'error'); + } + }} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: 0.5, + px: 1.25, + py: 0.5, + cursor: 'pointer', + borderRadius: 999, + border: `1.5px solid ${active ? '#f59e0b' : edge('#f59e0b')}`, + bgcolor: active ? '#f59e0b' : tint('#f59e0b'), + color: active ? '#fff' : '#f59e0b', + fontSize: 12, + fontWeight: 800, + transition: 'all 0.15s', + boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : 'none', + '&:hover': { + borderColor: '#f59e0b', + boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : `0 0 0 3px ${ring('#f59e0b')}` + } + }} + > + {dayjs(val).format('hh:mm A')} + + ); + } + })} + + + + + + + + + + } + title="Distance & Pricing" + subtitle="Auto-calculated from pickup → drop" + /> + + {showDistance && ( + + + + + + + + + )} + + Category + } + onChange={(event, value) => setCategory(value || null)} + onInputChange={(event, value, reason) => { + if (reason === 'input') setCategory(value || null); + }} + /> + + Weight + + { + handleChipClick('1-10kgs'); + setWeight('1-10kgs'); + }} + /> + { + handleChipClick('11-20kgs'); + setWeight('11-20kgs'); + }} + /> + { + handleChipClick('21-30kgs'); + setWeight('21-30kgs'); + }} + /> + + + SMS Delivery + { + setIsSms(e.target.checked ? 1 : 0); + }} + /> + + + + +
    +
    +
    + + {/* ============================== || Cash Collect & Quantity || ============================== */} + + } + title="Collection & Quantity" + subtitle="Cash to collect on delivery, total items" + /> + + + + setCollectionamt(e.target.value)} + inputProps={{ min: 0 }} + /> + + + setQuantity(e.target.value)} + inputProps={{ min: 1 }} + /> + + + + + + {/* ================================================= || Notes || ================================================= */} + + } + title="Notes" + subtitle="Add anything the rider should know" + /> + + setOtherinstructions(e.target.value)} + /> + + + {!showDistance && ( + + Set pickup & drop to enable + + )} + + + + +
    +
    + + {/* + + + Select Time + + + + + + {timeslotarr.map((val) => { + if ( + dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 + // && currentsno + ) { + return ( + <> + + { + setSelectedtime(dayjs(val).format('hh:mm A')); + }} + /> + + + ); + } + })} + + + + + + + + */} + + {/* ============================================= || saved address Dialog || ============================================= */} + setIsCustomerOpen(false)} + fullWidth + maxWidth="sm" + PaperProps={{ sx: { borderRadius: 3 } }} + > + + + + + + + + Saved Contacts + + + Pick a saved contact's name & phone for {pickordrop === 1 ? 'Pickup' : 'Drop'} — address isn't stored, search it below + + + + + + setSearchCustList(e.target.value)} + sx={{ + flex: 1, + '& .MuiOutlinedInput-input': { p: '6px 4px', fontSize: 13, fontWeight: 600 }, + '& fieldset': { border: 'none' } + }} + endAdornment={ + searchCustList && ( + setSearchCustList('')} sx={{ p: 0.25, color: BRAND }}> + + + ) + } + autoComplete="off" + /> + + + + {customerlist.length === 0 ? ( + + + + + + No saved contacts + + + {searchCustList ? 'Try a different keyword.' : 'Place an order with a new contact — it will show up here next time.'} + + + ) : ( + + {customerlist.map((address, index) => { + // GET /admin/customers (B2C directory) — { appcustomerid, + // createdat, email, name, phone, totalbookings }. No + // address/coordinates at all (that was a jupiter-only field + // on the old customer record). Picking a saved contact here + // only pre-fills the name & phone; the operator still has + // to search the address below like a brand-new contact. + const contactPhone = address.phone; + const isUsed = !!contactPhone && pickCust.phone === contactPhone; + return ( + { + if (isUsed) return; + setIsCustomerOpen(false); + const contact = { firstname: address.name, contactno: contactPhone, phone: contactPhone }; + if (pickordrop === 1) { + setAddId1(1); + setPickCust((prev) => ({ ...prev, ...contact })); + setPickNum(contactPhone); + } else { + setAddId2(1); + setDropCust((prev) => ({ ...prev, ...contact })); + setdropNum(contactPhone); + } + }} + sx={{ + display: 'flex', + alignItems: 'flex-start', + gap: 1.25, + p: 1.5, + borderRadius: 2, + border: '1px solid', + borderColor: isUsed ? edge('#94a3b8') : DT.borderSubtle, + bgcolor: isUsed ? DT.surfaceAlt : '#fff', + cursor: isUsed ? 'not-allowed' : 'pointer', + opacity: isUsed ? 0.6 : 1, + transition: 'all 0.15s', + '&:hover': isUsed + ? {} + : { + borderColor: edge(BRAND), + bgcolor: tint(BRAND), + boxShadow: DT.shadowSoft + } + }} + > + + {String(address.name || '?') + .charAt(0) + .toUpperCase()} + + + + + {address.name || 'Unnamed'} + + + {contactPhone || '—'} + + {isUsed && ( + + Already used + + )} + + + {address.email || 'No address on file — search it below'} + + + + ); + })} + + )} + + + + + + + setOpen4(false)} PaperProps={{ sx: { borderRadius: 3 } }}> + + + + + + + + Out of Service Area + + + This drop point is outside the supported radius + + + + + + + Service is not available at this location. Try a different drop point within the coverage zone. + + + + + + +
    + + ); +}; + +export default Createorder1; diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js index b3ab2ae..45293a7 100644 --- a/src/pages/nearle/orders/multipleOrders.js +++ b/src/pages/nearle/orders/multipleOrders.js @@ -1,1836 +1,1835 @@ -/* eslint-disable no-unused-vars */ -import React, { useEffect, useMemo, useState, useRef } from 'react'; -import { getAdminPricing, getAdminTenants, getTenantLocations, getAdminCustomers, createExpressBookingBulk } from 'pages/api/doormileApi'; -import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; -import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; -import Papa from 'papaparse'; -import * as XLSX from 'xlsx'; -import dayjs from 'dayjs'; -import { useNavigate } from 'react-router'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { enqueueSnackbar } from 'notistack'; -import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance'; - -import { - FormControl, - InputAdornment, - Grid, - Typography, - Stack, - Box, - Card, - Button, - TextField, - Autocomplete, - Divider, - Dialog, - DialogTitle, - DialogContent, - Checkbox, - DialogActions, - CircularProgress, - IconButton, - OutlinedInput, - FormGroup, - FormControlLabel, - Table, - TableContainer, - TableCell, - TableBody, - TableRow, - Paper, - TableHead, - Backdrop, - Chip, - Tooltip -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import ClearIcon from '@mui/icons-material/Clear'; -import MyLocationIcon from '@mui/icons-material/MyLocation'; -import { - SearchOutlined, - CloseOutlined, - ExclamationCircleOutlined, - FileAddOutlined, - CalendarOutlined, - ClockCircleOutlined, - FileTextOutlined, - InboxOutlined -} from '@ant-design/icons'; -import { Empty } from 'antd'; -import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa'; -import { FaLocationDot } from 'react-icons/fa6'; -import { MdOutlineCloudUpload } from 'react-icons/md'; - -import Loader from 'components/Loader'; -import CircularLoader from 'components/CircularLoader'; -import './OrdersRedesign.css'; - -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -const cellHeaderSx = { fontSize: 11.5, fontWeight: 700, color: '#475569', py: 0.75, px: 1 }; -const cellBodySx = { fontSize: 12, py: 0.6, px: 1 }; - -// GET /admin/tenants/:id/locations carries no `slots` field (no per-location -// pickup-hours endpoint in the new API) — the pickup-slot Autocomplete used -// to read tenantLocation.slots, which was always undefined, leaving the -// dropdown permanently empty and the page unsubmittable. Falls back to a -// fixed 09:00-21:00 / 30-min window, same rationale as createorder1.js's -// fetchTiming(). -const buildDefaultPickupSlots = () => { - const arr = []; - for (let t = dayjs().hour(9).minute(0).second(0); t.hour() < 21 || (t.hour() === 21 && t.minute() === 0); t = t.add(30, 'm')) { - arr.push({ name: t.format('hh:mm A'), time: t.format('HH:mm') }); - } - return arr; -}; - -// Some saved locations come back with a blank locationname (live-confirmed, -// e.g. tenant 1's only location) — fall back to the address rather than -// showing an empty field/option. -const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location'; - -// Tenants with many locations in the same city (DailyGrubs has 23, all in -// Coimbatore) made a "(city)" suffix useless for telling them apart — sort -// primary-first then alphabetically; the full address is shown per-option -// instead (see renderOption on the Business Location Autocomplete below). -const sortLocations = (locations) => - [...(locations || [])].sort((a, b) => { - if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1; - return locationDisplayName(a).localeCompare(locationDisplayName(b)); - }); - -const MultipleOrders = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const locationRef = useRef(null); - const userid = localStorage.getItem('userid'); - // Per express-console-api.md's "Tenant scoping" section: token tenantid - // 0/null means Doormile's own staff, who can act on any tenant — NOT an - // error state. A client (tenant) login carries a real tenantid and never - // needs the picker below. `tenantid` (used throughout this file already) - // now resolves to whichever the operator is currently working as. - const loggedInTenantId = localStorage.getItem('tenantid'); - const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; - - // ============================== state ============================== - const [locations, setLocations] = useState([]); - const [tenantLocations, setTenantlocations] = useState([]); - const [loading, setLoading] = useState(false); - const [btnLoading, setBtnLoading] = useState(false); - - const [appId, setAppId] = useState(0); - const [locationid, setLocationid] = useState(0); - const [locationValue, setLocationValue] = useState(null); - const [locationsStatus, setLocationsStatus] = useState('loading'); - const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId); - const [staffTenantList, setStaffTenantList] = useState([]); - const [staffTenantListLoading, setStaffTenantListLoading] = useState(false); - const tenantid = selectedTenantId; - - const [basePrice, setBasePrice] = useState(0); - const [pricePerKm, setPricePerKm] = useState(0); - const [minKm, setMinKm] = useState(0); - - const [pickCust, setPickCust] = useState(null); - const [dropCust, setDropCust] = useState([]); - const [customerlist, setCustomerlist] = useState([]); - const [isCustomerOpen, setIsCustomerOpen] = useState(false); - const [searchCustList, setSearchCustList] = useState(''); - - const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); - const [selectedtime, setSelectedtime] = useState(''); - const [pickupSlotsList, setPickupSlotsList] = useState(buildDefaultPickupSlots); - const [pickupSlot, setPickupSlot] = useState(null); - - const [otherinstructions, setOtherinstructions] = useState(''); - const [admintoken, setAdmintoken] = useState(); - - const [totaldist, settotaldist] = useState(0); - const [totalAmt, settotalAmt] = useState(0); - const [totalQty, settotalQty] = useState(0); - const [totalCash, settotalCash] = useState(0); - - const [uploadType, setUploadType] = useState(null); - const [users, setUsers] = useState([]); - const [fileName, setFileName] = useState(''); - const [slotDropdownOpen, setSlotDropdownOpen] = useState(false); - - const toastCacheRef = useRef({}); - - // ============================== toast ============================== - const opentoast = (message, variant, time) => { - enqueueSnackbar(message, { - variant, - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: time || 1500 - }); - }; - - const OpenToast = (message, type = 'info', timeout = 3000) => { - const key = `${type}-${message}`; - if (toastCacheRef.current[key]) return; - opentoast(message, type, timeout); - toastCacheRef.current[key] = true; - setTimeout(() => delete toastCacheRef.current[key], 3000); - }; - - // ============================== reset chains ============================== - useEffect(() => { - setLocationid(0); - setLocationValue(null); - setDropCust([]); - setUsers([]); - setUploadType(null); - setFileName(''); - setPickupSlotsList(buildDefaultPickupSlots()); - setPickupSlot(null); - setSelectedtime(''); - }, [appId]); - - // ============================== fetchAppLocations ============================== - const fetchAppLocations = async () => { - setLoading(true); - try { - const locations = await fetchZoneLocations(); - setLocations(locations || []); - } catch (err) { - OpenToast(err.message, 'error', 5000); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchAppLocations(); - }, []); - - // ============================== fetchTenantPricing ============================== - const fetchTenantPricing = async (id) => { - try { - const pricing = (await getAdminPricing()) || []; - // id comes from localStorage (always a string) but pricing.tenantid is - // numeric — a strict === here never matched, so basePrice/pricePerKm/ - // minKm stayed at their 0 defaults and every order priced at ₹0. - const match = pricing.find((p) => String(p.tenantid) === String(id)); - if (match) { - setBasePrice(match.baseprice || 0); - setPricePerKm(match.priceperkm || 0); - setMinKm(match.basedistance || 0); - } - } catch (err) { - // swallow - } - }; - - useEffect(() => { - if (tenantid) fetchTenantPricing(tenantid); - }, [tenantid]); - - // ============================== gettenantlocations ============================== - // Drives what shows in place of the Business Location picker. Previously - // this function was only ever called when `tenantid` was truthy, so an - // operator on a no-tenant login (the shared admin/developer account has - // tenantid: null) got a permanently empty picker with zero feedback — not - // even a toast, since the function body that would show one never ran. - const gettenantlocations = async (id) => { - if (!id) { - // Staff login with no tenant picked yet — the picker UI covers this. - setLocationsStatus('no-tenant'); - return; - } - setLocationsStatus('loading'); - try { - const details = (await getTenantLocations(id)) || []; - setTenantlocations(details); - if (details.length === 1) { - setLocationsStatus('ready'); - setPickCust(details[0]); - setLocationid(details[0].tenantlocationid); - setLocationValue(details[0]); - } else if (details.length === 0) { - setLocationsStatus('empty'); - opentoast('No saved business locations for this tenant', 'info', 3000); - } else { - setLocationsStatus('ready'); - } - } catch (err) { - // doormileAxios's response interceptor rejects with error.response.data - // (not the raw axios error), so the message lives at err.message here. - setTenantlocations([]); - setLocationsStatus('error'); - opentoast(err?.message || 'Could not load business locations', 'error', 4000); - } - }; - - useEffect(() => { - gettenantlocations(tenantid); - }, [tenantid]); - - // ============================== fetchStaffTenantList (Doormile staff picker) ============================== - // Only staff logins (loggedInTenantId blank/0) need this — a client login's - // tenant is already fixed by their token and this never fetches for them. - useEffect(() => { - if (!isStaffLogin) return; - setStaffTenantListLoading(true); - getAdminTenants() - .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active'))) - .catch((err) => { - opentoast(err?.message || 'Could not load tenant list', 'error', 4000); - }) - .finally(() => setStaffTenantListLoading(false)); - }, []); - - // ============================== clientdetails ============================== - // GET /admin/customers (B2C directory) — not /admin/tenantcustomers, which - // is an empty, manually-added contact book with no address/coordinates. - // This resource auto-grows from real orders (see the identical fix on - // createorder1.js's Saved Contacts) and has a confirmed live shape of - // { appcustomerid, createdat, email, name, phone, totalbookings } — no - // search/query param exists, so it's fetched once and filtered in-memory. - const clientdetails = async () => { - try { - const customers = (await getAdminCustomers()) || []; - setCustomerlist(customers); - } catch (err) { - opentoast('server error', 'warning'); - } - }; - - useEffect(() => { - if (!tenantid || !isCustomerOpen) return; - clientdetails(); - }, [tenantid, isCustomerOpen]); - - const filteredCustomerList = useMemo(() => { - const q = searchCustList.trim().toLowerCase(); - if (!q) return customerlist; - return customerlist.filter((c) => c.name?.toLowerCase().includes(q) || c.phone?.includes(searchCustList)); - }, [customerlist, searchCustList]); - - // ============================== totals ============================== - useEffect(() => { - let a1 = 0, - a2 = 0, - a3 = 0, - a4 = 0; - dropCust.forEach((c) => { - a1 += Number(c.distance) || 0; - a2 += Number(c.totalcharge) || 0; - a3 += Number(c.quantity) || 0; - a4 += Number(c.collectionamt) || 0; - }); - settotaldist(a1); - settotalAmt(a2); - settotalQty(a3); - settotalCash(a4); - }, [dropCust]); - - // ============================== distance (Google) ============================== - // ============================== distance (OSRM/Haversine) ============================== - const calculateDistance = async (customer) => { - try { - if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.'); - if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.'); - - const roundedDistance = await calculateDrivingDistance(pickCust, customer); - const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm); - return { roundedDistance, totalcharge }; - } catch (error) { - if (error.message.includes('Origin') || error.message.includes('customer') || error.message.includes('coordinates')) { - OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000); - } else { - OpenToast('Unexpected error during distance calculation.', 'error', 3000); - } - throw error; - } - }; - - // ============================== checkbox handlers ============================== - // GET /admin/customers has no address/coordinates at all, so a saved - // customer can no longer be distance-calculated at selection time (that's - // what used to throw "Invalid coordinates" and silently drop the row every - // time). Add the row immediately instead — its Address cell renders an - // editable AddressAutocomplete (see the Drop List table) until the - // operator picks a real address, which is what fills in lat/lng and lets - // distance/charge be computed per-row. - const handleCheckboxChange = (event, customer) => { - const rowkey = `sel-${customer.appcustomerid}`; - if (event.target.checked) { - setDropCust((prev) => { - if (prev.some((c) => c._rowkey === rowkey)) return prev; - return [ - ...prev, - { - _rowkey: rowkey, - appcustomerid: customer.appcustomerid, - firstname: customer.name || '', - contactno: customer.phone || '', - address: '', - city: '', - postcode: '', - latitude: null, - longitude: null, - distance: 0, - totalcharge: 0, - quantity: 0, - collectionamt: 0 - } - ]; - }); - } else { - setDropCust((prev) => prev.filter((c) => c._rowkey !== rowkey)); - } - }; - - // Still used by the CSV "Continue" promotion and the Drop List remove - // button. Safe to keep calling calculateDistance here — CSV rows always - // carry real latitude/longitude from the sheet, unlike Selection rows. - const handleCheckboxChange1 = async (customer) => { - const rowkey = customer._rowkey; - let wasSelected = false; - setDropCust((prev) => { - wasSelected = prev.some((c) => c._rowkey === rowkey); - if (wasSelected) return prev.filter((c) => c._rowkey !== rowkey); - return prev; - }); - if (wasSelected) return; - setLoading(true); - try { - const { roundedDistance, totalcharge } = await calculateDistance(customer); - setDropCust((prev) => { - if (prev.some((c) => c._rowkey === rowkey)) return prev; - return [...prev, { ...customer, distance: roundedDistance, totalcharge }]; - }); - } catch (err) { - console.error('Failed to calculate distance:', err); - OpenToast(`Could not calculate distance for ${customer.firstname || 'this row'}`, 'warning', 3000); - } finally { - setLoading(false); - } - }; - - // Parses the AddressAutocomplete `place` result (see createorder1.js's - // handleDropPlaceSelected for the same pattern) for one Drop List row, - // updates that row's address/coordinates, then recomputes its distance and - // charge now that coordinates finally exist. - const handleRowPlaceSelected = async (rowkey, place) => { - const parsed = { suburb: '', city: '', postcode: '' }; - place.address_components.forEach((component) => { - component.types.forEach((type) => { - switch (type) { - case 'sublocality_level_1': - case 'sublocality': - parsed.suburb = component.long_name; - break; - case 'locality': - parsed.city = component.long_name; - break; - case 'postal_code': - parsed.postcode = component.long_name; - break; - } - }); - }); - const latitude = place.geometry.location.lat(); - const longitude = place.geometry.location.lng(); - const address = place.formatted_address || ''; - - setDropCust((prev) => - prev.map((c) => (c._rowkey === rowkey ? { ...c, address, city: parsed.city, postcode: parsed.postcode, latitude, longitude } : c)) - ); - - try { - const { roundedDistance, totalcharge } = await calculateDistance({ latitude, longitude }); - setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, distance: roundedDistance, totalcharge } : c))); - } catch (err) { - console.error('Failed to calculate distance:', err); - } - }; - - // No zone-admin-token or bulk-notify endpoint in the new API. - const fetchAppAdminTokens = async () => setAdmintoken([]); - - useEffect(() => { - if (appId) fetchAppAdminTokens(); - }, [appId]); - - // ============================== sendnotifications ============================== - const sendnotifications = async () => {}; - - // ============================== CSV / XLSX upload ============================== - const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name); - const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, ''); - - const headerMap = { - 'pickupdate(yyyy-mmm-dd)': 'date', - 'sendername*': 'locationname', - 'senderphone*': 'locationcontact', - 'senderaddress*': 'locationaddress', - 'receivername*': 'firstname', - receiverphone: 'contactno', - 'receiveralternatephone*': 'altcontactno', - receiverfulladdress: 'address', - // Best-effort — no live sample sheet to confirm these are the tenant's - // actual column headers. Without them deliverycity/deliverypincode in - // the submit payload were always sent blank; if these names are wrong, - // behavior is unchanged from before (still blank), not worse. - receivercity: 'city', - receiverpincode: 'postcode', - receiverlatitude: 'latitude', - receiverlongitude: 'longitude', - 'itemdescription*': 'description', - Quantity: 'quantity', - ' Collect Cash': 'collectionamt' - }; - - const handleFileDirectUpload = (event) => { - try { - const file = event.target.files?.[0]; - if (!file) { - opentoast('No file selected.', 'warning'); - return; - } - const fileNameLower = file.name.toLowerCase(); - const isCSV = fileNameLower.endsWith('.csv'); - const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx'); - if (!isCSV && !isExcel) { - opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); - return; - } - - const processData = (data, headers) => { - const normalizedMap = {}; - for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key]; - const mappedData = data.map((row, idx) => { - const newRow = { _rowkey: `csv-${idx}` }; - for (const key in row) { - const cleanKey = normalizeHeader(key); - const newKey = normalizedMap[cleanKey] || cleanKey; - let value = row[key]; - if (newKey === 'firstname') value = cleanReceiverName(value); - newRow[newKey] = value; - } - return newRow; - }); - const requiredCols = Object.keys(headerMap).filter((k) => k.trim().endsWith('*')); - const missingRequired = requiredCols.filter((clientCol) => !headers.includes(normalizeHeader(clientCol))); - if (missingRequired.length > 0) { - opentoast(`Missing columns: ${missingRequired.join(', ')}`, 'warning', 3000); - } - setUsers(mappedData); - opentoast('File uploaded successfully', 'success', 2000); - opentoast('Press Continue to add as drop customers', 'info', 2500); - }; - - if (isCSV) { - Papa.parse(file, { - header: true, - dynamicTyping: true, - skipEmptyLines: true, - complete: (results) => { - if (!results.data?.length) { - opentoast('CSV file is empty or has no valid rows.', 'warning'); - setUsers([]); - return; - } - processData(results.data, results.meta.fields.map(normalizeHeader)); - }, - error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning') - }); - } - if (isExcel) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const workbook = XLSX.read(e.target.result, { type: 'binary' }); - const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }); - if (!jsonData?.length) { - opentoast('Excel file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - processData(jsonData, Object.keys(jsonData[0]).map(normalizeHeader)); - } catch (err) { - opentoast(`Error reading Excel: ${err.message}`, 'warning'); - } - }; - reader.readAsBinaryString(file); - } - } catch (err) { - opentoast(`Unexpected error: ${err.message}`, 'warning'); - } - }; - - const removeFileExtension = (n) => n.replace(/\.[^/.]+$/, ''); - const onFileChange = (event) => { - const file = event.target.files[0]; - if (!file) return; - const cleanedName = removeFileExtension(file.name); - setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName)); - handleFileDirectUpload(event); - }; - - // ============================== row editors ============================== - const handleQuantityChange = (rowkey, value) => { - setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, quantity: Number(value) || 0 } : c))); - }; - const handleCollectionAmtChange = (rowkey, value) => { - setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, collectionamt: Number(value) || 0 } : c))); - }; - - // ============================== buildDeliveryTime ============================== - const buildDeliveryTime = () => { - if (pickupSlot) { - const parsed = dayjs(pickupSlot, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); - if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); - } - if (startdate && selectedtime) { - const parsed = dayjs(`${dayjs(startdate).format('YYYY-MM-DD')} ${selectedtime}`, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); - if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); - } - return dayjs().format('YYYY-MM-DD HH:mm:ss'); - }; - - // ============================== createorders ============================== - const createorders = async () => { - if (!tenantid) { - opentoast('Client not found. Please re-login.', 'warning'); - return; - } - if (!pickCust) { - opentoast('Pickup location required', 'warning'); - return; - } - if (!pickupSlot) { - opentoast('Select a pickup slot', 'warning'); - return; - } - if (!dropCust.length) { - opentoast('Add at least one drop customer', 'warning'); - return; - } - // A Selection-sourced row (picked from Saved Customers) starts with no - // address/coordinates until the operator fills in its Address cell — - // block submit rather than send a booking with lat/lng silently at 0. - const incomplete = dropCust.find((c) => c.latitude == null || c.longitude == null); - if (incomplete) { - opentoast(`Add a delivery address for ${incomplete.firstname || 'a selected customer'} before submitting`, 'warning', 4000); - return; - } - - const deliverytime = buildDeliveryTime(); - // POST /admin/expressbooking/bulk takes { bookings: [...] } where each - // entry is a flat booking object (tenantid, pickup* fields, customer_*, - // delivery*, service_option, finalprice, notes, parcels[]) — not the old - // per-row orders/pickup/drop shape. - // tenantid comes from the logged-in session (see the top-level `tenantid` - // const above), not from pickCust — pickCust is a tenant *location* - // object (GET /admin/tenants/:id/locations) and was never confirmed to - // carry a tenantid back-reference, so `pickCust.tenantid` was silently - // undefined on every submit. - // - // Always send raw pickup* fields, never pickuplocationid — live-confirmed - // pickuplocationid currently 500s on POST /admin/expressbooking (and by - // extension the bulk endpoint, same handler); a saved location's own - // address/pincode/city/lat/lng (already on pickCust) work fine sent raw. - // Note: pickCust.pincode, not .postcode — that was also wrong (the real - // GET /admin/tenants/:id/locations field is `pincode`). - const bookings = dropCust.map((customer) => ({ - tenantid: Number(tenantid), - pickupaddress: pickCust.address || '', - pickuppincode: pickCust.pincode || '', - pickupcity: pickCust.city || '', - pickuplatitude: Number(pickCust.latitude) || 0, - pickuplongitude: Number(pickCust.longitude) || 0, - customer_phone: customer.contactno?.toString() || '', - customer_name: customer.firstname || '', - deliveryaddress: customer.address || '', - deliverypincode: customer.postcode || '', - deliverycity: customer.city || '', - deliverylatitude: Number(customer.latitude) || 0, - deliverylongitude: Number(customer.longitude) || 0, - deliverytime, - service_option: 'Normal', - finalprice: +customer.totalcharge || 0, - notes: otherinstructions || '', - parcels: [{ itemcategory: 'General', itemdescription: 'Order', declaredvalue: +customer.totalcharge || 0 }] - })); - - setLoading(true); - setBtnLoading(true); - try { - const res = await createExpressBookingBulk(bookings); - if (res.success) { - opentoast('Orders Created Successfully', 'success', 2000); - navigate('/doormile/orders'); - } else { - opentoast(res?.message || 'Order creation failed. Please try again.', 'warning', 3000); - } - } catch (err) { - // doormileAxios's response interceptor rejects with error.response.data - // (not the raw axios error), so the message lives at err.message here — - // err.response was always undefined at this point, so this always fell - // through to the generic "Something went wrong." fallback before. - opentoast(err?.message || 'Something went wrong.', 'error', 3000); - } finally { - setLoading(false); - setBtnLoading(false); - } - }; - - // ============================== derived ============================== - const canSubmit = !!locationid && !!pickupSlot && dropCust.length > 0; - const prereqOk = !!locationid; - const previewMode = dropCust.length > 0 ? 'drops' : users.length > 0 ? 'preview' : 'empty'; - - // ============================== render ============================== - return ( - <> - {loading && } - t.zIndex.drawer + 1 }} open={btnLoading}> - - - - - {/* Title bar */} - - - Create Multiple Orders - - - - - Bulk-create deliveries from CSV/Excel or saved customers. - - - - {/* 50 / 50 workspace */} - - {/* LEFT — input fields */} - - {/* Card: Schedule & Pickup */} - - - - Schedule & Pickup - - - - - {locationsStatus === 'loading' ? ( - - Loading business locations… - - ) : locationsStatus === 'no-tenant' && isStaffLogin ? ( - // Staff login (express-console-api.md: token tenantid - // 0/null = "Doormile's own staff... sees everything, all - // tenants") — pick which tenant this bulk order is for. - option?.tenantname || ''} - isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid} - onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')} - renderInput={(params) => ( - - )} - /> - ) : locationsStatus === 'no-tenant' ? ( - - This login has no tenant assigned — sign in as a tenant user - - ) : locationsStatus === 'error' ? ( - gettenantlocations(tenantid)} - sx={{ - display: 'flex', - alignItems: 'center', - px: 1.5, - py: 1, - borderRadius: '10px', - bgcolor: '#fef2f2', - border: '1px solid #fecaca', - color: '#b91c1c', - fontWeight: 600, - fontSize: 12.5, - cursor: 'pointer' - }} - > - Could not load business locations — tap to retry - - ) : locationsStatus === 'empty' ? ( - - No saved business locations for this tenant - - ) : tenantLocations?.length === 1 ? ( - - - - ) - }} - sx={{ - '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', alignItems: 'center' }, - '& .MuiOutlinedInput-input': { padding: '0 14px !important' } - }} - /> - ) : ( - (option ? locationDisplayName(option) : '')} - renderOption={(props, option) => ( - - - - - {locationDisplayName(option)} - - {option.isprimary && ( - - PRIMARY - - )} - - - {option.address || option.city || ''} - - - - )} - value={locationValue} - onChange={(event, value, reason) => { - if (reason === 'clear' || !value) { - setLocationid(0); - setLocationValue(null); - setPickCust(null); - } else { - setLocationid(value.tenantlocationid || 0); - setLocationValue(value); - setPickCust(value); - } - }} - renderInput={(params) => ( - - )} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - height: '36px', - alignItems: 'center', - padding: '0 9px !important' - }, - '& .MuiAutocomplete-input': { padding: '0 !important' } - }} - /> - )} - - - - { - if (!e || !dayjs(e).isValid()) { - setStartdate(dayjs().format('MM-DD-YYYY')); - return; - } - const diffDays = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd'); - if (diffDays <= 0) { - setStartdate(dayjs(e).format('MM-DD-YYYY')); - setSelectedtime(''); - setPickupSlot(null); - } else { - opentoast('Choose an upcoming date', 'warning'); - setStartdate(dayjs().format('MM-DD-YYYY')); - } - }} - disablePast - slotProps={{ - textField: { - size: 'small', - fullWidth: true, - InputLabelProps: { shrink: true }, - InputProps: { - startAdornment: ( - - - - ) - }, - sx: { '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } } - } - }} - /> - - - - { - if (!locationid) { - OpenToast('Please select a Business Location first.', 'warning', 3000); - return; - } - setSlotDropdownOpen(true); - }} - onClose={() => setSlotDropdownOpen(false)} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - height: '36px', - alignItems: 'center', - padding: '0 9px !important' - }, - '& .MuiAutocomplete-input': { padding: '0 !important' } - }} - onChange={(e, newValue, reason) => { - if (reason === 'clear' || !newValue) { - setSelectedtime(null); - setPickupSlot(null); - return; - } - if (!newValue.time) { - OpenToast('This slot has no time configured.', 'warning', 3000); - return; - } - const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A'); - setSelectedtime(formattedTime); - const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); - setPickupSlot(finalDateTime); - }} - getOptionLabel={(option) => (option ? option.name : '')} - renderInput={(params) => ( - - - - ) - }} - /> - )} - /> - - - {pickCust ? ( - - - - - - - {pickCust.locationname || '—'} - - - {pickCust.address || '—'} - - - - ) : ( - Pickup auto-fills once a Business Location is selected. - )} - - - - - {/* Card: Notes */} - - - Order Notes - Applied to every order - - - - - setOtherinstructions(e.target.value)} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - height: '36px', - alignItems: 'center', - fontSize: '12px', - background: '#ffffff' - }, - '& .MuiOutlinedInput-input': { padding: '0 14px !important', fontSize: '12px' } - }} - /> - - - - - {/* Card: Summary + Submit */} - - - - Bulk Summary - - - Live totals - - - {(() => { - const metric = ({ icon: Icon, label, value, accent, active }) => ( - - - - - - - {label} - - - {value} - - - - ); - return ( - - - {metric({ - icon: FaRoute, - label: 'Distance', - value: totaldist ? `${totaldist} km` : '—', - accent: '#1890ff', - active: !!totaldist - })} - - - {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#16a34a', active: !!totalQty })} - - - {metric({ - icon: FaMoneyBillWave, - label: 'Cash Collect', - value: `₹${Number(totalCash).toFixed(2)}`, - accent: '#d97706', - active: !!totalCash - })} - - - {metric({ - icon: FaTruck, - label: 'Deliveries', - value: dropCust.length, - accent: '#65387a', - active: dropCust.length > 0 - })} - - - ); - })()} - - {dropCust.length > 0 && ( -
    -
    - -
    Total Charge
    -
    -
    - ₹{Number(totalAmt).toFixed(2)} -
    -
    - )} - - - - -
    -
    - - {/* RIGHT — file / drop preview */} - - - {/* Preview header */} - - - - {previewMode === 'drops' ? 'Drop List' : previewMode === 'preview' ? 'File Preview' : 'Preview'} - - - - {previewMode === 'drops' && ( - - )} - {previewMode === 'preview' && ( - } - sx={{ - height: 22, - fontWeight: 700, - fontSize: 11, - bgcolor: 'rgba(245,158,11,0.12)', - color: '#b45309', - border: '1px solid rgba(245,158,11,0.30)', - '& .MuiChip-icon': { color: '#b45309' } - }} - /> - )} - {previewMode !== 'empty' && - (() => { - const handleHeaderPick = (val) => { - if (!prereqOk) { - OpenToast('Please select a Business Location first.', 'warning', 3000); - return; - } - setUploadType(val); - if (val === 0) document.getElementById('upload-file')?.click(); - else if (val === 1) { - setIsCustomerOpen(true); - setSearchCustList(''); - } - }; - return ( - - - - - ); - })()} - - - {fileName && ( - - - - {fileName} - - - )} - - {previewMode === 'preview' && users.length >= 1 && ( - - - - Process & Calculate Distances - - - Click continue to import spreadsheet rows and calculate drop charges. - - - - - )} - - {/* Scrollable preview body */} - - {/* Drops — table */} - {previewMode === 'drops' && ( - - - - - # - Customer - Address - - Qty - - - Cash - - Km - - Charge - - - {' '} - - - - - {dropCust.map((customer, index) => ( - - {index + 1} - - {customer.firstname} - - - {customer.latitude == null || customer.longitude == null ? ( - - setDropCust((prev) => prev.map((c) => (c._rowkey === customer._rowkey ? { ...c, address: text } : c))) - } - onPlaceSelected={(place) => handleRowPlaceSelected(customer._rowkey, place)} - bias={pickCust?.latitude ? { lat: pickCust.latitude, lng: pickCust.longitude } : undefined} - textFieldSx={{ '& .MuiOutlinedInput-root': { height: 30, fontSize: 12, borderRadius: '8px' } }} - /> - ) : ( - - - {customer.address} - - - )} - - - {uploadType === 0 ? ( - customer.quantity - ) : ( - handleQuantityChange(customer._rowkey, e.target.value)} - inputProps={{ min: 0 }} - sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} - /> - )} - - - {uploadType === 0 ? ( - `₹${Number(customer.collectionamt || 0).toFixed(2)}` - ) : ( - { - const v = Number(e.target.value); - handleCollectionAmtChange(customer._rowkey, v > 0 ? v : 0); - }} - inputProps={{ min: 0 }} - InputProps={{ startAdornment: ₹ }} - sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} - /> - )} - - {customer.distance || '—'} - - ₹{Number(customer?.totalcharge || 0).toFixed(2)} - - - - handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5 }}> - - - - - - ))} - - Total - - - {totalQty} - - - ₹{Number(totalCash).toFixed(2)} - - {totaldist} - - ₹{Number(totalAmt).toFixed(2)} - - - - -
    -
    - )} - - {/* File preview — table */} - {previewMode === 'preview' && ( - - - - - # - Name - Contact - Address - - Qty - - - Cash - - - - - {users.map((u, i) => ( - - {i + 1} - - {u.firstname || '—'} - - {u.contactno || '—'} - - - - {u.address || '—'} - - - - - {u.quantity ?? '—'} - - - {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} - - - ))} - -
    -
    - )} - - {/* Empty state */} - {previewMode === 'empty' && - (() => { - const handleEmptyPick = (val) => { - if (!prereqOk) { - OpenToast('Please select a Business Location first.', 'warning', 3000); - return; - } - setUploadType(val); - setDropCust([]); - setUsers([]); - setFileName(''); - if (val === 0) document.getElementById('upload-file')?.click(); - else if (val === 1) { - setIsCustomerOpen(true); - setSearchCustList(''); - } - }; - const tile = ({ value, icon: Icon, title, sub, accent }) => ( - handleEmptyPick(value)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleEmptyPick(value); - } - }} - sx={{ - flex: 1, - minWidth: 0, - display: 'flex', - alignItems: 'center', - textAlign: 'left', - gap: 1.25, - px: 1.5, - py: 1.25, - borderRadius: '10px', - border: '1.5px solid #eef2f6', - bgcolor: '#fff', - opacity: prereqOk ? 1 : 0.6, - cursor: prereqOk ? 'pointer' : 'not-allowed', - transition: 'all 0.18s ease', - '&:hover': prereqOk - ? { borderColor: accent, boxShadow: `0 4px 12px -4px ${accent}40`, transform: 'translateY(-1px)' } - : undefined - }} - > - - - - - {title} - - {sub} - - - - ); - return ( - - - - - Choose a Drop Source to begin - - Select a source below to import or pick your delivery customers. - - - {tile({ - value: 0, - icon: MdOutlineCloudUpload, - title: 'Excel / CSV', - sub: 'Bulk upload a sheet', - accent: '#1890ff' - })} - {tile({ value: 1, icon: FaUsers, title: 'Selection', sub: 'Pick saved customers', accent: '#65387a' })} - - - ); - })()} -
    -
    -
    -
    -
    - - - - {/* Saved customers dialog */} - setIsCustomerOpen(false)} - fullWidth - fullScreen={isMobile} - sx={{ '& .MuiDialog-paper': { borderRadius: { xs: 0, sm: '16px' }, overflow: 'hidden' } }} - > - - - - {`Select Drop Customers (${dropCust.length || 0})`} - - - setSearchCustList(e.target.value)} - sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} - startAdornment={ - - - - } - endAdornment={ - setSearchCustList('')}> - - - } - autoComplete="off" - /> - - - - - - {filteredCustomerList?.length === 0 ? ( - - - - ) : ( - - {filteredCustomerList?.map((customer, index) => { - const rowkey = `sel-${customer.appcustomerid}`; - const checked = dropCust.some((c) => c._rowkey === rowkey); - return ( - - handleCheckboxChange(event, customer)} />} - label={ - - - {customer.name} ({customer.phone}) - - - Address not on file — add one after selecting - - - } - /> - - ); - })} - - )} - - - - - - - - ); -}; - -export default MultipleOrders; +/* eslint-disable no-unused-vars */ +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { getAdminPricing, getAdminTenants, getTenantLocations, getAdminCustomers, createExpressBookingBulk } from 'pages/api/doormileApi'; +import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; +import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; +import Papa from 'papaparse'; +import * as XLSX from 'xlsx'; +import dayjs from 'dayjs'; +import { useNavigate } from 'react-router'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { enqueueSnackbar } from 'notistack'; +import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance'; + +import { + FormControl, + InputAdornment, + Grid, + Typography, + Stack, + Box, + Card, + Button, + TextField, + Autocomplete, + Divider, + Dialog, + DialogTitle, + DialogContent, + Checkbox, + DialogActions, + CircularProgress, + IconButton, + OutlinedInput, + FormGroup, + FormControlLabel, + Table, + TableContainer, + TableCell, + TableBody, + TableRow, + Paper, + TableHead, + Backdrop, + Chip, + Tooltip +} from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import ClearIcon from '@mui/icons-material/Clear'; +import MyLocationIcon from '@mui/icons-material/MyLocation'; +import { + SearchOutlined, + CloseOutlined, + ExclamationCircleOutlined, + FileAddOutlined, + CalendarOutlined, + ClockCircleOutlined, + FileTextOutlined, + InboxOutlined +} from '@ant-design/icons'; +import { Empty } from 'antd'; +import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa'; +import { FaLocationDot } from 'react-icons/fa6'; +import { MdOutlineCloudUpload } from 'react-icons/md'; + +import Loader from 'components/Loader'; +import CircularLoader from 'components/CircularLoader'; +import './OrdersRedesign.css'; + +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +const cellHeaderSx = { fontSize: 11.5, fontWeight: 700, color: '#475569', py: 0.75, px: 1 }; +const cellBodySx = { fontSize: 12, py: 0.6, px: 1 }; + +// GET /admin/tenants/:id/locations carries no `slots` field (no per-location +// pickup-hours endpoint in the new API) — the pickup-slot Autocomplete used +// to read tenantLocation.slots, which was always undefined, leaving the +// dropdown permanently empty and the page unsubmittable. Falls back to a +// fixed 09:00-21:00 / 30-min window, same rationale as createorder1.js's +// fetchTiming(). +const buildDefaultPickupSlots = () => { + const arr = []; + for (let t = dayjs().hour(9).minute(0).second(0); t.hour() < 21 || (t.hour() === 21 && t.minute() === 0); t = t.add(30, 'm')) { + arr.push({ name: t.format('hh:mm A'), time: t.format('HH:mm') }); + } + return arr; +}; + +// Some saved locations come back with a blank locationname (live-confirmed, +// e.g. tenant 1's only location) — fall back to the address rather than +// showing an empty field/option. +const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location'; + +// Tenants with many locations in the same city (DailyGrubs has 23, all in +// Coimbatore) made a "(city)" suffix useless for telling them apart — sort +// primary-first then alphabetically; the full address is shown per-option +// instead (see renderOption on the Business Location Autocomplete below). +const sortLocations = (locations) => + [...(locations || [])].sort((a, b) => { + if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1; + return locationDisplayName(a).localeCompare(locationDisplayName(b)); + }); + +const MultipleOrders = () => { + const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const locationRef = useRef(null); + const userid = localStorage.getItem('userid'); + // Per express-console-api.md's "Tenant scoping" section: token tenantid + // 0/null means Doormile's own staff, who can act on any tenant — NOT an + // error state. A client (tenant) login carries a real tenantid and never + // needs the picker below. `tenantid` (used throughout this file already) + // now resolves to whichever the operator is currently working as. + const loggedInTenantId = localStorage.getItem('tenantid'); + const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; + + // ============================== state ============================== + const [locations, setLocations] = useState([]); + const [tenantLocations, setTenantlocations] = useState([]); + const [loading, setLoading] = useState(false); + const [btnLoading, setBtnLoading] = useState(false); + + const [appId, setAppId] = useState(0); + const [locationid, setLocationid] = useState(0); + const [locationValue, setLocationValue] = useState(null); + const [locationsStatus, setLocationsStatus] = useState('loading'); + const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId); + const [staffTenantList, setStaffTenantList] = useState([]); + const [staffTenantListLoading, setStaffTenantListLoading] = useState(false); + const tenantid = selectedTenantId; + + const [basePrice, setBasePrice] = useState(0); + const [pricePerKm, setPricePerKm] = useState(0); + const [minKm, setMinKm] = useState(0); + + const [pickCust, setPickCust] = useState(null); + const [dropCust, setDropCust] = useState([]); + const [customerlist, setCustomerlist] = useState([]); + const [isCustomerOpen, setIsCustomerOpen] = useState(false); + const [searchCustList, setSearchCustList] = useState(''); + + const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); + const [selectedtime, setSelectedtime] = useState(''); + const [pickupSlotsList, setPickupSlotsList] = useState(buildDefaultPickupSlots); + const [pickupSlot, setPickupSlot] = useState(null); + + const [otherinstructions, setOtherinstructions] = useState(''); + const [admintoken, setAdmintoken] = useState(); + + const [totaldist, settotaldist] = useState(0); + const [totalAmt, settotalAmt] = useState(0); + const [totalQty, settotalQty] = useState(0); + const [totalCash, settotalCash] = useState(0); + + const [uploadType, setUploadType] = useState(null); + const [users, setUsers] = useState([]); + const [fileName, setFileName] = useState(''); + const [slotDropdownOpen, setSlotDropdownOpen] = useState(false); + + const toastCacheRef = useRef({}); + + // ============================== toast ============================== + const opentoast = (message, variant, time) => { + enqueueSnackbar(message, { + variant, + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: time || 1500 + }); + }; + + const OpenToast = (message, type = 'info', timeout = 3000) => { + const key = `${type}-${message}`; + if (toastCacheRef.current[key]) return; + opentoast(message, type, timeout); + toastCacheRef.current[key] = true; + setTimeout(() => delete toastCacheRef.current[key], 3000); + }; + + // ============================== reset chains ============================== + useEffect(() => { + setLocationid(0); + setLocationValue(null); + setDropCust([]); + setUsers([]); + setUploadType(null); + setFileName(''); + setPickupSlotsList(buildDefaultPickupSlots()); + setPickupSlot(null); + setSelectedtime(''); + }, [appId]); + + // ============================== fetchAppLocations ============================== + const fetchAppLocations = async () => { + setLoading(true); + try { + const locations = await fetchZoneLocations(); + setLocations(locations || []); + } catch (err) { + OpenToast(err.message, 'error', 5000); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchAppLocations(); + }, []); + + // ============================== fetchTenantPricing ============================== + const fetchTenantPricing = async (id) => { + try { + const pricing = (await getAdminPricing()) || []; + // id comes from localStorage (always a string) but pricing.tenantid is + // numeric — a strict === here never matched, so basePrice/pricePerKm/ + // minKm stayed at their 0 defaults and every order priced at ₹0. + const match = pricing.find((p) => String(p.tenantid) === String(id)); + if (match) { + setBasePrice(match.baseprice || 0); + setPricePerKm(match.priceperkm || 0); + setMinKm(match.basedistance || 0); + } + } catch (err) { + // swallow + } + }; + + useEffect(() => { + if (tenantid) fetchTenantPricing(tenantid); + }, [tenantid]); + + // ============================== gettenantlocations ============================== + // Drives what shows in place of the Business Location picker. Previously + // this function was only ever called when `tenantid` was truthy, so an + // operator on a no-tenant login (the shared admin/developer account has + // tenantid: null) got a permanently empty picker with zero feedback — not + // even a toast, since the function body that would show one never ran. + const gettenantlocations = async (id) => { + if (!id) { + // Staff login with no tenant picked yet — the picker UI covers this. + setLocationsStatus('no-tenant'); + return; + } + setLocationsStatus('loading'); + try { + const details = (await getTenantLocations(id)) || []; + setTenantlocations(details); + if (details.length === 1) { + setLocationsStatus('ready'); + setPickCust(details[0]); + setLocationid(details[0].tenantlocationid); + setLocationValue(details[0]); + } else if (details.length === 0) { + setLocationsStatus('empty'); + opentoast('No saved business locations for this tenant', 'info', 3000); + } else { + setLocationsStatus('ready'); + } + } catch (err) { + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here. + setTenantlocations([]); + setLocationsStatus('error'); + opentoast(err?.message || 'Could not load business locations', 'error', 4000); + } + }; + + useEffect(() => { + gettenantlocations(tenantid); + }, [tenantid]); + + // ============================== fetchStaffTenantList (Doormile staff picker) ============================== + // Only staff logins (loggedInTenantId blank/0) need this — a client login's + // tenant is already fixed by their token and this never fetches for them. + useEffect(() => { + if (!isStaffLogin) return; + setStaffTenantListLoading(true); + getAdminTenants() + .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active'))) + .catch((err) => { + opentoast(err?.message || 'Could not load tenant list', 'error', 4000); + }) + .finally(() => setStaffTenantListLoading(false)); + }, []); + + // ============================== clientdetails ============================== + // GET /admin/customers (B2C directory) — not /admin/tenantcustomers, which + // is an empty, manually-added contact book with no address/coordinates. + // This resource auto-grows from real orders (see the identical fix on + // createorder1.js's Saved Contacts) and has a confirmed live shape of + // { appcustomerid, createdat, email, name, phone, totalbookings } — no + // search/query param exists, so it's fetched once and filtered in-memory. + const clientdetails = async () => { + try { + const customers = (await getAdminCustomers()) || []; + setCustomerlist(customers); + } catch (err) { + opentoast('server error', 'warning'); + } + }; + + useEffect(() => { + if (!tenantid || !isCustomerOpen) return; + clientdetails(); + }, [tenantid, isCustomerOpen]); + + const filteredCustomerList = useMemo(() => { + const q = searchCustList.trim().toLowerCase(); + if (!q) return customerlist; + return customerlist.filter((c) => c.name?.toLowerCase().includes(q) || c.phone?.includes(searchCustList)); + }, [customerlist, searchCustList]); + + // ============================== totals ============================== + useEffect(() => { + let a1 = 0, + a2 = 0, + a3 = 0, + a4 = 0; + dropCust.forEach((c) => { + a1 += Number(c.distance) || 0; + a2 += Number(c.totalcharge) || 0; + a3 += Number(c.quantity) || 0; + a4 += Number(c.collectionamt) || 0; + }); + settotaldist(a1); + settotalAmt(a2); + settotalQty(a3); + settotalCash(a4); + }, [dropCust]); + + // ============================== distance (Google) ============================== + // ============================== distance (OSRM/Haversine) ============================== + const calculateDistance = async (customer) => { + try { + if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.'); + if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.'); + + const roundedDistance = await calculateDrivingDistance(pickCust, customer); + const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm); + return { roundedDistance, totalcharge }; + } catch (error) { + if (error.message.includes('Origin') || error.message.includes('customer') || error.message.includes('coordinates')) { + OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000); + } else { + OpenToast('Unexpected error during distance calculation.', 'error', 3000); + } + throw error; + } + }; + + // ============================== checkbox handlers ============================== + // GET /admin/customers has no address/coordinates at all, so a saved + // customer can no longer be distance-calculated at selection time (that's + // what used to throw "Invalid coordinates" and silently drop the row every + // time). Add the row immediately instead — its Address cell renders an + // editable AddressAutocomplete (see the Drop List table) until the + // operator picks a real address, which is what fills in lat/lng and lets + // distance/charge be computed per-row. + const handleCheckboxChange = (event, customer) => { + const rowkey = `sel-${customer.appcustomerid}`; + if (event.target.checked) { + setDropCust((prev) => { + if (prev.some((c) => c._rowkey === rowkey)) return prev; + return [ + ...prev, + { + _rowkey: rowkey, + appcustomerid: customer.appcustomerid, + firstname: customer.name || '', + contactno: customer.phone || '', + address: '', + city: '', + postcode: '', + latitude: null, + longitude: null, + distance: 0, + totalcharge: 0, + quantity: 0, + collectionamt: 0 + } + ]; + }); + } else { + setDropCust((prev) => prev.filter((c) => c._rowkey !== rowkey)); + } + }; + + // Still used by the CSV "Continue" promotion and the Drop List remove + // button. Safe to keep calling calculateDistance here — CSV rows always + // carry real latitude/longitude from the sheet, unlike Selection rows. + const handleCheckboxChange1 = async (customer) => { + const rowkey = customer._rowkey; + let wasSelected = false; + setDropCust((prev) => { + wasSelected = prev.some((c) => c._rowkey === rowkey); + if (wasSelected) return prev.filter((c) => c._rowkey !== rowkey); + return prev; + }); + if (wasSelected) return; + setLoading(true); + try { + const { roundedDistance, totalcharge } = await calculateDistance(customer); + setDropCust((prev) => { + if (prev.some((c) => c._rowkey === rowkey)) return prev; + return [...prev, { ...customer, distance: roundedDistance, totalcharge }]; + }); + } catch (err) { + console.error('Failed to calculate distance:', err); + OpenToast(`Could not calculate distance for ${customer.firstname || 'this row'}`, 'warning', 3000); + } finally { + setLoading(false); + } + }; + + // Parses the AddressAutocomplete `place` result (see createorder1.js's + // handleDropPlaceSelected for the same pattern) for one Drop List row, + // updates that row's address/coordinates, then recomputes its distance and + // charge now that coordinates finally exist. + const handleRowPlaceSelected = async (rowkey, place) => { + const parsed = { suburb: '', city: '', postcode: '' }; + place.address_components.forEach((component) => { + component.types.forEach((type) => { + switch (type) { + case 'sublocality_level_1': + case 'sublocality': + parsed.suburb = component.long_name; + break; + case 'locality': + parsed.city = component.long_name; + break; + case 'postal_code': + parsed.postcode = component.long_name; + break; + } + }); + }); + const latitude = place.geometry.location.lat(); + const longitude = place.geometry.location.lng(); + const address = place.formatted_address || ''; + + setDropCust((prev) => + prev.map((c) => (c._rowkey === rowkey ? { ...c, address, city: parsed.city, postcode: parsed.postcode, latitude, longitude } : c)) + ); + + try { + const { roundedDistance, totalcharge } = await calculateDistance({ latitude, longitude }); + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, distance: roundedDistance, totalcharge } : c))); + } catch (err) { + console.error('Failed to calculate distance:', err); + } + }; + + // No zone-admin-token or bulk-notify endpoint in the new API. + const fetchAppAdminTokens = async () => setAdmintoken([]); + + useEffect(() => { + if (appId) fetchAppAdminTokens(); + }, [appId]); + + // ============================== sendnotifications ============================== + const sendnotifications = async () => {}; + + // ============================== CSV / XLSX upload ============================== + const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name); + const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, ''); + + const headerMap = { + 'pickupdate(yyyy-mmm-dd)': 'date', + 'sendername*': 'locationname', + 'senderphone*': 'locationcontact', + 'senderaddress*': 'locationaddress', + 'receivername*': 'firstname', + receiverphone: 'contactno', + 'receiveralternatephone*': 'altcontactno', + receiverfulladdress: 'address', + // Best-effort — no live sample sheet to confirm these are the tenant's + // actual column headers. Without them deliverycity/deliverypincode in + // the submit payload were always sent blank; if these names are wrong, + // behavior is unchanged from before (still blank), not worse. + receivercity: 'city', + receiverpincode: 'postcode', + receiverlatitude: 'latitude', + receiverlongitude: 'longitude', + 'itemdescription*': 'description', + Quantity: 'quantity', + ' Collect Cash': 'collectionamt' + }; + + const handleFileDirectUpload = (event) => { + try { + const file = event.target.files?.[0]; + if (!file) { + opentoast('No file selected.', 'warning'); + return; + } + const fileNameLower = file.name.toLowerCase(); + const isCSV = fileNameLower.endsWith('.csv'); + const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx'); + if (!isCSV && !isExcel) { + opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); + return; + } + + const processData = (data, headers) => { + const normalizedMap = {}; + for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key]; + const mappedData = data.map((row, idx) => { + const newRow = { _rowkey: `csv-${idx}` }; + for (const key in row) { + const cleanKey = normalizeHeader(key); + const newKey = normalizedMap[cleanKey] || cleanKey; + let value = row[key]; + if (newKey === 'firstname') value = cleanReceiverName(value); + newRow[newKey] = value; + } + return newRow; + }); + const requiredCols = Object.keys(headerMap).filter((k) => k.trim().endsWith('*')); + const missingRequired = requiredCols.filter((clientCol) => !headers.includes(normalizeHeader(clientCol))); + if (missingRequired.length > 0) { + opentoast(`Missing columns: ${missingRequired.join(', ')}`, 'warning', 3000); + } + setUsers(mappedData); + opentoast('File uploaded successfully', 'success', 2000); + opentoast('Press Continue to add as drop customers', 'info', 2500); + }; + + if (isCSV) { + Papa.parse(file, { + header: true, + dynamicTyping: true, + skipEmptyLines: true, + complete: (results) => { + if (!results.data?.length) { + opentoast('CSV file is empty or has no valid rows.', 'warning'); + setUsers([]); + return; + } + processData(results.data, results.meta.fields.map(normalizeHeader)); + }, + error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning') + }); + } + if (isExcel) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const workbook = XLSX.read(e.target.result, { type: 'binary' }); + const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }); + if (!jsonData?.length) { + opentoast('Excel file is empty or invalid.', 'warning'); + setUsers([]); + return; + } + processData(jsonData, Object.keys(jsonData[0]).map(normalizeHeader)); + } catch (err) { + opentoast(`Error reading Excel: ${err.message}`, 'warning'); + } + }; + reader.readAsBinaryString(file); + } + } catch (err) { + opentoast(`Unexpected error: ${err.message}`, 'warning'); + } + }; + + const removeFileExtension = (n) => n.replace(/\.[^/.]+$/, ''); + const onFileChange = (event) => { + const file = event.target.files[0]; + if (!file) return; + const cleanedName = removeFileExtension(file.name); + setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName)); + handleFileDirectUpload(event); + }; + + // ============================== row editors ============================== + const handleQuantityChange = (rowkey, value) => { + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, quantity: Number(value) || 0 } : c))); + }; + const handleCollectionAmtChange = (rowkey, value) => { + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, collectionamt: Number(value) || 0 } : c))); + }; + + // ============================== buildDeliveryTime ============================== + const buildDeliveryTime = () => { + if (pickupSlot) { + const parsed = dayjs(pickupSlot, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); + if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); + } + if (startdate && selectedtime) { + const parsed = dayjs(`${dayjs(startdate).format('YYYY-MM-DD')} ${selectedtime}`, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); + if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); + } + return dayjs().format('YYYY-MM-DD HH:mm:ss'); + }; + + // ============================== createorders ============================== + const createorders = async () => { + if (!tenantid) { + opentoast('Client not found. Please re-login.', 'warning'); + return; + } + if (!pickCust) { + opentoast('Pickup location required', 'warning'); + return; + } + if (!pickupSlot) { + opentoast('Select a pickup slot', 'warning'); + return; + } + if (!dropCust.length) { + opentoast('Add at least one drop customer', 'warning'); + return; + } + // A Selection-sourced row (picked from Saved Customers) starts with no + // address/coordinates until the operator fills in its Address cell — + // block submit rather than send a booking with lat/lng silently at 0. + const incomplete = dropCust.find((c) => c.latitude == null || c.longitude == null); + if (incomplete) { + opentoast(`Add a delivery address for ${incomplete.firstname || 'a selected customer'} before submitting`, 'warning', 4000); + return; + } + + const deliverytime = buildDeliveryTime(); + // POST /admin/expressbooking/bulk takes { bookings: [...] } where each + // entry is a flat booking object (tenantid, pickup* fields, customer_*, + // delivery*, service_option, finalprice, notes, parcels[]) — not the old + // per-row orders/pickup/drop shape. + // tenantid comes from the logged-in session (see the top-level `tenantid` + // const above), not from pickCust — pickCust is a tenant *location* + // object (GET /admin/tenants/:id/locations) and was never confirmed to + // carry a tenantid back-reference, so `pickCust.tenantid` was silently + // undefined on every submit. + // + // Always send raw pickup* fields, never pickuplocationid — live-confirmed + // pickuplocationid currently 500s on POST /admin/expressbooking (and by + // extension the bulk endpoint, same handler); a saved location's own + // address/pincode/city/lat/lng (already on pickCust) work fine sent raw. + // Note: pickCust.pincode, not .postcode — that was also wrong (the real + // GET /admin/tenants/:id/locations field is `pincode`). + const bookings = dropCust.map((customer) => ({ + tenantid: Number(tenantid), + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.pincode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0, + customer_phone: customer.contactno?.toString() || '', + customer_name: customer.firstname || '', + deliveryaddress: customer.address || '', + deliverypincode: customer.postcode || '', + deliverycity: customer.city || '', + deliverylatitude: Number(customer.latitude) || 0, + deliverylongitude: Number(customer.longitude) || 0, + deliverytime, + service_option: 'Normal', + finalprice: +customer.totalcharge || 0, + notes: otherinstructions || '', + parcels: [{ itemcategory: 'General', itemdescription: 'Order', declaredvalue: +customer.totalcharge || 0 }] + })); + + setLoading(true); + setBtnLoading(true); + try { + const res = await createExpressBookingBulk(bookings); + if (res.success) { + opentoast('Orders Created Successfully', 'success', 2000); + navigate('/doormile/orders'); + } else { + opentoast(res?.message || 'Order creation failed. Please try again.', 'warning', 3000); + } + } catch (err) { + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here — + // err.response was always undefined at this point, so this always fell + // through to the generic "Something went wrong." fallback before. + opentoast(err?.message || 'Something went wrong.', 'error', 3000); + } finally { + setLoading(false); + setBtnLoading(false); + } + }; + + // ============================== derived ============================== + const canSubmit = !!locationid && !!pickupSlot && dropCust.length > 0; + const prereqOk = !!locationid; + const previewMode = dropCust.length > 0 ? 'drops' : users.length > 0 ? 'preview' : 'empty'; + + // ============================== render ============================== + return ( + <> + {loading && } + t.zIndex.drawer + 1 }} open={btnLoading}> + + + + + {/* Title bar */} + + + Create Multiple Orders + + + + + Bulk-create deliveries from CSV/Excel or saved customers. + + + + {/* 50 / 50 workspace */} + + {/* LEFT — input fields */} + + {/* Card: Schedule & Pickup */} + + + + Schedule & Pickup + + + + + {locationsStatus === 'loading' ? ( + + Loading business locations… + + ) : locationsStatus === 'no-tenant' && isStaffLogin ? ( + // Staff login (express-console-api.md: token tenantid + // 0/null = "Doormile's own staff... sees everything, all + // tenants") — pick which tenant this bulk order is for. + option?.tenantname || ''} + isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid} + onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')} + renderInput={(params) => ( + + )} + /> + ) : locationsStatus === 'no-tenant' ? ( + + This login has no tenant assigned — sign in as a tenant user + + ) : locationsStatus === 'error' ? ( + gettenantlocations(tenantid)} + sx={{ + display: 'flex', + alignItems: 'center', + px: 1.5, + py: 1, + borderRadius: '10px', + bgcolor: '#fef2f2', + border: '1px solid #fecaca', + color: '#b91c1c', + fontWeight: 600, + fontSize: 12.5, + cursor: 'pointer' + }} + > + Could not load business locations — tap to retry + + ) : locationsStatus === 'empty' ? ( + + No saved business locations for this tenant + + ) : tenantLocations?.length === 1 ? ( + + + + ) + }} + sx={{ + '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', alignItems: 'center' }, + '& .MuiOutlinedInput-input': { padding: '0 14px !important' } + }} + /> + ) : ( + (option ? locationDisplayName(option) : '')} + renderOption={(props, option) => ( + + + + + {locationDisplayName(option)} + + {option.isprimary && ( + + PRIMARY + + )} + + + {option.address || option.city || ''} + + + + )} + value={locationValue} + onChange={(event, value, reason) => { + if (reason === 'clear' || !value) { + setLocationid(0); + setLocationValue(null); + setPickCust(null); + } else { + setLocationid(value.tenantlocationid || 0); + setLocationValue(value); + setPickCust(value); + } + }} + renderInput={(params) => ( + + )} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + padding: '0 9px !important' + }, + '& .MuiAutocomplete-input': { padding: '0 !important' } + }} + /> + )} + + + + { + if (!e || !dayjs(e).isValid()) { + setStartdate(dayjs().format('MM-DD-YYYY')); + return; + } + const diffDays = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd'); + if (diffDays <= 0) { + setStartdate(dayjs(e).format('MM-DD-YYYY')); + setSelectedtime(''); + setPickupSlot(null); + } else { + opentoast('Choose an upcoming date', 'warning'); + setStartdate(dayjs().format('MM-DD-YYYY')); + } + }} + disablePast + slotProps={{ + textField: { + size: 'small', + fullWidth: true, + InputLabelProps: { shrink: true }, + InputProps: { + startAdornment: ( + + + + ) + }, + sx: { '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } } + } + }} + /> + + + + { + if (!locationid) { + OpenToast('Please select a Business Location first.', 'warning', 3000); + return; + } + setSlotDropdownOpen(true); + }} + onClose={() => setSlotDropdownOpen(false)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + padding: '0 9px !important' + }, + '& .MuiAutocomplete-input': { padding: '0 !important' } + }} + onChange={(e, newValue, reason) => { + if (reason === 'clear' || !newValue) { + setSelectedtime(null); + setPickupSlot(null); + return; + } + if (!newValue.time) { + OpenToast('This slot has no time configured.', 'warning', 3000); + return; + } + const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A'); + setSelectedtime(formattedTime); + const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); + setPickupSlot(finalDateTime); + }} + getOptionLabel={(option) => (option ? option.name : '')} + renderInput={(params) => ( + + + + ) + }} + /> + )} + /> + + + {pickCust ? ( + + + + + + + {pickCust.locationname || '—'} + + + {pickCust.address || '—'} + + + + ) : ( + Pickup auto-fills once a Business Location is selected. + )} + + + + + {/* Card: Notes */} + + + Order Notes + Applied to every order + + + + + setOtherinstructions(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + fontSize: '12px', + background: '#ffffff' + }, + '& .MuiOutlinedInput-input': { padding: '0 14px !important', fontSize: '12px' } + }} + /> + + + + + {/* Card: Summary + Submit */} + + + + Bulk Summary + + + Live totals + + + {(() => { + const metric = ({ icon: Icon, label, value, accent, active }) => ( + + + + + + + {label} + + + {value} + + + + ); + return ( + + + {metric({ + icon: FaRoute, + label: 'Distance', + value: totaldist ? `${totaldist} km` : '—', + accent: '#0ea5e9', + active: !!totaldist + })} + + + {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#10b981', active: !!totalQty })} + + + {metric({ + icon: FaMoneyBillWave, + label: 'Cash Collect', + value: `₹${Number(totalCash).toFixed(2)}`, + accent: '#f59e0b', + active: !!totalCash + })} + + + {metric({ + icon: FaTruck, + label: 'Deliveries', + value: dropCust.length, + accent: '#8b5cf6', + active: dropCust.length > 0 + })} + + + ); + })()} + + {dropCust.length > 0 && ( +
    +
    + +
    Total Charge
    +
    +
    + ₹{Number(totalAmt).toFixed(2)} +
    +
    + )} + + + + +
    +
    + + {/* RIGHT — file / drop preview */} + + + {/* Preview header */} + + + + {previewMode === 'drops' ? 'Drop List' : previewMode === 'preview' ? 'File Preview' : 'Preview'} + + + + {previewMode === 'drops' && ( + + )} + {previewMode === 'preview' && ( + } + sx={{ + height: 22, + fontWeight: 700, + fontSize: 11, + bgcolor: 'rgba(245,158,11,0.12)', + color: '#b45309', + border: '1px solid rgba(245,158,11,0.30)', + '& .MuiChip-icon': { color: '#b45309' } + }} + /> + )} + {previewMode !== 'empty' && + (() => { + const handleHeaderPick = (val) => { + if (!prereqOk) { + OpenToast('Please select a Business Location first.', 'warning', 3000); + return; + } + setUploadType(val); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { + setIsCustomerOpen(true); + setSearchCustList(''); + } + }; + return ( + + + + + ); + })()} + + + {fileName && ( + + + + {fileName} + + + )} + + {previewMode === 'preview' && users.length >= 1 && ( + + + + Process & Calculate Distances + + + Click continue to import spreadsheet rows and calculate drop charges. + + + + + )} + + {/* Scrollable preview body */} + + {/* Drops — table */} + {previewMode === 'drops' && ( + + + + + # + Customer + Address + + Qty + + + Cash + + Km + + Charge + + + {' '} + + + + + {dropCust.map((customer, index) => ( + + {index + 1} + + {customer.firstname} + + + {customer.latitude == null || customer.longitude == null ? ( + + setDropCust((prev) => prev.map((c) => (c._rowkey === customer._rowkey ? { ...c, address: text } : c))) + } + onPlaceSelected={(place) => handleRowPlaceSelected(customer._rowkey, place)} + bias={pickCust?.latitude ? { lat: pickCust.latitude, lng: pickCust.longitude } : undefined} + /> + ) : ( + + + {customer.address} + + + )} + + + {uploadType === 0 ? ( + customer.quantity + ) : ( + handleQuantityChange(customer._rowkey, e.target.value)} + inputProps={{ min: 0 }} + sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} + /> + )} + + + {uploadType === 0 ? ( + `₹${Number(customer.collectionamt || 0).toFixed(2)}` + ) : ( + { + const v = Number(e.target.value); + handleCollectionAmtChange(customer._rowkey, v > 0 ? v : 0); + }} + inputProps={{ min: 0 }} + InputProps={{ startAdornment: ₹ }} + sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} + /> + )} + + {customer.distance || '—'} + + ₹{Number(customer?.totalcharge || 0).toFixed(2)} + + + + handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5 }}> + + + + + + ))} + + Total + + + {totalQty} + + + ₹{Number(totalCash).toFixed(2)} + + {totaldist} + + ₹{Number(totalAmt).toFixed(2)} + + + + +
    +
    + )} + + {/* File preview — table */} + {previewMode === 'preview' && ( + + + + + # + Name + Contact + Address + + Qty + + + Cash + + + + + {users.map((u, i) => ( + + {i + 1} + + {u.firstname || '—'} + + {u.contactno || '—'} + + + + {u.address || '—'} + + + + + {u.quantity ?? '—'} + + + {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} + + + ))} + +
    +
    + )} + + {/* Empty state */} + {previewMode === 'empty' && + (() => { + const handleEmptyPick = (val) => { + if (!prereqOk) { + OpenToast('Please select a Business Location first.', 'warning', 3000); + return; + } + setUploadType(val); + setDropCust([]); + setUsers([]); + setFileName(''); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { + setIsCustomerOpen(true); + setSearchCustList(''); + } + }; + const tile = ({ value, icon: Icon, title, sub, accent }) => ( + handleEmptyPick(value)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleEmptyPick(value); + } + }} + sx={{ + flex: 1, + minWidth: 0, + display: 'flex', + alignItems: 'center', + textAlign: 'left', + gap: 1.25, + px: 1.5, + py: 1.25, + borderRadius: '10px', + border: '1.5px solid #eef2f6', + bgcolor: '#fff', + opacity: prereqOk ? 1 : 0.6, + cursor: prereqOk ? 'pointer' : 'not-allowed', + transition: 'all 0.18s ease', + '&:hover': prereqOk + ? { borderColor: accent, boxShadow: `0 4px 12px -4px ${accent}40`, transform: 'translateY(-1px)' } + : undefined + }} + > + + + + + {title} + + {sub} + + + + ); + return ( + + + + + Choose a Drop Source to begin + + Select a source below to import or pick your delivery customers. + + + {tile({ + value: 0, + icon: MdOutlineCloudUpload, + title: 'Excel / CSV', + sub: 'Bulk upload a sheet', + accent: '#0ea5e9' + })} + {tile({ value: 1, icon: FaUsers, title: 'Selection', sub: 'Pick saved customers', accent: '#8b5cf6' })} + + + ); + })()} +
    +
    +
    +
    +
    + + + + {/* Saved customers dialog */} + setIsCustomerOpen(false)} + fullWidth + fullScreen={isMobile} + sx={{ '& .MuiDialog-paper': { borderRadius: { xs: 0, sm: '16px' }, overflow: 'hidden' } }} + > + + + + {`Select Drop Customers (${dropCust.length || 0})`} + + + setSearchCustList(e.target.value)} + sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} + startAdornment={ + + + + } + endAdornment={ + setSearchCustList('')}> + + + } + autoComplete="off" + /> + + + + + + {filteredCustomerList?.length === 0 ? ( + + + + ) : ( + + {filteredCustomerList?.map((customer, index) => { + const rowkey = `sel-${customer.appcustomerid}`; + const checked = dropCust.some((c) => c._rowkey === rowkey); + return ( + + handleCheckboxChange(event, customer)} />} + label={ + + + {customer.name} ({customer.phone}) + + + Address not on file — add one after selecting + + + } + /> + + ); + })} + + )} + + + + + + + + ); +}; + +export default MultipleOrders; diff --git a/src/pages/nearle/orders/optimisedOrderPreview.js b/src/pages/nearle/orders/optimisedOrderPreview.js deleted file mode 100644 index 214943e..0000000 --- a/src/pages/nearle/orders/optimisedOrderPreview.js +++ /dev/null @@ -1,750 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { - Autocomplete, - Button, - Chip, - Divider, - Grid, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - Backdrop, - IconButton, - Badge, - Accordion, - AccordionSummary, - AccordionActions, - AccordionDetails, - useMediaQuery -} from '@mui/material'; -import React, { Fragment, useEffect, useMemo, useState } from 'react'; -import { useTheme } from '@mui/material/styles'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { useLocation, useNavigate } from 'react-router-dom'; -import dayjs from 'dayjs'; -import MainCard from 'components/MainCard'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { createAutomationDeliveries, fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from 'pages/api/api'; -import { OpenToast } from 'components/third-party/OpenToast'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import Loader from 'components/Loader'; -import CircularLoader from 'components/CircularLoader'; -import { Empty } from 'antd'; -import HoverSocialCard from 'components/cards/statistics/HoverSocialCard'; -import { DashboardFilled, DashboardOutlined, OpenAIFilled } from '@ant-design/icons'; -import { MdDirectionsBike } from 'react-icons/md'; -import { FaMapLocationDot } from 'react-icons/fa6'; -import { HiOutlineArrowLeft } from 'react-icons/hi'; -import { IoReload } from 'react-icons/io5'; -import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; -import CheckBoxIcon from '@mui/icons-material/CheckBox'; -import { GiProfit } from 'react-icons/gi'; -import { ArrowRightAltOutlined } from '@mui/icons-material'; -import LocalShippingIcon from '@mui/icons-material/LocalShipping'; -import WarningIcon from '@mui/icons-material/Warning'; -import BoltIcon from '@mui/icons-material/Bolt'; -import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; -import SavingsIcon from '@mui/icons-material/Savings'; -import EnergySavingsLeafIcon from '@mui/icons-material/EnergySavingsLeaf'; -import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined'; -import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; -import { exportToExcel } from 'components/exportToExcel'; -import CSVExport from 'components/third-party/ReactTable'; - -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -const OptimisedOrderPreview = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const navigate = useNavigate(); - const location = useLocation(); - console.log('location.state', location.state); - const [rider, setRider] = useState(null); - const [payment, setPayment] = useState(null); - const [finaldeliveryList, setFinalDeliveryList] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [deliverylist, setDeliverylist] = useState(location.state?.deliverylist); - const [zoneData, setZoneData] = useState(location?.state?.zoneData); - const [metaData, setMetaData] = useState(location.state?.metaData); - const riderToken = location.state?.riderToken; - const appId = location.state?.appId; - const aiMode = location.state?.aiMode; - const reassignOrders = location.state?.reassignOrders; - const [csvExportData, setCsvExportData] = useState([]); - - useEffect(() => { - console.log('called'); - - console.log('aiMode', aiMode); - console.log('riderToken', riderToken); - console.log('zoneData', zoneData); - console.log('metaData', metaData); - console.log('reassignOrders', reassignOrders); - }, [zoneData, metaData, deliverylist]); - - useEffect(() => { - const filtered = finaldeliveryList.map((item) => ({ - zone_name: item.zone_name, - ordernotes: item.ordernotes, - rider: item.rider, - step: item.step, - ordertype: item.ordertype, - orderamount: item.orderamount, - riderkms: item.riderkms, - cumulativekms: item.cumulativekms, - baseprice: item.baseprice, - minkm: item.minkm, - priceperkm: item.priceperkm, - kms: item.kms, - actualkms: item.actualkms, - rider_charge: item.rider_charge, - deliveryamt: item.deliveryamt, - deliverycharges: item.deliverycharges, - profit: item.profit - })); - - setCsvExportData(filtered); - - console.log('csvExportData', filtered); - }, [finaldeliveryList]); - useEffect(() => { - if (!deliverylist?.length) return; - const updateDeliveryAmtList = deliverylist.map((list) => { - const cumulativeKms = Number(list.cumulativekms || 0); - const minKm = Number(list.minkm || 0); - const basePrice = Number(list.baseprice || 0); - const pricePerKm = Number(list.priceperkm || 0); - if (cumulativeKms <= minKm) { - return { - ...list, - deliveryamt: basePrice - }; - } - return { - ...list, - deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice - }; - }); - - setFinalDeliveryList(updateDeliveryAmtList); - }, [deliverylist]); - - // ==============================|| fetchPaymentType ||============================== // - - const { - data: paymentModes = [], - isLoading: paymentModesLoading, - isError: paymentModesError, - error: paymentModesErrorMessage - } = useQuery({ - queryKey: ['paymentmodes'], - queryFn: fetchPaymentType - }); - - // ==============================|| fetchRidersList ||============================== // - - const { - data: ridersList = [], - isLoading: ridersListLoading, - isError: ridersListError, - error: ridersListErrorMessage, - refetch: ridersListRefetch - } = useQuery({ - queryKey: ['ridersList', appId], // Unique key for caching & re-fetching - queryFn: fetchRidersList, - enabled: appId !== 0 // Ensures query runs only when appId is valid - }); - - const getRiderName = async (userid) => { - await ridersList.map((rider) => { - if (rider.userid == userid) { - return rider.firstname; - } - }); - }; - - // ======================================================= || notifyRiderMutation || ======================================================= - - const notifyRiderMutation = useMutation({ - mutationFn: notifyRider, // Using the separate function - onSuccess: () => { - OpenToast('Notification sent Successfully', 'success', 2000); - }, - onError: (error) => { - OpenToast(error.message, 'error', 2000); - } - }); - - const createFinalDeliveryMutation = useMutation({ - mutationFn: finalCreatedeliveries, // for optimised delivery create - - onSuccess: (data) => { - // rider state here is never set (no setRider call anywhere in this - // file), so rider.userfcmtoken threw on every success — also the - // wrong argument type regardless (notifyRider needs a milerprofileid, - // not an FCM token; see dispatch/Preview.js's identical fix). Notify - // every rider actually present in the committed deliveries instead. - const notifiedRiderIds = new Set(); - (finaldeliveryList || []).forEach((order) => { - const riderId = order.userid; - if (riderId == null || notifiedRiderIds.has(String(riderId))) return; - notifiedRiderIds.add(String(riderId)); - notifyRiderMutation.mutate(riderId); - }); - if (data.status == 'accepted') { - OpenToast('Delivery Created Successfully', 'success', 2000); - } - }, - onError: (error) => { - OpenToast(error.message, 'error', 4000); - }, - onSettled: () => { - setTimeout(() => { - setIsLoading(false); - navigate('/doormile/deliveries'); - }, 2000); - } - }); - - const handleFinalCreateDelivery = async () => { - // console.log('Final delivery list:', finaldeliveryList); - - setIsLoading(true); - createFinalDeliveryMutation.mutate({ - deliveries: finaldeliveryList - }); - }; - const createDeliveryMutation = useMutation({ - // 0 -> manual rider assign 1 -> ai rider assign - mutationFn: createAutomationDeliveries, - onSuccess: (data, variables) => { - console.log('data', data); - console.log('varialbles', variables); - OpenToast('Orders Optimised Successfully', 'success', 2000); - // notifyRiderMutation.mutate(variables?.riderToken); // Call notifyRider after success - setZoneData(data?.zones); - setDeliverylist(data?.details); - setMetaData(data?.meta); - setIsLoading(false); - }, - onError: (error) => { - OpenToast(error.message, 'error', 4000); - setIsLoading(false); - }, - onSettled: () => { - setIsLoading(false); - } - }); - - const tuningTypes = [ - { - tuneid: 1, - type: 'Rider' - }, - { - tuneid: 2, - type: 'Pickup' - }, - { - tuneid: 3, - type: 'Zone' - } - ]; - - return ( - - { - theme.zIndex.drawer + 1 - }} - open={paymentModesLoading || ridersListLoading || isLoading} // when loader = true, backdrop covers the page - > - - - } - {(paymentModesLoading || ridersListLoading || isLoading) && ( - <> - - - - )} - - - - navigate('/doormile/orders')} - sx={{ - bgcolor: 'action.hover', - '&:hover': { bgcolor: 'action.selected' } - }} - > - - - - - Assign Orders - - - - {/* */} - option.type} - renderOption={(props, option, { selected }) => { - const { id, ...optionProps } = props; - const SelectionIcon = selected ? CheckBoxIcon : CheckBoxOutlineBlankIcon; - - return ( -
  • - - {option.type} -
  • - ); - }} - sx={{ width: { xs: '100%', md: 400 } }} - renderInput={(params) => ( - - )} - /> - {/* */} - -
    - - - - } - color={theme.palette.success.main} - sx={{ cursor: 'pointer' }} - /> - - - } - color={theme.palette.warning.main} - /> - - - } - color={theme.palette.info.main} - /> - - - } - color={theme.palette.error.main} - /> - - - - - {zoneData?.map((zone, index) => { - const riders = zone?.riders.flatMap((rider) => rider); - const orders = zone?.riders?.flatMap((rider) => rider.orders); - - return ( - - - - - Zone {index + 1} : - - - - - - - - - - - - - - - - {isMobile ? ( - - {orders.map((val, i) => { - const typeAccent = - val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#C01227'; - return ( - - - - - - {`${val.locationname}-(${val.locationsuburb})`} - - - - ) : val.ordertype === 'Risky' ? ( - - ) : ( - - ) - } - color={val.ordertype === 'Economy' ? 'success' : val.ordertype === 'Risky' ? 'error' : 'primary'} - variant="light" - /> - - - - - - {val.rider} - - ID : {val.userid} - - - - - {val.pickupcustomer} - - - {val.pickupcontactno} - - - - {val.pickupsuburb || val.pickupaddress.slice(0, 20)} - - - - - - - - - {val.deliverycustomer} - - - {val.deliverycontactno} - - - - {val.deliverysuburb || val.deliveryaddress.slice(0, 20)} - - - - - - {val.ordernotes ? : null} - - - - - - - - - - - - - - - ); - })} - - ) : ( - - - - - # - {/* Tenant */} - Order Location - Pickup - Delivery - Notes - Rider - Type - Profit - Charges - KMS - - - - - {orders.map((val, i) => ( - - - - {i + 1} - - {/* - - - {val.tenantname} - - - {val.tenantsuburb} -
    -
    - - - {val.applocation} - -
    -
    */} - - - - {`${val.locationname}-(${val.locationsuburb})`} - - - - - {val.orderid} - - - - - - {dayjs(val.pickupslot).format('DD/MM/YYYY')} - - - - - - - {dayjs(val.expecteddeliverytime).format('DD/MM/YYYY')} - - - - - - - - - {val.pickupcustomer} - {val.pickupcontactno} - - - {val.pickupsuburb || val.pickupaddress.slice(0, 20)} - - - - - - - - - {val.deliverycustomer} - {val.deliverycontactno} - - - {val.deliverysuburb || val.deliveryaddress.slice(0, 20)} - - - - - - - - {val.ordernotes} - - - - - - {val.rider} - -
    - ID : {val.userid} -
    - - - - ) : val.ordertype === 'Risky' ? ( - - ) : ( - - ) - } - color={val.ordertype === 'Economy' ? 'success' : val.ordertype === 'Risky' ? 'error' : 'primary'} - variant="light" - /> - - - - - - - - - - - {/* - - */} - - - - - - - - - - - - -
    -
    - ))} -
    -
    -
    - )} -
    -
    - ); - })} - - - - - - -
    -
    - ); -}; - -export default OptimisedOrderPreview; diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js index 7ed0eb2..edafb83 100644 --- a/src/pages/nearle/orders/orders.js +++ b/src/pages/nearle/orders/orders.js @@ -67,37 +67,16 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { fetchPercentageData, createAutomationDeliveries, getallriders, buildMilerLookup, notifyRider } from '../../api/api'; import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack, batchAssignBookings, getMilers } from 'pages/api/doormileApi'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; +import { DT, a, tint, soft, ring, edge } from 'themes/dt/tokens'; -// ============================================================================ -// Design tokens — shared with the rest of the redesigned operator pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 16, - shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', - shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', - shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; const dtA = (c, suffix) => `${c}${suffix}`; -const tint = (c) => dtA(c, '08'); -const soft = (c) => dtA(c, '18'); -const ring = (c) => dtA(c, '26'); -const edge = (c) => dtA(c, '55'); - const dtTint = tint; const dtSoft = soft; const dtRing = ring; const dtEdge = edge; -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; // Semantic per-row status palette, keyed by GET /admin/bookings' real `status` // enum (lowercased — StatusBadge lowercases before lookup). pending_pickup, @@ -455,7 +434,9 @@ const Orders = () => { } }, { - root: null, + // MainLayout's AppShell scrolls content inside `.astryx-layout-content` + // (height="fill"), not the window — root: null would never intersect. + root: document.querySelector('.astryx-layout-content'), rootMargin: '0px 0px 400px 0px', threshold: 0 } @@ -830,46 +811,51 @@ const Orders = () => { py: 0.5, flexShrink: 0, cursor: 'pointer', - borderRadius: 999, - border: `1.5px solid ${active ? t.color : edge(t.color)}`, - bgcolor: active ? t.color : tint(t.color), - color: active ? '#fff' : t.color, - fontWeight: 700, - boxShadow: active ? `0 6px 18px ${ring(t.color)}` : 'none', - transition: 'all 0.18s', + borderRadius: DT.radiusField + 'px', + // Selected tab always shows the brand red, not the status's own + // semantic color (pending=amber, delivered=emerald, etc.) — the + // status color survives on the avatar below, which is where + // operators actually read it. Shape/weight/hover match + // deliveries.js, the design reference for every list page. + border: `1px solid ${active ? BRAND : DT.borderSubtle}`, + bgcolor: active ? BRAND : DT.surface, + color: active ? '#fff' : DT.textSecondary, + fontWeight: 600, + boxShadow: 'none', + transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', '&:hover': { - borderColor: t.color, - boxShadow: active ? `0 6px 18px ${ring(t.color)}` : `0 0 0 3px ${ring(t.color)}` + borderColor: active ? BRAND : DT.borderHover, + bgcolor: active ? BRAND : DT.surfaceAlt } }} > - + - + {t.label} {count ?? 0} diff --git a/src/pages/nearle/reports/RidersRoutes.js b/src/pages/nearle/reports/RidersRoutes.js index c143d69..bfcdd1c 100644 --- a/src/pages/nearle/reports/RidersRoutes.js +++ b/src/pages/nearle/reports/RidersRoutes.js @@ -2,9 +2,17 @@ import React, { useEffect, useMemo, useState } from 'react'; import { MapContainer, TileLayer, Polyline, Marker, Popup, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; -import { Box, IconButton, Stack, Typography, CircularProgress } from '@mui/material'; import { MdClose, MdRoute } from 'react-icons/md'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Center } from '@astryxdesign/core/Center'; +import { Text } from '@astryxdesign/core/Text'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; + +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; + const containerStyle = { width: '100%', height: '100%' }; // Numbered step icon — brand red to match the planned-route polyline below. @@ -12,7 +20,7 @@ const containerStyle = { width: '100%', height: '100%' }; // the SVG without juggling external marker assets. const stepIcon = (n, isFocused) => { const size = isFocused ? 38 : 32; - const color = isFocused ? '#910E1D' : '#C01227'; + const color = isFocused ? '#262626' : '#000000'; const svg = encodeURIComponent( `` + `` + @@ -93,48 +101,51 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o }, [dropPath]); const headerBar = ( - - - Planned route{riderName ? ` — ${riderName}` : ''} - {dateRange && {dateRange}} - + + + Planned route{riderName ? ` — ${riderName}` : ''} + + {dateRange && ( + + {dateRange} + + )} + {details && details.length > 0 && ( - + {details.length} {details.length === 1 ? 'stop' : 'stops'} {routeLoading ? ' · resolving route…' : ''} - + )} - {onClose && ( - - - - )} - + {onClose && } onClick={onClose} />} +
    ); // Loading state — parent is still fetching the planned route data. if (loading) { return ( - + {headerBar} - - - Loading planned route… - - +
    + + + Loading planned route… + +
    + ); } @@ -142,22 +153,22 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o // selected window. if (!details || details.length === 0) { return ( - + {headerBar} - - No planned route for this rider - - There are no deliveries with drop coordinates assigned to this rider for the selected date range. - - - +
    + +
    + ); } return ( - + {headerBar} - +
    @@ -165,14 +176,14 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o {routePath.length > 0 ? ( <> {/* Translucent backdrop so the route stays legible on busy tiles. */} - + {/* Road-following planned route from OSRM. */} - + ) : ( // Fallback while OSRM is in flight (or if it fails) — dashed // straight-line skeleton between drop pins in step order. - + )} {details.map((d, i) => { @@ -189,22 +200,32 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o zIndexOffset={isFocused ? 1000 : stepNum} > setFocusedStep(null)}> - - + + Step {stepNum} · {d.customer} - - {d.address && {d.address}} - {d.expectedTime && ( - ETA {String(d.expectedTime).slice(11, 16) || d.expectedTime} + + {d.address && ( + + {d.address} + )} - {d.orderid && Order #{d.orderid}} - + {d.expectedTime && ( + + ETA {String(d.expectedTime).slice(11, 16) || d.expectedTime} + + )} + {d.orderid && ( + + Order #{d.orderid} + + )} + ); })} - - +
    +
    ); } diff --git a/src/pages/nearle/reports/mapWithRoute.js b/src/pages/nearle/reports/mapWithRoute.js index 5db77b6..0787d63 100644 --- a/src/pages/nearle/reports/mapWithRoute.js +++ b/src/pages/nearle/reports/mapWithRoute.js @@ -4,8 +4,13 @@ import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import dayjs from 'dayjs'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; -import { Chip, Stack, Typography, Box } from '@mui/material'; import { CloseCircleOutlined } from '@ant-design/icons'; + +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { Token } from '@astryxdesign/core/Token'; +import { Button } from '@astryxdesign/core/Button'; + import CircularLoader from 'components/CircularLoader'; import { DT } from 'themes/dt/tokens'; @@ -122,38 +127,26 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { const center = coordinates[Math.floor(coordinates.length / 2)]; const InfoItem = ({ label, value }) => ( - - - {label}: - - - + + {label}: + + ); return ( - +
    {loading && } {/* CLOSE BUTTON */} - } - onClick={() => setMapOpen(false)} - sx={{ - position: 'absolute', - top: 12, - right: 12, - zIndex: 2000, - bgcolor: DT.brand, - color: '#fff', - fontWeight: 600, - borderRadius: DT.radiusInner + 'px', - px: 1.5, - py: 0.5, - boxShadow: DT.shadowMd, - cursor: 'pointer', - '& .MuiChip-icon': { color: '#fff' } - }} - /> +
    +
    {/* MAP */} @@ -174,18 +167,17 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { {/* BOTTOM DETAILS */} - - + @@ -193,9 +185,9 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { - - - + +
    + ); }; diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js index a3ec9de..158d39e 100644 --- a/src/pages/nearle/reports/ordersDetails.js +++ b/src/pages/nearle/reports/ordersDetails.js @@ -1,1771 +1,1703 @@ -import { React, useState, useEffect, useMemo, useRef } from 'react'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Avatar, - Backdrop, - Box, - Button, - Chip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Grid, - IconButton, - List, - ListItem, - Paper, - Skeleton, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - Autocomplete, - useMediaQuery -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { - MdAssignment, - MdMyLocation, - MdGroups, - MdPlace, - MdDirectionsBike, - MdCalendarMonth, - MdFileDownload, - MdHourglassEmpty, - MdPersonPin, - MdLocationOn, - MdInventory2, - MdRoute, - MdSkipNext, - MdCheckCircle, - MdCancel, - MdList, - MdLocalShipping, - MdStraighten, - MdCurrencyRupee, - MdMap, - MdNoteAlt, - MdClose, - MdOutlineLocalShipping, - MdOutlineCheckCircle, - MdOutlinePendingActions, - MdOutlineCancel -} from 'react-icons/md'; -import { FaCircleCheck } from 'react-icons/fa6'; - -import MapWithRoute from './mapWithRoute'; -import CircularLoader from 'components/CircularLoader'; -import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api'; -import { getConsignmentLogs } from 'pages/api/doormileApi'; -import { CSVExport } from 'components/third-party/ReactTable'; -import Loader from 'components/Loader'; -import { enqueueSnackbar } from 'notistack'; -import DateFilterDialog from 'components/DateFilterDialog'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import dayjs from 'dayjs'; -import { OpenToast } from 'components/third-party/OpenToast'; -import TableLoader from 'components/nearle_components/TableLoader'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -const opentoast = (message, variant, time) => { - enqueueSnackbar(message, { - variant: variant, - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: time ? time : 1500 - }); -}; - -// ============================================================================ -// Design tokens — shared with deliveries / tenants / customers / pricing pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; - -// Haversine-derived kms come back with long floating-point tails -// (8.47316323423434532 km) — round to 2 decimals for display everywhere. -const formatKm = (value) => Number(value || 0).toFixed(2); +import { React, useState, useEffect, useMemo, useRef } from 'react'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; + +// material-ui +import { + Avatar, + Backdrop, + Box, + Button, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + IconButton, + Paper, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, + Autocomplete, + useMediaQuery +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { + MdAssignment, + MdGroups, + MdPlace, + MdDirectionsBike, + MdCalendarMonth, + MdFileDownload, + MdHourglassEmpty, + MdPersonPin, + MdLocationOn, + MdInventory2, + MdRoute, + MdSkipNext, + MdCheckCircle, + MdCancel, + MdList, + MdStraighten, + MdCurrencyRupee, + MdMap, + MdNoteAlt, + MdClose, + MdOutlineLocalShipping, + MdOutlineCheckCircle, + MdOutlinePendingActions, + MdOutlineCancel +} from 'react-icons/md'; +import { FaCircleCheck } from 'react-icons/fa6'; + +import MapWithRoute from './mapWithRoute'; +import CircularLoader from 'components/CircularLoader'; +import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api'; +import { getConsignmentLogs } from 'pages/api/doormileApi'; +import { CSVExport } from 'components/third-party/ReactTable'; +import Loader from 'components/Loader'; +import { enqueueSnackbar } from 'notistack'; +import DateFilterDialog from 'components/DateFilterDialog'; +import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; +import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; +import PageHeader from 'components/nearle_components/PageHeader'; +import StatCard from 'components/nearle_components/StatCard'; +import dayjs from 'dayjs'; +import { OpenToast } from 'components/third-party/OpenToast'; +import TableLoader from 'components/nearle_components/TableLoader'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; +import { DT, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; +// Local SoftPaper: this page still renders MUI Autocompletes, which need a +// `PaperComponent`. The shared themes/dt/primitives.js is Astryx-only now, so +// the popup surface lives here until this page's dropdowns become Astryx +// Selector/Typeahead — at which point this goes away with them. const SoftPaper = (props) => ( ); -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -const pillFieldSx = (color) => ({ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - } -}); - -// Status visual meta — semantic colours, NOT brand. Each lifecycle state has -// its own colour so operators can recognise it at a glance. -const STATUS_META = { - all: { label: 'All', color: BRAND, icon: MdList }, - pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, - arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, - picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, - active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, - delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, - skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, - cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } -}; - -const STATUS_TABS = ['all', 'pending', 'accepted', 'arrived', 'picked', 'active', 'delivered', 'skipped', 'cancelled']; - -// Soft pill used for metric cells (km, charges) inside the table. -const MetricPill = ({ color, icon, label, tooltip }) => ( - - - {icon} - {label} - - -); - -// Stamp cell — date + time stack with skeleton fallback for empty timestamps. -const StampCell = ({ value, formatDate, formatTime, success }) => { - if (!value) { - return ( - - - - - ); - } - return ( - - - {formatDate(value)} - - - - {formatTime(value)} - - {success && } - - - ); -}; - -// ==============================|| Orders Details ||============================== // - -export default function OrdersDetails() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const loadMoreRef = useRef(); - const containerRef = useRef(); - const locationRef = useRef(null); - const tenantRef = useRef(null); - const userid = localStorage.getItem('userid'); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(50); - const [locaName, setLocoName] = useState('All'); - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [open, setOpen] = useState(false); - const [mapOpen, setMapOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [appId, setAppId] = useState(0); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [riderCoordinates, setRiderCoordinates] = useState([]); - const [riderStart, setRiderStart] = useState(); - const [riderEnd, setRiderEnd] = useState(); - const [mapTenant, setMapTenant] = useState({}); - const [currentStatus, setCurrentStatus] = useState('All'); - const [locationid, setLocationid] = useState(0); - const [tenantid, setTenantid] = useState(0); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [selectedRider, setSelectedRider] = useState(); - const [riderValue, setRiderValue] = useState(null); - const [reportDialog, setReportDialog] = useState(false); - const [logsLoading, setLogsLoading] = useState(false); - - // Cascading clears so changing a parent filter resets its children. - useEffect(() => { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - setSelectedRider(null); - setRiderValue(null); - }, [appId]); - - useEffect(() => { - setLocationid(0); - setLocationValue(null); - setRiderValue(null); - }, [tenantid]); - - useEffect(() => { - setRiderValue(null); - }, [locationid]); - - // Haversine sum over consecutive points — total distance for the map's - // route summary. Re-added alongside getdeliverylogs below since - // GET /admin/consignments/:id/logs is now wired up for real. - const calculateTotalDistance = (routeCoordinates) => { - const R = 6371; - const toRad = (d) => (d * Math.PI) / 180; - let total = 0; - for (let i = 0; i < routeCoordinates.length - 1; i++) { - const { lat: lat1, lng: lon1 } = routeCoordinates[i]; - const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1]; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); - const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; - total += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - } - return total; - }; - - // GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md, - // Status: Done) as the GPS trail for one consignment. `id` here is - // `row.deliveryid`, which this page's own data source (fetchDeliveries) - // sets to the underlying bookingid, not a confirmed consignmentid — same - // caveat as Dispatch.js's Compare map. Field names on a log entry aren't - // documented anywhere, so this accepts several likely lat/lng/date key - // spellings defensively rather than assuming one. - const getdeliverylogs = async (id) => { - setLogsLoading(true); - try { - const logs = (await getConsignmentLogs(id)) || []; - const points = (Array.isArray(logs) ? logs : []) - .map((r) => ({ - lat: parseFloat(r?.latitude ?? r?.lat), - lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), - logdate: r?.logdate ?? r?.createdat ?? r?.timestamp - })) - .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) - .sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0)); - - if (points.length === 0) { - setRiderCoordinates([]); - opentoast('No Logs Found ', 'error', 2000); - return; - } - setRiderStart(points[0].logdate); - setRiderEnd(points[points.length - 1].logdate); - const coData = points.map(({ lat, lng }) => ({ lat, lng })); - setRiderCoordinates(coData); - calculateTotalDistance(coData); - setMapOpen(true); - } catch (error) { - opentoast('No Logs Found ', 'error', 2000); - } finally { - setLogsLoading(false); - } - }; - - // ==============================|| fetchDeliveries (bulk, auto-drained) ||============================== // - // GET /admin/bookings has no status/date/tenant/location/rider filter - // params (see express-console-api.md's "Conventions") — fetchDeliveries in - // api.js only ever reads rowsPerPage off this query's key, so switching - // tabs/tenant/rider used to change nothing about which rows were fetched, - // only the cache key. The tab count badges were even further disconnected - // — sourced from GET /admin/dashboard, which also takes no params, so they - // never matched the (also unfiltered) table either. Fixed the same way - // deliveries.js already was: auto-drain every page into one client-side - // dataset, then filter/count from that single source so the tab badge and - // the table can never disagree. - const { - data: deliveriesData, - isLoading: fetchDeliveriesIsLoading, - isError: fetchDeliveriesIsError, - error: fetchDeliveriesError, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useInfiniteQuery({ - queryKey: ['fetchdeliveries-all', appId, userid, 'all', startdate, enddate, 200, '', 0, 0, 0], - queryFn: fetchDeliveries, - getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchOnReconnect: true - }); - const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]); - - // Unconditional drain — keeps pulling pages regardless of scroll position, - // so counts/filters are accurate even before the operator scrolls. - useEffect(() => { - if (hasNextPage && !isFetchingNextPage) fetchNextPage(); - }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]); - - const rows = useMemo(() => { - const wantStatus = String(currentStatus || '').toLowerCase(); - const wantTenantId = Number(tenantid) || 0; - const wantRiderId = Number(selectedRider?.userid) || 0; - const q = String(debouncedSearch || '').trim().toLowerCase(); - return allRows.filter((r) => { - if (wantStatus !== 'all' && String(r.orderstatus || '').toLowerCase() !== wantStatus) return false; - if (wantTenantId && Number(r.tenantid) !== wantTenantId) return false; - if (wantRiderId && Number(r.userid) !== wantRiderId) return false; - if (q) { - const hay = [r.deliverycustomer, r.deliveryaddress, r.pickupcustomer, r.pickupaddress, r.orderid, r.tenantname, r.ridername] - .map((v) => String(v || '').toLowerCase()) - .join(' '); - if (!hay.includes(q)) return false; - } - return true; - }); - }, [allRows, currentStatus, tenantid, selectedRider, debouncedSearch]); - - // Sourced from the exact same allRows the table renders, so a tab's badge - // always matches what clicking it actually shows. - const statusCountByKey = useMemo(() => { - const c = { all: allRows.length, pending: 0, accepted: 0, arrived: 0, picked: 0, active: 0, delivered: 0, skipped: 0, cancelled: 0 }; - allRows.forEach((r) => { - const s = String(r.orderstatus || '').toLowerCase(); - if (c[s] !== undefined) c[s] += 1; - }); - return c; - }, [allRows]); - const total = statusCountByKey.all; - const pendingLenght = statusCountByKey.pending; - const deliveredLenght = statusCountByKey.delivered; - const cancelLenght = statusCountByKey.cancelled; - - useEffect(() => { - if (!hasNextPage) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - fetchNextPage(); - } - }, - { - root: document.querySelector('.MuiTableContainer-root'), - rootMargin: '0px', - threshold: 1.0 - } - ); - if (loadMoreRef.current) observer.observe(loadMoreRef.current); - return () => { - if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); - }; - }, [hasNextPage, fetchNextPage]); - - const handleScroll = (event) => { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; - if (scrollTop + clientHeight >= scrollHeight - 50) { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - } - }; - - // ==============================|| Tenant / Location / Rider lookups ||============================== // - - const { - data: tenantlist, - isLoading: fetchtenantsIsLoading, - isError: fetchtenantsIsError, - error: fetchtenantsError - } = useQuery({ - queryKey: ['tenantlist', appId], - queryFn: () => getTenants(appId), - // appId 0 is a real, deliberate choice — the synthetic "All" zone option - // (fetchAppLocations in api.js) — not "nothing picked yet"; only - // null/undefined means that now. See LocationAutocomplete.js's onChange. - enabled: appId != null - }); - - const { - data: ridersList, - isLoading: getriderbydeliveryIsLoading, - isError: getriderbydeliveryIsError, - error: getriderbydeliveryError - } = useQuery({ - queryKey: ['fetchRidersList', appId], - queryFn: fetchRidersList, - enabled: appId != null - }); - - const { - data: locationlist, - isLoading: fetchlocationsIsLoading, - isError: fetchlocationsIsError, - error: fetchlocationsError - } = useQuery({ - queryKey: ['gettenantlocations', tenantid], - queryFn: () => gettenantlocations(tenantid), - enabled: tenantid !== 0 - }); - - - // CSV export payload — flat schema preserved for backwards compatibility. - const csvData = rows?.map((order) => ({ - tenantname: order.tenantname, - tenantcity: order.tenantcity, - tenantcontactno: order.tenantcontactno, - rider: order.ridername, - orderid: order.orderid, - paymenttype: order.paymenttype == 64 ? 'Pay Later' : order.paymenttype == 42 ? 'Pay on Delivery' : 'Digital', - deliverydate: order.deliverydate, - orderstatus: order.orderstatus, - ordernotes: order.ordernotes, - kms: order.kms, - cumulativekms: order.cumulativekms, - assigntime: order.assigntime, - starttime: order.starttime, - arrivaltime: order.arrivaltime, - pickuptime: order.pickuptime, - deliverytime: order.deliverytime, - canceltime: order.canceltime, - deliverycharge: order.deliverycharges, - deliveryamt: order.deliveryamt, - pickupcustomer: order.pickupcustomer, - pickupcontactno: order.pickupcontactno, - Pickupaddress: order.pickupaddress, - pickupsuburb: order.pickupsuburb, - pickupcity: order.applocation, - pickuplat: order.pickuplat, - pickuplong: order.pickuplon, - deliverycustomer: order.deliverycustomer, - deliverycontactno: order.deliverycontactno, - deliveryaddress: order.deliveryaddress, - deliverysuburb: order.locationsuburb, - deliverylat: order.deliverylat, - deliverylong: order.deliverylong, - locationname: order.locationname, - locationsuburb: order.pickuplocation, - deliverylocation: order.deliverylocation, - locationcontactno: order.locationcontactno - })); - - function formatDate(dateString) { - return dayjs(dateString).format('DD/MM/YYYY '); - } - function formatTime(dateString) { - return dayjs(dateString).format(' hh:mm A'); - } - - const errormessage = fetchDeliveriesIsError - ? `An error has occurred: (fetchDeliveries) ${fetchDeliveriesError.message}` - : fetchtenantsIsError - ? `An error has occurred: (getTenants) ${fetchtenantsError.message}` - : fetchlocationsIsError - ? `An error has occurred: (gettenantlocations) ${fetchlocationsError.message}` - : getriderbydeliveryIsError - ? `An error has occurred: (getriderbydelivery) ${getriderbydeliveryError.message}` - : null; - - useEffect(() => { - if (errormessage) { - opentoast(errormessage, 'warning', 2000); - } - }, [errormessage]); - - const KPI_META = [ - { key: 'total', label: 'Total Orders', color: BRAND, icon: MdOutlineLocalShipping, value: total }, - { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredLenght }, - { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingLenght }, - { key: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdOutlineCancel, value: cancelLenght } - ]; - - return ( - <> - {(fetchDeliveriesIsLoading || - fetchtenantsIsLoading || - fetchlocationsIsLoading || - logsLoading || - getriderbydeliveryIsLoading || - isFetchingNextPage) && ( -
    - -
    - )} - {fetchDeliveriesIsLoading && ( - theme.zIndex.drawer + 1 - }} - open={fetchDeliveriesIsLoading} - /> - )} - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={fetchDeliveriesIsLoading} - /> - - ); - })} - - - {/* ============================================= || Filter Bar (tenant, location, rider, date, export) || ============================================= */} - - - - option?.tenantname || ''} - PaperComponent={SoftPaper} - onOpen={(event) => { - if (appId == null) { - event.preventDefault(); - OpenToast('Please select a your app location first!', 'warning', 3000); - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(val.locationid); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - (option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '')} - value={locationValue} - PaperComponent={SoftPaper} - onOpen={(event) => { - if (appId == null && !tenantid) { - event.preventDefault(); - OpenToast('Please select a your Location and Tenant first!', 'warning', 3000); - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select a your Tenant first!', 'warning', 3000); - setTimeout(() => { - tenantRef.current?.focus(); - }, 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - { - const name = option?.displayname || option?.authname || ''; - return option?.contactno ? `${name} (${option.contactno})` : name; - }} - PaperComponent={SoftPaper} - onOpen={() => { - if (appId == null) { - OpenToast('Select App Location First', 'warning', 2000); - } - }} - onChange={(event, value, reason) => { - if (reason === 'clear') { - setSelectedRider(null); - setRiderValue(null); - } else { - setSelectedRider(value); - setRiderValue(value); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - - - setOpen(true)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: 0.75, - px: 1.25, - py: 0.75, - borderRadius: 999, - cursor: 'pointer', - bgcolor: tint('#f59e0b'), - border: `1.5px solid ${edge('#f59e0b')}`, - color: '#f59e0b', - fontWeight: 800, - fontSize: 12, - transition: 'all 0.18s', - '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } - }} - > - - {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} - - - - - - - - - {/* ============================================= || Status Tabs + Search || ============================================= */} - - - - {STATUS_TABS.map((key) => { - const meta = STATUS_META[key]; - const Icon = meta.icon; - const active = currentStatus.toLowerCase() === key; - const count = statusCountByKey[key] ?? 0; - return ( - setCurrentStatus(key === 'all' ? 'All' : key)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: { xs: 0.625, md: 0.875 }, - pl: 0.5, - pr: { xs: 1, md: 1.25 }, - py: 0.5, - flexShrink: 0, - cursor: 'pointer', - borderRadius: 999, - border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`, - bgcolor: active ? '#C01227' : DT.surface, - color: active ? '#fff' : DT.textSecondary, - fontWeight: 600, - boxShadow: 'none', - transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', - '&:hover': { - borderColor: active ? '#C01227' : '#cbd5e1', - bgcolor: active ? '#C01227' : DT.surfaceAlt - } - }} - > - - - - - {meta.label} - - - {count} - - - ); - })} - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - - {isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {fetchDeliveriesIsLoading ? ( - - - - Loading orders… - - - ) : rows?.length == 0 ? ( - - - - - - No orders to show - - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} - - - ) : ( - rows?.map((row, index) => { - const statusKey = String(row.orderstatus || '').toLowerCase(); - const rowStatusMeta = STATUS_META[statusKey] || { - label: row.orderstatus || '—', - color: BRAND, - icon: MdAssignment - }; - const StatusIcon = rowStatusMeta.icon; - const cancelled = statusKey === 'cancelled'; - const isDelivered = row.orderstatus === 'delivered'; - return ( - - - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - - - - {row.tenantname} - - - #{row.orderid} - - - - - - { - if (isDelivered) { - getdeliverylogs(row.deliveryid); - setMapTenant(row); - } - }} - sx={{ - flexShrink: 0, - bgcolor: isDelivered ? soft(BRAND) : soft('#94a3b8'), - color: isDelivered ? BRAND : DT.textMuted, - border: `1px solid ${isDelivered ? edge(BRAND) : edge('#94a3b8')}`, - '&:hover': { - bgcolor: isDelivered ? BRAND : soft('#94a3b8'), - color: isDelivered ? '#fff' : DT.textMuted - } - }} - > - - - - - - - - {rowStatusMeta.label} - - {row.ridername && ( - - {row.ridername} - - )} - - {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} - - - - } - > - - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} - - {row.applocation && ( - - {row.applocation} - - )} - - - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} - tooltip="KMS" - /> - } - label={`${formatKm(row.cumulativekms)} km`} - tooltip="Actual KMS" - /> - } - label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} - tooltip="Rider KMS" - /> - - - - - } - label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} - tooltip="Delivery Charge" - /> - } - label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} - tooltip="Delivery Amount" - /> - - - - {row.ordernotes && ( - - - - - {row.ordernotes} - - - - )} - - - ); - }) - )} - - ) : ( - - - - # - Map - Client - Pickup - Drop - Status / Rider - Assigned - Accepted - Arrived - Picked - Active - Delivered - Cancelled - Notes - KMS - Charges - - - - - {fetchDeliveriesIsLoading ? ( - - ) : rows?.length == 0 ? ( - - - - - - - - No orders to show - - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} - - - - - ) : ( - rows?.map((row, index) => { - const statusKey = String(row.orderstatus || '').toLowerCase(); - const rowStatusMeta = STATUS_META[statusKey] || { - label: row.orderstatus || '—', - color: BRAND, - icon: MdAssignment - }; - const StatusIcon = rowStatusMeta.icon; - const cancelled = statusKey === 'cancelled'; - return ( - - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - {/* ====================== Map button ====================== */} - - - - { - if (row.orderstatus === 'delivered') { - getdeliverylogs(row.deliveryid); - setMapTenant(row); - } - }} - sx={{ - bgcolor: row.orderstatus === 'delivered' ? soft(BRAND) : soft('#94a3b8'), - color: row.orderstatus === 'delivered' ? BRAND : DT.textMuted, - border: `1px solid ${row.orderstatus === 'delivered' ? edge(BRAND) : edge('#94a3b8')}`, - '&:hover': { - bgcolor: row.orderstatus === 'delivered' ? BRAND : soft('#94a3b8'), - color: row.orderstatus === 'delivered' ? '#fff' : DT.textMuted - } - }} - > - - - - - - - {/* ====================== Client ====================== */} - - - - - - - - {row.tenantname} - - - - #{row.orderid} - - - - {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} - - - - - - {/* ====================== Pickup ====================== */} - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - - {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} - - - {row.applocation && ( - - {row.applocation} - - )} - - - - {/* ====================== Drop ====================== */} - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - - {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} - - - - - - {/* ====================== Status / Rider ====================== */} - - - - {rowStatusMeta.label} - - {row.ridername && ( - - - - - {row.ridername} - - - - )} - - - - {/* ====================== Timestamps ====================== */} - - - - - - - - - - - {/* GET /admin/bookings has no per-event timestamps (accepted/ - arrived/pickup/start/cancel) — only assigntime (updatedat) and - deliverydate exist on the mapped row (see fetchDeliveries in - api.js), so row.pickuptime is always undefined and this cell - always renders the loading skeleton. The old `success` check - compared against a `deliverystatus` field that never existed on - this row either (the real field is `orderstatus`) — dropped - rather than guessed, since orderstatus's confirmed values - (pending/accepted/active/delivered/cancelled) have no distinct - "picked" state to map it to. */} - - - - - - - - - - - - - {/* ====================== Notes ====================== */} - - {row.ordernotes ? ( - - - - {row.ordernotes} - - - ) : ( - — - )} - - - {/* ====================== KMS ====================== */} - - - } - label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} - tooltip="KMS" - /> - } - label={`${formatKm(row.cumulativekms)} km`} - tooltip="Actual KMS" - /> - } - label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} - tooltip="Rider KMS" - /> - - - - {/* ====================== Charges ====================== */} - - - } - label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} - tooltip="Delivery Charge" - /> - } - label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} - tooltip="Delivery Amount" - /> - - - - ); - }) - )} - -
    - )} - - {rows?.length !== 0 && ( - - - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more orders - - )} - - - )} -
    -
    - - {/* ============================================= || Export Dialog || ============================================= */} - setReportDialog(false)} - fullWidth - maxWidth="sm" - fullScreen={isMobile} - PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 }, overflow: 'hidden' } }} - > - - - - - - - - - Report - - - Export Orders - - - - setReportDialog(false)} - sx={{ color: '#fff', bgcolor: 'rgba(255,255,255,0.18)', '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }} - > - - - - - - {fetchDeliveriesIsLoading && } - {[ - { label: 'App Location', value: locaName, color: BRAND }, - { label: 'Tenant', value: tenantValue?.tenantname, color: '#0ea5e9' }, - { label: 'Business Location', value: locationValue?.locationname, color: '#10b981' }, - { label: 'Status', value: currentStatus, color: '#f59e0b' }, - { label: 'Rider', value: riderValue ? `${riderValue.firstname} ${riderValue.lastname}` : null, color: '#8b5cf6' }, - { label: 'Keyword', value: searchword, color: '#06b6d4' }, - { label: 'Start Date', value: startdate, color: '#14b8a6' }, - { label: 'End Date', value: enddate, color: '#ef4444' } - ].map((item, idx) => ( - - - {item.label} - - - - ))} - - - - { - setTimeout(() => setReportDialog(false), 0); - }} - /> - - - - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - {/* ============================================= || Map Dialog || ============================================= */} - { - setMapOpen(false); - }} - fullScreen - fullWidth - > - {riderCoordinates && ( -
    - -
    - )} -
    - - ); -} +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +const opentoast = (message, variant, time) => { + enqueueSnackbar(message, { + variant: variant, + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: time ? time : 1500 + }); +}; + +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; + +// Haversine-derived kms come back with long floating-point tails +// (8.47316323423434532 km) — round to 2 decimals for display everywhere. +const formatKm = (value) => Number(value || 0).toFixed(2); + +// Status visual meta — semantic colours, NOT brand. Each lifecycle state has +// its own colour so operators can recognise it at a glance. +const STATUS_META = { + all: { label: 'All', color: BRAND, icon: MdList }, + pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, + accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, + arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, + picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, + active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, + delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, + cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } +}; + +const STATUS_TABS = ['all', 'pending', 'accepted', 'arrived', 'picked', 'active', 'delivered', 'skipped', 'cancelled']; + +// Soft pill used for metric cells (km, charges) inside the table. +const MetricPill = ({ color, icon, label, tooltip }) => ( + + + {icon} + {label} + + +); + +// Stamp cell — date + time stack with skeleton fallback for empty timestamps. +const StampCell = ({ value, formatDate, formatTime, success }) => { + if (!value) { + return ( + + + + + ); + } + return ( + + + {formatDate(value)} + + + + {formatTime(value)} + + {success && } + + + ); +}; + +// ==============================|| Orders Details ||============================== // + +export default function OrdersDetails() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const loadMoreRef = useRef(); + const containerRef = useRef(); + const locationRef = useRef(null); + const tenantRef = useRef(null); + const userid = localStorage.getItem('userid'); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(50); + const [locaName, setLocoName] = useState('All'); + const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); + const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); + const [open, setOpen] = useState(false); + const [mapOpen, setMapOpen] = useState(false); + const [datestatus, setDatestatus] = useState('Today'); + const [appId, setAppId] = useState(0); + const [searchword, setSearchword] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [riderCoordinates, setRiderCoordinates] = useState([]); + const [riderStart, setRiderStart] = useState(); + const [riderEnd, setRiderEnd] = useState(); + const [mapTenant, setMapTenant] = useState({}); + const [currentStatus, setCurrentStatus] = useState('All'); + const [locationid, setLocationid] = useState(0); + const [tenantid, setTenantid] = useState(0); + const [tenantValue, setTenantValue] = useState(null); + const [locationValue, setLocationValue] = useState(null); + const [selectedRider, setSelectedRider] = useState(); + const [riderValue, setRiderValue] = useState(null); + const [reportDialog, setReportDialog] = useState(false); + const [logsLoading, setLogsLoading] = useState(false); + + // Cascading clears so changing a parent filter resets its children. + useEffect(() => { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + setSelectedRider(null); + setRiderValue(null); + }, [appId]); + + useEffect(() => { + setLocationid(0); + setLocationValue(null); + setRiderValue(null); + }, [tenantid]); + + useEffect(() => { + setRiderValue(null); + }, [locationid]); + + // Haversine sum over consecutive points — total distance for the map's + // route summary. Re-added alongside getdeliverylogs below since + // GET /admin/consignments/:id/logs is now wired up for real. + const calculateTotalDistance = (routeCoordinates) => { + const R = 6371; + const toRad = (d) => (d * Math.PI) / 180; + let total = 0; + for (let i = 0; i < routeCoordinates.length - 1; i++) { + const { lat: lat1, lng: lon1 } = routeCoordinates[i]; + const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1]; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + total += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + } + return total; + }; + + // GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md, + // Status: Done) as the GPS trail for one consignment. `id` here is + // `row.deliveryid`, which this page's own data source (fetchDeliveries) + // sets to the underlying bookingid, not a confirmed consignmentid — same + // caveat as Dispatch.js's Compare map. Field names on a log entry aren't + // documented anywhere, so this accepts several likely lat/lng/date key + // spellings defensively rather than assuming one. + const getdeliverylogs = async (id) => { + setLogsLoading(true); + try { + const logs = (await getConsignmentLogs(id)) || []; + const points = (Array.isArray(logs) ? logs : []) + .map((r) => ({ + lat: parseFloat(r?.latitude ?? r?.lat), + lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), + logdate: r?.logdate ?? r?.createdat ?? r?.timestamp + })) + .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) + .sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0)); + + if (points.length === 0) { + setRiderCoordinates([]); + opentoast('No Logs Found ', 'error', 2000); + return; + } + setRiderStart(points[0].logdate); + setRiderEnd(points[points.length - 1].logdate); + const coData = points.map(({ lat, lng }) => ({ lat, lng })); + setRiderCoordinates(coData); + calculateTotalDistance(coData); + setMapOpen(true); + } catch (error) { + opentoast('No Logs Found ', 'error', 2000); + } finally { + setLogsLoading(false); + } + }; + + // ==============================|| fetchDeliveries (bulk, auto-drained) ||============================== // + // GET /admin/bookings has no status/date/tenant/location/rider filter + // params (see express-console-api.md's "Conventions") — fetchDeliveries in + // api.js only ever reads rowsPerPage off this query's key, so switching + // tabs/tenant/rider used to change nothing about which rows were fetched, + // only the cache key. The tab count badges were even further disconnected + // — sourced from GET /admin/dashboard, which also takes no params, so they + // never matched the (also unfiltered) table either. Fixed the same way + // deliveries.js already was: auto-drain every page into one client-side + // dataset, then filter/count from that single source so the tab badge and + // the table can never disagree. + const { + data: deliveriesData, + isLoading: fetchDeliveriesIsLoading, + isError: fetchDeliveriesIsError, + error: fetchDeliveriesError, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useInfiniteQuery({ + queryKey: ['fetchdeliveries-all', appId, userid, 'all', startdate, enddate, 200, '', 0, 0, 0], + queryFn: fetchDeliveries, + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true + }); + const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]); + + // Unconditional drain — keeps pulling pages regardless of scroll position, + // so counts/filters are accurate even before the operator scrolls. + useEffect(() => { + if (hasNextPage && !isFetchingNextPage) fetchNextPage(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]); + + const rows = useMemo(() => { + const wantStatus = String(currentStatus || '').toLowerCase(); + const wantTenantId = Number(tenantid) || 0; + const wantRiderId = Number(selectedRider?.userid) || 0; + const q = String(debouncedSearch || '').trim().toLowerCase(); + return allRows.filter((r) => { + if (wantStatus !== 'all' && String(r.orderstatus || '').toLowerCase() !== wantStatus) return false; + if (wantTenantId && Number(r.tenantid) !== wantTenantId) return false; + if (wantRiderId && Number(r.userid) !== wantRiderId) return false; + if (q) { + const hay = [r.deliverycustomer, r.deliveryaddress, r.pickupcustomer, r.pickupaddress, r.orderid, r.tenantname, r.ridername] + .map((v) => String(v || '').toLowerCase()) + .join(' '); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [allRows, currentStatus, tenantid, selectedRider, debouncedSearch]); + + // Sourced from the exact same allRows the table renders, so a tab's badge + // always matches what clicking it actually shows. + const statusCountByKey = useMemo(() => { + const c = { all: allRows.length, pending: 0, accepted: 0, arrived: 0, picked: 0, active: 0, delivered: 0, skipped: 0, cancelled: 0 }; + allRows.forEach((r) => { + const s = String(r.orderstatus || '').toLowerCase(); + if (c[s] !== undefined) c[s] += 1; + }); + return c; + }, [allRows]); + const total = statusCountByKey.all; + const pendingLenght = statusCountByKey.pending; + const deliveredLenght = statusCountByKey.delivered; + const cancelLenght = statusCountByKey.cancelled; + + useEffect(() => { + if (!hasNextPage) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + fetchNextPage(); + } + }, + { + root: document.querySelector('.MuiTableContainer-root'), + rootMargin: '0px', + threshold: 1.0 + } + ); + if (loadMoreRef.current) observer.observe(loadMoreRef.current); + return () => { + if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); + }; + }, [hasNextPage, fetchNextPage]); + + const handleScroll = (event) => { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; + if (scrollTop + clientHeight >= scrollHeight - 50) { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + } + }; + + // ==============================|| Tenant / Location / Rider lookups ||============================== // + + const { + data: tenantlist, + isLoading: fetchtenantsIsLoading, + isError: fetchtenantsIsError, + error: fetchtenantsError + } = useQuery({ + queryKey: ['tenantlist', appId], + queryFn: () => getTenants(appId), + // appId 0 is a real, deliberate choice — the synthetic "All" zone option + // (fetchAppLocations in api.js) — not "nothing picked yet"; only + // null/undefined means that now. See LocationAutocomplete.js's onChange. + enabled: appId != null + }); + + const { + data: ridersList, + isLoading: getriderbydeliveryIsLoading, + isError: getriderbydeliveryIsError, + error: getriderbydeliveryError + } = useQuery({ + queryKey: ['fetchRidersList', appId], + queryFn: fetchRidersList, + enabled: appId != null + }); + + const { + data: locationlist, + isLoading: fetchlocationsIsLoading, + isError: fetchlocationsIsError, + error: fetchlocationsError + } = useQuery({ + queryKey: ['gettenantlocations', tenantid], + queryFn: () => gettenantlocations(tenantid), + enabled: tenantid !== 0 + }); + + + // CSV export payload — flat schema preserved for backwards compatibility. + const csvData = rows?.map((order) => ({ + tenantname: order.tenantname, + tenantcity: order.tenantcity, + tenantcontactno: order.tenantcontactno, + rider: order.ridername, + orderid: order.orderid, + paymenttype: order.paymenttype == 64 ? 'Pay Later' : order.paymenttype == 42 ? 'Pay on Delivery' : 'Digital', + deliverydate: order.deliverydate, + orderstatus: order.orderstatus, + ordernotes: order.ordernotes, + kms: order.kms, + cumulativekms: order.cumulativekms, + assigntime: order.assigntime, + starttime: order.starttime, + arrivaltime: order.arrivaltime, + pickuptime: order.pickuptime, + deliverytime: order.deliverytime, + canceltime: order.canceltime, + deliverycharge: order.deliverycharges, + deliveryamt: order.deliveryamt, + pickupcustomer: order.pickupcustomer, + pickupcontactno: order.pickupcontactno, + Pickupaddress: order.pickupaddress, + pickupsuburb: order.pickupsuburb, + pickupcity: order.applocation, + pickuplat: order.pickuplat, + pickuplong: order.pickuplon, + deliverycustomer: order.deliverycustomer, + deliverycontactno: order.deliverycontactno, + deliveryaddress: order.deliveryaddress, + deliverysuburb: order.locationsuburb, + deliverylat: order.deliverylat, + deliverylong: order.deliverylong, + locationname: order.locationname, + locationsuburb: order.pickuplocation, + deliverylocation: order.deliverylocation, + locationcontactno: order.locationcontactno + })); + + function formatDate(dateString) { + return dayjs(dateString).format('DD/MM/YYYY '); + } + function formatTime(dateString) { + return dayjs(dateString).format(' hh:mm A'); + } + + const errormessage = fetchDeliveriesIsError + ? `An error has occurred: (fetchDeliveries) ${fetchDeliveriesError.message}` + : fetchtenantsIsError + ? `An error has occurred: (getTenants) ${fetchtenantsError.message}` + : fetchlocationsIsError + ? `An error has occurred: (gettenantlocations) ${fetchlocationsError.message}` + : getriderbydeliveryIsError + ? `An error has occurred: (getriderbydelivery) ${getriderbydeliveryError.message}` + : null; + + useEffect(() => { + if (errormessage) { + opentoast(errormessage, 'warning', 2000); + } + }, [errormessage]); + + const KPI_META = [ + { key: 'total', label: 'Total Orders', color: BRAND, icon: MdOutlineLocalShipping, value: total }, + { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredLenght }, + { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingLenght }, + { key: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdOutlineCancel, value: cancelLenght } + ]; + + return ( + <> + {(fetchDeliveriesIsLoading || + fetchtenantsIsLoading || + fetchlocationsIsLoading || + logsLoading || + getriderbydeliveryIsLoading || + isFetchingNextPage) && ( +
    + +
    + )} + {fetchDeliveriesIsLoading && ( + theme.zIndex.drawer + 1 + }} + open={fetchDeliveriesIsLoading} + /> + )} + + {/* ============================================= || Header || ============================================= */} + + } + /> + + {/* ============================================= || KPI Cards || ============================================= */} + + {KPI_META.map((item) => { + const Icon = item.icon; + return ( + + } + color={item.color} + loading={fetchDeliveriesIsLoading} + /> + + ); + })} + + + {/* ============================================= || Filter Bar (tenant, location, rider, date, export) || ============================================= */} + + + + option?.tenantname || ''} + PaperComponent={SoftPaper} + onOpen={(event) => { + if (appId == null) { + event.preventDefault(); + OpenToast('Please select a your app location first!', 'warning', 3000); + setTimeout(() => { + locationRef.current?.focus(); + }, 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + } else { + setTenantid(val?.tenantid || 0); + setTenantValue(val); + setLocationid(val.locationid); + setLocationValue(null); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + /> + )} + /> + + + + (option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '')} + value={locationValue} + PaperComponent={SoftPaper} + onOpen={(event) => { + if (appId == null && !tenantid) { + event.preventDefault(); + OpenToast('Please select a your Location and Tenant first!', 'warning', 3000); + setTimeout(() => { + locationRef.current?.focus(); + }, 0); + } else if (!tenantid) { + event.preventDefault(); + OpenToast('Please select a your Tenant first!', 'warning', 3000); + setTimeout(() => { + tenantRef.current?.focus(); + }, 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setLocationid(0); + setLocationValue(null); + } else { + setLocationid(val.locationid || 0); + setLocationValue(val); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + /> + )} + /> + + + + { + const name = option?.displayname || option?.authname || ''; + return option?.contactno ? `${name} (${option.contactno})` : name; + }} + PaperComponent={SoftPaper} + onOpen={() => { + if (appId == null) { + OpenToast('Select App Location First', 'warning', 2000); + } + }} + onChange={(event, value, reason) => { + if (reason === 'clear') { + setSelectedRider(null); + setRiderValue(null); + } else { + setSelectedRider(value); + setRiderValue(value); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + /> + )} + /> + + + + + + setOpen(true)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: 0.75, + px: 1.25, + py: 0.75, + borderRadius: 999, + cursor: 'pointer', + bgcolor: tint('#f59e0b'), + border: `1.5px solid ${edge('#f59e0b')}`, + color: '#f59e0b', + fontWeight: 800, + fontSize: 12, + transition: 'all 0.18s', + '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } + }} + > + + {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} + + + + + + + + + {/* ============================================= || Status Tabs + Search || ============================================= */} + + + + {STATUS_TABS.map((key) => { + const meta = STATUS_META[key]; + const Icon = meta.icon; + const active = currentStatus.toLowerCase() === key; + const count = statusCountByKey[key] ?? 0; + return ( + setCurrentStatus(key === 'all' ? 'All' : key)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: { xs: 0.625, md: 0.875 }, + pl: 0.5, + pr: { xs: 1, md: 1.25 }, + py: 0.5, + flexShrink: 0, + cursor: 'pointer', + borderRadius: DT.radiusField + 'px', + border: `1px solid ${active ? '#000000' : DT.borderSubtle}`, + bgcolor: active ? '#000000' : DT.surface, + color: active ? '#fff' : DT.textSecondary, + fontWeight: 600, + boxShadow: 'none', + transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', + '&:hover': { + borderColor: active ? '#000000' : DT.borderHover, + bgcolor: active ? '#000000' : DT.surfaceAlt + } + }} + > + + + + + {meta.label} + + + {count} + + + ); + })} + + + + + + + + + {/* ============================================= || Table || ============================================= */} + + + {isMobile ? ( + /* ===================== MOBILE: card list ===================== */ + + {fetchDeliveriesIsLoading ? ( + + + + Loading orders… + + + ) : rows?.length == 0 ? ( + + + + + + No orders to show + + + {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} + + + ) : ( + rows?.map((row, index) => { + const statusKey = String(row.orderstatus || '').toLowerCase(); + const rowStatusMeta = STATUS_META[statusKey] || { + label: row.orderstatus || '—', + color: BRAND, + icon: MdAssignment + }; + const StatusIcon = rowStatusMeta.icon; + const cancelled = statusKey === 'cancelled'; + const isDelivered = row.orderstatus === 'delivered'; + return ( + + + + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + + + + + {row.tenantname} + + + #{row.orderid} + + + + + + { + if (isDelivered) { + getdeliverylogs(row.deliveryid); + setMapTenant(row); + } + }} + sx={{ + flexShrink: 0, + bgcolor: isDelivered ? soft(BRAND) : soft('#94a3b8'), + color: isDelivered ? BRAND : DT.textMuted, + border: `1px solid ${isDelivered ? edge(BRAND) : edge('#94a3b8')}`, + '&:hover': { + bgcolor: isDelivered ? BRAND : soft('#94a3b8'), + color: isDelivered ? '#fff' : DT.textMuted + } + }} + > + + + + + + + + {rowStatusMeta.label} + + {row.ridername && ( + + {row.ridername} + + )} + + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} + + + + } + > + + + + + {row.pickupcustomer || '—'} + + + {row.pickupcontactno} + + + {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} + + {row.applocation && ( + + {row.applocation} + + )} + + + + + + {row.deliverycustomer || '—'} + + + {row.deliverycontactno} + + + {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} + tooltip="KMS" + /> + } + label={`${formatKm(row.cumulativekms)} km`} + tooltip="Actual KMS" + /> + } + label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} + tooltip="Rider KMS" + /> + + + + + } + label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} + tooltip="Delivery Charge" + /> + } + label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} + tooltip="Delivery Amount" + /> + + + + {row.ordernotes && ( + + + + + {row.ordernotes} + + + + )} + + + ); + }) + )} + + ) : ( + + + + # + Map + Client + Pickup + Drop + Status / Rider + Assigned + Accepted + Arrived + Picked + Active + Delivered + Cancelled + Notes + KMS + Charges + + + + + {fetchDeliveriesIsLoading ? ( + + ) : rows?.length == 0 ? ( + + + + + + + + No orders to show + + + {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} + + + + + ) : ( + rows?.map((row, index) => { + const statusKey = String(row.orderstatus || '').toLowerCase(); + const rowStatusMeta = STATUS_META[statusKey] || { + label: row.orderstatus || '—', + color: BRAND, + icon: MdAssignment + }; + const StatusIcon = rowStatusMeta.icon; + const cancelled = statusKey === 'cancelled'; + return ( + + + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + + {/* ====================== Map button ====================== */} + + + + { + if (row.orderstatus === 'delivered') { + getdeliverylogs(row.deliveryid); + setMapTenant(row); + } + }} + sx={{ + bgcolor: row.orderstatus === 'delivered' ? soft(BRAND) : soft('#94a3b8'), + color: row.orderstatus === 'delivered' ? BRAND : DT.textMuted, + border: `1px solid ${row.orderstatus === 'delivered' ? edge(BRAND) : edge('#94a3b8')}`, + '&:hover': { + bgcolor: row.orderstatus === 'delivered' ? BRAND : soft('#94a3b8'), + color: row.orderstatus === 'delivered' ? '#fff' : DT.textMuted + } + }} + > + + + + + + + {/* ====================== Client ====================== */} + + + + + + + + {row.tenantname} + + + + #{row.orderid} + + + + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} + + + + + + {/* ====================== Pickup ====================== */} + + + + {row.pickupcustomer || '—'} + + + {row.pickupcontactno} + + + + {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} + + + {row.applocation && ( + + {row.applocation} + + )} + + + + {/* ====================== Drop ====================== */} + + + + {row.deliverycustomer || '—'} + + + {row.deliverycontactno} + + + + {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} + + + + + + {/* ====================== Status / Rider ====================== */} + + + + {rowStatusMeta.label} + + {row.ridername && ( + + + + + {row.ridername} + + + + )} + + + + {/* ====================== Timestamps ====================== */} + + + + + + + + + + + {/* GET /admin/bookings has no per-event timestamps (accepted/ + arrived/pickup/start/cancel) — only assigntime (updatedat) and + deliverydate exist on the mapped row (see fetchDeliveries in + api.js), so row.pickuptime is always undefined and this cell + always renders the loading skeleton. The old `success` check + compared against a `deliverystatus` field that never existed on + this row either (the real field is `orderstatus`) — dropped + rather than guessed, since orderstatus's confirmed values + (pending/accepted/active/delivered/cancelled) have no distinct + "picked" state to map it to. */} + + + + + + + + + + + + + {/* ====================== Notes ====================== */} + + {row.ordernotes ? ( + + + + {row.ordernotes} + + + ) : ( + — + )} + + + {/* ====================== KMS ====================== */} + + + } + label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} + tooltip="KMS" + /> + } + label={`${formatKm(row.cumulativekms)} km`} + tooltip="Actual KMS" + /> + } + label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} + tooltip="Rider KMS" + /> + + + + {/* ====================== Charges ====================== */} + + + } + label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} + tooltip="Delivery Charge" + /> + } + label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} + tooltip="Delivery Amount" + /> + + + + ); + }) + )} + +
    + )} + + {rows?.length !== 0 && ( + + + {isFetchingNextPage || hasNextPage ? ( + + ) : ( + + No more orders + + )} + + + )} +
    +
    + + {/* ============================================= || Export Dialog || ============================================= */} + setReportDialog(false)} + fullWidth + maxWidth="sm" + fullScreen={isMobile} + PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 }, overflow: 'hidden' } }} + > + + + + + + + + + Report + + + Export Orders + + + + setReportDialog(false)} + sx={{ color: '#fff', bgcolor: 'rgba(255,255,255,0.18)', '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }} + > + + + + + + {fetchDeliveriesIsLoading && } + {[ + { label: 'App Location', value: locaName, color: BRAND }, + { label: 'Tenant', value: tenantValue?.tenantname, color: '#0ea5e9' }, + { label: 'Business Location', value: locationValue?.locationname, color: '#10b981' }, + { label: 'Status', value: currentStatus, color: '#f59e0b' }, + { label: 'Rider', value: riderValue ? `${riderValue.firstname} ${riderValue.lastname}` : null, color: '#8b5cf6' }, + { label: 'Keyword', value: searchword, color: '#06b6d4' }, + { label: 'Start Date', value: startdate, color: '#14b8a6' }, + { label: 'End Date', value: enddate, color: '#ef4444' } + ].map((item, idx) => ( + + + {item.label} + + + + ))} + + + + { + setTimeout(() => setReportDialog(false), 0); + }} + /> + + + + {/* ============================================= || Date Filter Dialog || ============================================= */} + setOpen(false)} + onSelect={(range) => { + setStartdate(range.startDate); + setEnddate(range.endDate); + setDatestatus(range.label); + }} + /> + + {/* ============================================= || Map Dialog || ============================================= */} + { + setMapOpen(false); + }} + fullScreen + fullWidth + > + {riderCoordinates && ( +
    + +
    + )} +
    + + ); +} diff --git a/src/pages/nearle/reports/ordersSummary.js b/src/pages/nearle/reports/ordersSummary.js index 65a6320..24281b4 100644 --- a/src/pages/nearle/reports/ordersSummary.js +++ b/src/pages/nearle/reports/ordersSummary.js @@ -1,1352 +1,1285 @@ -import React, { useState, useEffect, useMemo, useRef } from 'react'; -import { useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Autocomplete, - Avatar, - Box, - Chip, - Collapse, - Divider, - Grid, - IconButton, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { - MdAssignment, - MdCalendarMonth, - MdCancel, - MdCheckCircle, - MdCurrencyRupee, - MdExpandLess, - MdExpandMore, - MdGroups, - MdHourglassEmpty, - MdLocalShipping, - MdLocationOn, - MdMyLocation, - MdPerson, - MdStraighten, - MdStore, - MdOutlineLocalShipping, - MdOutlinePendingActions, - MdOutlineCheckCircle, - MdOutlineCurrencyRupee -} from 'react-icons/md'; - -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { getreportlocationsummary, getreportsummary, gettenantlocations, getTenants } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DateFilterDialog from 'components/DateFilterDialog'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { OpenToast } from 'components/third-party/OpenToast'; - -// ============================================================================ -// Design tokens — shared with deliveries / ridersSummary / customers / -// tenants pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Pill used in cells for km / amount metrics. -const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( - - - {icon} - {label} - - -); - -// Coloured numeric cell — non-zero values get a soft accent badge to draw -// the eye, preserving the legacy "red when present" affordance with the -// modern soft-pill aesthetic. -const CountCell = ({ value, color = '#ef4444', icon }) => { - const n = Number(value) || 0; - if (n === 0) { - return ( - - 0 - - ); - } - return ( - - {icon} - {n} - - ); -}; - -// Pill-style filter inputs for the Tenant / Location autocompletes. -const pillFieldSx = (color) => ({ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - } -}); - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -// ============================================================================ -// Orders Summary -// ============================================================================ -export default function OrdersReport() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - const locationRef = useRef(null); - const tenantRef = useRef(null); - - const [appId, setAppId] = useState(0); - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [locaName, setLocoName] = useState('All'); - const [open, setOpen] = useState(false); - const [openRow, setOpenRow] = useState(null); - const [datestatus, setDatestatus] = useState('Today'); - const [ridersdata, setRidersdata] = useState([]); - const [loading, setLoading] = useState(false); - const [locationid, setLocationid] = useState(0); - const [tenantid, setTenantid] = useState(0); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [, setPage] = useState(0); - - // Clear nested filters when scope widens. - useEffect(() => { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - setOpenRow(null); - }, [appId]); - - useEffect(() => { - setLocationid(0); - setLocationValue(null); - setOpenRow(null); - }, [tenantid]); - - // ==============================|| primary summary query ||============================== // - // The api.js destructure expects [appId, tenantid, locationid, startdate, enddate] - // — do NOT prepend a name slot without updating api.js in lockstep. - const { - data: rows, - isLoading: isLoadingReports, - isError: isErrorReports, - error: reportsError - } = useQuery({ - queryKey: [appId, tenantid, locationid, startdate, enddate], - queryFn: tenantid ? getreportlocationsummary : getreportsummary - }); - - // ==============================|| tenant list ||============================== // - const { data: tenantlist } = useQuery({ - queryKey: ['tenantlist', appId], - queryFn: () => getTenants(appId), - enabled: appId !== 0 - }); - - // ==============================|| tenant locations ||============================== // - const { data: locationlist } = useQuery({ - queryKey: ['gettenantlocations', tenantid], - queryFn: () => gettenantlocations(tenantid), - enabled: tenantid !== 0 - }); - - // Defensive: backend sometimes returns null for empty results. - const safeRows = useMemo(() => (Array.isArray(rows) ? rows : []), [rows]); - - // Client-side filter on tenant / location name. - const filteredRows = useMemo(() => { - if (!debouncedSearch) return safeRows; - const q = debouncedSearch.toLowerCase().trim(); - return safeRows.filter((r) => - [r.tenantname, r.locationname, String(r.tenantid || ''), String(r.locationid || '')] - .filter(Boolean) - .some((field) => String(field).toLowerCase().includes(q)) - ); - }, [safeRows, debouncedSearch]); - - // KPIs + grand total derived directly from the loaded summary. - const stats = useMemo(() => { - return filteredRows.reduce( - (acc, row) => { - acc.totalOrders += Number(row.totalorders) || 0; - acc.orderPend += Number(row.Orderspending) || 0; - acc.orderComplete += Number(row.orderscompleted) || 0; - acc.orderCancel += Number(row.orderscancelled) || 0; - acc.deliPend += Number(row.deliveriespending) || 0; - acc.deliComplete += Number(row.deliveriescompleted) || 0; - acc.deliCancel += Number(row.deliveriescancelled) || 0; - acc.collection += Number(row.collectionamt) || 0; - acc.kms += Number(row.kms) || 0; - acc.cumulativekms += Number(row.cumulativekms) || 0; - acc.amount += Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - return acc; - }, - { - totalOrders: 0, - orderPend: 0, - orderComplete: 0, - orderCancel: 0, - deliPend: 0, - deliComplete: 0, - deliCancel: 0, - collection: 0, - kms: 0, - cumulativekms: 0, - amount: 0 - } - ); - }, [filteredRows]); - - // No per-tenant or per-location rider-breakdown endpoint in the new API. - const getuserreportsummary = async () => { - setLoading(true); - setRidersdata([]); - setLoading(false); - }; - - const getriderlocationsummary = async () => { - setLoading(true); - setRidersdata([]); - setLoading(false); - }; - - if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message); - - const KPI_META = [ - { - key: 'orders', - label: 'Total Orders', - color: BRAND, - icon: MdOutlineLocalShipping, - value: stats.totalOrders - }, - { - key: 'pending', - label: 'Orders Pending', - color: '#f59e0b', - icon: MdOutlinePendingActions, - value: stats.orderPend - }, - { - key: 'completed', - label: 'Orders Completed', - color: '#10b981', - icon: MdOutlineCheckCircle, - value: stats.orderComplete - }, - { - key: 'amount', - label: 'Total Amount', - color: '#0ea5e9', - icon: MdOutlineCurrencyRupee, - value: formatNumberToRupees(stats.amount) - } - ]; - - const isLocationGroup = Boolean(tenantid); - - return ( - <> - {(loading || isLoadingReports) && } - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoadingReports} - /> - - ); - })} - - - {/* ============================================= || Filter bar || ============================================= */} - - - - - setOpen(true)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: 0.75, - px: 1.25, - py: 0.75, - borderRadius: 999, - cursor: 'pointer', - bgcolor: tint('#f59e0b'), - border: `1.5px solid ${edge('#f59e0b')}`, - color: '#f59e0b', - fontWeight: 800, - fontSize: 12, - transition: 'all 0.18s', - '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } - }} - > - - Orders · {datestatus} · {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} - - - - option?.tenantname || option?.label || ''} - isOptionEqualToValue={(opt, val) => opt?.tenantid === val?.tenantid} - PaperComponent={SoftPaper} - sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx(BRAND) }} - onOpen={(event) => { - if (!appId) { - event.preventDefault(); - OpenToast('Please select a Zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear' || !val) { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(val?.locationid || 0); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - - - option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '' - } - isOptionEqualToValue={(opt, val) => opt?.locationid === val?.locationid} - PaperComponent={SoftPaper} - sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx('#0ea5e9') }} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - OpenToast('Please select Zone and Tenant first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select a Tenant first!', 'warning', 3000); - setTimeout(() => tenantRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear' || !val) { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val?.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - {isMobile ? ( - <> - {isLoadingReports && ( - - {[0, 1, 2, 3].map((i) => ( - - - - ))} - - )} - - {!isLoadingReports && filteredRows.length === 0 && ( - - - - - - No {isLocationGroup ? 'locations' : 'tenants'} to show - - - {debouncedSearch - ? 'Try a different keyword.' - : isErrorReports - ? 'Something went wrong fetching the summary. Try a different filter.' - : 'Pick a zone, tenant, or date range to load the summary.'} - - - )} - - {!isLoadingReports && filteredRows.length > 0 && ( - - {filteredRows.map((row, index) => { - const rowKey = isLocationGroup ? row.locationname : row.tenantname; - const isOpen = openRow === rowKey; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - const rowAccent = isLocationGroup ? '#0ea5e9' : BRAND; - - return ( - - - {isLocationGroup ? : } - - - - {(isLocationGroup ? row.locationname : row.tenantname) || '—'} - - - ID #{isLocationGroup ? row.locationid : row.tenantid} - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? rowKey : null); - if (!isOpening) return; - setRidersdata([]); - if (isLocationGroup) { - getriderlocationsummary(row.locationid); - } else { - getuserreportsummary(row.tenantid); - } - }} - sx={{ - flexShrink: 0, - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - } - > - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={Number(row.collectionamt || 0).toFixed(2)} - tooltip="Collection Amount" - minWidth={90} - /> - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - {/* Collapsible rider breakdown — mobile */} - - - - - - - - Rider Breakdown - - - - {!loading && (!ridersdata || ridersdata.length === 0) ? ( - - No rider activity for this row. - - ) : ( - - {ridersdata.map((sub, sidx) => { - const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); - return ( - - - - - - - - {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} - - - {sub.ridercontact || `ID #${sub.userid}`} - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={Number(sub.collectionamt || 0).toFixed(2)} - tooltip="Collection" - minWidth={80} - /> - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - } - label={formatNumberToRupees(subAmount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - ); - })} - - )} - - - - ); - })} - - )} - - ) : ( - - - - - # - {isLocationGroup ? 'Location' : 'Tenant'} - - All - - - Orders - - - Deliveries - - - Collection - - - KMS / Actual - - - Amount - - - Action - - - - - Pending - - - Cancelled - - - Completed - - - Pending - - - Cancelled - - - Completed - - - - - - {isLoadingReports && } - - {!isLoadingReports && filteredRows.length === 0 && ( - - - - - - - - No {isLocationGroup ? 'locations' : 'tenants'} to show - - - {debouncedSearch - ? 'Try a different keyword.' - : isErrorReports - ? 'Something went wrong fetching the summary. Try a different filter.' - : 'Pick a zone, tenant, or date range to load the summary.'} - - - - - )} - - {filteredRows.map((row, index) => { - const rowKey = isLocationGroup ? row.locationname : row.tenantname; - const isOpen = openRow === rowKey; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - - {isLocationGroup ? : } - - - - {(isLocationGroup ? row.locationname : row.tenantname) || '—'} - - - ID #{isLocationGroup ? row.locationid : row.tenantid} - - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - - } - label={Number(row.collectionamt || 0).toFixed(2)} - tooltip="Collection Amount" - minWidth={90} - /> - - - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? rowKey : null); - if (!isOpening) return; - setRidersdata([]); - if (isLocationGroup) { - getriderlocationsummary(row.locationid); - } else { - getuserreportsummary(row.tenantid); - } - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - - {/* ============================================= || Collapsible rider breakdown || ============================================= */} - {isOpen && ( - - - - - - - - - - - Rider Breakdown · {isLocationGroup ? row.locationname : row.tenantname} - - - -
    - - - # - Rider - Orders - Deliveries - Pending - Cancelled - Completed - Collection - KMS / Actual - Amount - - - - {loading && } - {!loading && (!ridersdata || ridersdata.length === 0) ? ( - - - - No rider activity for this row. - - - - ) : ( - ridersdata.map((sub, sidx) => { - const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); - return ( - - - - {String(sidx + 1).padStart(2, '0')} - - - - - - - - - - {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} - - - {sub.ridercontact || `ID #${sub.userid}`} - - - - - - } /> - - - } /> - - - } - /> - - - } /> - - - } - /> - - - } - label={Number(sub.collectionamt || 0).toFixed(2)} - tooltip="Collection" - minWidth={80} - /> - - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - } - label={formatNumberToRupees(subAmount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - ); - }) - )} - -
    -
    -
    - - - - )} - - ); - })} - - - - )} - - {/* ============================================= || Total Bar || ============================================= */} - {filteredRows.length > 0 && ( - <> - - - - - Grand Total - - - - - - - - - )} - - - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - ); -} +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { useQuery } from '@tanstack/react-query'; + +// material-ui +import { + Autocomplete, + Avatar, + Box, + Chip, + Collapse, + Divider, + Grid, + IconButton, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, + useMediaQuery, + useTheme +} from '@mui/material'; +import { + MdAssignment, + MdCalendarMonth, + MdCancel, + MdCheckCircle, + MdCurrencyRupee, + MdExpandLess, + MdExpandMore, + MdGroups, + MdHourglassEmpty, + MdLocalShipping, + MdLocationOn, + MdPerson, + MdStraighten, + MdStore, + MdOutlineLocalShipping, + MdOutlinePendingActions, + MdOutlineCheckCircle, + MdOutlineCurrencyRupee +} from 'react-icons/md'; + +import dayjs from 'dayjs'; +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +import { getreportlocationsummary, getreportsummary, gettenantlocations, getTenants } from 'pages/api/api'; +import Loader from 'components/Loader'; +import DateFilterDialog from 'components/DateFilterDialog'; +import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; +import PageHeader from 'components/nearle_components/PageHeader'; +import StatCard from 'components/nearle_components/StatCard'; +import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; +import { OpenToast } from 'components/third-party/OpenToast'; +import { DT, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; + +// Local SoftPaper: this page still renders MUI Autocompletes, which need a +// `PaperComponent`. The shared themes/dt/primitives.js is Astryx-only now, so +// the popup surface lives here until this page's dropdowns become Astryx +// Selector/Typeahead — at which point this goes away with them. +const SoftPaper = (props) => ( + +); + + +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; + +// Pill used in cells for km / amount metrics. +const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( + + + {icon} + {label} + + +); + +// Coloured numeric cell — non-zero values get a soft accent badge to draw +// the eye, preserving the legacy "red when present" affordance with the +// modern soft-pill aesthetic. +const CountCell = ({ value, color = '#ef4444', icon }) => { + const n = Number(value) || 0; + if (n === 0) { + return ( + + 0 + + ); + } + return ( + + {icon} + {n} + + ); +}; + +function formatNumberToRupees(value) { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 2 + }).format(Number(value) || 0); +} + +// ============================================================================ +// Orders Summary +// ============================================================================ +export default function OrdersReport() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const locationRef = useRef(null); + const tenantRef = useRef(null); + + const [appId, setAppId] = useState(0); + const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); + const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); + const [locaName, setLocoName] = useState('All'); + const [open, setOpen] = useState(false); + const [openRow, setOpenRow] = useState(null); + const [datestatus, setDatestatus] = useState('Today'); + const [ridersdata, setRidersdata] = useState([]); + const [loading, setLoading] = useState(false); + const [locationid, setLocationid] = useState(0); + const [tenantid, setTenantid] = useState(0); + const [tenantValue, setTenantValue] = useState(null); + const [locationValue, setLocationValue] = useState(null); + const [searchword, setSearchword] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [, setPage] = useState(0); + + // Clear nested filters when scope widens. + useEffect(() => { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + setOpenRow(null); + }, [appId]); + + useEffect(() => { + setLocationid(0); + setLocationValue(null); + setOpenRow(null); + }, [tenantid]); + + // ==============================|| primary summary query ||============================== // + // The api.js destructure expects [appId, tenantid, locationid, startdate, enddate] + // — do NOT prepend a name slot without updating api.js in lockstep. + const { + data: rows, + isLoading: isLoadingReports, + isError: isErrorReports, + error: reportsError + } = useQuery({ + queryKey: [appId, tenantid, locationid, startdate, enddate], + queryFn: tenantid ? getreportlocationsummary : getreportsummary + }); + + // ==============================|| tenant list ||============================== // + const { data: tenantlist } = useQuery({ + queryKey: ['tenantlist', appId], + queryFn: () => getTenants(appId), + enabled: appId !== 0 + }); + + // ==============================|| tenant locations ||============================== // + const { data: locationlist } = useQuery({ + queryKey: ['gettenantlocations', tenantid], + queryFn: () => gettenantlocations(tenantid), + enabled: tenantid !== 0 + }); + + // Defensive: backend sometimes returns null for empty results. + const safeRows = useMemo(() => (Array.isArray(rows) ? rows : []), [rows]); + + // Client-side filter on tenant / location name. + const filteredRows = useMemo(() => { + if (!debouncedSearch) return safeRows; + const q = debouncedSearch.toLowerCase().trim(); + return safeRows.filter((r) => + [r.tenantname, r.locationname, String(r.tenantid || ''), String(r.locationid || '')] + .filter(Boolean) + .some((field) => String(field).toLowerCase().includes(q)) + ); + }, [safeRows, debouncedSearch]); + + // KPIs + grand total derived directly from the loaded summary. + const stats = useMemo(() => { + return filteredRows.reduce( + (acc, row) => { + acc.totalOrders += Number(row.totalorders) || 0; + acc.orderPend += Number(row.Orderspending) || 0; + acc.orderComplete += Number(row.orderscompleted) || 0; + acc.orderCancel += Number(row.orderscancelled) || 0; + acc.deliPend += Number(row.deliveriespending) || 0; + acc.deliComplete += Number(row.deliveriescompleted) || 0; + acc.deliCancel += Number(row.deliveriescancelled) || 0; + acc.collection += Number(row.collectionamt) || 0; + acc.kms += Number(row.kms) || 0; + acc.cumulativekms += Number(row.cumulativekms) || 0; + acc.amount += Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); + return acc; + }, + { + totalOrders: 0, + orderPend: 0, + orderComplete: 0, + orderCancel: 0, + deliPend: 0, + deliComplete: 0, + deliCancel: 0, + collection: 0, + kms: 0, + cumulativekms: 0, + amount: 0 + } + ); + }, [filteredRows]); + + // No per-tenant or per-location rider-breakdown endpoint in the new API. + const getuserreportsummary = async () => { + setLoading(true); + setRidersdata([]); + setLoading(false); + }; + + const getriderlocationsummary = async () => { + setLoading(true); + setRidersdata([]); + setLoading(false); + }; + + if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message); + + const KPI_META = [ + { + key: 'orders', + label: 'Total Orders', + color: BRAND, + icon: MdOutlineLocalShipping, + value: stats.totalOrders + }, + { + key: 'pending', + label: 'Orders Pending', + color: '#f59e0b', + icon: MdOutlinePendingActions, + value: stats.orderPend + }, + { + key: 'completed', + label: 'Orders Completed', + color: '#10b981', + icon: MdOutlineCheckCircle, + value: stats.orderComplete + }, + { + key: 'amount', + label: 'Total Amount', + color: '#0ea5e9', + icon: MdOutlineCurrencyRupee, + value: formatNumberToRupees(stats.amount) + } + ]; + + const isLocationGroup = Boolean(tenantid); + + return ( + <> + {(loading || isLoadingReports) && } + + {/* ============================================= || Header || ============================================= */} + + } + /> + + {/* ============================================= || KPI Cards || ============================================= */} + + {KPI_META.map((item) => { + const Icon = item.icon; + return ( + + } + color={item.color} + loading={isLoadingReports} + /> + + ); + })} + + + {/* ============================================= || Filter bar || ============================================= */} + + + + + setOpen(true)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: 0.75, + px: 1.25, + py: 0.75, + borderRadius: 999, + cursor: 'pointer', + bgcolor: tint('#f59e0b'), + border: `1.5px solid ${edge('#f59e0b')}`, + color: '#f59e0b', + fontWeight: 800, + fontSize: 12, + transition: 'all 0.18s', + '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } + }} + > + + Orders · {datestatus} · {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} + + + + option?.tenantname || option?.label || ''} + isOptionEqualToValue={(opt, val) => opt?.tenantid === val?.tenantid} + PaperComponent={SoftPaper} + sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx(BRAND) }} + onOpen={(event) => { + if (!appId) { + event.preventDefault(); + OpenToast('Please select a Zone first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear' || !val) { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + } else { + setTenantid(val?.tenantid || 0); + setTenantValue(val); + setLocationid(val?.locationid || 0); + setLocationValue(null); + } + }} + renderInput={(params) => ( + + + + + {params.InputProps.startAdornment} + + ) + }} + /> + )} + /> + + + option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '' + } + isOptionEqualToValue={(opt, val) => opt?.locationid === val?.locationid} + PaperComponent={SoftPaper} + sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx('#0ea5e9') }} + onOpen={(event) => { + if (!appId && !tenantid) { + event.preventDefault(); + OpenToast('Please select Zone and Tenant first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } else if (!tenantid) { + event.preventDefault(); + OpenToast('Please select a Tenant first!', 'warning', 3000); + setTimeout(() => tenantRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear' || !val) { + setLocationid(0); + setLocationValue(null); + } else { + setLocationid(val?.locationid || 0); + setLocationValue(val); + } + }} + renderInput={(params) => ( + + + + + {params.InputProps.startAdornment} + + ) + }} + /> + )} + /> + + + + + + + + + {/* ============================================= || Table || ============================================= */} + + {isMobile ? ( + <> + {isLoadingReports && ( + + {[0, 1, 2, 3].map((i) => ( + + + + ))} + + )} + + {!isLoadingReports && filteredRows.length === 0 && ( + + + + + + No {isLocationGroup ? 'locations' : 'tenants'} to show + + + {debouncedSearch + ? 'Try a different keyword.' + : isErrorReports + ? 'Something went wrong fetching the summary. Try a different filter.' + : 'Pick a zone, tenant, or date range to load the summary.'} + + + )} + + {!isLoadingReports && filteredRows.length > 0 && ( + + {filteredRows.map((row, index) => { + const rowKey = isLocationGroup ? row.locationname : row.tenantname; + const isOpen = openRow === rowKey; + const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); + const rowAccent = isLocationGroup ? '#0ea5e9' : BRAND; + + return ( + + + {isLocationGroup ? : } + + + + {(isLocationGroup ? row.locationname : row.tenantname) || '—'} + + + ID #{isLocationGroup ? row.locationid : row.tenantid} + + + + { + const isOpening = !isOpen; + setOpenRow(isOpening ? rowKey : null); + if (!isOpening) return; + setRidersdata([]); + if (isLocationGroup) { + getriderlocationsummary(row.locationid); + } else { + getuserreportsummary(row.tenantid); + } + }} + sx={{ + flexShrink: 0, + bgcolor: isOpen ? BRAND : soft(BRAND), + color: isOpen ? '#fff' : BRAND, + border: `1px solid ${edge(BRAND)}`, + '&:hover': { bgcolor: BRAND, color: '#fff' } + }} + > + {isOpen ? : } + + + + } + > + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } + label={Number(row.collectionamt || 0).toFixed(2)} + tooltip="Collection Amount" + minWidth={90} + /> + + + } + label={formatNumberToRupees(amount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + } + label={`${Number(row.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + + + } + label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + + {/* Collapsible rider breakdown — mobile */} + + + + + + + + Rider Breakdown + + + + {!loading && (!ridersdata || ridersdata.length === 0) ? ( + + No rider activity for this row. + + ) : ( + + {ridersdata.map((sub, sidx) => { + const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); + return ( + + + + + + + + {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} + + + {sub.ridercontact || `ID #${sub.userid}`} + + + + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } + label={Number(sub.collectionamt || 0).toFixed(2)} + tooltip="Collection" + minWidth={80} + /> + + + } + label={`${Number(sub.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + + + } + label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + } + label={formatNumberToRupees(subAmount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + + ); + })} + + )} + + + + ); + })} + + )} + + ) : ( + + + + + # + {isLocationGroup ? 'Location' : 'Tenant'} + + All + + + Orders + + + Deliveries + + + Collection + + + KMS / Actual + + + Amount + + + Action + + + + + Pending + + + Cancelled + + + Completed + + + Pending + + + Cancelled + + + Completed + + + + + + {isLoadingReports && } + + {!isLoadingReports && filteredRows.length === 0 && ( + + + + + + + + No {isLocationGroup ? 'locations' : 'tenants'} to show + + + {debouncedSearch + ? 'Try a different keyword.' + : isErrorReports + ? 'Something went wrong fetching the summary. Try a different filter.' + : 'Pick a zone, tenant, or date range to load the summary.'} + + + + + )} + + {filteredRows.map((row, index) => { + const rowKey = isLocationGroup ? row.locationname : row.tenantname; + const isOpen = openRow === rowKey; + const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); + + return ( + + + + + {String(index + 1).padStart(2, '0')} + + + + + + {isLocationGroup ? : } + + + + {(isLocationGroup ? row.locationname : row.tenantname) || '—'} + + + ID #{isLocationGroup ? row.locationid : row.tenantid} + + + + + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + } /> + + + + } + label={Number(row.collectionamt || 0).toFixed(2)} + tooltip="Collection Amount" + minWidth={90} + /> + + + + + } + label={`${Number(row.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + } + label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + + + } + label={formatNumberToRupees(amount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + + + { + const isOpening = !isOpen; + setOpenRow(isOpening ? rowKey : null); + if (!isOpening) return; + setRidersdata([]); + if (isLocationGroup) { + getriderlocationsummary(row.locationid); + } else { + getuserreportsummary(row.tenantid); + } + }} + sx={{ + bgcolor: isOpen ? BRAND : soft(BRAND), + color: isOpen ? '#fff' : BRAND, + border: `1px solid ${edge(BRAND)}`, + '&:hover': { bgcolor: BRAND, color: '#fff' } + }} + > + {isOpen ? : } + + + + + + {/* ============================================= || Collapsible rider breakdown || ============================================= */} + {isOpen && ( + + + + + + + + + + + Rider Breakdown · {isLocationGroup ? row.locationname : row.tenantname} + + + +
    + + + # + Rider + Orders + Deliveries + Pending + Cancelled + Completed + Collection + KMS / Actual + Amount + + + + {loading && } + {!loading && (!ridersdata || ridersdata.length === 0) ? ( + + + + No rider activity for this row. + + + + ) : ( + ridersdata.map((sub, sidx) => { + const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); + return ( + + + + {String(sidx + 1).padStart(2, '0')} + + + + + + + + + + {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} + + + {sub.ridercontact || `ID #${sub.userid}`} + + + + + + } /> + + + } /> + + + } + /> + + + } /> + + + } + /> + + + } + label={Number(sub.collectionamt || 0).toFixed(2)} + tooltip="Collection" + minWidth={80} + /> + + + + } + label={`${Number(sub.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + } + label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + + } + label={formatNumberToRupees(subAmount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + ); + }) + )} + +
    +
    +
    + + + + )} + + ); + })} + + + + )} + + {/* ============================================= || Total Bar || ============================================= */} + {filteredRows.length > 0 && ( + <> + + + + + Grand Total + + + + + + + + + )} + + + {/* ============================================= || Date Filter Dialog || ============================================= */} + setOpen(false)} + onSelect={(range) => { + setStartdate(range.startDate); + setEnddate(range.endDate); + setDatestatus(range.label); + }} + /> + + ); +} diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js index 58cb71b..3477e12 100644 --- a/src/pages/nearle/reports/profitability.js +++ b/src/pages/nearle/reports/profitability.js @@ -1,10 +1,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; -// material-ui -import { Box, Paper, TextField, Typography } from '@mui/material'; import { MdCalendarMonth } from 'react-icons/md'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { DateInput } from '@astryxdesign/core/DateInput'; + import dayjs from 'dayjs'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; @@ -37,14 +41,7 @@ import { SKIPPED_STATUSES } from '../dispatch/dispatchShared'; // same pattern as reports/ordersDetails.js and deliveries.js. // ============================================================================ -const DT = { - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - textSecondary: '#64748b', - borderSubtle: '#e2e8f0', - surface: '#ffffff' -}; -const BRAND = '#C01227'; +const BRAND = '#000000'; // Profit formulas + batch windows imported from ProfitabilitySection.js // (the single source of truth) rather than duplicated here — used to total @@ -142,47 +139,22 @@ const Profitability = () => { subtitle={`Live · ${dayjs(selectedDate).format('DD MMM YYYY')}`} live action={ - + - e.target.value && setSelectedDate(e.target.value)} - InputProps={{ disableUnderline: true }} - sx={{ '& input': { fontWeight: 700, color: DT.textSecondary, p: 0 } }} - /> - + v && setSelectedDate(v)} /> + } /> - + {!isLoading && riders.length === 0 ? ( - - No deliveries for {dayjs(selectedDate).format('DD MMM YYYY')} - + + + ) : ( )} - + ); }; diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js index 9d36667..cf6ee5d 100644 --- a/src/pages/nearle/reports/ridersSummary.js +++ b/src/pages/nearle/reports/ridersSummary.js @@ -1,1075 +1,1009 @@ -import React, { useState, useMemo } from 'react'; -import { useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Avatar, - Box, - Chip, - Collapse, - Dialog, - DialogContent, - Divider, - Grid, - IconButton, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { - MdDirectionsBike, - MdMyLocation, - MdCalendarMonth, - MdLocalShipping, - MdCheckCircle, - MdHourglassEmpty, - MdCancel, - MdCurrencyRupee, - MdStraighten, - MdMap, - MdExpandMore, - MdExpandLess, - MdGroups, - MdPerson, - MdOutlineLocalShipping, - MdOutlineCheckCircle, - MdOutlineCurrencyRupee -} from 'react-icons/md'; - -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { fetchRidersSummary } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DateFilterDialog from 'components/DateFilterDialog'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import RidersRoutes from './RidersRoutes'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; - -// ============================================================================ -// Design tokens — shared with deliveries / tenants / customers / pricing / -// orders-details pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Pill used in cells for numeric / km / amount metrics. -const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( - - - {icon} - {label} - - -); - -// Coloured numeric cell — non-zero values get a soft red badge to draw the eye, -// preserving the legacy "red when present" affordance. -const CountCell = ({ value, color = '#ef4444', icon }) => { - const n = Number(value) || 0; - if (n === 0) { - return ( - - 0 - - ); - } - return ( - - {icon} - {n} - - ); -}; - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -// ==============================|| Riders Summary ||============================== // - -export default function RidersSummary() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [locaName, setLocoName] = useState('All'); - const [open, setOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [tenantData, setTenantData] = useState([]); - const [openRow, setOpenRow] = useState(null); - const [appId, setAppId] = useState(0); - const [loading, setLoading] = useState(false); - const [mapOpen, setMapOpen] = useState(false); - const [logDetails, setLogDetails] = useState(null); - const [selectedRider, setSelectedRider] = useState(null); - const [routeLoading, setRouteLoading] = useState(false); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - - // ==============================|| fetchRidersSummary ||============================== // - const { isLoading: isLoadingReports, data: rows } = useQuery({ - queryKey: ['ridersummary', appId, startdate, enddate], - queryFn: fetchRidersSummary - }); - - // Client-side filter across rider name + id. - // GET /admin/milers/summary confirmed live: { milerprofileid, userid, - // displayname, phone, availabilitystatus, rating, assigned, accepted, - // rejected, completed, cancelled, delivered, riderkms, ridercharges, ... } - // — no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/ - // cumulativekms field exists at all, which is why this whole page showed - // "—" everywhere. - const filteredRows = useMemo(() => { - if (!rows) return []; - if (!debouncedSearch) return rows; - const q = debouncedSearch.toLowerCase().trim(); - return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))); - }, [rows, debouncedSearch]); - - // Aggregate KPIs from the loaded summary. - const stats = useMemo(() => { - if (!rows || rows.length === 0) return { riders: 0, orders: 0, delivered: 0, amount: 0 }; - return rows.reduce( - (acc, r) => { - acc.riders += 1; - acc.orders += Number(r.assigned) || 0; - acc.delivered += Number(r.delivered) || 0; - acc.amount += Number(r.ridercharges) || 0; - return acc; - }, - { riders: 0, orders: 0, delivered: 0, amount: 0 } - ); - }, [rows]); - - // No per-rider tenant-breakdown endpoint in the new API. - const fetchTenantSummary = async () => { - setLoading(true); - setTenantData(null); - setLoading(false); - }; - - // ==============================|| rider planned route (for map) ||============================== // - // /admin/consignments has no documented per-row schema (lat/lng/step/ - // customer fields aren't shown anywhere in the doc), so building a planned - // route from it would be guesswork that risks plotting wrong pins on the - // map. Degrades to an empty route instead. - const getuserdeliverylogs = async () => { - setRouteLoading(true); - setLogDetails([]); - setRouteLoading(false); - }; - - // Total Amount sum (preserved from legacy bottom bar). - const total = useMemo(() => { - if (!rows) return 0; - return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0); - }, [rows]); - - const KPI_META = [ - { key: 'riders', label: 'Active Riders', color: BRAND, icon: MdDirectionsBike, value: stats.riders }, - { key: 'orders', label: 'Total Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping, value: stats.orders }, - { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: stats.delivered }, - { key: 'amount', label: 'Total Amount', color: '#f59e0b', icon: MdOutlineCurrencyRupee, value: formatNumberToRupees(total) } - ]; - - return ( - <> - {(isLoadingReports || loading) && } - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoadingReports} - /> - - ); - })} - - - {/* ============================================= || Filter Bar (date + search) || ============================================= */} - - - - - - - - - Orders · {datestatus} - - - {filteredRows.length} riders · {stats.orders} orders - - - - setOpen(true)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: 0.75, - px: 1.25, - py: 0.75, - borderRadius: 999, - cursor: 'pointer', - bgcolor: tint('#f59e0b'), - border: `1.5px solid ${edge('#f59e0b')}`, - color: '#f59e0b', - fontWeight: 800, - fontSize: 12, - ml: 1, - transition: 'all 0.18s', - '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } - }} - > - - {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} - - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - {isMobile ? ( - - {isLoadingReports && ( - - - Loading riders… - - - )} - {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} - - - ) : ( - filteredRows.map((row, index) => { - const isOpen = openRow === row.userid; - const amount = Number(row.ridercharges) || 0; - const riderName = row?.displayname || '—'; - return ( - - - - - - - - {riderName} - - - #{String(index + 1).padStart(2, '0')} · ID #{row.userid} - - - - - - { - setSelectedRider({ - userid: row?.userid, - name: row?.displayname || `Rider ${row?.userid}` - }); - setLogDetails(null); - setMapOpen(true); - getuserdeliverylogs(row?.userid); - }} - sx={{ - bgcolor: soft('#0ea5e9'), - color: '#0ea5e9', - border: `1px solid ${edge('#0ea5e9')}`, - '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } - }} - > - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? row.userid : null); - if (isOpening) fetchTenantSummary(row.userid); - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - } - > - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={`${Number(row.riderkms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - {/* per-tenant breakdown */} - {isOpen && ( - - - - - - - - Tenant Breakdown - - - }> - {loading && ( - - Loading… - - )} - {!loading && (!tenantData || tenantData.length === 0) ? ( - - No tenant breakdown available. - - ) : ( - tenantData?.map((sub, sidx) => ( - - - - - - - {sub.tenantname || '—'} - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - } - label={formatNumberToRupees( - Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) - ) - .replace('₹', '') - .trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - )) - )} - - - - )} - - ); - }) - )} - - ) : ( - - - - - # - Rider - Orders - Accepted - Cancelled - Delivered - KMS - Amount - Action - - - - - {isLoadingReports && } - {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( - - - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} - - - - - ) : ( - filteredRows.map((row, index) => { - const isOpen = openRow === row.userid; - const amount = Number(row.ridercharges) || 0; - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - - - - - - - {row?.displayname || '—'} - - - ID #{row.userid} - - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - - } - label={`${Number(row.riderkms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - - - { - setSelectedRider({ - userid: row?.userid, - name: row?.displayname || `Rider ${row?.userid}` - }); - setLogDetails(null); - setMapOpen(true); - getuserdeliverylogs(row?.userid); - }} - sx={{ - bgcolor: soft('#0ea5e9'), - color: '#0ea5e9', - border: `1px solid ${edge('#0ea5e9')}`, - '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } - }} - > - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? row.userid : null); - if (isOpening) fetchTenantSummary(row.userid); - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - - - {/* ============================================= || Collapsible per-tenant breakdown || ============================================= */} - {isOpen && ( - - - - - - - - - - - Tenant Breakdown · {row?.displayname || ''} - - - -
    - - - # - Client - All - Pending - Completed - Cancelled - KMS - Amount - - - - {loading && } - {!loading && (!tenantData || tenantData.length === 0) ? ( - - - - No tenant breakdown available. - - - - ) : ( - tenantData?.map((sub, sidx) => ( - - - - {String(sidx + 1).padStart(2, '0')} - - - - - - - - - {sub.tenantname || '—'} - - - - - } /> - - - } /> - - - } /> - - - } /> - - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - } - label={formatNumberToRupees( - Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) - ) - .replace('₹', '') - .trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - )) - )} - -
    -
    - - - - - )} - - ); - }) - )} - - - - )} - - {/* ============================================= || Total Bar || ============================================= */} - {filteredRows.length > 0 && ( - <> - - - - Grand Total - - - - - )} -
    - - {/* ============================================= || Map Dialog || ============================================= */} - { - setMapOpen(false); - setLogDetails(null); - setSelectedRider(null); - }} - > - - { - setMapOpen(false); - setLogDetails(null); - setSelectedRider(null); - }} - /> - - - - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - ); -} +import React, { useState, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; + +// material-ui +import { + Avatar, + Box, + Chip, + Collapse, + Dialog, + DialogContent, + Divider, + Grid, + IconButton, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, + useMediaQuery, + useTheme +} from '@mui/material'; +import { + MdDirectionsBike, + MdCalendarMonth, + MdLocalShipping, + MdCheckCircle, + MdHourglassEmpty, + MdCancel, + MdCurrencyRupee, + MdStraighten, + MdMap, + MdExpandMore, + MdExpandLess, + MdGroups, + MdPerson, + MdOutlineLocalShipping, + MdOutlineCheckCircle, + MdOutlineCurrencyRupee +} from 'react-icons/md'; + +import dayjs from 'dayjs'; +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +import { fetchRidersSummary } from 'pages/api/api'; +import Loader from 'components/Loader'; +import DateFilterDialog from 'components/DateFilterDialog'; +import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; +import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; +import PageHeader from 'components/nearle_components/PageHeader'; +import StatCard from 'components/nearle_components/StatCard'; +import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; +import RidersRoutes from './RidersRoutes'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import { DT, tint, soft, ring, edge } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; + +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; + +// Pill used in cells for numeric / km / amount metrics. +const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( + + + {icon} + {label} + + +); + +// Coloured numeric cell — non-zero values get a soft red badge to draw the eye, +// preserving the legacy "red when present" affordance. +const CountCell = ({ value, color = '#ef4444', icon }) => { + const n = Number(value) || 0; + if (n === 0) { + return ( + + 0 + + ); + } + return ( + + {icon} + {n} + + ); +}; + +function formatNumberToRupees(value) { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 2 + }).format(Number(value) || 0); +} + +// ==============================|| Riders Summary ||============================== // + +export default function RidersSummary() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); + const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); + const [locaName, setLocoName] = useState('All'); + const [open, setOpen] = useState(false); + const [datestatus, setDatestatus] = useState('Today'); + const [tenantData, setTenantData] = useState([]); + const [openRow, setOpenRow] = useState(null); + const [appId, setAppId] = useState(0); + const [loading, setLoading] = useState(false); + const [mapOpen, setMapOpen] = useState(false); + const [logDetails, setLogDetails] = useState(null); + const [selectedRider, setSelectedRider] = useState(null); + const [routeLoading, setRouteLoading] = useState(false); + const [searchword, setSearchword] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + + // ==============================|| fetchRidersSummary ||============================== // + const { isLoading: isLoadingReports, data: rows } = useQuery({ + queryKey: ['ridersummary', appId, startdate, enddate], + queryFn: fetchRidersSummary + }); + + // Client-side filter across rider name + id. + // GET /admin/milers/summary confirmed live: { milerprofileid, userid, + // displayname, phone, availabilitystatus, rating, assigned, accepted, + // rejected, completed, cancelled, delivered, riderkms, ridercharges, ... } + // — no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/ + // cumulativekms field exists at all, which is why this whole page showed + // "—" everywhere. + const filteredRows = useMemo(() => { + if (!rows) return []; + if (!debouncedSearch) return rows; + const q = debouncedSearch.toLowerCase().trim(); + return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))); + }, [rows, debouncedSearch]); + + // Aggregate KPIs from the loaded summary. + const stats = useMemo(() => { + if (!rows || rows.length === 0) return { riders: 0, orders: 0, delivered: 0, amount: 0 }; + return rows.reduce( + (acc, r) => { + acc.riders += 1; + acc.orders += Number(r.assigned) || 0; + acc.delivered += Number(r.delivered) || 0; + acc.amount += Number(r.ridercharges) || 0; + return acc; + }, + { riders: 0, orders: 0, delivered: 0, amount: 0 } + ); + }, [rows]); + + // No per-rider tenant-breakdown endpoint in the new API. + const fetchTenantSummary = async () => { + setLoading(true); + setTenantData(null); + setLoading(false); + }; + + // ==============================|| rider planned route (for map) ||============================== // + // /admin/consignments has no documented per-row schema (lat/lng/step/ + // customer fields aren't shown anywhere in the doc), so building a planned + // route from it would be guesswork that risks plotting wrong pins on the + // map. Degrades to an empty route instead. + const getuserdeliverylogs = async () => { + setRouteLoading(true); + setLogDetails([]); + setRouteLoading(false); + }; + + // Total Amount sum (preserved from legacy bottom bar). + const total = useMemo(() => { + if (!rows) return 0; + return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0); + }, [rows]); + + const KPI_META = [ + { key: 'riders', label: 'Active Riders', color: BRAND, icon: MdDirectionsBike, value: stats.riders }, + { key: 'orders', label: 'Total Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping, value: stats.orders }, + { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: stats.delivered }, + { key: 'amount', label: 'Total Amount', color: '#f59e0b', icon: MdOutlineCurrencyRupee, value: formatNumberToRupees(total) } + ]; + + return ( + <> + {(isLoadingReports || loading) && } + + {/* ============================================= || Header || ============================================= */} + + } + /> + + {/* ============================================= || KPI Cards || ============================================= */} + + {KPI_META.map((item) => { + const Icon = item.icon; + return ( + + } + color={item.color} + loading={isLoadingReports} + /> + + ); + })} + + + {/* ============================================= || Filter Bar (date + search) || ============================================= */} + + + + + + + + + Orders · {datestatus} + + + {filteredRows.length} riders · {stats.orders} orders + + + + setOpen(true)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: 0.75, + px: 1.25, + py: 0.75, + borderRadius: 999, + cursor: 'pointer', + bgcolor: tint('#f59e0b'), + border: `1.5px solid ${edge('#f59e0b')}`, + color: '#f59e0b', + fontWeight: 800, + fontSize: 12, + ml: 1, + transition: 'all 0.18s', + '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } + }} + > + + {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} + + + + + + + + + + {/* ============================================= || Table || ============================================= */} + + {isMobile ? ( + + {isLoadingReports && ( + + + Loading riders… + + + )} + {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} + + + ) : ( + filteredRows.map((row, index) => { + const isOpen = openRow === row.userid; + const amount = Number(row.ridercharges) || 0; + const riderName = row?.displayname || '—'; + return ( + + + + + + + + {riderName} + + + #{String(index + 1).padStart(2, '0')} · ID #{row.userid} + + + + + + { + setSelectedRider({ + userid: row?.userid, + name: row?.displayname || `Rider ${row?.userid}` + }); + setLogDetails(null); + setMapOpen(true); + getuserdeliverylogs(row?.userid); + }} + sx={{ + bgcolor: soft('#0ea5e9'), + color: '#0ea5e9', + border: `1px solid ${edge('#0ea5e9')}`, + '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } + }} + > + + + + + { + const isOpening = !isOpen; + setOpenRow(isOpening ? row.userid : null); + if (isOpening) fetchTenantSummary(row.userid); + }} + sx={{ + bgcolor: isOpen ? BRAND : soft(BRAND), + color: isOpen ? '#fff' : BRAND, + border: `1px solid ${edge(BRAND)}`, + '&:hover': { bgcolor: BRAND, color: '#fff' } + }} + > + {isOpen ? : } + + + + + } + > + + + } /> + + + } /> + + + } /> + + + } /> + + + } + label={`${Number(row.riderkms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + + + } + label={formatNumberToRupees(amount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + + {/* per-tenant breakdown */} + {isOpen && ( + + + + + + + + Tenant Breakdown + + + }> + {loading && ( + + Loading… + + )} + {!loading && (!tenantData || tenantData.length === 0) ? ( + + No tenant breakdown available. + + ) : ( + tenantData?.map((sub, sidx) => ( + + + + + + + {sub.tenantname || '—'} + + + + + } /> + + + } /> + + + } /> + + + } /> + + + } + label={`${Number(sub.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + + + } + label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + } + label={formatNumberToRupees( + Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) + ) + .replace('₹', '') + .trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + + )) + )} + + + + )} + + ); + }) + )} + + ) : ( + + + + + # + Rider + Orders + Accepted + Cancelled + Delivered + KMS + Amount + Action + + + + + {isLoadingReports && } + {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( + + + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} + + + + + ) : ( + filteredRows.map((row, index) => { + const isOpen = openRow === row.userid; + const amount = Number(row.ridercharges) || 0; + return ( + + + + + {String(index + 1).padStart(2, '0')} + + + + + + + + + + + {row?.displayname || '—'} + + + ID #{row.userid} + + + + + + + } /> + + + } /> + + + } /> + + + } /> + + + + } + label={`${Number(row.riderkms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + + + + } + label={formatNumberToRupees(amount).replace('₹', '').trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + + + + { + setSelectedRider({ + userid: row?.userid, + name: row?.displayname || `Rider ${row?.userid}` + }); + setLogDetails(null); + setMapOpen(true); + getuserdeliverylogs(row?.userid); + }} + sx={{ + bgcolor: soft('#0ea5e9'), + color: '#0ea5e9', + border: `1px solid ${edge('#0ea5e9')}`, + '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } + }} + > + + + + + { + const isOpening = !isOpen; + setOpenRow(isOpening ? row.userid : null); + if (isOpening) fetchTenantSummary(row.userid); + }} + sx={{ + bgcolor: isOpen ? BRAND : soft(BRAND), + color: isOpen ? '#fff' : BRAND, + border: `1px solid ${edge(BRAND)}`, + '&:hover': { bgcolor: BRAND, color: '#fff' } + }} + > + {isOpen ? : } + + + + + + + {/* ============================================= || Collapsible per-tenant breakdown || ============================================= */} + {isOpen && ( + + + + + + + + + + + Tenant Breakdown · {row?.displayname || ''} + + + +
    + + + # + Client + All + Pending + Completed + Cancelled + KMS + Amount + + + + {loading && } + {!loading && (!tenantData || tenantData.length === 0) ? ( + + + + No tenant breakdown available. + + + + ) : ( + tenantData?.map((sub, sidx) => ( + + + + {String(sidx + 1).padStart(2, '0')} + + + + + + + + + {sub.tenantname || '—'} + + + + + } /> + + + } /> + + + } /> + + + } /> + + + + } + label={`${Number(sub.kms || 0).toFixed(2)} km`} + tooltip="KMS" + /> + } + label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} + tooltip="Actual KMS" + /> + + + + } + label={formatNumberToRupees( + Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) + ) + .replace('₹', '') + .trim()} + tooltip="Total Amount" + minWidth={100} + /> + + + )) + )} + +
    +
    + + + + + )} + + ); + }) + )} + + + + )} + + {/* ============================================= || Total Bar || ============================================= */} + {filteredRows.length > 0 && ( + <> + + + + Grand Total + + + + + )} + + + {/* ============================================= || Map Dialog || ============================================= */} + { + setMapOpen(false); + setLogDetails(null); + setSelectedRider(null); + }} + > + + { + setMapOpen(false); + setLogDetails(null); + setSelectedRider(null); + }} + /> + + + + {/* ============================================= || Date Filter Dialog || ============================================= */} + setOpen(false)} + onSelect={(range) => { + setStartdate(range.startDate); + setEnddate(range.endDate); + setDatestatus(range.label); + }} + /> + + ); +} diff --git a/src/pages/nearle/riders/RiderSubstitution.js b/src/pages/nearle/riders/RiderSubstitution.js index 8481748..a643cc7 100644 --- a/src/pages/nearle/riders/RiderSubstitution.js +++ b/src/pages/nearle/riders/RiderSubstitution.js @@ -208,7 +208,7 @@ export default function RiderSubstitution({ bgcolor: BRAND, color: '#fff', '&:hover': { - bgcolor: '#910E1D' + bgcolor: '#262626' } } } @@ -270,7 +270,7 @@ export default function RiderSubstitution({ }} > {isMobile ? ( - + {filteredRows?.length === 0 && ( @@ -593,7 +593,7 @@ export default function RiderSubstitution({ fontWeight: 700, textTransform: 'none', '&:hover': { - bgcolor: '#910E1D' + bgcolor: '#262626' } }} > diff --git a/src/pages/nearle/riders/createrider.js b/src/pages/nearle/riders/createrider.js index 2521cbe..3a5a64e 100644 --- a/src/pages/nearle/riders/createrider.js +++ b/src/pages/nearle/riders/createrider.js @@ -148,7 +148,7 @@ const Createrider = () => { boxShadow: DT.shadowSoft, border: '1px solid', borderColor: DT.borderSubtle, - background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)` + background: `linear-gradient(135deg, ${tint('#000000')} 0%, ${tint('#3A3A3A')} 100%)` }} > @@ -159,10 +159,10 @@ const Createrider = () => { - + - + diff --git a/src/pages/nearle/riders/editRider.js b/src/pages/nearle/riders/editRider.js index 49a07bd..0c06a03 100644 --- a/src/pages/nearle/riders/editRider.js +++ b/src/pages/nearle/riders/editRider.js @@ -135,7 +135,6 @@ const EditRider = () => { )} { width: '100%', height: '100%', p: 2, - background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)` + background: `linear-gradient(135deg, ${tint('#000000')} 0%, ${tint('#3A3A3A')} 100%)` }} > diff --git a/src/pages/nearle/riders/riders.js b/src/pages/nearle/riders/riders.js index a580ac6..c0f941b 100644 --- a/src/pages/nearle/riders/riders.js +++ b/src/pages/nearle/riders/riders.js @@ -1,1892 +1,1827 @@ -import * as React from 'react'; -import { useState, useEffect, useRef, Fragment } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Avatar, - Paper, - Stack, - Typography, - Table, - TableCell, - TableBody, - TableHead, - IconButton, - TableRow, - Tooltip, - TableContainer, - Backdrop, - Collapse, - Grid, - Box, - Skeleton, - useMediaQuery, - ToggleButtonGroup, - ToggleButton, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Autocomplete, - TextField -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -var utc = require('dayjs/plugin/utc'); -import dayjs from 'dayjs'; -dayjs.extend(utc); -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { - MdMyLocation, - MdCheckCircle, - MdCancel, - MdGroups, - MdOutlineGroups, - MdOutlineCheckCircle, - MdOutlineCancel, - MdEdit, - MdKeyboardArrowDown, - MdKeyboardArrowUp, - MdLocationOn, - MdBatteryStd, - MdPowerSettingsNew, - MdSpeed, - MdGpsFixed, - MdAccessTime, - MdInventory2, - MdTwoWheeler, - MdArrowForward, - MdDelete, - MdBlock, - MdDirectionsCar -} from 'react-icons/md'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import CircularLoader from 'components/CircularLoader'; -import { fetchAllRiders, getallridersummary } from 'pages/api/api'; -import { getMilers, blockMiler, assignMilerVehicle, getVehicles, getMilerLogs, getMiler } from 'pages/api/doormileApi'; -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { OpenToast } from 'components/third-party/OpenToast'; -import RiderSubstitution from './RiderSubstitution'; - -// ============================================================================ -// Design tokens — shared with the deliveries / tenants / customers pages so -// every surface (header, KPI tiles, table, badges, dialog) speaks the same -// visual language. Brand purple `#C01227` is the canonical primary; status -// colours are semantic and distinct from the brand. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Status palette — semantic only (do NOT swap for brand purple). Used by the -// per-row status badges and the lifecycle tabs (ALL, Active). -// GET /admin/milers returns `availabilitystatus` (confirmed live: Available, -// Assigned, On_Pickup, Offline) — not the "active"/"inactive" this used to -// assume (there is no `status` field on a miler at all). -const STATUS_META = { - active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, - inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel }, - online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, - offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, - available: { label: 'Available', color: '#10b981', icon: MdCheckCircle }, - assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler }, - on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler }, - // Confirmed live: PUT /admin/milers/:id/block sets availabilitystatus to - // "Blocked" — a value this map didn't have, so a blocked rider fell - // through to "unknown" instead of clearly reading as blocked. - blocked: { label: 'Blocked', color: '#7f1d1d', icon: MdBlock }, - idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, - unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } -}; - -// Pill-tab definitions for the rider listing tabs. Keeps brand purple for the -// "ALL" view and emerald for "Active" so the colour matches the count's meaning. -const TAB_META = [ - { key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' }, - { key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' }, - { key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' }, - { key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' } -]; - -const KPI_META = (summary) => [ - { key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 }, - { key: 'active', label: 'Active Riders', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.active ?? 0 }, - { key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 } -]; - -const Riders = () => { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const loadMoreRef = useRef(); - const containerRef = useRef(); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [locaName, setLocoName] = useState('All'); - const [appId, setAppId] = useState(0); - const [tabvalue, setTabvalue] = useState(0); - const roleid = localStorage.getItem('roleid'); - const totalCols = roleid == 1 ? 11 : 10; - const [logsRow, setLogsRow] = useState(null); - const [riderLogsdata, setRiderLogsdata] = useState(null); - const [historyDate, setHistoryDate] = useState(dayjs()); - const [historyStatus, setHistoryStatus] = useState('all'); - const [selectedDate, setSelectedDate] = useState(dayjs()); - const [editingSub, setEditingSub] = useState(null); - const [newSubRider, setNewSubRider] = useState(null); - - const getHistoryStatusColor = (status) => { - const s = (status || '').toLowerCase(); - if (s === 'cancelled' || s === 'deleted' || s === 'inactive') return '#ef4444'; // Red - if (s === 'scheduled') return '#0ea5e9'; // Blue - if (s === 'active' || s === 'completed') return '#10b981'; // Green - return '#64748b'; // Gray - }; - - // The rider-substitution feature (/substitutions/*) has no equivalent in the - // new Doormile Express API at all — no such resource is documented. These - // handlers now no-op with an explanatory toast instead of calling jupiter. - const handleDeleteSubstitution = async (row) => { - if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) { - OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); - } - }; - - const handleUpdateSubstitutionSubmit = async () => { - if (!newSubRider) { - OpenToast('Please select a substitute rider', 'error', 2000); - return; - } - OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); - setEditingSub(null); - setNewSubRider(null); - }; - - const [substituteAssignments, setSubstituteAssignments] = useState(() => { - try { - const saved = localStorage.getItem('rider_substitutions'); - return saved ? JSON.parse(saved) : {}; - } catch { - return {}; - } - }); - - const handleAssignSubstitute = (activeRiderId, substituteRider) => { - const updated = { - ...substituteAssignments, - [activeRiderId]: substituteRider - }; - setSubstituteAssignments(updated); - localStorage.setItem('rider_substitutions', JSON.stringify(updated)); - if (substituteRider) { - OpenToast(`Assigned ${substituteRider.username || substituteRider.fullname || 'Rider'} as substitute`, 'success', 2000); - } else { - OpenToast(`Removed substitute assignment`, 'info', 2000); - } - }; - - const handleFinalizeSuccess = () => { - setSubstituteAssignments({}); - localStorage.removeItem('rider_substitutions'); - queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] }); - queryClient.invalidateQueries({ queryKey: ['subRidersLogs'] }); - queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] }); - }; - - const { data: substituteRidersList } = useQuery({ - queryKey: ['allRidersForSub', appId], - queryFn: async () => { - try { - const allRiders = (await getMilers()) || []; - const allowedIds = [1121, 772, 1116]; - return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid ?? rider.milerid))); - } catch (err) { - console.error(err); - return []; - } - } - }); - - // No rider login/checkout audit-log endpoint in the new API. - const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({ - queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')], - queryFn: async () => [] - }); - - // The /substitutions resource has no equivalent in the new API. - const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({ - queryKey: ['substitutionsHistory', appId, historyDate, historyStatus], - queryFn: async () => [] - }); - - const handleChangetab = (i) => { - setTabvalue(i); - setLogsRow(null); - }; - - // ==============================|| getallridersummary||============================== // - const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({ - queryKey: ['allriders', appId, tabvalue], - queryFn: getallridersummary - }); - - // ==============================|| getRiderLogs (riders)||============================== // - // GET /admin/milers/:id/logs is confirmed live (Status: Done) — id is - // milerprofileid, not userid. A log entry's own field names are still - // unconfirmed (every rider in the tenant we tested against had an empty - // log history), so this falls back to the miler's own last-known position - // (currentlatitude/currentlongitude/lastlocationupdatedat/ - // availabilitystatus — all confirmed fields) when there's no ping yet, - // rather than showing nothing. - const getRiderLogs = async (milerprofileid) => { - if (!milerprofileid) return; - setRiderLogsdata(null); - try { - const logs = await getMilerLogs(milerprofileid, undefined, undefined, 1); - const latest = Array.isArray(logs) ? logs[0] : null; - if (latest) { - setRiderLogsdata(latest); - } else { - const miler = await getMiler(milerprofileid); - setRiderLogsdata( - miler - ? { - latitude: miler.currentlatitude, - longitude: miler.currentlongitude, - logdate: miler.lastlocationupdatedat || miler.lastpingat, - status: miler.availabilitystatus - } - : null - ); - } - } catch (err) { - OpenToast('Could not load rider logs', 'error', 2000); - } - }; - - // ==============================|| fetchAllRiders||============================== // - // GET /admin/milers isn't paginated (fetchAllRiders always returns the full - // list in one shot) — getNextPageParam must not request a "next page" or it - // re-fetches and appends the identical full list again, duplicating every - // rider in `rows`. - const { - data: allRidersData, - isLoading: allRidersLoading, - isFetchingNextPage, - fetchNextPage, - hasNextPage - } = useInfiniteQuery({ - queryKey: ['allriders', appId, debouncedSearch, tabvalue], - queryFn: fetchAllRiders, - getNextPageParam: () => undefined - }); - - const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean); - - // GET /admin/milers accepts no search/status params — debouncedSearch and - // tabvalue only trigger a refetch (via the queryKey) that returns the exact - // same full list every time. Search and the "Active" tab were both silently - // no-ops until filtered client-side here. - const filteredRows = rows.filter((row) => { - // "Active" here means currently online/working (Available/Assigned/ - // On_Pickup) — Offline and Blocked (confirmed live enum) both mean not - // working. - const status = String(row.availabilitystatus || '').toLowerCase(); - if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false; - if (!debouncedSearch) return true; - const q = debouncedSearch.toLowerCase(); - return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q)); - }); - - useEffect(() => { - if (!hasNextPage) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - fetchNextPage(); - } - }, - { - root: document.querySelector('.MuiTableContainer-root'), - rootMargin: '0px', - threshold: 1.0 - } - ); - if (loadMoreRef.current) observer.observe(loadMoreRef.current); - return () => { - if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); - }; - }, [hasNextPage, fetchNextPage]); - - const handleScroll = (event) => { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; - if (scrollTop + clientHeight >= scrollHeight - 50) { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - } - }; - - // Per-row status meta — falls back to "unknown" if the rider's state key - // isn't in the palette (e.g. brand-new status string from the backend). - // Previously joined against a separate rider-status endpoint on every tab - // except "All Riders" — that endpoint has no equivalent in the new API and - // always returned null, so every rider on every other tab showed "Unknown" - // regardless of their real state. Now reads the miler's own - // `availabilitystatus` field directly on every tab (confirmed live — there - // is no `status` field on a miler at all). - const getRowStatusMeta = (row) => { - const key = (row?.availabilitystatus || '').toLowerCase(); - return STATUS_META[key] || STATUS_META.unknown; - }; - - // PUT /admin/milers/:id/block — body shape isn't documented (Status: Built, - // never exercised against a real request), so no fields are sent beyond the - // id in the URL. Blocks whichever rider is passed after a confirm prompt. - const blockRiderMutation = useMutation({ - // /admin/milers/:id routes key off milerprofileid, not userid (confirmed - // live — GET /admin/milers/:userid 404s; the miler's own userid field - // points at the underlying app-user account, a different resource). - mutationFn: (row) => blockMiler(row.milerprofileid), - onSuccess: (res) => { - if (res.success) { - OpenToast('Rider blocked', 'success', 2000); - queryClient.invalidateQueries({ queryKey: ['allriders'] }); - } else { - OpenToast(res.message || 'Failed to block rider', 'error', 2000); - } - }, - onError: (err) => { - OpenToast(err.response?.data?.message || err.message || 'Failed to block rider', 'error', 2000); - } - }); - - const handleBlockRider = (row) => { - if (window.confirm(`Block ${row.displayname || row.authname || 'this rider'}? They will not be able to log in to the miler app.`)) { - blockRiderMutation.mutate(row); - } - }; - - // PUT /admin/milers/:id/assign-vehicle — body shape isn't documented - // (Status: Built, never exercised), so this assumes `vehicleid` following - // the codebase's own id convention (GET /admin/vehicles/:id). - const [assignVehicleRider, setAssignVehicleRider] = useState(null); - const [selectedVehicle, setSelectedVehicle] = useState(null); - - const { data: vehicles = [] } = useQuery({ - queryKey: ['riders-vehicles'], - queryFn: getVehicles, - enabled: !!assignVehicleRider - }); - - const assignVehicleMutation = useMutation({ - mutationFn: () => assignMilerVehicle(assignVehicleRider.milerprofileid, { vehicleid: selectedVehicle?.vehicleid }), - onSuccess: (res) => { - if (res.success) { - OpenToast('Vehicle assigned', 'success', 2000); - queryClient.invalidateQueries({ queryKey: ['allriders'] }); - setAssignVehicleRider(null); - setSelectedVehicle(null); - } else { - OpenToast(res.message || 'Failed to assign vehicle', 'error', 2000); - } - }, - onError: (err) => { - OpenToast(err.response?.data?.message || err.message || 'Failed to assign vehicle', 'error', 2000); - } - }); - - return ( - <> - theme.zIndex.drawer + 1 }} - open={ - allRidersLoading || - riderSummarysLoading || - (tabvalue === 2 && subRidersLogsLoading) || - (tabvalue === 3 && subHistoryLoading) - } - > - - - - {/* ============================================= || Header | ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards | ============================================= */} - - {KPI_META(allRidersSummary).map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={riderSummarysLoading} - /> - - ); - })} - - - {/* ============================================= || Status Tabs + Search || ============================================= */} - - - - {TAB_META.map((t) => { - const Icon = t.icon; - const active = tabvalue === t.key; - const count = - t.key === 2 - ? subRidersLogsData?.length || 0 - : t.key === 3 - ? subHistoryData?.length || 0 - : allRidersSummary?.[t.countKey] ?? 0; - const countLoading = t.key === 2 ? subRidersLogsLoading : t.key === 3 ? subHistoryLoading : riderSummarysLoading; - return ( - handleChangetab(t.key)} - sx={{ - display: 'inline-flex', - alignItems: 'center', - gap: { xs: 0.625, md: 0.875 }, - pl: 0.5, - pr: { xs: 1, md: 1.25 }, - py: 0.5, - flexShrink: 0, - cursor: 'pointer', - borderRadius: '10px', - border: `1px solid ${active ? t.color : DT.borderSubtle}`, - bgcolor: active ? t.color : DT.surface, - color: active ? '#fff' : DT.textSecondary, - fontWeight: 600, - boxShadow: 'none', - transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', - '&:hover': { - borderColor: active ? t.color : '#cbd5e1', - bgcolor: active ? t.color : '#f8fafc' - } - }} - > - - - - - {t.label} - - - {countLoading ? : count} - - - ); - })} - - - - - - - - - {tabvalue === 3 && ( - - - - - Status: - - val && setHistoryStatus(val)} - aria-label="status selection" - size="small" - sx={{ - '& .MuiToggleButton-root': { - borderRadius: '8px', - mx: 0.5, - border: `1px solid ${DT.borderSubtle}`, - color: DT.textSecondary, - fontWeight: 600, - textTransform: 'capitalize', - px: 2, - py: 0.5, - '&.Mui-selected': { - bgcolor: BRAND, - color: '#fff', - '&:hover': { - bgcolor: '#910E1D' - } - } - } - }} - > - All - Scheduled - - - - - newValue && setHistoryDate(newValue)} - slotProps={{ - textField: { - size: 'small', - sx: { - width: 180, - '& .MuiOutlinedInput-root': { - borderRadius: '20px', - '& fieldset': { borderColor: DT.borderSubtle }, - '&:hover fieldset': { borderColor: BRAND }, - '&.Mui-focused fieldset': { borderColor: BRAND } - } - } - } - }} - /> - - - - - )} - - {tabvalue === 3 ? ( - - {subHistoryData?.length === 0 ? ( - - - - - - No substitution history to show - - - Historical substitution logs will be listed here. - - - ) : isMobile ? ( - - {subHistoryData?.map((row, index) => ( - - - - {dayjs(row.sub_date).format('DD MMM YYYY')} - - - {row.status || 'scheduled'} - - - - - - Absent Rider - - - {row.absent_rider_name} - - - #{row.absent_rider_id} - - - - - - Substitute - - - {row.sub_rider_name} - - - #{row.sub_rider_id} - - - - {row.reason && ( - - - Reason: {row.reason} - - - )} - - } - /> - ))} - - ) : ( - - - - - Date - Absent Rider - - Substitute Rider - Reason - Status - Action - - - - {subHistoryData?.map((row, index) => ( - - - - {dayjs(row.sub_date).format('DD MMM YYYY')} - - - - - - {(row.absent_rider_name || '?').charAt(0).toUpperCase()} - - - - {row.absent_rider_name} - - - #{row.absent_rider_id} - - - - - - - - - - - {(row.sub_rider_name || '?').charAt(0).toUpperCase()} - - - - {row.sub_rider_name} - - - #{row.sub_rider_id} - - - - - - - {row.reason || '—'} - - - - - {row.status || 'scheduled'} - - - - {(row.status || '').toLowerCase() !== 'cancelled' && ( - - - { - setEditingSub(row); - setNewSubRider(null); - }} - > - - - - - { - handleDeleteSubstitution(row); - }} - > - - - - - )} - - - ))} - -
    -
    - )} - - ) : tabvalue === 2 ? ( - - ) : ( - - - {isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {allRidersLoading && ( - - - - Loading riders… - - - )} - - {filteredRows?.length === 0 && !allRidersLoading && ( - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} - - - )} - - {filteredRows?.length !== 0 && - filteredRows?.map((row, index) => { - const statusMeta = getRowStatusMeta(row); - const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.milerprofileid; - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - #{row?.userid} - - - - - - - {statusMeta.label} - - - - {roleid == 1 && ( - - { - navigate('/doormile/riders/edit', { state: { riderdata: row } }); - }} - > - - - handleBlockRider(row)} - > - - - setAssignVehicleRider(row)} - > - - - {tabvalue != 0 && ( - { - if (row.milerprofileid == logsRow) { - setLogsRow(null); - } else { - setLogsRow(row.milerprofileid); - getRiderLogs(row.milerprofileid); - } - }} - > - {expanded ? : } - - )} - - )} - - - - {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} - - - - {row.displayname || row.authname || '—'} - - - {row.phone || '—'} - - - - - } - > - - - - {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} - - - {row.city || ''} - - - - - - - - - {row.defaultvehicletype || '—'} - - - - - - - - {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} - - - {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} - - - - - - - - {expanded && tabvalue !== 0 && ( - - - - - - - Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} - - - - - - - - - - - - - - )} - - ); - })} - - {filteredRows?.length !== 0 && ( -
    - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more riders - - )} -
    - )} -
    - ) : ( - - - - # - ID - Rider - Address - Vehicle - Shift - Time - Fare - Fuel - Status - {roleid == 1 && Action} - - - - {allRidersLoading && } - - {filteredRows?.length === 0 && !allRidersLoading && ( - - - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} - - - - - )} - - {filteredRows?.length !== 0 && - filteredRows?.map((row, index) => { - const statusMeta = getRowStatusMeta(row); - const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.milerprofileid; - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - #{row?.userid} - - - - - - {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} - - - - {row.displayname || row.authname || '—'} - - - {row.phone || '—'} - - - - - - - - - {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} - - - {row.city || ''} - - - - - - - - - - - {row.defaultvehicletype || '—'} - - - - - - #{row.shiftid ?? '—'} - - - - - - {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} - - - {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} - - - - - - {row.basefare ?? '—'} - - - - - {row.fuelcharge ?? '—'} - - - - - - - - - {statusMeta.label} - - - - {roleid == 1 && ( - - - - { - navigate('/doormile/riders/edit', { state: { riderdata: row } }); - }} - > - - - - - handleBlockRider(row)} - > - - - - - setAssignVehicleRider(row)} - > - - - - {tabvalue != 0 && ( - - { - if (row.milerprofileid == logsRow) { - setLogsRow(null); - } else { - setLogsRow(row.milerprofileid); - getRiderLogs(row.milerprofileid); - } - }} - > - {expanded ? : } - - - )} - - - )} - - - {/* ============ Collapsible row — live rider logs ============ */} - {expanded && tabvalue !== 0 && ( - - - - - - - - - - Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} - - - - - - - - - - - - - - - - - )} - - ); - })} - - {filteredRows?.length !== 0 && ( - - -
    - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more riders - - )} -
    -
    -
    - )} -
    -
    - )} -
    -
    - )} - - setEditingSub(null)} - maxWidth="xs" - fullWidth - PaperProps={{ - sx: { - borderRadius: `${DT.radiusCard / 8}px`, - p: 1 - } - }} - > - Update Substitution - - - - Choose a new substitute rider for {editingSub?.absent_rider_name}. - - option?.userid === value?.userid} - getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`} - value={newSubRider} - onChange={(event, newValue) => setNewSubRider(newValue)} - renderInput={(params) => ( - - )} - /> - - - - - - - - - { - setAssignVehicleRider(null); - setSelectedVehicle(null); - }} - maxWidth="xs" - fullWidth - > - - Assign Vehicle · {assignVehicleRider?.displayname || assignVehicleRider?.authname} - - - `${option.vehicleno || ''}${option.vehicletype ? ` (${option.vehicletype})` : ''}`} - value={selectedVehicle} - onChange={(e, value) => setSelectedVehicle(value)} - renderInput={(params) => } - /> - - - - - - - - ); -}; - -// Inline stat chip used in the rider-logs collapse row. Mirrors the StatChip -// pattern from the pricing page so the telemetry block reads at a glance. -const LogChip = ({ color, icon: Icon, label, value }) => ( - - - - - - - - {label} - - - {value} - - - - -); - -export default Riders; +import * as React from 'react'; +import { useState, useEffect, useRef, Fragment } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Avatar, + Paper, + Stack, + Typography, + Table, + TableCell, + TableBody, + TableHead, + IconButton, + TableRow, + Tooltip, + TableContainer, + Backdrop, + Collapse, + Grid, + Box, + Skeleton, + useMediaQuery, + ToggleButtonGroup, + ToggleButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Autocomplete, + TextField +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +var utc = require('dayjs/plugin/utc'); +import dayjs from 'dayjs'; +dayjs.extend(utc); +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { + MdCheckCircle, + MdCancel, + MdGroups, + MdOutlineGroups, + MdOutlineCheckCircle, + MdOutlineCancel, + MdEdit, + MdKeyboardArrowDown, + MdKeyboardArrowUp, + MdLocationOn, + MdBatteryStd, + MdPowerSettingsNew, + MdSpeed, + MdGpsFixed, + MdAccessTime, + MdInventory2, + MdTwoWheeler, + MdArrowForward, + MdDelete, + MdBlock, + MdDirectionsCar +} from 'react-icons/md'; +import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; +import PageHeader from 'components/nearle_components/PageHeader'; +import StatCard from 'components/nearle_components/StatCard'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; +import CircularLoader from 'components/CircularLoader'; +import { fetchAllRiders, getallridersummary } from 'pages/api/api'; +import { getMilers, blockMiler, assignMilerVehicle, getVehicles, getMilerLogs, getMiler } from 'pages/api/doormileApi'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; +import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; +import { OpenToast } from 'components/third-party/OpenToast'; +import RiderSubstitution from './RiderSubstitution'; +import { DT, tint, soft, edge } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; + +const BRAND = '#000000'; + +// Status palette — semantic only (do NOT swap for brand purple). Used by the +// per-row status badges and the lifecycle tabs (ALL, Active). +// GET /admin/milers returns `availabilitystatus` (confirmed live: Available, +// Assigned, On_Pickup, Offline) — not the "active"/"inactive" this used to +// assume (there is no `status` field on a miler at all). +const STATUS_META = { + active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, + inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel }, + online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, + offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, + available: { label: 'Available', color: '#10b981', icon: MdCheckCircle }, + assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler }, + on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler }, + // Confirmed live: PUT /admin/milers/:id/block sets availabilitystatus to + // "Blocked" — a value this map didn't have, so a blocked rider fell + // through to "unknown" instead of clearly reading as blocked. + blocked: { label: 'Blocked', color: '#7f1d1d', icon: MdBlock }, + idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, + unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } +}; + +// Pill-tab definitions for the rider listing tabs. Keeps brand purple for the +// "ALL" view and emerald for "Active" so the colour matches the count's meaning. +const TAB_META = [ + { key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' }, + { key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' }, + { key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' }, + { key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' } +]; + +const KPI_META = (summary) => [ + { key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 }, + { key: 'active', label: 'Active Riders', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.active ?? 0 }, + { key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 } +]; + +const Riders = () => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const loadMoreRef = useRef(); + const containerRef = useRef(); + const [searchword, setSearchword] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [locaName, setLocoName] = useState('All'); + const [appId, setAppId] = useState(0); + const [tabvalue, setTabvalue] = useState(0); + const roleid = localStorage.getItem('roleid'); + const totalCols = roleid == 1 ? 11 : 10; + const [logsRow, setLogsRow] = useState(null); + const [riderLogsdata, setRiderLogsdata] = useState(null); + const [historyDate, setHistoryDate] = useState(dayjs()); + const [historyStatus, setHistoryStatus] = useState('all'); + const [selectedDate, setSelectedDate] = useState(dayjs()); + const [editingSub, setEditingSub] = useState(null); + const [newSubRider, setNewSubRider] = useState(null); + + const getHistoryStatusColor = (status) => { + const s = (status || '').toLowerCase(); + if (s === 'cancelled' || s === 'deleted' || s === 'inactive') return '#ef4444'; // Red + if (s === 'scheduled') return '#0ea5e9'; // Blue + if (s === 'active' || s === 'completed') return '#10b981'; // Green + return '#64748b'; // Gray + }; + + // The rider-substitution feature (/substitutions/*) has no equivalent in the + // new Doormile Express API at all — no such resource is documented. These + // handlers now no-op with an explanatory toast instead of calling jupiter. + const handleDeleteSubstitution = async (row) => { + if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) { + OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); + } + }; + + const handleUpdateSubstitutionSubmit = async () => { + if (!newSubRider) { + OpenToast('Please select a substitute rider', 'error', 2000); + return; + } + OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); + setEditingSub(null); + setNewSubRider(null); + }; + + const [substituteAssignments, setSubstituteAssignments] = useState(() => { + try { + const saved = localStorage.getItem('rider_substitutions'); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } + }); + + const handleAssignSubstitute = (activeRiderId, substituteRider) => { + const updated = { + ...substituteAssignments, + [activeRiderId]: substituteRider + }; + setSubstituteAssignments(updated); + localStorage.setItem('rider_substitutions', JSON.stringify(updated)); + if (substituteRider) { + OpenToast(`Assigned ${substituteRider.username || substituteRider.fullname || 'Rider'} as substitute`, 'success', 2000); + } else { + OpenToast(`Removed substitute assignment`, 'info', 2000); + } + }; + + const handleFinalizeSuccess = () => { + setSubstituteAssignments({}); + localStorage.removeItem('rider_substitutions'); + queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] }); + queryClient.invalidateQueries({ queryKey: ['subRidersLogs'] }); + queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] }); + }; + + const { data: substituteRidersList } = useQuery({ + queryKey: ['allRidersForSub', appId], + queryFn: async () => { + try { + const allRiders = (await getMilers()) || []; + const allowedIds = [1121, 772, 1116]; + return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid ?? rider.milerid))); + } catch (err) { + console.error(err); + return []; + } + } + }); + + // No rider login/checkout audit-log endpoint in the new API. + const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({ + queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')], + queryFn: async () => [] + }); + + // The /substitutions resource has no equivalent in the new API. + const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({ + queryKey: ['substitutionsHistory', appId, historyDate, historyStatus], + queryFn: async () => [] + }); + + const handleChangetab = (i) => { + setTabvalue(i); + setLogsRow(null); + }; + + // ==============================|| getallridersummary||============================== // + const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({ + queryKey: ['allriders', appId, tabvalue], + queryFn: getallridersummary + }); + + // ==============================|| getRiderLogs (riders)||============================== // + // GET /admin/milers/:id/logs is confirmed live (Status: Done) — id is + // milerprofileid, not userid. A log entry's own field names are still + // unconfirmed (every rider in the tenant we tested against had an empty + // log history), so this falls back to the miler's own last-known position + // (currentlatitude/currentlongitude/lastlocationupdatedat/ + // availabilitystatus — all confirmed fields) when there's no ping yet, + // rather than showing nothing. + const getRiderLogs = async (milerprofileid) => { + if (!milerprofileid) return; + setRiderLogsdata(null); + try { + const logs = await getMilerLogs(milerprofileid, undefined, undefined, 1); + const latest = Array.isArray(logs) ? logs[0] : null; + if (latest) { + setRiderLogsdata(latest); + } else { + const miler = await getMiler(milerprofileid); + setRiderLogsdata( + miler + ? { + latitude: miler.currentlatitude, + longitude: miler.currentlongitude, + logdate: miler.lastlocationupdatedat || miler.lastpingat, + status: miler.availabilitystatus + } + : null + ); + } + } catch (err) { + OpenToast('Could not load rider logs', 'error', 2000); + } + }; + + // ==============================|| fetchAllRiders||============================== // + // GET /admin/milers isn't paginated (fetchAllRiders always returns the full + // list in one shot) — getNextPageParam must not request a "next page" or it + // re-fetches and appends the identical full list again, duplicating every + // rider in `rows`. + const { + data: allRidersData, + isLoading: allRidersLoading, + isFetchingNextPage, + fetchNextPage, + hasNextPage + } = useInfiniteQuery({ + queryKey: ['allriders', appId, debouncedSearch, tabvalue], + queryFn: fetchAllRiders, + getNextPageParam: () => undefined + }); + + const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean); + + // GET /admin/milers accepts no search/status params — debouncedSearch and + // tabvalue only trigger a refetch (via the queryKey) that returns the exact + // same full list every time. Search and the "Active" tab were both silently + // no-ops until filtered client-side here. + const filteredRows = rows.filter((row) => { + // "Active" here means currently online/working (Available/Assigned/ + // On_Pickup) — Offline and Blocked (confirmed live enum) both mean not + // working. + const status = String(row.availabilitystatus || '').toLowerCase(); + if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false; + if (!debouncedSearch) return true; + const q = debouncedSearch.toLowerCase(); + return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q)); + }); + + useEffect(() => { + if (!hasNextPage) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + fetchNextPage(); + } + }, + { + root: document.querySelector('.MuiTableContainer-root'), + rootMargin: '0px', + threshold: 1.0 + } + ); + if (loadMoreRef.current) observer.observe(loadMoreRef.current); + return () => { + if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); + }; + }, [hasNextPage, fetchNextPage]); + + const handleScroll = (event) => { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; + if (scrollTop + clientHeight >= scrollHeight - 50) { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + } + }; + + // Per-row status meta — falls back to "unknown" if the rider's state key + // isn't in the palette (e.g. brand-new status string from the backend). + // Previously joined against a separate rider-status endpoint on every tab + // except "All Riders" — that endpoint has no equivalent in the new API and + // always returned null, so every rider on every other tab showed "Unknown" + // regardless of their real state. Now reads the miler's own + // `availabilitystatus` field directly on every tab (confirmed live — there + // is no `status` field on a miler at all). + const getRowStatusMeta = (row) => { + const key = (row?.availabilitystatus || '').toLowerCase(); + return STATUS_META[key] || STATUS_META.unknown; + }; + + // PUT /admin/milers/:id/block — body shape isn't documented (Status: Built, + // never exercised against a real request), so no fields are sent beyond the + // id in the URL. Blocks whichever rider is passed after a confirm prompt. + const blockRiderMutation = useMutation({ + // /admin/milers/:id routes key off milerprofileid, not userid (confirmed + // live — GET /admin/milers/:userid 404s; the miler's own userid field + // points at the underlying app-user account, a different resource). + mutationFn: (row) => blockMiler(row.milerprofileid), + onSuccess: (res) => { + if (res.success) { + OpenToast('Rider blocked', 'success', 2000); + queryClient.invalidateQueries({ queryKey: ['allriders'] }); + } else { + OpenToast(res.message || 'Failed to block rider', 'error', 2000); + } + }, + onError: (err) => { + OpenToast(err.response?.data?.message || err.message || 'Failed to block rider', 'error', 2000); + } + }); + + const handleBlockRider = (row) => { + if (window.confirm(`Block ${row.displayname || row.authname || 'this rider'}? They will not be able to log in to the miler app.`)) { + blockRiderMutation.mutate(row); + } + }; + + // PUT /admin/milers/:id/assign-vehicle — body shape isn't documented + // (Status: Built, never exercised), so this assumes `vehicleid` following + // the codebase's own id convention (GET /admin/vehicles/:id). + const [assignVehicleRider, setAssignVehicleRider] = useState(null); + const [selectedVehicle, setSelectedVehicle] = useState(null); + + const { data: vehicles = [] } = useQuery({ + queryKey: ['riders-vehicles'], + queryFn: getVehicles, + enabled: !!assignVehicleRider + }); + + const assignVehicleMutation = useMutation({ + mutationFn: () => assignMilerVehicle(assignVehicleRider.milerprofileid, { vehicleid: selectedVehicle?.vehicleid }), + onSuccess: (res) => { + if (res.success) { + OpenToast('Vehicle assigned', 'success', 2000); + queryClient.invalidateQueries({ queryKey: ['allriders'] }); + setAssignVehicleRider(null); + setSelectedVehicle(null); + } else { + OpenToast(res.message || 'Failed to assign vehicle', 'error', 2000); + } + }, + onError: (err) => { + OpenToast(err.response?.data?.message || err.message || 'Failed to assign vehicle', 'error', 2000); + } + }); + + return ( + <> + theme.zIndex.drawer + 1 }} + open={ + allRidersLoading || + riderSummarysLoading || + (tabvalue === 2 && subRidersLogsLoading) || + (tabvalue === 3 && subHistoryLoading) + } + > + + + + {/* ============================================= || Header | ============================================= */} + + } + /> + + {/* ============================================= || KPI Cards | ============================================= */} + + {KPI_META(allRidersSummary).map((item) => { + const Icon = item.icon; + return ( + + } + color={item.color} + loading={riderSummarysLoading} + /> + + ); + })} + + + {/* ============================================= || Status Tabs + Search || ============================================= */} + + + + {TAB_META.map((t) => { + const Icon = t.icon; + const active = tabvalue === t.key; + const count = + t.key === 2 + ? subRidersLogsData?.length || 0 + : t.key === 3 + ? subHistoryData?.length || 0 + : allRidersSummary?.[t.countKey] ?? 0; + const countLoading = t.key === 2 ? subRidersLogsLoading : t.key === 3 ? subHistoryLoading : riderSummarysLoading; + return ( + handleChangetab(t.key)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: { xs: 0.625, md: 0.875 }, + pl: 0.5, + pr: { xs: 1, md: 1.25 }, + py: 0.5, + flexShrink: 0, + cursor: 'pointer', + borderRadius: '10px', + // Selected tab always shows the brand red, not the status's own + // semantic color (pending=amber, delivered=emerald, etc.) — the + // status color still tints the unselected/resting state below. + border: `1px solid ${active ? BRAND : DT.borderSubtle}`, + bgcolor: active ? BRAND : DT.surface, + color: active ? '#fff' : DT.textSecondary, + fontWeight: 600, + boxShadow: 'none', + transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', + '&:hover': { + borderColor: active ? BRAND : '#cbd5e1', + bgcolor: active ? BRAND : '#f8fafc' + } + }} + > + + + + + {t.label} + + + {countLoading ? : count} + + + ); + })} + + + + + + + + + {tabvalue === 3 && ( + + + + + Status: + + val && setHistoryStatus(val)} + aria-label="status selection" + size="small" + sx={{ + '& .MuiToggleButton-root': { + borderRadius: '8px', + mx: 0.5, + border: `1px solid ${DT.borderSubtle}`, + color: DT.textSecondary, + fontWeight: 600, + textTransform: 'capitalize', + px: 2, + py: 0.5, + '&.Mui-selected': { + bgcolor: BRAND, + color: '#fff', + '&:hover': { + bgcolor: '#262626' + } + } + } + }} + > + All + Scheduled + + + + + newValue && setHistoryDate(newValue)} + slotProps={{ + textField: { + size: 'small', + sx: { + width: 180, + '& .MuiOutlinedInput-root': { + borderRadius: '20px', + '& fieldset': { borderColor: DT.borderSubtle }, + '&:hover fieldset': { borderColor: BRAND }, + '&.Mui-focused fieldset': { borderColor: BRAND } + } + } + } + }} + /> + + + + + )} + + {tabvalue === 3 ? ( + + {subHistoryData?.length === 0 ? ( + + + + + + No substitution history to show + + + Historical substitution logs will be listed here. + + + ) : isMobile ? ( + + {subHistoryData?.map((row, index) => ( + + + + {dayjs(row.sub_date).format('DD MMM YYYY')} + + + {row.status || 'scheduled'} + + + + + + Absent Rider + + + {row.absent_rider_name} + + + #{row.absent_rider_id} + + + + + + Substitute + + + {row.sub_rider_name} + + + #{row.sub_rider_id} + + + + {row.reason && ( + + + Reason: {row.reason} + + + )} + + } + /> + ))} + + ) : ( + + + + + Date + Absent Rider + + Substitute Rider + Reason + Status + Action + + + + {subHistoryData?.map((row, index) => ( + + + + {dayjs(row.sub_date).format('DD MMM YYYY')} + + + + + + {(row.absent_rider_name || '?').charAt(0).toUpperCase()} + + + + {row.absent_rider_name} + + + #{row.absent_rider_id} + + + + + + + + + + + {(row.sub_rider_name || '?').charAt(0).toUpperCase()} + + + + {row.sub_rider_name} + + + #{row.sub_rider_id} + + + + + + + {row.reason || '—'} + + + + + {row.status || 'scheduled'} + + + + {(row.status || '').toLowerCase() !== 'cancelled' && ( + + + { + setEditingSub(row); + setNewSubRider(null); + }} + > + + + + + { + handleDeleteSubstitution(row); + }} + > + + + + + )} + + + ))} + +
    +
    + )} +
    + ) : tabvalue === 2 ? ( + + ) : ( + + + {isMobile ? ( + /* ===================== MOBILE: card list ===================== */ + + {allRidersLoading && ( + + + + Loading riders… + + + )} + + {filteredRows?.length === 0 && !allRidersLoading && ( + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} + + + )} + + {filteredRows?.length !== 0 && + filteredRows?.map((row, index) => { + const statusMeta = getRowStatusMeta(row); + const StatusIcon = statusMeta.icon; + const expanded = logsRow === row.milerprofileid; + return ( + + + + + {String(index + 1).padStart(2, '0')} + + + #{row?.userid} + + + + + + + {statusMeta.label} + + + + {roleid == 1 && ( + + { + navigate('/doormile/riders/edit', { state: { riderdata: row } }); + }} + > + + + handleBlockRider(row)} + > + + + setAssignVehicleRider(row)} + > + + + {tabvalue != 0 && ( + { + if (row.milerprofileid == logsRow) { + setLogsRow(null); + } else { + setLogsRow(row.milerprofileid); + getRiderLogs(row.milerprofileid); + } + }} + > + {expanded ? : } + + )} + + )} + + + + {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} + + + + {row.displayname || row.authname || '—'} + + + {row.phone || '—'} + + + + + } + > + + + + {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} + + + {row.city || ''} + + + + + + + + + {row.defaultvehicletype || '—'} + + + + + + + + {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} + + + {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} + + + + + + + + {expanded && tabvalue !== 0 && ( + + + + + + + Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} + + + + + + + + + + + + + + )} + + ); + })} + + {filteredRows?.length !== 0 && ( +
    + {isFetchingNextPage || hasNextPage ? ( + + ) : ( + + No more riders + + )} +
    + )} +
    + ) : ( + + + + # + ID + Rider + Address + Vehicle + Shift + Time + Fare + Fuel + Status + {roleid == 1 && Action} + + + + {allRidersLoading && } + + {filteredRows?.length === 0 && !allRidersLoading && ( + + + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} + + + + + )} + + {filteredRows?.length !== 0 && + filteredRows?.map((row, index) => { + const statusMeta = getRowStatusMeta(row); + const StatusIcon = statusMeta.icon; + const expanded = logsRow === row.milerprofileid; + return ( + + + + + {String(index + 1).padStart(2, '0')} + + + + + #{row?.userid} + + + + + + {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} + + + + {row.displayname || row.authname || '—'} + + + {row.phone || '—'} + + + + + + + + + {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} + + + {row.city || ''} + + + + + + + + + + + {row.defaultvehicletype || '—'} + + + + + + #{row.shiftid ?? '—'} + + + + + + {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} + + + {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} + + + + + + {row.basefare ?? '—'} + + + + + {row.fuelcharge ?? '—'} + + + + + + + + + {statusMeta.label} + + + + {roleid == 1 && ( + + + + { + navigate('/doormile/riders/edit', { state: { riderdata: row } }); + }} + > + + + + + handleBlockRider(row)} + > + + + + + setAssignVehicleRider(row)} + > + + + + {tabvalue != 0 && ( + + { + if (row.milerprofileid == logsRow) { + setLogsRow(null); + } else { + setLogsRow(row.milerprofileid); + getRiderLogs(row.milerprofileid); + } + }} + > + {expanded ? : } + + + )} + + + )} + + + {/* ============ Collapsible row — live rider logs ============ */} + {expanded && tabvalue !== 0 && ( + + + + + + + + + + Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} + + + + + + + + + + + + + + + + + )} + + ); + })} + + {filteredRows?.length !== 0 && ( + + +
    + {isFetchingNextPage || hasNextPage ? ( + + ) : ( + + No more riders + + )} +
    +
    +
    + )} +
    +
    + )} +
    +
    + )} + + setEditingSub(null)} + maxWidth="xs" + fullWidth + PaperProps={{ + sx: { + borderRadius: `${DT.radiusCard / 8}px`, + p: 1 + } + }} + > + Update Substitution + + + + Choose a new substitute rider for {editingSub?.absent_rider_name}. + + option?.userid === value?.userid} + getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`} + value={newSubRider} + onChange={(event, newValue) => setNewSubRider(newValue)} + renderInput={(params) => ( + + )} + /> + + + + + + + + + { + setAssignVehicleRider(null); + setSelectedVehicle(null); + }} + maxWidth="xs" + fullWidth + > + + Assign Vehicle · {assignVehicleRider?.displayname || assignVehicleRider?.authname} + + + `${option.vehicleno || ''}${option.vehicletype ? ` (${option.vehicletype})` : ''}`} + value={selectedVehicle} + onChange={(e, value) => setSelectedVehicle(value)} + renderInput={(params) => } + /> + + + + + + + + ); +}; + +// Inline stat chip used in the rider-logs collapse row. Mirrors the StatChip +// pattern from the pricing page so the telemetry block reads at a glance. +const LogChip = ({ color, icon: Icon, label, value }) => ( + + + + + + + + {label} + + + {value} + + + + +); + +export default Riders; diff --git a/src/pages/nearle/tripsheets/tripsheets.js b/src/pages/nearle/tripsheets/tripsheets.js index d9511a7..aadef9b 100644 --- a/src/pages/nearle/tripsheets/tripsheets.js +++ b/src/pages/nearle/tripsheets/tripsheets.js @@ -42,6 +42,7 @@ import { getMilers } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens'; // ============================================================================ // Hub-to-hub transport. GET /admin/tripsheets response shape isn't documented @@ -50,19 +51,7 @@ import { enqueueSnackbar } from 'notistack'; // { sourcehubid, destinationhubid, vehicleid, driveruserid }. // ============================================================================ -const DT = { - radiusCard: 14, - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const soft = (c) => a(c, '18'); -const BRAND = '#C01227'; +const BRAND = '#000000'; const emptyForm = { sourceHub: null, destinationHub: null, vehicle: null, driver: null }; @@ -171,10 +160,10 @@ const Tripsheets = () => { - + - + # Source Hub Destination Hub @@ -199,7 +188,7 @@ const Tripsheets = () => { ) : ( tripsheets.map((row, index) => ( - + {index + 1} {hubMap.get(row.sourcehubid)?.hubname || (row.sourcehubid ? `Hub #${row.sourcehubid}` : '—')} {hubMap.get(row.destinationhubid)?.hubname || (row.destinationhubid ? `Hub #${row.destinationhubid}` : '—')} diff --git a/src/pages/nearle/vehicles/vehicles.js b/src/pages/nearle/vehicles/vehicles.js index 357e396..5472791 100644 --- a/src/pages/nearle/vehicles/vehicles.js +++ b/src/pages/nearle/vehicles/vehicles.js @@ -1,60 +1,33 @@ import React, { useMemo, useState } from 'react'; -import { - Autocomplete, - Avatar, - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - InputLabel, - MenuItem, - Paper, - Select, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; import { MdDirectionsCar, MdEdit, MdDeleteOutline, MdAdd, MdBatteryStd } from 'react-icons/md'; + +import { Grid } from '@astryxdesign/core/Grid'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { NumberInput } from '@astryxdesign/core/NumberInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import Loader from 'components/Loader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; +import TableLoader from 'components/nearle_components/TableLoader'; import { getVehicles, createVehicle, updateVehicle, deleteVehicle, getPartners } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, STATUS } from 'themes/dt/tokens'; +import { TableScroll } from 'themes/dt/primitives'; -const DT = { - radiusCard: 14, - radiusField: 10, - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - borderHover: '#cbd5e1', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const BRAND = '#C01227'; -const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van']; +const BRAND = DT.brand; +const VEHICLE_TYPE_OPTIONS = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => ({ value: v, label: v })); const emptyForm = { vehicleid: null, @@ -73,8 +46,6 @@ const emptyForm = { }; const Vehicles = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); const queryClient = useQueryClient(); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); @@ -154,6 +125,8 @@ const Vehicles = () => { if (window.confirm(`Delete vehicle "${row.vehicleno}"?`)) deleteMutation.mutate(row.vehicleid); }; + const set = (key) => (value) => setForm((fm) => ({ ...fm, [key]: value })); + const handleSave = () => { if (!form.vehicleno) { opentoast('Fill Vehicle No'); @@ -180,195 +153,152 @@ const Vehicles = () => { title="Vehicles" subtitle="Live · Fleet Directory" live - action={ - - } + action={
    - - - # - Vehicle No - Type - Partner - Max Weight - Battery - Status - Actions - - - - {isLoading && } - {rows.length === 0 && !isLoading ? ( + + +
    + - - - - - - - No vehicles to show - - - + # + Vehicle No + Type + Partner + Max Weight + Battery + Status + Actions - ) : ( - rows.map((row, index) => ( - - {index + 1} - - - {row.vehicleno} - - - {row.vehicletype || '—'} - {partnerMap.get(row.partnerid)?.partnername || (row.partnerid ? `Partner #${row.partnerid}` : '—')} - {row.maxweight ?? '—'} - {row.batterypercentage != null ? `${row.batterypercentage}%` : '—'} - {row.status || '—'} - - - - openEdit(row)} sx={{ color: BRAND }}> - - - - - handleDelete(row)} sx={{ color: '#ef4444' }}> - - - - + + + {isLoading && } + {rows.length === 0 && !isLoading ? ( + + + } /> - )) - )} - -
    -
    -
    + ) : ( + rows.map((row, index) => ( + + {index + 1} + + {row.vehicleno} + + {row.vehicletype || '—'} + + {partnerMap.get(row.partnerid)?.partnername || (row.partnerid ? `Partner #${row.partnerid}` : '—')} + + {row.maxweight ?? '—'} + {row.batterypercentage != null ? `${row.batterypercentage}%` : '—'} + {row.status || '—'} + + + } + onClick={() => openEdit(row)} + /> + } + onClick={() => handleDelete(row)} + /> + + + + )) + )} + + + + + - setDialogOpen(false)} maxWidth="sm" fullWidth> - {form.vehicleid ? 'Edit Vehicle' : 'New Vehicle'} - - - - - Vehicle No - setForm((f) => ({ ...f, vehicleno: e.target.value }))} /> - - - - - Vehicle Type - - - - - - Partner - option.partnername || ''} - value={form.partner} - onChange={(e, value) => setForm((f) => ({ ...f, partner: value }))} - renderInput={(params) => } - /> - - - - - Status - {/* Only "Available" is confirmed to work against POST - /admin/vehicles (Active/Inactive/Unavailable/Maintenance/ - Offline all rejected live) — not offering a second option - that's known to fail. On edit this preserves whatever the - vehicle's real status already is. */} - - - - - - Max Weight (kg) - setForm((f) => ({ ...f, maxweight: e.target.value }))} /> - - - - - Max Volume - setForm((f) => ({ ...f, maxvolume: e.target.value }))} /> - - - - - Battery % - setForm((f) => ({ ...f, batterypercentage: e.target.value }))} - /> - - + !next && setDialogOpen(false)} width={640} purpose="form"> + + + + + + ({ value: String(p.partnerid), label: p.partnername || `Partner #${p.partnerid}` }))} + value={form.partner ? String(form.partner.partnerid) : undefined} + onChange={(v) => set('partner')(v ? partnerMap.get(Number(v)) || null : null)} + /> + {/* Only "Available" is confirmed to work against POST /admin/vehicles + (Active/Inactive/Unavailable/Maintenance/Offline all rejected + live) — not offering a second option that's known to fail. On + edit this preserves whatever the vehicle's real status already is. */} + - - - - - + + + set('maxweight')(v ?? '')} + isOptional + /> + set('maxvolume')(v ?? '')} + isOptional + /> + set('batterypercentage')(v ?? '')} + isOptional + /> + + + + ); diff --git a/src/pages/nearle/viewProfile.js b/src/pages/nearle/viewProfile.js index 8920761..384262e 100644 --- a/src/pages/nearle/viewProfile.js +++ b/src/pages/nearle/viewProfile.js @@ -7,27 +7,12 @@ import Loader from 'components/Loader'; import { getusers } from 'pages/api/api'; import { updateProfilePassword } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; +import { DT, tint, soft } from 'themes/dt/tokens'; const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' }; -// ---- shared design tokens (mirrors the DT block used across the console) ---- -const DT = { - radiusCard: 16, - radiusInner: 12, - shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D25463'; -const tint = (c) => `${c}08`; -const soft = (c) => `${c}18`; - +const BRAND = '#000000'; +const BRAND_LIGHT = '#3A3A3A'; const initialsOf = (name) => (name || '') .trim() @@ -193,7 +178,7 @@ const ViewProfile = () => { color: '#fff', fontSize: { xs: 24, md: 28 }, fontWeight: 700, - boxShadow: '0 10px 24px rgba(192, 18, 39, 0.35)' + boxShadow: '0 10px 24px rgba(0, 0, 0, 0.35)' }} > {initialsOf(fullname)} diff --git a/src/pages/titleCard.js b/src/pages/titleCard.js deleted file mode 100644 index b7595d3..0000000 --- a/src/pages/titleCard.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { useTheme } from '@mui/material/styles'; -import { CardActions, Grid, Stack, Typography } from '@mui/material'; - -const TitleCard = ({ title, secondary, sx }) => { - const theme = useTheme(); - - return ( - - - - - {title} - {secondary} - - - - - ); -}; - -export default TitleCard; diff --git a/src/routes/LoginRoutes.js b/src/routes/LoginRoutes.js index 43cd990..1057073 100644 --- a/src/routes/LoginRoutes.js +++ b/src/routes/LoginRoutes.js @@ -6,11 +6,6 @@ import CommonLayout from 'layout/CommonLayout'; import Loadable from 'components/Loadable'; // render - login -// const AuthLogin = Loadable(lazy(() => import('pages/auth/login'))); -// const AuthRegister = Loadable(lazy(() => import('pages/auth/register'))); -// const AuthForgotPassword = Loadable(lazy(() => import('pages/auth/forgot-password'))); -// const AuthCheckMail = Loadable(lazy(() => import('pages/auth/check-mail'))); -// const AuthResetPassword = Loadable(lazy(() => import('pages/auth/reset-password'))); const Login = Loadable(lazy(() => import('pages/nearle/login'))); // ==============================|| AUTH ROUTING ||============================== // @@ -34,31 +29,6 @@ const LoginRoutes = { path: 'login', element: } - - // { - // path: '/', - // element: - // }, - // { - // path: 'login', - // element: - // }, - // { - // path: 'register', - // element: - // }, - // { - // path: 'forgot-password', - // element: - // }, - // { - // path: 'check-mail', - // element: - // }, - // { - // path: 'reset-password', - // element: - // } ] } ] diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index 966f6d4..3b3a1df 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -12,10 +12,6 @@ const MaintenanceError500 = Loadable(lazy(() => import('pages/maintenance/500')) const MaintenanceUnderConstruction = Loadable(lazy(() => import('pages/maintenance/under-construction'))); const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/coming-soon'))); -// render - sample page -// const SamplePage = Loadable(lazy(() => import('pages/extra-pages/sample-page'))); -// const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard'))); - const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders'))); const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview'))); diff --git a/src/sections/apps/chat/ChatDrawer.js b/src/sections/apps/chat/ChatDrawer.js deleted file mode 100644 index 627c3af..0000000 --- a/src/sections/apps/chat/ChatDrawer.js +++ /dev/null @@ -1,275 +0,0 @@ -import PropTypes from 'prop-types'; -import { useState } from 'react'; -import { Link } from 'react-router-dom'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { - Box, - Chip, - Drawer, - Grid, - InputAdornment, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - OutlinedInput, - Stack, - Typography, - useMediaQuery -} from '@mui/material'; - -// project imports -import UserAvatar from './UserAvatar'; -// import UserList from './UserList'; -import MainCard from 'components/MainCard'; -import IconButton from 'components/@extended/IconButton'; -import SimpleBar from 'components/third-party/SimpleBar'; - -import { ThemeMode } from 'config'; -import useAuth from 'hooks/useAuth'; - -// assets -import { - CheckCircleFilled, - ClockCircleFilled, - LogoutOutlined, - MinusCircleFilled, - RightOutlined, - SearchOutlined, - SettingOutlined -} from '@ant-design/icons'; - -// ==============================|| CHAT DRAWER ||============================== // - -function ChatDrawer({ handleDrawerOpen, openChatDrawer, setUser }) { - const theme = useTheme(); - const { user } = useAuth(); - - const matchDownLG = useMediaQuery(theme.breakpoints.down('lg')); - const drawerBG = theme.palette.mode === ThemeMode.DARK ? 'dark.main' : 'white'; - - // show menu to set current user status - const [anchorEl, setAnchorEl] = useState(); - const handleClickRightMenu = (event) => { - setAnchorEl(event?.currentTarget); - }; - - const handleCloseRightMenu = () => { - setAnchorEl(null); - }; - - // set user status on status menu click - const [status, setStatus] = useState('available'); - const handleRightMenuItemClick = (userStatus) => () => { - setStatus(userStatus); - handleCloseRightMenu(); - }; - - const [search, setSearch] = useState(''); - const handleSearch = async (event) => { - const newString = event?.target.value; - setSearch(newString); - }; - - return ( - - - - - - - Messages - - - - - - - - } - /> - - - - - - {/* */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {user?.name} - - - {user?.role} - - - - - - - - - - - - - Active - - - - - - Away - - - - - - Do not disturb - - - - - - - - - - ); -} - -ChatDrawer.propTypes = { - handleDrawerOpen: PropTypes.func, - openChatDrawer: PropTypes.bool, - setUser: PropTypes.func -}; - -export default ChatDrawer; diff --git a/src/sections/apps/chat/ChatHistory.js b/src/sections/apps/chat/ChatHistory.js deleted file mode 100644 index e12ecd2..0000000 --- a/src/sections/apps/chat/ChatHistory.js +++ /dev/null @@ -1,119 +0,0 @@ -import PropTypes from 'prop-types'; -import { useCallback, useEffect, useRef } from 'react'; - -// material-ui -import { Card, CardContent, Grid, Stack, Typography } from '@mui/material'; - -// project imports -import UserAvatar from './UserAvatar'; -// import ChatMessageAction from './ChatMessageAction'; -import IconButton from 'components/@extended/IconButton'; -import { ThemeMode } from 'config'; - -// assets -import { EditOutlined } from '@ant-design/icons'; - -// ==============================|| CHAT MESSAGE HISTORY ||============================== // - -const ChatHistory = ({ data, theme, user }) => { - // scroll to bottom when new message is sent or received - const wrapper = useRef(document.createElement('div')); - const el = wrapper.current; - const scrollToBottom = useCallback(() => { - el.scrollIntoView(false); - }, [el]); - - useEffect(() => { - scrollToBottom(); - }, [data.length, scrollToBottom]); - - return ( - - {data.map((history, index) => ( - - {history.from !== user.name ? ( - - - - - - - {/* */} - - - - - - - - - {history.text} - - - - - - - - - - {history.time} - - - - - - ) : ( - - - - - - - - - - - {history.text} - - - - - - - - - {history.time} - - - - - )} - - ))} - - ); -}; - -ChatHistory.propTypes = { - data: PropTypes.array, - theme: PropTypes.object, - user: PropTypes.object -}; - -export default ChatHistory; diff --git a/src/sections/apps/chat/UserAvatar.js b/src/sections/apps/chat/UserAvatar.js deleted file mode 100644 index 139623d..0000000 --- a/src/sections/apps/chat/UserAvatar.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Badge } from '@mui/material'; - -// project imports -// import AvatarStatus from './AvatarStatus'; -import Avatar from 'components/@extended/Avatar'; - -// assets -const avatarImage = require.context('assets/images/users', true); - -// ==============================|| CHAT USER AVATAR WITH STATUS ICON ||============================== // - -const UserAvatar = ({ user }) => ( - } - anchorOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - sx={{ '& .MuiBox-root': { width: 6, height: 6 }, padding: 0, minWidth: 12, '& svg': { background: '#fff', borderRadius: '50%' } }} - > - - -); - -UserAvatar.propTypes = { - user: PropTypes.object -}; - -export default UserAvatar; diff --git a/src/sections/apps/chat/UserDetails.js b/src/sections/apps/chat/UserDetails.js deleted file mode 100644 index e4b9aa2..0000000 --- a/src/sections/apps/chat/UserDetails.js +++ /dev/null @@ -1,303 +0,0 @@ -import PropTypes from 'prop-types'; -import { useState } from 'react'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { useMediaQuery, Box, Chip, Collapse, Divider, Grid, Stack, Switch, Typography } from '@mui/material'; - -// project imports -// import AvatarStatus from './AvatarStatus'; -import MainCard from 'components/MainCard'; -import Avatar from 'components/@extended/Avatar'; -import IconButton from 'components/@extended/IconButton'; -import SimpleBar from 'components/third-party/SimpleBar'; -import { ThemeMode } from 'config'; - -// assets -import { - CloseOutlined, - DownOutlined, - FileDoneOutlined, - FileSyncOutlined, - FolderOpenOutlined, - LinkOutlined, - MessageOutlined, - MoreOutlined, - PhoneOutlined, - PictureOutlined, - RightOutlined, - VideoCameraOutlined -} from '@ant-design/icons'; - -const avatarImage = require.context('assets/images/users', true); - -// ==============================|| USER PROFILE / DETAILS ||============================== // - -const UserDetails = ({ user, onClose }) => { - const theme = useTheme(); - const matchDownLG = useMediaQuery(theme.breakpoints.down('md')); - - const [checked, setChecked] = useState(true); - if (Object.keys(user).length === 0) return ...Loading; - - let statusBGColor; - let statusColor; - if (user.online_status === 'available') { - statusBGColor = theme.palette.success.lighter; - statusColor = theme.palette.success.main; - } else if (user.online_status === 'do_not_disturb') { - statusBGColor = theme.palette.grey.A100; - statusColor = theme.palette.grey.A200; - } else { - statusBGColor = theme.palette.warning.lighter; - statusColor = theme.palette.warning.main; - } - - return ( - - - {onClose && ( - - - - )} - - - - - - {user.name} - - - {user.role} - - - - - - {/* */} - - - - - - - - - - - - - - - - - - - - - - - All File - - - 231 - - - - All Link - - - 231 - - - - - - - setChecked(!checked)} - > - - Information - - - - - - - - - - - - - Address - {user.location} - - - Email - {user.personal_email} - - - Phone - {user.personal_phone} - - - Last visited - {user.lastMessage} - - - - - - Notification - - - - - - - - - File type - - - - - - - - - - - - - - - - Document - 123 files, 193MB - - - - - - - - - - - - - - - - Photos - 53 files, 321MB - - - - - - - - - - - - - - - - Other - 49 files, 193MB - - - - - - - - - - - - - - ); -}; - -UserDetails.propTypes = { - user: PropTypes.object, - onClose: PropTypes.func -}; - -export default UserDetails; diff --git a/src/sections/auth/AuthCard.js b/src/sections/auth/AuthCard.js deleted file mode 100644 index db4eade..0000000 --- a/src/sections/auth/AuthCard.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/material'; - -// project import -import MainCard from 'components/MainCard'; - -// ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== // - -const AuthCard = ({ children, ...other }) => { - const theme = useTheme(); - return ( - *': { - flexGrow: 1, - flexBasis: '50%' - } - }} - content={false} - {...other} - border={false} - boxShadow - shadow={theme.customShadows.z1} - > - {children} - - ); -}; - -AuthCard.propTypes = { - children: PropTypes.node -}; - -export default AuthCard; diff --git a/src/sections/auth/AuthWrapper.js b/src/sections/auth/AuthWrapper.js deleted file mode 100644 index 6ea539c..0000000 --- a/src/sections/auth/AuthWrapper.js +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; - -// material-ui -import { Box, Grid } from '@mui/material'; - -// project import -import AuthFooter from 'components/cards/AuthFooter'; -import Logo from 'components/logo'; -import AuthCard from './AuthCard'; - -// assets -import AuthBackground from 'assets/images/auth/AuthBackground'; - -// ==============================|| AUTHENTICATION - WRAPPER ||============================== // - -const AuthWrapper = ({ children }) => ( - - - - - - - - - - {children} - - - - - - - - -); - -AuthWrapper.propTypes = { - children: PropTypes.node -}; - -export default AuthWrapper; diff --git a/src/sections/auth/auth-forms/AuthForgotPassword.js b/src/sections/auth/auth-forms/AuthForgotPassword.js deleted file mode 100644 index c1eec45..0000000 --- a/src/sections/auth/auth-forms/AuthForgotPassword.js +++ /dev/null @@ -1,126 +0,0 @@ -import { useNavigate } from 'react-router-dom'; - -// material-ui -import { Button, FormHelperText, Grid, InputLabel, OutlinedInput, Stack, Typography } from '@mui/material'; - -// third party -import * as Yup from 'yup'; -import { Formik } from 'formik'; - -// project import -import AnimateButton from 'components/@extended/AnimateButton'; - -import useAuth from 'hooks/useAuth'; -import useScriptRef from 'hooks/useScriptRef'; -import { dispatch } from 'store'; -import { openSnackbar } from 'store/reducers/snackbar'; - -// ============================|| FIREBASE - FORGOT PASSWORD ||============================ // - -const AuthForgotPassword = () => { - const scriptedRef = useScriptRef(); - const navigate = useNavigate(); - - const { isLoggedIn, resetPassword } = useAuth(); - - return ( - <> - { - try { - await resetPassword(values.email).then( - () => { - setStatus({ success: true }); - setSubmitting(false); - dispatch( - openSnackbar({ - open: true, - message: 'Check mail for reset password link', - variant: 'alert', - alert: { - color: 'success' - }, - close: false - }) - ); - setTimeout(() => { - navigate(isLoggedIn ? '/auth/check-mail' : '/check-mail', { replace: true }); - }, 1500); - - // WARNING: do not set any formik state here as formik might be already destroyed here. You may get following error by doing so. - // Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. - // To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. - // github issue: https://github.com/formium/formik/issues/2430 - }, - (err) => { - setStatus({ success: false }); - setErrors({ submit: err.message }); - setSubmitting(false); - } - ); - } catch (err) { - console.error(err); - if (scriptedRef.current) { - setStatus({ success: false }); - setErrors({ submit: err.message }); - setSubmitting(false); - } - } - }} - > - {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( -
    - - - - Email Address - - {touched.email && errors.email && ( - - {errors.email} - - )} - - - {errors.submit && ( - - {errors.submit} - - )} - - Do not forgot to check SPAM box. - - - - - - - -
    - )} -
    - - ); -}; - -export default AuthForgotPassword; diff --git a/src/sections/auth/auth-forms/AuthLogin.js b/src/sections/auth/auth-forms/AuthLogin.js deleted file mode 100644 index 058b4e8..0000000 --- a/src/sections/auth/auth-forms/AuthLogin.js +++ /dev/null @@ -1,176 +0,0 @@ -import React from 'react'; -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import { - Button, - Checkbox, - FormControlLabel, - FormHelperText, - Grid, - Link, - InputAdornment, - InputLabel, - OutlinedInput, - Stack, - Typography -} from '@mui/material'; - -// third party -import * as Yup from 'yup'; -import { Formik } from 'formik'; - -// project import -import useAuth from 'hooks/useAuth'; -import useScriptRef from 'hooks/useScriptRef'; -import IconButton from 'components/@extended/IconButton'; -import AnimateButton from 'components/@extended/AnimateButton'; - -// assets -import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; - -// ============================|| JWT - LOGIN ||============================ // - -const AuthLogin = () => { - const [checked, setChecked] = React.useState(false); - - const { login } = useAuth(); - const scriptedRef = useScriptRef(); - - const [showPassword, setShowPassword] = React.useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = (event) => { - event.preventDefault(); - }; - - return ( - <> - { - try { - await login(values.email, values.password); - if (scriptedRef.current) { - setStatus({ success: true }); - setSubmitting(false); - } - } catch (err) { - console.error(err); - if (scriptedRef.current) { - setStatus({ success: false }); - setErrors({ submit: err.message }); - setSubmitting(false); - } - } - }} - > - {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( -
    - - - - Email Address - - {touched.email && errors.email && ( - - {errors.email} - - )} - - - - - Password - - - {showPassword ? : } - - - } - placeholder="Enter password" - /> - {touched.password && errors.password && ( - - {errors.password} - - )} - - - - - - setChecked(event.target.checked)} - name="checked" - color="primary" - size="small" - /> - } - label={Keep me sign in} - /> - - Forgot Password? - - - - {errors.submit && ( - - {errors.submit} - - )} - - - - - - -
    - )} -
    - - ); -}; - -export default AuthLogin; diff --git a/src/sections/auth/auth-forms/AuthRegister.js b/src/sections/auth/auth-forms/AuthRegister.js deleted file mode 100644 index f9f5372..0000000 --- a/src/sections/auth/auth-forms/AuthRegister.js +++ /dev/null @@ -1,282 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Link as RouterLink, useNavigate } from 'react-router-dom'; - -// material-ui -import { - Box, - Button, - FormControl, - FormHelperText, - Grid, - Link, - InputAdornment, - InputLabel, - OutlinedInput, - Stack, - Typography -} from '@mui/material'; - -// third party -import * as Yup from 'yup'; -import { Formik } from 'formik'; - -// project import -import IconButton from 'components/@extended/IconButton'; -import AnimateButton from 'components/@extended/AnimateButton'; - -import useAuth from 'hooks/useAuth'; -import useScriptRef from 'hooks/useScriptRef'; -import { dispatch } from 'store'; -import { openSnackbar } from 'store/reducers/snackbar'; -import { strengthColor, strengthIndicator } from 'utils/password-strength'; - -// assets -import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; - -// ============================|| JWT - REGISTER ||============================ // - -const AuthRegister = () => { - const { register } = useAuth(); - const scriptedRef = useScriptRef(); - const navigate = useNavigate(); - - const [level, setLevel] = useState(); - const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = (event) => { - event.preventDefault(); - }; - - const changePassword = (value) => { - const temp = strengthIndicator(value); - setLevel(strengthColor(temp)); - }; - - useEffect(() => { - changePassword(''); - }, []); - - return ( - <> - { - try { - await register(values.email, values.password, values.firstname, values.lastname); - if (scriptedRef.current) { - setStatus({ success: true }); - setSubmitting(false); - dispatch( - openSnackbar({ - open: true, - message: 'Your registration has been successfully completed.', - variant: 'alert', - alert: { - color: 'success' - }, - close: false - }) - ); - - setTimeout(() => { - navigate('/login', { replace: true }); - }, 1500); - } - } catch (err) { - console.error(err); - if (scriptedRef.current) { - setStatus({ success: false }); - setErrors({ submit: err.message }); - setSubmitting(false); - } - } - }} - > - {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( -
    - - - - First Name* - - {touched.firstname && errors.firstname && ( - - {errors.firstname} - - )} - - - - - Last Name* - - {touched.lastname && errors.lastname && ( - - {errors.lastname} - - )} - - - - - Company - - {touched.company && errors.company && ( - - {errors.company} - - )} - - - - - Email Address* - - {touched.email && errors.email && ( - - {errors.email} - - )} - - - - - Password - { - handleChange(e); - changePassword(e.target.value); - }} - endAdornment={ - - - {showPassword ? : } - - - } - placeholder="******" - inputProps={{}} - /> - {touched.password && errors.password && ( - - {errors.password} - - )} - - - - - - - - - {level?.label} - - - - - - - - By Signing up, you agree to our   - - Terms of Service - -   and   - - Privacy Policy - - - - {errors.submit && ( - - {errors.submit} - - )} - - - - - - -
    - )} -
    - - ); -}; - -export default AuthRegister; diff --git a/src/sections/auth/auth-forms/AuthResetPassword.js b/src/sections/auth/auth-forms/AuthResetPassword.js deleted file mode 100644 index e39671d..0000000 --- a/src/sections/auth/auth-forms/AuthResetPassword.js +++ /dev/null @@ -1,201 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -// material-ui -import { - Box, - Button, - FormControl, - FormHelperText, - Grid, - InputAdornment, - InputLabel, - OutlinedInput, - Stack, - Typography -} from '@mui/material'; - -// third party -import * as Yup from 'yup'; -import { Formik } from 'formik'; - -// project import -import IconButton from 'components/@extended/IconButton'; -import AnimateButton from 'components/@extended/AnimateButton'; - -import useAuth from 'hooks/useAuth'; -import useScriptRef from 'hooks/useScriptRef'; -import { dispatch } from 'store'; -import { openSnackbar } from 'store/reducers/snackbar'; -import { strengthColor, strengthIndicator } from 'utils/password-strength'; - -// assets -import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; - -// ============================|| STATIC - RESET PASSWORD ||============================ // - -const AuthResetPassword = () => { - const scriptedRef = useScriptRef(); - const navigate = useNavigate(); - - const { isLoggedIn } = useAuth(); - - const [level, setLevel] = useState(); - const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = (event) => { - event.preventDefault(); - }; - - const changePassword = (value) => { - const temp = strengthIndicator(value); - setLevel(strengthColor(temp)); - }; - - useEffect(() => { - changePassword(''); - }, []); - - return ( - yup.parent.password === confirmPassword) - })} - onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { - try { - // password reset - if (scriptedRef.current) { - setStatus({ success: true }); - setSubmitting(false); - - dispatch( - openSnackbar({ - open: true, - message: 'Successfuly reset password.', - variant: 'alert', - alert: { - color: 'success' - }, - close: false - }) - ); - - setTimeout(() => { - navigate(isLoggedIn ? '/auth/login' : '/login', { replace: true }); - }, 1500); - } - } catch (err) { - console.error(err); - if (scriptedRef.current) { - setStatus({ success: false }); - setErrors({ submit: err.message }); - setSubmitting(false); - } - } - }} - > - {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( -
    - - - - Password - { - handleChange(e); - changePassword(e.target.value); - }} - endAdornment={ - - - {showPassword ? : } - - - } - placeholder="Enter password" - /> - {touched.password && errors.password && ( - - {errors.password} - - )} - - - - - - - - - {level?.label} - - - - - - - - Confirm Password - - {touched.confirmPassword && errors.confirmPassword && ( - - {errors.confirmPassword} - - )} - - - - {errors.submit && ( - - {errors.submit} - - )} - - - - - - -
    - )} -
    - ); -}; - -export default AuthResetPassword; diff --git a/src/sections/auth/auth-forms/FirebaseSocial.js b/src/sections/auth/auth-forms/FirebaseSocial.js deleted file mode 100644 index 2d92f3b..0000000 --- a/src/sections/auth/auth-forms/FirebaseSocial.js +++ /dev/null @@ -1,82 +0,0 @@ -// material-ui -import { useTheme } from '@mui/material/styles'; -import { useMediaQuery, Button, Stack } from '@mui/material'; - -// project import -import useAuth from 'hooks/useAuth'; - -// assets -import Google from 'assets/images/icons/google.svg'; -import Twitter from 'assets/images/icons/twitter.svg'; -import Facebook from 'assets/images/icons/facebook.svg'; - -// ==============================|| FIREBASE - SOCIAL BUTTON ||============================== // - -const FirebaseSocial = () => { - const theme = useTheme(); - const matchDownSM = useMediaQuery(theme.breakpoints.down('sm')); - - const { firebaseFacebookSignIn, firebaseGoogleSignIn, firebaseTwitterSignIn } = useAuth(); - const googleHandler = async () => { - try { - await firebaseGoogleSignIn(); - } catch (err) { - console.error(err); - } - }; - - const twitterHandler = async () => { - try { - await firebaseTwitterSignIn(); - } catch (err) { - console.error(err); - } - }; - - const facebookHandler = async () => { - try { - await firebaseFacebookSignIn(); - } catch (err) { - console.error(err); - } - }; - - return ( - - - - - - ); -}; - -export default FirebaseSocial; diff --git a/src/sections/widget/data/ApplicationSales.js b/src/sections/widget/data/ApplicationSales.js deleted file mode 100644 index 5c4020e..0000000 --- a/src/sections/widget/data/ApplicationSales.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import { Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; - -// project imports -import MainCard from 'components/MainCard'; - -// table data -function createData(name, designation, product, date, badgeText, badgeType) { - return { name, designation, product, date, badgeText, badgeType }; -} - -const rows = [ - createData('Materially', 'Powerful Admin Theme', '16,300', '$53', '$15,652'), - createData('Photoshop', 'Design Software', '26,421', '$35', '$8,785'), - createData('Guruable', 'Best Admin Template', '8,265', '$98', '$9,652'), - createData('Flatable', 'Admin App', '10,652', '$20', '$7,856') -]; -// =========================|| DATA WIDGET - APPLICATION SALES ||========================= // - -const ApplicationSales = () => ( - - View all - - } - > - - - - - Application - Sales - Avg. Price - - Total - - - - - {rows.map((row, index) => ( - - {/* - - {row.name} - - - {row.designation} - - - {row.product} - {row.date} - - {row.badgeText} - */} - - ))} - -
    -
    -
    -); - -export default ApplicationSales; diff --git a/src/sections/widget/data/TasksCard.js b/src/sections/widget/data/TasksCard.js deleted file mode 100644 index 742bd5c..0000000 --- a/src/sections/widget/data/TasksCard.js +++ /dev/null @@ -1,175 +0,0 @@ -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import { CardContent, Grid, Link, Typography } from '@mui/material'; - -// project imports -import MainCard from 'components/MainCard'; -import Avatar from 'components/@extended/Avatar'; - -// assets -import { TwitterCircleFilled, ClockCircleFilled, BugFilled, MobileFilled, WarningFilled } from '@ant-design/icons'; - -// ==============================|| DATA WIDGET - TASKS CARD ||============================== // - -const TasksCard = () => ( - - View all - - } - > - - *': { - position: 'relative', - zIndex: '5' - }, - '&:after': { - content: '""', - position: 'absolute', - top: 10, - left: 38, - width: 2, - height: '100%', - background: '#ebebeb', - zIndex: '1' - } - }} - > - - - - - - - - - - - - 8:50 - - - - - You’re getting more and more followers, keep it up! - - - - - - - - - - - - - - - - - - Sat, 5 Mar - - - - - Design mobile Application - - - - - - - - - - - - - - - - - - Sun, 17 Feb - - - - - - Jenny - {' '} - assign you a task{' '} - - Mockup Design - - . - - - - - - - - - - - - - - - - - - Sat, 18 Mar - - - - - Design logo - - - - - - - - - - - - - - - - - - Sat, 22 Mar - - - - - Design mobile Application - - - - - - - - - -); - -export default TasksCard; diff --git a/src/themes/astryx.js b/src/themes/astryx.js index c8b2126..f36f480 100644 --- a/src/themes/astryx.js +++ b/src/themes/astryx.js @@ -1,15 +1,39 @@ // Astryx brand theme — extends the stock "neutral" theme with Doormile's -// brand red so migrated surfaces match the existing DT token palette +// brand colour so migrated surfaces match the existing DT token palette // (see themes/dt/tokens.js). Runtime-injected (not `astryx theme build`) // since this repo doesn't do SSR — see `astryx docs theme`. import { defineTheme } from '@astryxdesign/core/theme'; import { neutralTheme } from '@astryxdesign/theme-neutral'; import { DT } from 'themes/dt/tokens'; +// The brand is pure black. Astryx's `color.accent` generator derives a whole +// ramp from the accent's hue + lightness, and black has NO hue and zero +// lightness — feeding it `#000000` made the generator clamp it into a mid-tone +// and invent a hue, which rendered every primary button magenta (#A23760). +// +// So the accent tokens are pinned explicitly via `tokens` instead, which +// `defineTheme` documents as taking precedence over scale-generated values. +// Each entry is a [light, dark] pair. `--color-accent-muted` has to be listed +// too: the generated value inlines the accent hex inside a color-mix() rather +// than referencing var(--color-accent), so it does not follow automatically. +const BRAND_ON_LIGHT = DT.brand; // #000000 +const BRAND_ON_DARK = '#FFFFFF'; + export const doormileTheme = defineTheme({ name: 'doormile', extends: neutralTheme, - color: { accent: DT.brand, neutralStyle: 'cool' }, + color: { neutralStyle: 'cool' }, + tokens: { + '--color-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], + '--color-icon-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], + '--color-text-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], + // Text/icon colour that sits ON a filled accent surface — inverted. + '--color-on-accent': ['#FFFFFF', '#000000'], + '--color-accent-muted': [ + `color-mix(in srgb, ${BRAND_ON_LIGHT} 12%, transparent)`, + `color-mix(in srgb, ${BRAND_ON_DARK} 20%, transparent)` + ] + }, components: { card: { base: { borderRadius: `${DT.radiusCard}px` } }, 'text-input': { base: { borderRadius: `${DT.radiusInner}px` } } diff --git a/src/themes/dt/primitives.js b/src/themes/dt/primitives.js new file mode 100644 index 0000000..a47790b --- /dev/null +++ b/src/themes/dt/primitives.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; + +import { Center } from '@astryxdesign/core/Center'; + +import { DT, soft } from './tokens'; + +// ============================================================================ +// Shared DT primitives — the tiny components that every DT-styled operator +// page used to redeclare verbatim at the top of its own file. +// +// They were byte-identical in all 6 copies, so the duplication bought nothing +// and made the pages drift apart whenever one was tweaked. `deliveries.js` is +// the visual reference for both (see root CLAUDE.md §6). +// ============================================================================ + +// Soft popup surface. Under MUI this was a `PaperComponent` handed to every +// Autocomplete; Astryx's Selector/Typeahead own their own popup chrome, so +// this now only serves the few hand-rolled dropdowns that remain. Kept as a +// style object rather than a component for exactly that reason. +export const softPopupStyle = { + marginTop: 6, + background: DT.surface, + borderRadius: DT.radiusInner + 'px', + boxShadow: DT.shadowPop, + border: `1px solid ${DT.borderSubtle}`, + overflow: 'hidden' +}; + +// Scroll container for a table. Astryx's Table renders a bare with no +// sticky-header or max-height affordance (MUI's TableContainer used to provide that), +// so pages wrap it in this. The brand-red scrollbar can't be expressed inline +// — ::-webkit-scrollbar-* are pseudo-elements — hence the one-time +
    +
    {children}
    +
    + +); + +TableScroll.propTypes = { + maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + children: PropTypes.node +}; + +// Colored icon chip — flips between filled and soft based on `selected`. +// Astryx's own `Avatar` is for people (src / initials / status corner), so a +// tinted `Center` is the right primitive for an icon swatch. The accent is a +// caller-supplied hex, hence the inline style. +export const AccentAvatar = ({ color, selected, size = 24, children }) => ( +
    + {children} +
    +); + +AccentAvatar.propTypes = { + color: PropTypes.string, + selected: PropTypes.bool, + size: PropTypes.number, + children: PropTypes.node +}; diff --git a/src/themes/dt/tokens.js b/src/themes/dt/tokens.js index 3896018..ee0fc7f 100644 --- a/src/themes/dt/tokens.js +++ b/src/themes/dt/tokens.js @@ -23,7 +23,31 @@ export const DT = { divider: '#f1f5f9', surface: '#ffffff', surfaceAlt: '#f8fafc', - brand: '#C01227' + brand: '#000000' +}; + +// --------------------------------------------------------------------------- +// Semantic status palette — lifecycle colours, deliberately distinct from the +// brand (see root CLAUDE.md §6). Pages have historically inlined these hexes; +// new code should reference STATUS so there is one place to change them. +// This also replaces the `theme.palette.error/success/...` lookups that used +// to pull the same colours out of the MUI theme. +// --------------------------------------------------------------------------- +export const STATUS = { + pending: '#f59e0b', // amber + accepted: '#6366f1', // indigo + arrived: '#06b6d4', // cyan + picked: '#8b5cf6', // light purple + active: '#14b8a6', // teal + skipped: '#f97316', // orange + delivered: '#10b981', // emerald + cancelled: '#ef4444', // red + info: '#0ea5e9', // sky + muted: '#94a3b8', // slate + // Aliases for non-lifecycle usage (network banners, validation, …). + success: '#10b981', + warning: '#f59e0b', + error: '#ef4444' }; // Quick alpha helpers (hex + percentage suffix). @@ -54,3 +78,53 @@ export const pillFieldSx = () => ({ }, '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted } }); + +// --------------------------------------------------------------------------- +// Table chrome — lifted verbatim from deliveries.js, the design reference for +// every operator list page (root CLAUDE.md §6). These three used to be +// copy-pasted into each page's //, which +// is how pages drifted apart (grey scrollbars on some, brand-red on others). +// Spread them instead of restating the rules: +// +// +//
    +// ... +// ... +// --------------------------------------------------------------------------- + +// Brand-red scrollbar. Track is the page's alt surface so the gutter reads as +// part of the table rather than a hole in it. +export const tableScrollSx = { + overflow: 'auto', + '&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: edge(DT.brand), + borderRadius: '8px', + cursor: 'pointer', + '&:hover': { backgroundColor: DT.brand } + }, + '&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt } +}; + +// Uppercase muted header row on the alt surface. +export const tableHeadSx = { + '& th': { + backgroundColor: DT.surfaceAlt, + color: DT.textSecondary, + fontSize: { xs: 10, md: 11 }, + fontWeight: 800, + letterSpacing: 0.6, + textTransform: 'uppercase', + whiteSpace: 'nowrap', + borderBottom: `1px solid ${DT.borderSubtle}`, + py: { xs: 1, md: 1.25 }, + px: { xs: 1, md: 2 } + } +}; + +// Body row: hairline divider, tinted hover. No shadow on hover — see the +// design-system "don'ts" in root CLAUDE.md §6. +export const tableRowSx = { + '& td': { borderBottom: `1px solid ${DT.divider}` }, + '&:hover': { backgroundColor: DT.surfaceAlt } +}; diff --git a/src/themes/index.js b/src/themes/index.js index 115d2b6..c04c5ab 100644 --- a/src/themes/index.js +++ b/src/themes/index.js @@ -117,11 +117,11 @@ export default function ThemeCustomization({ children }) { textRendering: 'optimizeLegibility' }, // Thin, unobtrusive scrollbars (Stripe/Linear style) instead of the - // chunky 14px solid-purple bar. Track is transparent; the thumb is a - // soft slate that tints toward brand purple on hover. + // chunky 14px solid bar. Track is transparent; the thumb is a soft + // translucent black that deepens toward the brand black on hover. '*': { scrollbarWidth: 'thin', // Firefox - scrollbarColor: 'rgba(192, 18, 39, 0.28) transparent' + scrollbarColor: 'rgba(0, 0, 0, 0.28) transparent' }, '*::-webkit-scrollbar': { width: '10px', @@ -131,13 +131,13 @@ export default function ThemeCustomization({ children }) { backgroundColor: 'transparent' }, '*::-webkit-scrollbar-thumb': { - backgroundColor: 'rgba(192, 18, 39, 0.26)', + backgroundColor: 'rgba(0, 0, 0, 0.26)', borderRadius: '8px', border: '2px solid transparent', backgroundClip: 'padding-box' }, '*::-webkit-scrollbar-thumb:hover': { - backgroundColor: 'rgba(192, 18, 39, 0.45)' + backgroundColor: 'rgba(0, 0, 0, 0.45)' }, '*::-webkit-scrollbar-corner': { backgroundColor: 'transparent' diff --git a/src/themes/overrides/Autocomplete.js b/src/themes/overrides/Autocomplete.js index e999218..4d44e08 100644 --- a/src/themes/overrides/Autocomplete.js +++ b/src/themes/overrides/Autocomplete.js @@ -23,10 +23,10 @@ export default function Autocomplete() { borderRadius: 8, margin: '1px 0', '&[aria-selected="true"]': { - backgroundColor: 'rgba(192, 18, 39, 0.08)' + backgroundColor: 'rgba(0, 0, 0, 0.08)' }, '&.Mui-focused': { - backgroundColor: 'rgba(192, 18, 39, 0.06)' + backgroundColor: 'rgba(0, 0, 0, 0.06)' } } }, diff --git a/src/themes/theme/default.js b/src/themes/theme/default.js index bb25d38..5d85294 100644 --- a/src/themes/theme/default.js +++ b/src/themes/theme/default.js @@ -40,16 +40,16 @@ const Default = (colors) => { // contrastText // }, primary: { - lighter: '#F5DBDE', - 100: '#E9ADB4', - 200: '#DD7E8A', - light: '#D25463', - 400: '#D25463', - main: '#C01227', - dark: '#910E1D', - 700: '#6D0A16', - darker: '#48070F', - 900: '#2B0409', + lighter: '#F2F2F2', + 100: '#D9D9D9', + 200: '#BFBFBF', + light: '#3A3A3A', + 400: '#3A3A3A', + main: '#000000', + dark: '#262626', + 700: '#1A1A1A', + darker: '#000000', + 900: '#000000', contrastText: '#FFFFFF' }, secondary: {