diff --git a/src/pages/nearle/exceptions/exceptions.js b/src/pages/nearle/exceptions/exceptions.js
index b09b00a..998a38f 100644
--- a/src/pages/nearle/exceptions/exceptions.js
+++ b/src/pages/nearle/exceptions/exceptions.js
@@ -37,23 +37,9 @@ import StatCard from 'components/nearle_components/StatCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { getExceptions, createException, updateExceptionStatus, getHubs } from 'pages/api/doormileApi';
import { enqueueSnackbar } from 'notistack';
+import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens';
-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 EXCEPTION_TYPES = ['Lost', 'Damaged', 'Misrouted', 'Receiver_Refused', 'Missing_Contents', 'Undeliverable'];
const SEVERITIES = ['Low', 'Medium', 'High', 'Critical'];
@@ -181,26 +167,16 @@ const Exceptions = () => {
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder="Search exceptions (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)}` }
- }}
/>
-
+
-
+
#
Consignment
Hub
@@ -228,7 +204,7 @@ const Exceptions = () => {
) : (
rows.map((row, index) => (
-
+
{index + 1}
{row.consignmentid}
{hubMap.get(row.hubid)?.hubname || (row.hubid ? `Hub #${row.hubid}` : '—')}
diff --git a/src/pages/nearle/hubs/hubs.js b/src/pages/nearle/hubs/hubs.js
index fb41168..6f933f9 100644
--- a/src/pages/nearle/hubs/hubs.js
+++ b/src/pages/nearle/hubs/hubs.js
@@ -1,59 +1,42 @@
import React, { useMemo, useState } from 'react';
-import {
- 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 { MdOutlineWarehouse, MdEdit, MdDeleteOutline, MdAdd, MdPlace } from 'react-icons/md';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { enqueueSnackbar } from 'notistack';
+
+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 Loader from 'components/Loader';
+import TableLoader from 'components/nearle_components/TableLoader';
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 { getHubs, createHub, updateHub, deleteHub } 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 HUB_TYPES = ['sorting_center', 'delivery_hub'];
+const BRAND = DT.brand;
+
+const HUB_TYPE_OPTIONS = [
+ { value: 'sorting_center', label: 'Sorting Center' },
+ { value: 'delivery_hub', label: 'Delivery Hub' }
+];
+
+const STATUS_OPTIONS = [
+ { value: 'Active', label: 'Active' },
+ { value: 'Inactive', label: 'Inactive' }
+];
const emptyForm = {
hubid: null,
@@ -72,8 +55,6 @@ const emptyForm = {
};
const Hubs = () => {
- const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const queryClient = useQueryClient();
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
@@ -85,7 +66,9 @@ const Hubs = () => {
const rows = useMemo(() => {
if (!debouncedSearch) return hubs;
const q = debouncedSearch.toLowerCase().trim();
- return hubs.filter((row) => [row.hubname, row.hubtype, row.address, row.pincode].filter(Boolean).some((f) => String(f).toLowerCase().includes(q)));
+ return hubs.filter((row) =>
+ [row.hubname, row.hubtype, row.address, row.pincode].filter(Boolean).some((f) => String(f).toLowerCase().includes(q))
+ );
}, [hubs, debouncedSearch]);
const stats = useMemo(() => {
@@ -167,6 +150,8 @@ const Hubs = () => {
});
};
+ const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value }));
+
return (
<>
{(isLoading || saveMutation.isPending || deleteMutation.isPending) && }
@@ -175,200 +160,143 @@ const Hubs = () => {
title="Hubs"
subtitle="Live · Sorting Centers & Delivery Hubs"
live
- action={
- } onClick={openCreate} sx={{ bgcolor: BRAND, whiteSpace: 'nowrap' }}>
- New Hub
-
- }
+ action={ } onClick={openCreate} />}
/>
-
-
- } color={BRAND} />
-
-
- } color="#0ea5e9" />
-
-
- } color="#10b981" />
-
+
+ } color={BRAND} />
+ } color={STATUS.info} />
+ } color={STATUS.delivered} />
-
-
-
- {hubs.length} total · {rows.length} shown
-
-
-
-
-
-
+
+
+
+
+ {hubs.length} total · {rows.length} shown
+
+
+
+
+
+
-
-
-
-
-
- #
- Hub Name
- Type
- City
- Address
- Pincode
- Status
- Actions
-
-
-
- {isLoading && }
- {rows.length === 0 && !isLoading ? (
+
+
+
+
-
-
-
-
-
-
- No hubs to show
-
-
-
+ #
+ Hub Name
+ Type
+ City
+ Address
+ Pincode
+ Status
+ Actions
- ) : (
- rows.map((row, index) => (
-
- {index + 1}
-
-
- {row.hubname}
-
-
- {row.hubtype === 'sorting_center' ? 'Sorting Center' : 'Delivery Hub'}
- {row.applocationid ?? '—'}
- {row.address || '—'}
- {row.pincode || '—'}
- {row.status || '—'}
-
-
-
- openEdit(row)} sx={{ color: BRAND }}>
-
-
-
-
- handleDelete(row)} sx={{ color: '#ef4444' }}>
-
-
-
-
+
+
+ {isLoading && }
+ {rows.length === 0 && !isLoading ? (
+
+
+ }
+ />
- ))
- )}
-
-
-
-
+ ) : (
+ rows.map((row, index) => (
+
+ {index + 1}
+
+ {row.hubname}
+
+ {row.hubtype === 'sorting_center' ? 'Sorting Center' : 'Delivery Hub'}
+ {row.applocationid ?? '—'}
+ {row.address || '—'}
+ {row.pincode || '—'}
+ {row.status || '—'}
+
+
+ }
+ onClick={() => openEdit(row)}
+ />
+ }
+ onClick={() => handleDelete(row)}
+ />
+
+
+
+ ))
+ )}
+
+
+
+
+
- setDialogOpen(false)} maxWidth="sm" fullWidth>
- {form.hubid ? 'Edit Hub' : 'New Hub'}
-
-
-
-
- Hub Name
- setForm((f) => ({ ...f, hubname: e.target.value }))} />
-
-
-
-
- Type
- setForm((f) => ({ ...f, hubtype: e.target.value }))}>
- {HUB_TYPES.map((t) => (
-
- {t === 'sorting_center' ? 'Sorting Center' : 'Delivery Hub'}
-
- ))}
-
-
-
-
-
- City (applocationid)
- setForm((f) => ({ ...f, applocationid: e.target.value }))}
- helperText="Read the correct id from GET /admin/hubs rather than guessing"
- />
-
-
-
-
- Contact No
- setForm((f) => ({ ...f, contactno: e.target.value }))} />
-
-
-
-
- Address
- setForm((f) => ({ ...f, address: e.target.value }))} />
-
-
-
-
- Pincode
- setForm((f) => ({ ...f, pincode: e.target.value }))} />
-
-
-
-
- Latitude
- setForm((f) => ({ ...f, latitude: e.target.value }))} />
-
-
-
-
- Longitude
- setForm((f) => ({ ...f, longitude: e.target.value }))} />
-
-
-
-
- Status
- setForm((f) => ({ ...f, status: e.target.value }))}>
- Active
- Inactive
-
-
-
+ !next && setDialogOpen(false)} width={640} purpose="form">
+
+
+
+
+
+ set('applocationid')(v ?? '')}
+ description="Read the correct id from GET /admin/hubs rather than guessing"
+ isRequired
+ />
+
-
-
- setDialogOpen(false)}>Cancel
-
- {form.hubid ? 'Save Changes' : 'Create'}
-
-
+
+
+
+
+
+ set('latitude')(v ?? '')}
+ isOptional
+ />
+ set('longitude')(v ?? '')}
+ isOptional
+ />
+
+
+
+
+
+
+
+ setDialogOpen(false)} />
+
+
+
>
);
diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js
index 754baf2..c30383a 100644
--- a/src/pages/nearle/login.js
+++ b/src/pages/nearle/login.js
@@ -40,7 +40,7 @@ const brandPanelStyle = {
height: '100vh',
color: '#fff',
padding: 48,
- background: `linear-gradient(150deg, ${DT.brand} 0%, #D25463 100%)`
+ background: `linear-gradient(150deg, ${DT.brand} 0%, #3A3A3A 100%)`
};
const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 };
diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js
index 10a1c4b..3360f6d 100644
--- a/src/pages/nearle/orders/createorder1.js
+++ b/src/pages/nearle/orders/createorder1.js
@@ -1,2577 +1,2504 @@
-/* eslint-disable no-unused-vars */
-import * as React from 'react';
-import { useEffect, useState, useRef, Fragment } from 'react';
-import {
- FormControl,
- InputAdornment,
- Grid,
- Typography,
- Stack,
- Box,
- Button,
- TextField,
- Autocomplete,
- CardActions,
- Chip,
- Avatar,
- Divider,
- DialogTitle,
- DialogContent,
- Checkbox,
- DialogActions,
- CircularProgress,
- ButtonGroup,
- FormLabel,
- IconButton,
- Drawer,
- InputLabel,
- Select,
- MenuItem,
- Switch,
- CardHeader,
- Card,
- OutlinedInput,
- FormGroup,
- FormControlLabel
-} from '@mui/material';
-import CloseIcon from '@mui/icons-material/Close';
-import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
-import { Empty } from 'antd';
-import { FaPhoneAlt } from 'react-icons/fa';
-import { GiDoorHandle } from 'react-icons/gi';
-import { FaLandmarkDome } from 'react-icons/fa6';
-import ClearIcon from '@mui/icons-material/Clear';
-import { useNavigate } from 'react-router';
-import { MdLocationCity } from 'react-icons/md';
-import { TbMapPinCode } from 'react-icons/tb';
-import { FaLocationDot } from 'react-icons/fa6';
-import {
- getAdminPricing,
- getAdminTenant,
- getAdminTenants,
- getAdminCustomers,
- getTenantLocations,
- createExpressBooking
-} from 'pages/api/doormileApi';
-import { useTheme } from '@mui/material/styles';
-import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
-import Loader from 'components/Loader';
-import * as geolib from 'geolib';
-import MainCard from 'components/MainCard';
-import { FaUser } from 'react-icons/fa6';
-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 Dialog from '@mui/material/Dialog';
-import dayjs from 'dayjs';
-import { enqueueSnackbar } from 'notistack';
-var utc = require('dayjs/plugin/utc');
-dayjs.extend(utc);
-import LocationOnIcon from '@mui/icons-material/LocationOn';
-import parse from 'autosuggest-highlight/parse';
-import { debounce } from '@mui/material/utils';
-import CardContent from 'themes/overrides/CardContent';
-import { PhoneAndroid } from '@mui/icons-material';
-import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
-import MyLocationIcon from '@mui/icons-material/MyLocation';
-import HighlightOffIcon from '@mui/icons-material/HighlightOff';
-import { Paper } from '@mui/material';
-import {
- MdAddShoppingCart,
- MdMyLocation,
- MdLocationOn,
- MdSchedule,
- MdLocalShipping,
- MdAttachMoney,
- MdNotes,
- MdCheckCircle,
- MdReceiptLong,
- MdInventory2,
- MdStraighten,
- MdPersonPin,
- MdPerson,
- MdHistoryToggleOff
-} from 'react-icons/md';
+/* eslint-disable no-unused-vars */
+import * as React from 'react';
+import { useEffect, useState, useRef, Fragment } from 'react';
+import {
+ FormControl,
+ InputAdornment,
+ Grid,
+ Typography,
+ Stack,
+ Box,
+ Button,
+ TextField,
+ Autocomplete,
+ CardActions,
+ Chip,
+ Avatar,
+ Divider,
+ DialogTitle,
+ DialogContent,
+ Checkbox,
+ DialogActions,
+ CircularProgress,
+ ButtonGroup,
+ FormLabel,
+ IconButton,
+ Drawer,
+ InputLabel,
+ Select,
+ MenuItem,
+ Switch,
+ CardHeader,
+ Card,
+ OutlinedInput,
+ FormGroup,
+ FormControlLabel
+} from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
+import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
+import { Empty } from 'antd';
+import { FaPhoneAlt } from 'react-icons/fa';
+import { GiDoorHandle } from 'react-icons/gi';
+import { FaLandmarkDome } from 'react-icons/fa6';
+import ClearIcon from '@mui/icons-material/Clear';
+import { useNavigate } from 'react-router';
+import { MdLocationCity } from 'react-icons/md';
+import { TbMapPinCode } from 'react-icons/tb';
+import { FaLocationDot } from 'react-icons/fa6';
+import {
+ getAdminPricing,
+ getAdminTenant,
+ getAdminTenants,
+ getAdminCustomers,
+ getTenantLocations,
+ createExpressBooking
+} from 'pages/api/doormileApi';
+import { useTheme } from '@mui/material/styles';
+import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
+import Loader from 'components/Loader';
+import * as geolib from 'geolib';
+import MainCard from 'components/MainCard';
+import { FaUser } from 'react-icons/fa6';
+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 Dialog from '@mui/material/Dialog';
+import dayjs from 'dayjs';
+import { enqueueSnackbar } from 'notistack';
+var utc = require('dayjs/plugin/utc');
+dayjs.extend(utc);
+import LocationOnIcon from '@mui/icons-material/LocationOn';
+import parse from 'autosuggest-highlight/parse';
+import { debounce } from '@mui/material/utils';
+import CardContent from 'themes/overrides/CardContent';
+import { PhoneAndroid } from '@mui/icons-material';
+import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
+import MyLocationIcon from '@mui/icons-material/MyLocation';
+import HighlightOffIcon from '@mui/icons-material/HighlightOff';
+import { Paper } from '@mui/material';
+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 BRAND = '#C01227';
-const BRAND_LIGHT = '#D25463';
-
-// 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}
-
-);
-
-// Popup paper for Autocomplete dropdowns — matches the rest of the design.
+// 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) => (
);
-// 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={
- }
- onClick={() => {
- if (!isLocation) {
- opentoast('Select Business Location', 'warning');
- } else {
- setIsCustomerOpen(true);
- setpickordrop(1);
- setPickCust({});
- setInputValue2('');
- setSearchCustList('');
- }
- }}
- sx={{
- borderRadius: 999,
- px: 1.5,
- py: 0.375,
- fontSize: 11.5,
- fontWeight: 700,
- textTransform: 'none',
- borderColor: edge('#0ea5e9'),
- color: '#0ea5e9',
- '&:hover': {
- borderColor: '#0ea5e9',
- bgcolor: tint('#0ea5e9'),
- boxShadow: `0 0 0 3px ${ring('#0ea5e9')}`
- }
- }}
- >
- Saved Contacts
-
- }
- />
-
-
-
-
- {/* ====================================== ||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={
- }
- onClick={() => {
- if (!isLocation) {
- opentoast('Select Business Location', 'warning');
- } else {
- setIsCustomerOpen(true);
- setpickordrop(2);
- setInputValue3('');
- setSearchCustList('');
- }
- }}
- sx={{
- borderRadius: 999,
- px: 1.5,
- py: 0.375,
- fontSize: 11.5,
- fontWeight: 700,
- textTransform: 'none',
- borderColor: edge(BRAND),
- color: BRAND,
- '&:hover': {
- borderColor: BRAND,
- bgcolor: tint(BRAND),
- boxShadow: `0 0 0 3px ${ring(BRAND)}`
- }
- }}
- >
- Saved Contacts
-
- }
- />
-
-
-
-
-
- Drop Details
-
-
- {/* Customer */}
- {/* {
- if (val) {
- setDropswitch(true);
- setClientinfo({});
- } else {
- setDropswitch(false);
- setClientinfo({});
- }
- }}
- size="small"
- />
- Business */}
- {
- if (!isLocation) {
- opentoast('Select Business Location', 'warning');
- } else {
- setIsCustomerOpen(true);
- setpickordrop(2);
-
- setInputValue3('');
- setSearchCustList('');
- }
- }}
- >
- Saved Locations
-
-
-
- {/* 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"
- />
-
-
- )}
-
-
- {/*
-
- 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
-
- )}
- }
- onClick={createsubmitobj2}
- sx={{
- borderRadius: 999,
- px: 3,
- py: 1,
- fontWeight: 800,
- textTransform: 'none',
- fontSize: 13,
- background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
- color: '#fff',
- boxShadow: `0 8px 22px ${ring(BRAND)}`,
- transition: 'all 0.18s',
- '&:hover': {
- background: `linear-gradient(135deg, #4D1C61 0%, ${BRAND} 100%)`,
- transform: 'translateY(-1px)',
- boxShadow: `0 10px 26px ${ring(BRAND)}`
- },
- '&.Mui-disabled': {
- background: DT.divider,
- color: DT.textMuted,
- boxShadow: 'none'
- }
- }}
- >
- {loading ? : 'Create Order'}
-
-
-
-
-
-
-
- {/*
-
-
- 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'));
- }}
- />
-
- >
- );
- }
- })}
-
-
-
-
- {
- closetimemodal();
- }}
- >
- Cancel
-
-
- OK
-
-
- */}
-
- {/* ============================================= || 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'}
-
-
-
- );
- })}
-
- )}
-
-
- setIsCustomerOpen(false)}
- sx={{
- borderRadius: 999,
- px: 2.5,
- borderColor: DT.borderSubtle,
- color: DT.textSecondary,
- fontWeight: 700,
- textTransform: 'none',
- '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
- }}
- >
- Cancel
-
-
-
-
- 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.
-
-
-
- setOpen4(false)}
- sx={{
- borderRadius: 999,
- px: 3,
- bgcolor: '#ef4444',
- fontWeight: 700,
- textTransform: 'none',
- boxShadow: `0 6px 18px ${ring('#ef4444')}`,
- '&:hover': { bgcolor: '#dc2626' }
- }}
- >
- Close
-
-
-
-
- >
- );
-};
-
-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={
+ }
+ onClick={() => {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(1);
+ setPickCust({});
+ setInputValue2('');
+ setSearchCustList('');
+ }
+ }}
+ sx={{
+ borderRadius: 999,
+ px: 1.5,
+ py: 0.375,
+ fontSize: 11.5,
+ fontWeight: 700,
+ textTransform: 'none',
+ borderColor: edge('#0ea5e9'),
+ color: '#0ea5e9',
+ '&:hover': {
+ borderColor: '#0ea5e9',
+ bgcolor: tint('#0ea5e9'),
+ boxShadow: `0 0 0 3px ${ring('#0ea5e9')}`
+ }
+ }}
+ >
+ Saved Contacts
+
+ }
+ />
+
+
+
+
+ {/* ====================================== ||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={
+ }
+ onClick={() => {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(2);
+ setInputValue3('');
+ setSearchCustList('');
+ }
+ }}
+ sx={{
+ borderRadius: 999,
+ px: 1.5,
+ py: 0.375,
+ fontSize: 11.5,
+ fontWeight: 700,
+ textTransform: 'none',
+ borderColor: edge(BRAND),
+ color: BRAND,
+ '&:hover': {
+ borderColor: BRAND,
+ bgcolor: tint(BRAND),
+ boxShadow: `0 0 0 3px ${ring(BRAND)}`
+ }
+ }}
+ >
+ Saved Contacts
+
+ }
+ />
+
+
+
+
+
+ Drop Details
+
+
+ {/* Customer */}
+ {/* {
+ if (val) {
+ setDropswitch(true);
+ setClientinfo({});
+ } else {
+ setDropswitch(false);
+ setClientinfo({});
+ }
+ }}
+ size="small"
+ />
+ Business */}
+ {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(2);
+
+ setInputValue3('');
+ setSearchCustList('');
+ }
+ }}
+ >
+ Saved Locations
+
+
+
+ {/* 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"
+ />
+
+
+ )}
+
+
+ {/*
+
+ 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
+
+ )}
+ }
+ onClick={createsubmitobj2}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ py: 1,
+ fontWeight: 800,
+ textTransform: 'none',
+ fontSize: 13,
+ background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
+ color: '#fff',
+ boxShadow: `0 8px 22px ${ring(BRAND)}`,
+ transition: 'all 0.18s',
+ '&:hover': {
+ background: `linear-gradient(135deg, #4D1C61 0%, ${BRAND} 100%)`,
+ transform: 'translateY(-1px)',
+ boxShadow: `0 10px 26px ${ring(BRAND)}`
+ },
+ '&.Mui-disabled': {
+ background: DT.divider,
+ color: DT.textMuted,
+ boxShadow: 'none'
+ }
+ }}
+ >
+ {loading ? : 'Create Order'}
+
+
+
+
+
+
+
+ {/*
+
+
+ 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'));
+ }}
+ />
+
+ >
+ );
+ }
+ })}
+
+
+
+
+ {
+ closetimemodal();
+ }}
+ >
+ Cancel
+
+
+ OK
+
+
+ */}
+
+ {/* ============================================= || 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'}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ setIsCustomerOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 2.5,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary,
+ fontWeight: 700,
+ textTransform: 'none',
+ '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
+ }}
+ >
+ Cancel
+
+
+
+
+ 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.
+
+
+
+ setOpen4(false)}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ bgcolor: '#ef4444',
+ fontWeight: 700,
+ textTransform: 'none',
+ boxShadow: `0 6px 18px ${ring('#ef4444')}`,
+ '&:hover': { bgcolor: '#dc2626' }
+ }}
+ >
+ Close
+
+
+
+
+ >
+ );
+};
+
+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
-
-
-
-
-
- Special Dispatch Notes
-
- 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 && (
-
-
-
- ₹{Number(totalAmt).toFixed(2)}
-
-
- )}
-
-
- }
- onClick={createorders}
- sx={{ minHeight: '36px !important' }}
- >
- {btnLoading ? (
-
- ) : (
- `Dispatch ${dropCust.length || ''} ${dropCust.length === 1 ? 'Order' : 'Orders'}`.trim()
- )}
-
-
-
-
-
- {/* 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 (
-
- handleHeaderPick(0)}
- startIcon={ }
- sx={{
- height: 24,
- py: 0,
- textTransform: 'none',
- borderRadius: '6px',
- fontSize: 10.5,
- fontWeight: 600,
- borderColor: '#cbd5e1',
- color: '#475569',
- '&:hover': { borderColor: '#1890ff', color: '#1890ff' }
- }}
- >
- Excel / CSV
-
- handleHeaderPick(1)}
- startIcon={ }
- sx={{
- height: 24,
- py: 0,
- textTransform: 'none',
- borderRadius: '6px',
- fontSize: 10.5,
- fontWeight: 600,
- borderColor: '#cbd5e1',
- color: '#475569',
- '&:hover': { borderColor: '#65387a', color: '#65387a' }
- }}
- >
- Selection
-
-
- );
- })()}
-
-
- {fileName && (
-
-
-
- {fileName}
-
-
- )}
-
- {previewMode === 'preview' && users.length >= 1 && (
-
-
-
- Process & Calculate Distances
-
-
- Click continue to import spreadsheet rows and calculate drop charges.
-
-
- users.forEach((customer) => handleCheckboxChange1(customer))}
- sx={{
- textTransform: 'none',
- borderRadius: '8px',
- fontWeight: 700,
- fontSize: 11,
- height: 28,
- px: 1.5,
- bgcolor: '#d97706',
- boxShadow: 'none',
- '&:hover': { bgcolor: '#b45309', boxShadow: 'none' }
- }}
- >
- Continue
-
-
- )}
-
- {/* 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})`}
-
-
-
-
-
-
-
- {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
-
-
- }
- />
-
- );
- })}
-
- )}
-
-
-
- setIsCustomerOpen(false)}
- sx={{ borderRadius: '10px', textTransform: 'none', fontWeight: 600, px: 3 }}
- >
- {dropCust.length === 0 ? 'Close' : 'Continue'}
-
-
-
- >
- );
-};
-
-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
+
+
+
+
+
+ Special Dispatch Notes
+
+ 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 && (
+
+
+
+ ₹{Number(totalAmt).toFixed(2)}
+
+
+ )}
+
+
+ }
+ onClick={createorders}
+ sx={{ minHeight: '36px !important' }}
+ >
+ {btnLoading ? (
+
+ ) : (
+ `Dispatch ${dropCust.length || ''} ${dropCust.length === 1 ? 'Order' : 'Orders'}`.trim()
+ )}
+
+
+
+
+
+ {/* 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 (
+
+ handleHeaderPick(0)}
+ startIcon={ }
+ sx={{
+ height: 24,
+ py: 0,
+ textTransform: 'none',
+ borderRadius: '6px',
+ fontSize: 10.5,
+ fontWeight: 600,
+ borderColor: '#cbd5e1',
+ color: '#475569',
+ '&:hover': { borderColor: '#0ea5e9', color: '#0ea5e9' }
+ }}
+ >
+ Excel / CSV
+
+ handleHeaderPick(1)}
+ startIcon={ }
+ sx={{
+ height: 24,
+ py: 0,
+ textTransform: 'none',
+ borderRadius: '6px',
+ fontSize: 10.5,
+ fontWeight: 600,
+ borderColor: '#cbd5e1',
+ color: '#475569',
+ '&:hover': { borderColor: '#8b5cf6', color: '#8b5cf6' }
+ }}
+ >
+ Selection
+
+
+ );
+ })()}
+
+
+ {fileName && (
+
+
+
+ {fileName}
+
+
+ )}
+
+ {previewMode === 'preview' && users.length >= 1 && (
+
+
+
+ Process & Calculate Distances
+
+
+ Click continue to import spreadsheet rows and calculate drop charges.
+
+
+ users.forEach((customer) => handleCheckboxChange1(customer))}
+ sx={{
+ textTransform: 'none',
+ borderRadius: '8px',
+ fontWeight: 700,
+ fontSize: 11,
+ height: 28,
+ px: 1.5,
+ bgcolor: '#f59e0b',
+ boxShadow: 'none',
+ '&:hover': { bgcolor: '#b45309', boxShadow: 'none' }
+ }}
+ >
+ Continue
+
+
+ )}
+
+ {/* 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})`}
+
+
+
+
+
+
+
+ {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
+
+
+ }
+ />
+
+ );
+ })}
+
+ )}
+
+
+
+ setIsCustomerOpen(false)}
+ sx={{ borderRadius: '10px', textTransform: 'none', fontWeight: 600, px: 3 }}
+ >
+ {dropCust.length === 0 ? 'Close' : 'Continue'}
+
+
+
+ >
+ );
+};
+
+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
-
-
-
- {/* }
- onClick={() => {
- setIsLoading(true);
- createDeliveryMutation.mutate({ deliveries: reassignOrders });
- }}
- >
- Tuning
- */}
- 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) => (
-
- )}
- />
- {/* }
- onClick={() => {
- exportToExcel(finaldeliveryList, 'users', 'UsersData');
- }}
- >
- Export Report
- */}
-
-
-
-
-
- }
- 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"
- />
-
-
-
-
-
-
-
-
-
-
- {/*
-
- */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
- )}
-
-
- );
- })}
-
-
-
- }
- onClick={() => {
- navigate('/doormile/orders');
- }}
- >
- Back
-
-
- Assign Orders
-
-
-
-
- );
-};
-
-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' }
- }}
- />
+
+ }
+ elevation="low"
+ onClick={() => setMapOpen(false)}
+ />
+
{/* 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')}
-
-
- }
- onClick={() => {
- setReportDialog(true);
- setTimeout(() => {
- setRowsPerPage('');
- }, 0);
- }}
- sx={{
- borderRadius: 999,
- px: 1.5,
- py: 0.75,
- fontWeight: 800,
- fontSize: 12,
- textTransform: 'none',
- background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
- '&:hover': {
- background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
- boxShadow: `0 8px 22px ${ring(BRAND)}`
- }
- }}
- >
- Export
-
-
-
-
-
-
- {/* ============================================= || 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}
-
-
-
- ))}
-
-
- setReportDialog(false)}
- sx={{
- borderRadius: 999,
- px: 2,
- textTransform: 'none',
- fontWeight: 700,
- borderColor: DT.borderSubtle,
- color: DT.textSecondary
- }}
- >
- Cancel
-
- {
- 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')}
+
+
+ }
+ onClick={() => {
+ setReportDialog(true);
+ setTimeout(() => {
+ setRowsPerPage('');
+ }, 0);
+ }}
+ sx={{
+ borderRadius: 999,
+ px: 1.5,
+ py: 0.75,
+ fontWeight: 800,
+ fontSize: 12,
+ textTransform: 'none',
+ background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
+ '&:hover': {
+ background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
+ boxShadow: `0 8px 22px ${ring(BRAND)}`
+ }
+ }}
+ >
+ Export
+
+
+
+
+
+
+ {/* ============================================= || 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}
+
+
+
+ ))}
+
+
+ setReportDialog(false)}
+ sx={{
+ borderRadius: 999,
+ px: 2,
+ textTransform: 'none',
+ fontWeight: 700,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary
+ }}
+ >
+ Cancel
+
+ {
+ 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) => (
-
- )}
- />
-
-
-
- setEditingSub(null)}
- sx={{
- borderRadius: '8px',
- textTransform: 'none',
- fontWeight: 600,
- borderColor: DT.borderSubtle,
- color: DT.textSecondary
- }}
- >
- Cancel
-
-
- Save Changes
-
-
-
-
- {
- 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) => }
- />
-
-
- {
- setAssignVehicleRider(null);
- setSelectedVehicle(null);
- }}
- >
- Cancel
-
- assignVehicleMutation.mutate()}>
- Assign
-
-
-
- >
- );
-};
-
-// 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) => (
+
+ )}
+ />
+
+
+
+ setEditingSub(null)}
+ sx={{
+ borderRadius: '8px',
+ textTransform: 'none',
+ fontWeight: 600,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary
+ }}
+ >
+ Cancel
+
+
+ Save Changes
+
+
+
+
+ {
+ 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) => }
+ />
+
+
+ {
+ setAssignVehicleRider(null);
+ setSelectedVehicle(null);
+ }}
+ >
+ Cancel
+
+ assignVehicleMutation.mutate()}>
+ Assign
+
+
+
+ >
+ );
+};
+
+// 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={
- } onClick={openCreate} sx={{ bgcolor: BRAND, whiteSpace: 'nowrap' }}>
- New Vehicle
-
- }
+ action={ } onClick={openCreate} />}
/>
-
-
- } color={BRAND} />
-
-
- } color="#10b981" />
-
-
- } color="#f59e0b" />
-
+
+ } color={BRAND} />
+ } color={STATUS.delivered} />
+ } color={STATUS.pending} />
-
-
-
- {vehicles.length} total · {rows.length} shown
-
-
-
-
-
-
+
+
+
+
+ {vehicles.length} total · {rows.length} shown
+
+
+
+
+
+
-
-
-
-
-
- #
- 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
- setForm((f) => ({ ...f, vehicletype: e.target.value }))}>
- {VEHICLE_TYPES.map((v) => (
-
- {v}
-
- ))}
-
-
-
-
-
- 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. */}
+
-
-
- setDialogOpen(false)}>Cancel
-
- {form.vehicleid ? 'Save Changes' : 'Create'}
-
-
+
+
+ set('maxweight')(v ?? '')}
+ isOptional
+ />
+ set('maxvolume')(v ?? '')}
+ isOptional
+ />
+ set('batterypercentage')(v ?? '')}
+ isOptional
+ />
+
+
+
+ setDialogOpen(false)} />
+
+
+
>
);
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}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-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 }) => (
-
- )}
-
- >
- );
-};
-
-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 }) => (
-
- )}
-
- >
- );
-};
-
-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 }) => (
-
- )}
-
- >
- );
-};
-
-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 }) => (
-
- )}
-
- );
-};
-
-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 (
-
- }
- onClick={googleHandler}
- >
- {!matchDownSM && 'Google'}
-
- }
- onClick={twitterHandler}
- >
- {!matchDownSM && 'Twitter'}
-
- }
- onClick={facebookHandler}
- >
- {!matchDownSM && 'Facebook'}
-
-
- );
-};
-
-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
+
+ >
+);
+
+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: {