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 && (
)}