import logger from '../../../utils/logger'; import { enqueueSnackbar } from 'notistack'; import { DeleteFilled, EditOutlined } from '@ant-design/icons'; import { useState, useEffect, Fragment, useRef, useMemo } from 'react'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); import axios from 'axios'; import { useTheme } from '@mui/material/styles'; import { MdOutlineDateRange, MdAccessTime, MdAllInclusive, MdLightMode, MdWbSunny, MdNightsStay, MdCheck, MdTrendingUp, MdTrendingDown, MdTrendingFlat, MdStorefront, MdLocationOn, MdDirectionsBike, MdLocalShipping, MdNotificationsActive, MdPersonPin, MdHistoryToggleOff, MdCheckCircle, MdCancel, MdInventory2, MdHourglassEmpty, MdRoute, MdSkipNext, MdTune, MdMyLocation } from 'react-icons/md'; import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; import { Avatar, Box, Button, Chip, Grid, IconButton, Stack, Typography, Table, TableCell, TableBody, TableHead, Collapse, Dialog, TableRow, DialogContent, DialogTitle, Tooltip, DialogActions, Checkbox, Autocomplete, TextField, TableContainer, Skeleton, Backdrop, MenuItem, Menu, Paper } from '@mui/material'; import { PopupTransition } from 'components/@extended/Transitions'; import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns'; import { DateRangePicker } from 'mui-daterange-picker'; import * as React from 'react'; import Loader from 'components/Loader'; import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material'; import { cancelDeliveryAPI, changeRiderAPI, fetchCountAPI, fetchDeliveries, fetchPercentageAPI, fetchRidersList, notifyRider, updateDeliveryAPI, getorderdetails, gettenantlocations, getTenants } from 'pages/api/api'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import { OpenToast } from 'components/third-party/OpenToast'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; // ============================================================================ // Design tokens — extracted from the polished "Batch" dropdown so every // surface on this page (filters, KPIs, tabs, status badges, dialogs) shares // the same visual language. All helpers take a color and emit MUI sx values. // ============================================================================ const DT = { radiusPill: 999, radiusCard: 16, radiusInner: 12, shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', textPrimary: '#0f172a', textSecondary: '#64748b', textMuted: '#94a3b8', borderSubtle: '#e2e8f0', divider: '#f1f5f9', surface: '#ffffff', surfaceAlt: '#f8fafc' }; // Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown // pattern (`${color}08`, `${color}18`, `${color}55`, `${color}26`). const a = (c, suffix) => `${c}${suffix}`; const tint = (c) => a(c, '08'); // very subtle surface tint const soft = (c) => a(c, '18'); // soft chip / avatar bg const ring = (c) => a(c, '26'); // focus ring color const edge = (c) => a(c, '55'); // resting border // Pill input sx — used by every filter Autocomplete/TextField on the page. // Accepts the accent color and returns sx for the outer TextField. Width is // driven by parent flex/grid so this helper stays width-agnostic. const pillFieldSx = (color) => ({ cursor: 'pointer', '& .MuiOutlinedInput-root': { borderRadius: DT.radiusPill + 'px', bgcolor: tint(color), fontWeight: 600, color: DT.textPrimary, paddingRight: '8px', cursor: 'pointer', transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s', '& fieldset': { borderColor: edge(color), borderWidth: 1.5 }, '&:hover fieldset': { borderColor: color }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, '&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 } }, '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color } }); // Status palette — drives tab pills, row status badges, dialogs. const STATUS_META = { pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; // Ordered status list driving the tabs row (left → right). Each entry binds // the visual meta above to the `currentStatus` key the queries use AND to the // `batchCounts` key (so the chip count for the tab is one lookup). const STATUS_TABS = [ { status: 'pending', countKey: 'uncoveredLength' }, { status: 'accepted', countKey: 'assignedLength' }, { status: 'arrived', countKey: 'arrivedLength' }, { status: 'picked', countKey: 'pickedLength' }, { status: 'active', countKey: 'activeLength' }, { status: 'skipped', countKey: 'skippedLength' }, { status: 'delivered', countKey: 'coveredLength' }, { status: 'cancelled', countKey: 'cancelLength' } ]; // KPI palette + icons — mirrors the four cards across the top of the page. const KPI_META = [ { key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdLocalShipping }, { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty }, { key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdCheckCircle }, { key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdCancel } ]; // Batches mirror the dispatch page's slot definitions so an operator who // segments the day there sees the same buckets here. Hours are 24h, half-open // [startHour, endHour) — a delivery at exactly endHour falls into the *next* // batch (or none, if the gap isn't covered). const BATCH_OPTIONS = [ { id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' }, { id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 }, { id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12 }, { id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 } ]; // Per-batch icon components — kept separate from BATCH_OPTIONS so the option // objects stay serializable / printable. Looked up at render time by iconKey. const BATCH_ICONS = { all: MdAllInclusive, morning: MdLightMode, afternoon: MdWbSunny, evening: MdNightsStay }; // Auto-pick the batch matching the operator's LOCAL wall-clock hour so the // page lands them on the slot they're most likely curious about. Falls back // to 'all' when the current hour lies in a configured gap (8–9 AM, 12 PM–4 // PM, after 7 PM). Local time, not UTC, to match the dispatch page's // bucketing — both pages must agree on which batch a given row belongs to. const detectInitialBatchId = () => { const now = dayjs(); const h = now.hour() + now.minute() / 60; for (const b of BATCH_OPTIONS) { if (b.id === 'all') continue; if (h >= b.startHour && h < b.endHour) return b.id; } return 'all'; }; // Bucket by `assigntime` only — matches the spec ("bucketed by assigntime") // and the dispatch page's default time-field selection (`Dispatch.js:763` // initialises `selectedTimeField` to `'assigned'`, whose `keys` is just // `['assigntime']`). The previous priority cascade started with // `deliverytime`, which mis-bucketed delivered orders into the wave they // were *completed* in rather than the wave they were *assigned to* — so an // order assigned at 07:00 (Morning) but delivered at 11:00 fell into the // 8–9 AM / 12 PM–4 PM gap and silently disappeared from every batch on this // page while still showing up on dispatch's Morning Batch. Rows without an // `assigntime` (unassigned pending orders) return null and intentionally // don't bucket — matches dispatch. const BATCH_TIME_KEYS = ['assigntime']; // Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null // when the row has no usable timestamp / falls in a gap between batches. const getRowBatchId = (row) => { let t = null; for (const k of BATCH_TIME_KEYS) { if (row?.[k]) { t = row[k]; break; } } if (!t) return null; const str = String(t).trim(); // Bare YYYY-MM-DD with no time component would always parse to midnight and // get mis-bucketed into Morning — skip those. if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null; // Parse in LOCAL time to match the dispatch page's bucketing exactly // (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment, // so a row that lands in Evening Batch on the dispatch page must also land // in Evening Batch here — otherwise an operator running in IST/PST/etc. // sees different totals on the two pages for the same underlying rows. const d = dayjs(t); if (!d.isValid()) return null; const h = d.hour() + d.minute() / 60; for (const b of BATCH_OPTIONS) { if (b.id === 'all') continue; if (h >= b.startHour && h < b.endHour) return b.id; } return null; }; // Shared Paper used by every Autocomplete popup on the page. Matches the // batch dropdown's soft elevated look. const SoftPaper = (props) => ( ); // Renders a colored avatar with an icon — used in pill input adornments and // in dropdown rows. Tracks `selected` so it can flip between filled and // soft variants identically to the batch dropdown. const AccentAvatar = ({ color, selected, size = 24, children }) => ( {children} ); // ================================================= || deliveries (initial point)|| ================================================= const Deliveries = () => { const userid = localStorage.getItem('userid'); const theme = useTheme(); const loadMoreRef = useRef(); const containerRef = useRef(); const [deliverylist, setDeliverylist] = useState([]); const [dialogopen, setDialogopen] = useState(false); const [locaName, setLocoName] = useState('All'); const [appId, setAppId] = useState(0); const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); const [tabstatus, setTabstatus] = useState('Pending'); const [tabvalue, setTabvalue] = useState(0); const [open, setOpen] = useState(false); const [datestatus, setDatestatus] = useState('Today'); const [kms, setKms] = useState(''); const [cumulativekms, setCumulativeKms] = useState(); const [deliveryamount, setDeliveryamount] = useState(); const [notes, setNotes] = useState(''); const [currentorder, setCurrentorder] = useState({}); const [deliverylat, setDeliverylat] = useState(''); const [deliverylong, setDeliverylong] = useState(''); const [currentStatus, setCurrentStatus] = useState('pending'); const [updateStatus, setUpdateStatus] = useState('delivered'); const locationRef = useRef(null); const tenantRef = useRef(null); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(50); const [totalCount, setTotalCount] = React.useState(); const [productCollapse, setProductCollapse] = useState(null); const [orderHeaderid, setOrderHeaderId] = useState(null); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); const [selectedRow, setSelectedRow] = useState(null); const [loading1, setLoading1] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const [open2, setOpen2] = useState(''); const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false); const [changeDialogOpen, setChangeDialogOpen] = useState(false); const [cancelFeed, setCancelFeed] = useState(''); const [selectedRider, setSelectedRider] = useState(null); const [tenantid, setTenantid] = useState(0); const [locationid, setLocationid] = useState(0); const [tenantValue, setTenantValue] = useState(null); const [locationValue, setLocationValue] = useState(null); const [riderid, setRiderid] = useState(0); // Selected batch id — drives the client-side row filter. Defaults to the // batch matching the current UTC hour (the operator is most likely curious // about "now"); never `null` since there's no longer an "All" option. const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId); const roleid = localStorage.getItem('roleid'); useEffect(() => { setTenantid(0); setTenantValue(null); setLocationid(0); setLocationValue(null); }, [appId]); // to clear the location autocomplete useEffect(() => { setLocationid(0); setLocationValue(null); }, [tenantid]); const menuOpen = Boolean(menuAnchorEl); const handleMenuOpen = (event, row) => { setSelectedRow(row); console.log('selectedRow', row); setMenuAnchorEl(event.currentTarget); }; const handleMenuClose = () => { setMenuAnchorEl(null); }; // =========================================== || cancelDelivery || =========================================== const { mutate: cancelDelivery } = useMutation({ mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed), onSuccess: () => { opentoast('Delivery Cancelled Successfully', 'success'); setCancelDeliveryOpen(false); fetchCountRefetch(); // Refresh count data fetchDeliveriesRefetch(); // Refresh deliveries countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips }, onError: (error) => { opentoast(error.message, 'error'); } }); // ==============================|| cancelridernotification ||============================== // const cancelridernotification = async () => { console.log('cancelridernotification', selectedRow); try { const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, { token: selectedRow.userfcmtoken, notification: { title: 'NearleXpress', body: `${selectedRow.orderid} have been Cancelled`, sound: 'ring', image: '' }, data: { type: 'cancel' } }); return response.data; } catch (err) { opentoast(err.message, 'error', 2000); console.log(err); } }; // ==============================|| getTenants ||============================== // const { data: tenantlist, isLoading: fetchtenantsIsLoading, isError: fetchtenantsIsError, error: fetchtenantsError } = useQuery({ queryKey: ['tenantlist', appId], queryFn: () => getTenants(appId), // Ensure appId is passed enabled: appId !== 0 // Ensures query runs only when appId is valid }); // ==============================|| gettenantlocations ||============================== // const { data: locationlist, isLoading: fetchlocationsIsLoading, isError: fetchlocationsIsError, error: fetchlocationsError } = useQuery({ queryKey: ['gettenantlocations', tenantid], queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed enabled: tenantid !== 0 // Ensures query runs only when appId is valid }); // =========================================== || notifyrider || =========================================== const notifyRiderMutation = useMutation({ mutationFn: notifyRider, // Using the separate function onSuccess: () => { enqueueSnackbar('Notification sent Successfully', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); }, onError: (error) => { enqueueSnackbar(error.message, { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }); // =========================================== || changerider || =========================================== const changeRiderMutation = useMutation({ mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow), onSuccess: (res, { selectedRider, selectedRow }) => { setLoading1(false); setChangeDialogOpen(false); if (res.data.message === 'Success') { logger.info(`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`); opentoast('Rider Changed Successfully', 'success'); } fetchCountRefetch(); // Refresh count data fetchDeliveriesRefetch(); // Refresh deliveries countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips notifyRiderMutation.mutate(selectedRider.userfcmtoken); }, onError: (err, { selectedRider, selectedRow }) => { logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err); opentoast(err.message, 'error'); setLoading1(false); } }); /* ============================================= || opentoast || ============================================= */ const opentoast = (message, variant) => { enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); }; // ==============================|| getorderdetails ||============================== // const { data: orderdetails } = useQuery({ queryKey: ['orderdetails', orderHeaderid], queryFn: () => getorderdetails(orderHeaderid), enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined }); const dialogclose = () => { setDialogopen(false); }; const handleChangetab = (e, i) => { setPage(0); setTabvalue(i); setRowsPerPage(50); if (i === 0) { setTabstatus('Pending'); setCurrentStatus('pending'); setTotalCount(countData?.uncoveredLength); } if (i === 1) { setTabstatus('Assigned'); setCurrentStatus('accepted'); setTotalCount(countData?.assignedLength); } if (i === 2) { setTabstatus('Arrived'); setCurrentStatus('arrived'); setTotalCount(countData?.arrivedLength); } if (i === 3) { setTabstatus('Picked'); setCurrentStatus('picked'); setTotalCount(countData?.pickedLength); } if (i === 4) { setTabstatus('Active'); setCurrentStatus('active'); setTotalCount(countData?.activeLength); } if (i === 5) { setTabstatus('Skipped'); setCurrentStatus('skipped'); setTotalCount(countData?.skippedLength); } if (i === 6) { setTabstatus('Delivered'); setCurrentStatus('delivered'); setTotalCount(countData?.coveredLength); } if (i === 7) { setTabstatus('Cancelled'); setCurrentStatus('cancelled'); setTotalCount(countData?.cancelLength); } console.log(i); setSearchword(''); }; const okclicked = () => { setOpen(false); }; /* ============================================= || fetchDeliveries | ============================================= */ const { data: deliveriesData, isLoading: fetchDeliveriesIsLoading, isError: fetchDeliveriesIsError, error: fetchDeliveriesError, fetchNextPage, hasNextPage, isFetchingNextPage, refetch: fetchDeliveriesRefetch } = useInfiniteQuery({ queryKey: [ 'fetchdeliveries', appId, userid, currentStatus, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid ], queryFn: fetchDeliveries, getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined }); const rows = deliveriesData?.pages.flatMap((page) => page.rows) || []; // (filteredRows is defined below, after countSourceRows / countSourceLoading // are in scope — they're the single source of truth for both the table and // the per-status chip counts.) // Parallel "all-statuses" query used ONLY to derive batch-aware tab counts. // The primary `fetchdeliveries` query above is keyed by `currentStatus`, so // its rows are scoped to the active tab — they can't tell us how many // delivered/cancelled/etc. exist in the same batch. We fetch the full day // once (status='all'), keep it cached, and re-derive the counts per status // whenever the operator switches batches. Search keyword is intentionally // dropped from this key so the chip counts reflect the batch totals rather // than the search-filtered subset. const { data: countSourceData, fetchNextPage: countFetchNext, hasNextPage: countHasNext, isFetchingNextPage: countIsFetchingNext, isLoading: countSourceIsLoading, refetch: countSourceRefetch } = useInfiniteQuery({ queryKey: [ 'fetchdeliveries-batchcounts', appId, userid, 'all', startdate, enddate, 200, '', tenantid, locationid, riderid ], queryFn: fetchDeliveries, getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined }); const countSourceRows = useMemo( () => (countSourceData?.pages || []).flatMap((p) => p.rows || []), [countSourceData] ); useEffect(() => { if (countHasNext && !countIsFetchingNext) countFetchNext(); }, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]); // Loading flag for the table empty state — true while we're still streaming // pages, so we don't show "No Orders" prematurely before the full // day's rows have arrived. Goes false once auto-pagination drains. const countSourceLoading = countHasNext || countIsFetchingNext; // The table now reads from the same source as the chip counts. That way the // "74" in the Delivered chip and the rows shown in the table can never // disagree (they used to, because the table consumed a separate per-status // paginated query while the chips read from the full all-statuses dataset). // Three filter steps: // 1. Batch — selected via the dropdown. 'all' bypasses this filter. // 2. Tab status — currentStatus comes from handleChangetab. // 3. Search — client-side substring match across customer/address/order // fields. Server-side search isn't needed since every row is loaded. const filteredRows = useMemo(() => { const wantStatus = String(currentStatus || '').toLowerCase(); const q = String(debouncedSearch || '').trim().toLowerCase(); return countSourceRows.filter((r) => { if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false; const s = String(r.orderstatus || '').toLowerCase(); if (wantStatus && s !== wantStatus) return false; if (q) { const hay = [ r.deliverycustomer, r.deliveryaddress, r.deliverysuburb, r.pickupcustomer, r.pickupaddress, r.pickupsuburb, r.orderid, r.tenantname, r.ridername, r.username ] .map((v) => String(v || '').toLowerCase()) .join(' '); if (!hay.includes(q)) return false; } return true; }); }, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]); // Counts per status, scoped to the selected batch. Keys mirror the legacy // *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical // (countData?.uncoveredLength → batchCounts.uncoveredLength). const batchCounts = useMemo(() => { const c = { uncoveredLength: 0, assignedLength: 0, arrivedLength: 0, pickedLength: 0, activeLength: 0, skippedLength: 0, coveredLength: 0, cancelLength: 0 }; countSourceRows.forEach((r) => { if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return; const s = String(r.orderstatus || '').toLowerCase(); switch (s) { case 'pending': c.uncoveredLength += 1; break; case 'accepted': case 'assigned': c.assignedLength += 1; break; case 'arrived': c.arrivedLength += 1; break; case 'picked': c.pickedLength += 1; break; case 'active': c.activeLength += 1; break; case 'skipped': c.skippedLength += 1; break; case 'delivered': c.coveredLength += 1; break; case 'cancelled': case 'canceled': c.cancelLength += 1; break; default: break; } }); return c; }, [countSourceRows, selectedBatch]); // Per-batch total (any status) for the dropdown badges. Computed once for // every batch + 'all' so each option in the menu can show its own count // without re-walking the list. Mirrors dispatch's batchCounts shape // (`Dispatch.js:1198–1206`) — totals here should match the per-batch numbers // visible on the dispatch page for the same date. const batchTotals = useMemo(() => { const totals = { all: countSourceRows.length }; BATCH_OPTIONS.forEach((b) => { if (b.id === 'all') return; totals[b.id] = 0; }); countSourceRows.forEach((r) => { const id = getRowBatchId(r); if (id) totals[id] = (totals[id] || 0) + 1; }); return totals; }, [countSourceRows]); // IntersectionObserver now watches the count-source query — that's the // single source of truth for both the table and the chip counts. When the // sentinel scrolls into view it nudges the next page of all-statuses // deliveries to keep the dataset complete. useEffect(() => { if (!countHasNext) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { countFetchNext(); } }, { root: document.querySelector('.MuiTableContainer-root'), rootMargin: '0px', threshold: 1.0 } ); if (loadMoreRef.current) observer.observe(loadMoreRef.current); return () => { if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); }; }, [countHasNext, countFetchNext]); const handleScroll = (event) => { const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; if (scrollTop + clientHeight >= scrollHeight - 50) { if (countHasNext && !countIsFetchingNext) { countFetchNext(); } } }; /* ============================================= || fetchPercentageAPI | ============================================= */ const { data: percentageData, isLoading: fetchPercentageIsLoading, isError: fetchPercentageIsError, error: fetchPercentageError } = useQuery({ queryKey: ['fetchpercentageaPI', appId], queryFn: () => fetchPercentageAPI(appId) }); useEffect(() => { if (percentageData) { console.log('percentageData', percentageData); } }, [percentageData]); /* ============================================= || fetchcount | ============================================= */ const { data: countData = {}, // Default to empty object isLoading: fetchCountIsLoading, isError: fetchCountIsError, error: fetchCountError, refetch: fetchCountRefetch } = useQuery({ queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus], queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) }); useEffect(() => { console.log('countData', countData); if (tabvalue === 0 && countData) { setTotalCount(countData.uncoveredLength); } }, [countData]); // ==============================|| fetchRidersList ||============================== // const { data: ridersList = [], isLoading: riderListIsLoading, isError: ridersListIsError, error: ridersListError } = useQuery({ queryKey: ['ridersList', appId], // Unique key for caching & re-fetching queryFn: fetchRidersList, enabled: Boolean(appId), onError: (err) => { OpenToast(err.message, 'error', 2000); } }); /* ============================================= || updatedelivery | ============================================= */ const updateDeliveryMutation = useMutation({ mutationFn: (orderData) => updateDeliveryAPI(orderData), onSuccess: (res) => { console.log(res); if (res.data.status) { opentoast('Updated Successfully', 'success'); setDeliveryamount(''); setNotes(''); setDialogopen(false); fetchDeliveriesRefetch(); fetchCountRefetch(); countSourceRefetch(); } }, onError: (err) => { console.log(err); opentoast(err.message, 'success'); } }); const errorMessage = fetchDeliveriesIsError ? `Error fetching percentages: ${fetchDeliveriesError?.message}` : fetchPercentageIsError ? `Error fetching percentages: ${fetchPercentageError?.message}` : fetchCountIsError ? `Error fetching percentages: ${fetchCountError?.message}` : ridersListIsError ? `Error fetching percentages: ${ridersListError?.message}` : fetchtenantsIsError ? `Error tenant list: ${fetchtenantsError?.message}` : fetchlocationsIsError ? `Error location list: ${fetchlocationsError?.message}` : null; if (errorMessage) { console.log('errorMessage', errorMessage); OpenToast(errorMessage, 'error', 2000); return null; // or return <> if inside a component } return ( <> {(fetchCountIsLoading || fetchPercentageIsLoading || countSourceIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading) && ( <> {/* */} )} { theme.zIndex.drawer + 1 }} open={ fetchCountIsLoading || fetchPercentageIsLoading || fetchDeliveriesIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading } > {/* */} } {/* ============================================= || Header | ============================================= */} Deliveries Live · {locaName || 'All Zones'} } placeholder="Select Zone" paperComponent={SoftPaper} sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} /> {/* ============================================= || KPI Cards | ============================================= */} {[ { ...KPI_META[0], value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage1 }, { ...KPI_META[1], value: percentageData?.assignedOrders, percentage: percentageData?.percentage2 }, { ...KPI_META[2], value: percentageData?.pickedOrders, percentage: percentageData?.percentage3 }, { ...KPI_META[3], value: percentageData?.coveredOrders, percentage: percentageData?.percentage4 } ].map((item) => { const Icon = item.icon; const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage); const hasPct = !Number.isNaN(pct) && item.percentage !== undefined && item.percentage !== null; const Trend = !hasPct ? MdTrendingFlat : pct > 0 ? MdTrendingUp : pct < 0 ? MdTrendingDown : MdTrendingFlat; const trendColor = !hasPct || pct === 0 ? DT.textMuted : pct > 0 ? '#10b981' : '#ef4444'; return ( {item.label} {fetchPercentageIsLoading ? ( ) : ( {item.value ?? 0} )} {hasPct && ( {Math.abs(pct)}% vs. yesterday )} ); })} {/* ============================================= || Filter Bar | ============================================= */} Filters {/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and "Orders-All" pills. Filters the loaded rows by the slot the operator picks; defaults to 'all'. Mirrors the dispatch page's batch model. UI mirrors the polished Absent Riders dropdown from the orders page — custom Paper, per-batch color/icon, selected indicator. */} {(() => { const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0]; const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime; return ( opts} options={BATCH_OPTIONS} value={activeBatch} onChange={(e, val) => val && setSelectedBatch(val.id)} getOptionLabel={(option) => option.label} isOptionEqualToValue={(option, value) => option.id === value.id} PaperComponent={SoftPaper} ListboxProps={{ sx: { py: 0, maxHeight: 360 } }} renderOption={(props, option, { selected }) => { const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime; const total = batchTotals[option.id] ?? 0; return (
  • {option.label} {option.range} {/* Per-batch total — same number you see next to each batch on the dispatch page. Lets the operator eyeball-compare without summing the per-status chip counts on the tabs. */} 0 ? `${option.color}18` : '#f1f5f9', color: total > 0 ? option.color : '#94a3b8', border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`, '& .MuiChip-label': { px: 0.75 } }} /> {selected && ( )}
  • ); }} renderInput={(params) => (
    ) }} sx={{ minWidth: { xs: 180, sm: 220, md: 240 }, cursor: 'pointer', '& .MuiOutlinedInput-root': { borderRadius: '999px', bgcolor: `${activeBatch.color}08`, fontWeight: 700, color: activeBatch.color, paddingRight: '8px', cursor: 'pointer', transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s', '& fieldset': { borderColor: `${activeBatch.color}55`, borderWidth: 1.5 }, '&:hover fieldset': { borderColor: activeBatch.color }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${activeBatch.color}26` }, '&.Mui-focused fieldset': { borderColor: activeBatch.color, borderWidth: 2 } }, '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: activeBatch.color } }} /> )} /> ); })()} {/* Date range pill — opens the date picker. Stays visible only when a date range is set. Uses the same pill aesthetic as Batch. */} {startdate && enddate && ( setOpen(true)} sx={{ pl: 0.5, pr: 1.25, py: 0.5, borderRadius: 999, cursor: 'pointer', bgcolor: tint('#f59e0b'), border: `1.5px solid ${edge('#f59e0b')}`, transition: 'border-color 0.15s, box-shadow 0.15s', '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } }} > {dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')} )}
    {/* Tenant */} { if (!appId) { event.preventDefault(); OpenToast('Please select your zone first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); } }} onChange={(e, val, reason) => { if (reason === 'clear') { setTenantid(0); setTenantValue(null); setLocationid(0); setLocationValue(null); } else { setTenantid(val?.tenantid || 0); setTenantValue(val); setLocationid(0); setLocationValue(null); } }} renderInput={(params) => ( ) }} sx={pillFieldSx('#0ea5e9')} /> )} /> {/* Location */} `${option.locationname} (${option.suburb})` || ''} value={locationValue} PaperComponent={SoftPaper} sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} onOpen={(event) => { if (!appId && !tenantid) { event.preventDefault(); OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); } else if (!tenantid) { event.preventDefault(); OpenToast('Please select your Tenant first!', 'warning', 3000); setTimeout(() => tenantRef.current?.focus(), 0); } }} onChange={(e, val, reason) => { if (reason === 'clear') { setLocationid(0); setLocationValue(null); } else { setLocationid(val.locationid || 0); setLocationValue(val); } }} renderInput={(params) => ( ) }} sx={pillFieldSx('#14b8a6')} /> )} /> {/* Rider */} `${option.firstname} ${option.lastname} (${option.contactno})`} sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} onChange={(e, value, reason) => { if (reason === 'clear') setRiderid(0); else setRiderid(value.userid); }} onOpen={(event) => { if (!appId) { event.preventDefault(); OpenToast('Please select your zone first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); } }} renderInput={(params) => ( ) }} sx={pillFieldSx('#8b5cf6')} /> )} />
    {/* ============================================= || Status Tabs + Search || ============================================= */} {STATUS_TABS.map((t, idx) => { const meta = STATUS_META[t.status]; const Icon = meta.icon; const active = tabvalue === idx; const count = batchCounts[t.countKey] ?? 0; return ( handleChangetab(e, idx)} sx={{ display: 'inline-flex', alignItems: 'center', gap: { xs: 0.625, md: 0.875 }, pl: 0.5, pr: { xs: 1, md: 1.25 }, py: 0.5, flexShrink: 0, cursor: 'pointer', borderRadius: 999, border: `1.5px solid ${active ? meta.color : edge(meta.color)}`, bgcolor: active ? meta.color : tint(meta.color), color: active ? '#fff' : meta.color, fontWeight: 700, boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : 'none', transition: 'all 0.18s', '&:hover': { borderColor: meta.color, boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : `0 0 0 3px ${ring(meta.color)}` } }} > {meta.label} {count} ); })} {/* ============================================= || Search | ============================================= */} {/* ============================================= || Table || ============================================= */} {(() => { const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered'; const showSelect = tabstatus == 'Created'; const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0); return ( {showSelect && ( 0 && deliverylist.length != filteredRows.length} onChange={(e) => { if (e.target.checked) { setDeliverylist([...filteredRows]); } else { setDeliverylist([]); } }} checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} /> )} # Status Tenant Order / Location Pickup Drop Rider ETA Transit Kms Amount Notes Step Qty COD {showAction && Action} {(loading1 || countSourceIsLoading) && } {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( No deliveries to show {selectedBatch === 'all' ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`} )} {filteredRows.length == 0 && countSourceLoading && ( Loading deliveries… )} {filteredRows.map((row, index) => { const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { label: row.orderstatus || '—', color: '#94a3b8', icon: MdHistoryToggleOff }; const RowStatusIcon = rowStatusMeta.icon; const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); return ( {tabstatus == 'Created' && ( { if (e.target.checked) { let arr = deliverylist; arr.push({ ...row, sno: deliverylist.length + 1 }); setDeliverylist([...arr]); } else { let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); if (res) { let arr = deliverylist; let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); arr.splice(res.sno - 1, 1); arr.map((val, i) => { val.sno = i + 1; }); setDeliverylist([...arr]); } } console.log('list', deliverylist); }} checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} /> )} {String(page * rowsPerPage + index + 1).padStart(2, '0')} {/* Status badge */} {rowStatusMeta.label} {/* Tenants */} {row.tenantname} {row.tenantsuburb} {row.applocation} {/* order details */} {`${row.locationname}-(${row.locationsuburb})`} {row.orderid} {dayjs(row.orderdate).utc().format('DD/MM/YYYY')} {dayjs(row.orderdate).utc().format('hh:mm A')} - {row.deliveryid} {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')} {dayjs(row.deliverydate).utc().format('hh:mm A')} {/* pickup */} {row.pickupcustomer} {row.pickupcontactno} {row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')} {/* drop */} {row.deliverycustomer} {row.deliverycontactno} {row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')} {/* rider */} {row.ridername ? ( {row.ridername} ID #{row.userid} · {row.ridercontact || '—'} ) : ( Unassigned )} {/* Estimated Delivery Time */} {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} {/* Transit Minutes */} {row.transitminutes || 0}m {/* kms */} {row.kms || 0} km {row.cumulativekms || 0} km {/* amount */} ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} {/* notes */} {row.notes ? ( {row.notes} ) : ( )} {/* step */} {row.step ? ( {row.step} ) : ( )} {/* qty */} {row.Quantity || '—'} {/* COD */} {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} {/* Action */} {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( {row.deliverytype == 'C' && ( { if (productCollapse?.orderid === row.orderid) { setProductCollapse(null); setOrderHeaderId(null); } else { setProductCollapse(row); setOrderHeaderId(row.orderheaderid); } }} sx={{ borderRadius: 999, bgcolor: tint('#06b6d4'), color: '#06b6d4', border: `1px solid ${edge('#06b6d4')}`, '&:hover': { bgcolor: soft('#06b6d4') } }} > {productCollapse?.orderid === row.orderid ? : } )} handleMenuOpen(e, row)} sx={{ borderRadius: 999, bgcolor: tint('#6366f1'), color: '#6366f1', border: `1px solid ${edge('#6366f1')}`, '&:hover': { bgcolor: soft('#6366f1') } }} > {selectedRow?.orderstatus !== 'delivered' && ( { notifyRiderMutation.mutate(selectedRow.userfcmtoken); handleMenuClose(); }} > Notify Rider )} {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && ( { if (!appId) { opentoast('Please select a location first!', 'warning'); locationRef.current?.focus(); return; } setChangeDialogOpen(true); handleMenuClose(); }} > Change Rider )} {(roleid == 1 || roleid == 2) && ( { setKms(selectedRow.kms); setCumulativeKms(selectedRow.cumulativekms); setDeliverylat(selectedRow.droplat); setDeliverylong(selectedRow.droplon); setNotes(selectedRow.notes); setDeliveryamount(selectedRow.deliveryamount); setUpdateStatus(selectedRow.orderstatus || 'delivered'); setCurrentorder(selectedRow); setDialogopen(true); handleMenuClose(); }} > Update Status )} {selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && ( { setCancelDeliveryOpen(true); handleMenuClose(); }} > Cancel Delivery )} )} {productCollapse?.orderid === row?.orderid && ( Product Details
    # Product Description Qty Cost Price Tax Amount {orderdetails?.details?.map((product, idx2) => ( {String(idx2 + 1).padStart(2, '0')} {product?.productname || 'Unnamed'} {product?.productdescription || '-'} {product?.orderqty || 0} ₹ {product?.price || 0} ₹ {(product?.productsumprice ?? 0).toFixed(2)} ₹ {(product?.taxamount ?? 0).toFixed(2)} ₹ {(product?.productsumprice + product?.taxamount).toFixed(2) || 0} ))} Total Amount ₹ {orderdetails?.pricedetails?.orderamount?.toFixed(2)}
    )} ); })} {countSourceRows?.length != 0 && (
    {countIsFetchingNext ? : countHasNext ? : ( · End of list · )}
    )} ); })()}
    {/* =============================== || cancel dialog || =============================== */} setCancelDeliveryOpen(false)} maxWidth="xs" fullWidth PaperProps={{ elevation: 0, sx: { borderRadius: 3, border: '1px solid', borderColor: DT.borderSubtle, boxShadow: DT.shadowPop, overflow: 'hidden' } }} > Cancel Delivery? This action will cancel the delivery and notify the rider. Please share a brief reason. setCancelFeed(e.target.value)} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2, bgcolor: DT.surfaceAlt, '& fieldset': { borderColor: DT.borderSubtle }, '&:hover fieldset': { borderColor: '#ef4444' }, '&.Mui-focused fieldset': { borderColor: '#ef4444', borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#ef4444')}` } } }} /> {/* =============================== || change rider dialog || =============================== */} setChangeDialogOpen(false)} maxWidth="sm" fullWidth TransitionComponent={PopupTransition} PaperProps={{ elevation: 0, sx: { borderRadius: 3, border: '1px solid', borderColor: DT.borderSubtle, boxShadow: DT.shadowPop, overflow: 'hidden' } }} > Change Rider Assign this delivery to a different rider `${option.firstname} ${option.lastname} (${option.contactno})`} onChange={(e, value) => { setSelectedRider(value); logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); }} renderInput={(params) => ( ) }} sx={pillFieldSx('#8b5cf6')} /> )} /> {/* =============================== || Date filter Dialog || =============================== */} setOpen(false)} PaperProps={{ elevation: 0, sx: { borderRadius: 3, border: '1px solid', borderColor: DT.borderSubtle, boxShadow: DT.shadowPop, overflow: 'hidden' } }} > Date Range Pick a window to filter the deliveries setOpen(!open)} id="daterange1" onChange={(range) => { if (range.label === 'All') { setStartdate(''); setEnddate(''); setOpen(false); } else { setStartdate(dayjs(range.startDate).format('YYYY-MM-DD')); setEnddate(dayjs(range.endDate).format('YYYY-MM-DD')); if (range.label) { setDatestatus(range.label); } else { setDatestatus(''); } } console.log(range); }} definedRanges={[ { label: 'Today', startDate: new Date(), endDate: new Date() }, { label: 'Yesterday', startDate: addDays(new Date(), -1), endDate: addDays(new Date(), -1) }, { label: 'Tomorrow', startDate: addDays(new Date(), +1), endDate: addDays(new Date(), +1) }, { label: 'This Week', startDate: startOfWeek(new Date()), endDate: endOfWeek(new Date()) }, { label: 'Last Week', startDate: startOfWeek(addWeeks(new Date(), -1)), endDate: endOfWeek(addWeeks(new Date(), -1)) }, { label: 'Last 7 Days', startDate: addWeeks(new Date(), -1), endDate: new Date() }, { label: 'This Month', startDate: startOfMonth(new Date()), endDate: endOfMonth(new Date()) }, { label: 'Last Month', startDate: startOfMonth(addMonths(new Date(), -1)), endDate: endOfMonth(addMonths(new Date(), -1)) } // { // label: 'All', // startDate: new Date(), // endDate: addDays(new Date(), -1), // }, ]} /> {/* =============================== || Update Delivery Dialog || =============================== */} Update Delivery Status {currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'} {[ { label: 'KMs', value: kms, set: (v) => setKms(v), color: '#ef4444' }, { label: 'Actual KMs', value: cumulativekms, set: (v) => setCumulativeKms(+v), color: '#10b981' }, { label: 'Delivery Latitude', value: deliverylat, set: (v) => setDeliverylat(v), color: '#06b6d4' }, { label: 'Delivery Longitude', value: deliverylong, set: (v) => setDeliverylong(v), color: '#06b6d4' } ].map((f) => ( {f.label} f.set(e.target.value)} sx={{ mt: 0.5, '& .MuiOutlinedInput-root': { borderRadius: 2, bgcolor: '#fff', '& fieldset': { borderColor: DT.borderSubtle }, '&:hover fieldset': { borderColor: f.color }, '&.Mui-focused fieldset': { borderColor: f.color, borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(f.color)}` } } }} /> ))} Amount setDeliveryamount(+e.target.value)} sx={{ mt: 0.5, '& .MuiOutlinedInput-root': { borderRadius: 2, bgcolor: '#fff', '& fieldset': { borderColor: DT.borderSubtle }, '&:hover fieldset': { borderColor: '#10b981' }, '&.Mui-focused fieldset': { borderColor: '#10b981', borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#10b981')}` } } }} /> Status setUpdateStatus(e.target.value)} SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 2, mt: 0.5, boxShadow: DT.shadowPop } } } }} sx={{ mt: 0.5, '& .MuiOutlinedInput-root': { borderRadius: 2, bgcolor: '#fff', '& fieldset': { borderColor: DT.borderSubtle }, '&:hover fieldset': { borderColor: '#6366f1' }, '&.Mui-focused fieldset': { borderColor: '#6366f1', borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#6366f1')}` } } }} > {['pending','accepted','started','arrived','delivered','cancelled'].map((s) => { const m = STATUS_META[s] || { label: s, color: '#6366f1', icon: MdHistoryToggleOff }; const Ic = m.icon; return ( {m.label} ); })} Notes setNotes(e.target.value)} sx={{ mt: 0.5, '& .MuiOutlinedInput-root': { borderRadius: 2, bgcolor: '#fff', '& fieldset': { borderColor: DT.borderSubtle }, '&:hover fieldset': { borderColor: '#6366f1' }, '&.Mui-focused fieldset': { borderColor: '#6366f1', borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#6366f1')}` } } }} /> ); }; export default Deliveries;