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 { Empty } from 'antd'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); import axios from 'axios'; import HoverSocialCard from 'components/cards/statistics/HoverSocialCard'; import { useTheme } from '@mui/material/styles'; import { MdOutlineDateRange, MdAccessTime, MdAllInclusive, MdLightMode, MdWbSunny, MdNightsStay, MdCheck } from 'react-icons/md'; import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; import { Avatar, Box, Button, Grid, Tabs, Tab, IconButton, Stack, Chip, Typography, Table, TableCell, TableBody, TableHead, Collapse, Dialog, TableRow, DialogContent, DialogTitle, Tooltip, DialogActions, Popper, ClickAwayListener, Checkbox, Autocomplete, TextField, FormLabel, TablePagination, TableContainer, Skeleton, useMediaQuery, Divider, CircularProgress, Backdrop, MenuItem, Menu, Paper } from '@mui/material'; import MainCard from 'components/MainCard'; import { MoreOutlined, CloseOutlined // NotificationOutlined } from '@ant-design/icons'; import { PopupTransition } from 'components/@extended/Transitions'; import { addDays, addMonths, addWeeks, // addYears, endOfMonth, endOfWeek, // endOfYear, startOfMonth, startOfWeek // startOfYear, } 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 CircularLoader from 'components/CircularLoader'; import { cancelDeliveryAPI, changeRiderAPI, fetchCountAPI, fetchDeliveries, fetchPercentageAPI, fetchRidersList, notifyRider, updateDeliveryAPI, getorderdetails, gettenantlocations, getTenants } from 'pages/api/api'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import TitleCard from 'components/nearle_components/TitleCard'; 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'; // 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; }; // ================================================= || 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 } > {/* */} } {/* ============================================= || TitleCard | ============================================= */} {/* ============================================= || hoverCard | ============================================= */} {[ { label: 'Created orders', value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage1, color: theme.palette.info.main }, { label: 'Pending orders', value: percentageData?.assignedOrders, percentage: percentageData?.percentage2, color: theme.palette.warning.main }, { label: 'Delivered orders', value: percentageData?.pickedOrders, percentage: percentageData?.percentage3, color: theme.palette.success.main }, { label: 'Cancelled Orders', value: percentageData?.coveredOrders, percentage: percentageData?.percentage4, color: theme.palette.error.main } ].map((item, index) => ( : item.value} percentage={item.percentage?.toString()} color={item.color} /> ))} {/* ============================================= || orderFilter | ============================================= */} {/* 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={(paperProps) => ( )} 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: 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 chip — opens the date picker. Stays visible only when a date range is set (matches prior behavior). */} {startdate && enddate && ( } label={ {dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')} } onClick={() => setOpen(true)} variant="combined" color="warning" sx={{ maxWidth: '100%', cursor: 'pointer' }} /> )}
    { if (!appId) { event.preventDefault(); OpenToast('Please select a your 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(0); setLocationValue(null); } }} renderInput={(params) => } /> `${option.locationname} (${option.suburb})` || ''} value={locationValue} sx={{ minWidth: 200, flex: 1 }} onOpen={(event) => { if (!appId && !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) => } /> `${option.firstname} ${option.lastname} (${option.contactno})`} renderInput={(params) => } onChange={(e, value, reason) => { if (reason === 'clear') { setRiderid(0); } else { setRiderid(value.userid); console.log('selected rider', value); } }} onOpen={(event) => { if (!appId) { event.preventDefault(); OpenToast('Please select a your location first!', 'warning', 3000); setTimeout(() => { locationRef.current?.focus(); }, 0); } }} /> {/* ============================================= || Tabs || ============================================= */} } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> } iconPosition="end" /> {/* ============================================= || searchOutlined | ============================================= */} {/* ============================================= || MainCard || ============================================= */} {tabstatus == 'Created' && ( 0 && deliverylist.length != filteredRows.length} onChange={(e) => { if (e.target.checked) { setDeliverylist([...filteredRows]); } else { setDeliverylist([]); } }} checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} /> )} S.No Tenant Order Location{' '} Pickup Drop Rider Est. Delivery Time Transit Kms Amount Notes Steps Qty COD {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( Action )} {(loading1 || countSourceIsLoading) && } {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( <> b.id === selectedBatch)?.label || 'this batch'}` } styles={{ description: { color: theme.palette.error.main } }} /> )} {filteredRows.length == 0 && countSourceLoading && ( Loading deliveries… )} {filteredRows.map((row, index) => { return ( <> res1.orderheaderid == row.orderheaderid) ? '#e1bee7' : '', '&:hover': { backgroundColor: deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid) ? '#e1bee7 !important' : '' } }} > {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)} /> )} {/* {row.sno} */} {page * rowsPerPage + index + 1} {/* 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.slice(0, 12) + '...'} {/* drop */} {row.deliverycustomer} {row.deliverycontactno} {`${row.deliverylocation || row.deliveryaddress.slice(0, 12) + '...'}`} {/* rider */} {row.ridername} {`ID: ${row.userid}`} {row.ridercontact} {/* Estimated Delivery Time */} {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : 'N/A'} {/* Transit Minutes */} {row.transitminutes || 0} min {/* kms */} {/* amount */} {/* notes */} {row.notes} {/* step */} {row.step ? ( ) : ( )} {/* qty */} {row.Quantity ? ( {row.Quantity} ) : ( {' '} {row.Quantity} )} {/* {cash} */} {row.collectionamt ? ( {' '} {`₹ ${row.collectionamt.toFixed(2)}`}{' '} ) : ( {' '} {`₹ ${row.collectionamt.toFixed(2)}`}{' '} )} {/* Action */} {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( {row.deliverytype == 'C' && ( { if (productCollapse?.orderid === row.orderid) { setProductCollapse(null); // Collapse if already open setOrderHeaderId(null); } else { setProductCollapse(row); // Open if different setOrderHeaderId(row.orderheaderid); // Open if different } }} > {productCollapse?.orderid === row.orderid ? : } )} handleMenuOpen(e, row)} > {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 Delivery Status )} {selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && ( { setCancelDeliveryOpen(true); handleMenuClose(); }} > Cancel Delivery )} )} {productCollapse?.orderid === row?.orderid && (
    S.No Product Name Description Quantity Cost Price Tax Amount {orderdetails?.details?.map((product, index) => ( {index + 1} {product?.productname} {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 ? : 'No More Deliveries'}
    )}
    {/* =============================== || cancel dialog || =============================== */} setCancelDeliveryOpen(false)} maxWidth="xs"> Are you sure you want to cancel this delivery? { setCancelFeed(e.target.value); }} /> {/* =============================== || change dialog || =============================== */} setChangeDialogOpen(false)} maxWidth="sm" fullWidth TransitionComponent={PopupTransition} > Change Rider `${option.firstname} ${option.lastname} | ${option.contactno}`} renderInput={(params) => } onChange={(e, value) => { setSelectedRider(value); logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); }} /> {/* =============================== || Date filter Dialog || =============================== */} Select Filter Options 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 Kms { setKms(e.target.value); console.log(e); }} /> Actual Kms setCumulativeKms(+e.target.value)} fullWidth /> Delivery lat { setDeliverylat(e.target.value); }} /> Delivery Long { setDeliverylong(e.target.value); }} fullWidth /> Amount setDeliveryamount(+e.target.value)} /> Status { setUpdateStatus(e.target.value); }} > Pending Accepted Started Arrived Delivered Cancelled Notes setNotes(e.target.value)} /> {' '} ); }; export default Deliveries;