Files
doormile_console_expresscopy/src/pages/nearle/deliveries/deliveries.js
Suriya 1a71732396 feat: Doormile console Phase 2 complete
Part A: remaining api.js legacy functions replaced
  - fetchRidersList, cancelOrder/Delivery, changeRiderAPI
  - fetchPercentageAPI, fetchCountAPI → Doormile status counts
  - getpricinglist, fetchLocations, fetchRidersLogs, getusers
  - fetchPaymentType, getorderdetails

Part B: deliveries.js status tabs → Doormile statuses

Part C: 3 new pages
  - pages/nearle/hubs/Hubs.js (hub CRUD + miler counts)
  - pages/nearle/bookings/BookingDetail.js (booking detail + timeline)
  - pages/nearle/dashboard/Dashboard.js (live KPIs + city breakdown)

Part D: routes + menu updated
2026-07-08 19:12:28 +05:30

2547 lines
107 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
MdStorefront,
MdLocationOn,
MdDirectionsBike,
MdNotificationsActive,
MdPersonPin,
MdHistoryToggleOff,
MdCheckCircle,
MdCancel,
MdInventory2,
MdHourglassEmpty,
MdTune,
MdMyLocation,
MdOutlineLocalShipping,
MdOutlinePendingActions,
MdOutlineCheckCircle,
MdOutlineCancel
} from 'react-icons/md';
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import {
Avatar,
Box,
Button,
Chip,
Grid,
IconButton,
Stack,
Typography,
Table,
TableCell,
TableBody,
TableHead,
Collapse,
Dialog,
TableRow,
DialogContent,
DialogTitle,
Tooltip,
DialogActions,
Checkbox,
Autocomplete,
TextField,
TableContainer,
Backdrop,
MenuItem,
Menu,
Paper,
useMediaQuery
} from '@mui/material';
import { PopupTransition } from 'components/@extended/Transitions';
import {
addDays,
addMonths,
addWeeks,
endOfMonth,
endOfWeek,
startOfMonth,
startOfWeek
} from 'date-fns';
import { DateRangePicker } from '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,
fetchRidersList,
notifyRider,
updateDeliveryAPI,
getorderdetails,
gettenantlocations,
getTenants
} from 'pages/api/api';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — 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: 14,
radiusInner: 10,
radiusField: 10,
// Restrained, low-contrast elevation — corporate (Linear/Stripe), not flashy.
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
borderHover: '#cbd5e1',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#C01227'
};
// 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.
// Neutral, corporate filter field: white surface, hairline border, brand focus
// ring. Colour is no longer used to tint the whole control (that produced the
// "rainbow" filter bar) — accent now lives only in the small start-adornment
// icon, which aids scanning without flooding the surface.
const pillFieldSx = () => ({
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
fontWeight: 600,
color: DT.textPrimary,
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` },
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
});
// Status palette — drives tab pills, row status badges, dialogs. Keys are
// Doormile's real booking statuses, lowercased (row.status is compared
// lowercased everywhere below so exact backend casing doesn't matter).
const STATUS_META = {
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
// Ordered status list driving the tabs row (left → right). `status` is the
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
// casing Doormile's /admin/bookings status filter expects.
const STATUS_TABS = [
{ status: 'all', apiStatus: 'all', countKey: 'all' },
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
{ status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
{ status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
];
// KPI palette + icons — mirrors the four cards across the top of the page.
const KPI_META = [
{ key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping },
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions },
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle },
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel }
];
// Batches mirror the dispatch page's slot definitions so an operator who
// segments the day there sees the same buckets here. Hours are 24h, half-open
// [startHour, endHour) — a delivery at exactly endHour falls into the *next*
// batch (or none, if the gap isn't covered).
const BATCH_OPTIONS = [
{ id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' },
{ id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 },
{ id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12:30 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12.5 },
{ id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 }
];
// Per-batch icon components — kept separate from BATCH_OPTIONS so the option
// objects stay serializable / printable. Looked up at render time by iconKey.
const BATCH_ICONS = {
all: MdAllInclusive,
morning: MdLightMode,
afternoon: MdWbSunny,
evening: MdNightsStay
};
// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the
// page lands them on the slot they're most likely curious about. Falls back
// to 'all' when the current hour lies in a configured gap (89 AM, 12 PM4
// 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
// 89 AM / 12 PM4 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) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
// 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 }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
// ================================================= || deliveries (initial point)|| =================================================
const Deliveries = () => {
const userid = localStorage.getItem('userid');
const theme = useTheme();
// Below `md` we swap the wide data table for an app-style card list. Desktop
// (md and up) keeps the exact same table — no behaviour change either way.
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const loadMoreRef = useRef();
const containerRef = useRef();
const [deliverylist, setDeliverylist] = useState([]);
const [dialogopen, setDialogopen] = useState(false);
const [locaName, setLocoName] = useState('All');
const [appId, setAppId] = useState(0);
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [tabstatus, setTabstatus] = useState('All');
const [tabvalue, setTabvalue] = useState(0);
const [open, setOpen] = useState(false);
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('all');
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 [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 [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, { 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);
const tab = STATUS_TABS[i];
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
setCurrentStatus(tab.apiStatus);
setSearchword('');
};
const okclicked = () => {
setOpen(false);
};
/* ============================================= || fetchDeliveries | ============================================= */
const {
isLoading: fetchDeliveriesIsLoading,
isError: fetchDeliveriesIsError,
error: fetchDeliveriesError,
refetch: fetchDeliveriesRefetch
} = useInfiniteQuery({
queryKey: [
'fetchdeliveries',
appId,
userid,
currentStatus,
startdate,
enddate,
rowsPerPage,
debouncedSearch,
tenantid,
locationid,
riderid
],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
});
// (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 <status> 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.status || '').toLowerCase();
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
if (q) {
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
.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 = {};
countSourceRows.forEach((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
const s = String(r.status || '').toLowerCase();
c[s] = (c[s] || 0) + 1;
c.all = (c.all || 0) + 1;
});
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:11981206`) — totals here should match the per-batch numbers
// visible on the dispatch page for the same date.
const batchTotals = useMemo(() => {
// `all` is the SUM of the three batch buckets — not the raw row count.
// Rows that fall in a gap (89 AM, 12:304 PM, after 7 PM) or have no
// `assigntime` belong to no batch, so they're excluded here too. This keeps
// the "All Batches" badge equal to Morning + Afternoon + Evening
// (e.g. 36 + 89 + 53 = 178) instead of over-reporting the un-bucketed total.
const totals = { all: 0 };
BATCH_OPTIONS.forEach((b) => {
if (b.id === 'all') return;
totals[b.id] = 0;
});
countSourceRows.forEach((r) => {
const id = getRowBatchId(r);
if (id) {
totals[id] = (totals[id] || 0) + 1;
totals.all += 1;
}
});
return totals;
}, [countSourceRows]);
// IntersectionObserver now watches the count-source query — that's the
// single source of truth for both the table and the chip counts. When the
// sentinel scrolls into view it nudges the next page of all-statuses
// deliveries to keep the dataset complete.
useEffect(() => {
if (!countHasNext) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
countFetchNext();
}
},
{
root: document.querySelector('.MuiTableContainer-root'),
rootMargin: '0px',
threshold: 1.0
}
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
};
}, [countHasNext, countFetchNext]);
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (countHasNext && !countIsFetchingNext) {
countFetchNext();
}
}
};
/* ============================================= || fetchPercentageAPI | ============================================= */
/* ============================================= || fetchcount | ============================================= */
const {
data: countData = {}, // Default to empty object
isLoading: fetchCountIsLoading,
isError: fetchCountIsError,
error: fetchCountError,
refetch: fetchCountRefetch
} = useQuery({
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
});
// ==============================|| 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 deliveries: ${fetchDeliveriesError?.message}`
: fetchCountIsError
? `Error fetching count summary: ${fetchCountError?.message}`
: ridersListIsError
? `Error fetching riders: ${ridersListError?.message}`
: fetchtenantsIsError
? `Error tenant list: ${fetchtenantsError?.message}`
: fetchlocationsIsError
? `Error location list: ${fetchlocationsError?.message}`
: null;
if (errorMessage) {
console.log('errorMessage', errorMessage);
OpenToast(errorMessage, 'error', 2000);
return null; // or return <></> if inside a component
}
return (
<>
{(fetchCountIsLoading ||
countSourceIsLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
riderListIsLoading) && (
<>
<Loader />
{/* <CircularLoader /> */}
</>
)}
{
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
open={
fetchCountIsLoading ||
fetchDeliveriesIsLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
riderListIsLoading
}
>
{/* <CircularLoader color="inherit" /> */}
</Backdrop>
}
{/* ============================================= || Header | ============================================= */}
<PageHeader
title="Deliveries"
subtitle={`Live · ${locaName || 'All Zones'}`}
live
action={
<LocationAutocomplete
ref={locationRef}
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* ============================================= || KPI Cards | ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{[
{ ...KPI_META[0], value: batchTotals.all, percentage: null },
{ ...KPI_META[1], value: countData?.uncoveredLength, percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 },
{ ...KPI_META[2], value: countData?.coveredLength, percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 },
{ ...KPI_META[3], value: countData?.cancelLength, percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 }
].map((item) => {
const Icon = item.icon;
const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage);
const hasPct = !Number.isNaN(pct) && item.percentage !== undefined && item.percentage !== null;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<StatCard
title={item.label}
value={item.value ?? 0}
icon={<Icon size={20} />}
color={item.color}
loading={fetchCountIsLoading}
caption={hasPct ? `${Math.abs(pct)}% of total` : undefined}
/>
</Grid>
);
})}
</Grid>
{/* ============================================= || Filter Bar | ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
mb: { xs: 1.5, md: 2 },
p: { xs: 1.5, md: 2 },
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
boxShadow: DT.shadowMd
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
<MdTune size={16} />
</Avatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
Filters
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={1.25} sx={{ flexWrap: 'wrap' }}>
<Stack
direction="row"
flexWrap="wrap"
gap={1.5}
alignItems="center"
>
{/* 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 (
<Autocomplete
disableClearable
size="small"
// Click-only dropdown: typing is disabled (readOnly input below),
// but the field still opens the menu on click. openOnFocus +
// selectOnFocus give the operator one-click access; the search
// filter built into Autocomplete is suppressed since there's
// nothing to search across only 4 options.
openOnFocus
selectOnFocus
filterOptions={(opts) => 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 (
<li
{...props}
key={option.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '10px 14px',
borderBottom: '1px solid #f1f5f9',
backgroundColor: selected ? `${option.color}10` : 'transparent',
transition: 'background-color 0.15s'
}}
>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: selected ? option.color : `${option.color}18`,
color: selected ? '#fff' : option.color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
<Icon size={20} />
</Avatar>
<Stack direction="column" spacing={0} flex={1} minWidth={0}>
<Typography
variant="body2"
fontWeight={700}
color="#0f172a"
noWrap
>
{option.label}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{option.range}
</Typography>
</Stack>
{/* 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. */}
<Chip
size="small"
label={total}
sx={{
minWidth: 36,
height: 22,
fontWeight: 800,
fontSize: 11,
bgcolor: total > 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 && (
<Avatar
sx={{
width: 22,
height: 22,
bgcolor: option.color,
color: '#fff'
}}
>
<MdCheck size={14} />
</Avatar>
)}
</li>
);
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Batch"
// readOnly disables typing; the input still receives focus
// and clicks, so the popup still opens. caretColor:transparent
// hides the blinking caret that focus would normally show.
inputProps={{
...params.inputProps,
readOnly: true,
style: { ...(params.inputProps?.style || {}), cursor: 'pointer', caretColor: 'transparent' }
}}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack
direction="row"
alignItems="center"
spacing={0.75}
sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}
>
<Avatar
sx={{
width: 24,
height: 24,
bgcolor: `${activeBatch.color}18`,
color: activeBatch.color
}}
>
<ActiveIcon size={14} />
</Avatar>
</Stack>
)
}}
sx={{
minWidth: { xs: 180, sm: 220, md: 240 },
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
bgcolor: '#ffffff',
fontWeight: 600,
color: '#0f172a',
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
}}
/>
)}
/>
);
})()}
{/* Date range pill — opens the date picker. Stays visible only when
a date range is set. Uses the same pill aesthetic as Batch. */}
{startdate && enddate && (
<Stack
direction="row"
alignItems="center"
spacing={0.75}
onClick={() => setOpen(true)}
sx={{
pl: 0.5,
pr: 1.25,
py: 0.5,
borderRadius: '10px',
cursor: 'pointer',
bgcolor: '#ffffff',
border: '1px solid #e2e8f0',
transition: 'border-color 0.15s',
'&:hover': { borderColor: '#cbd5e1' }
}}
>
<AccentAvatar color="#94a3b8">
<MdOutlineDateRange size={14} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', whiteSpace: 'nowrap' }}>
{dayjs(startdate).format('DD MMM')} {dayjs(enddate).format('DD MMM')}
</Typography>
</Stack>
)}
</Stack>
{/* Tenant */}
<Autocomplete
disablePortal
options={tenantlist || []}
value={tenantValue}
PaperComponent={SoftPaper}
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
onOpen={(event) => {
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) => (
<TextField
{...params}
inputRef={tenantRef}
placeholder="Select Tenant"
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
<AccentAvatar color="#94a3b8"><MdStorefront size={14} /></AccentAvatar>
</Stack>
)
}}
sx={pillFieldSx('#0ea5e9')}
/>
)}
/>
{/* Location */}
<Autocomplete
disablePortal
options={locationlist || []}
getOptionLabel={(option) => `${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) => (
<TextField
{...params}
placeholder="Select Location"
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
<AccentAvatar color="#94a3b8"><MdLocationOn size={14} /></AccentAvatar>
</Stack>
)
}}
sx={pillFieldSx('#14b8a6')}
/>
)}
/>
{/* Rider */}
<Autocomplete
disabled={riderListIsLoading}
options={ridersList}
PaperComponent={SoftPaper}
getOptionLabel={(option) => `${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) => (
<TextField
{...params}
placeholder="Choose Rider"
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
<AccentAvatar color="#94a3b8"><MdDirectionsBike size={14} /></AccentAvatar>
</Stack>
)
}}
sx={pillFieldSx('#8b5cf6')}
/>
)}
/>
</Stack>
</Paper>
{/* ============================================= || Status Tabs + Search || ============================================= */}
<Paper
elevation={0}
sx={{
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={1.5}
sx={{ flexWrap: 'wrap-reverse' }}
>
<Stack
direction="row"
spacing={0.75}
sx={{
flex: 1,
overflowX: 'auto',
py: 0.5,
px: 0.25,
'&::-webkit-scrollbar': { height: 6 },
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
}}
>
{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 (
<Box
key={t.status}
onClick={(e) => handleChangetab(e, idx)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: { xs: 0.625, md: 0.875 },
pl: 0.5,
pr: { xs: 1, md: 1.25 },
py: 0.5,
flexShrink: 0,
cursor: 'pointer',
borderRadius: DT.radiusField + 'px',
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? meta.color : DT.borderHover,
bgcolor: active ? meta.color : DT.surfaceAlt
}
}}
>
<Avatar
sx={{
width: { xs: 20, md: 22 },
height: { xs: 20, md: 22 },
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
color: active ? '#fff' : meta.color
}}
>
<Icon size={12} />
</Avatar>
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontSize: { xs: 11.5, md: 13 },
lineHeight: 1
}}
>
{meta.label}
</Typography>
<Box
sx={{
minWidth: { xs: 20, md: 24 },
height: { xs: 18, md: 20 },
px: 0.625,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999,
fontSize: { xs: 10, md: 11 },
fontWeight: 700,
bgcolor: active ? 'rgba(255,255,255,0.22)' : DT.surfaceAlt,
color: active ? '#fff' : DT.textSecondary,
border: 'none'
}}
>
{count}
</Box>
</Box>
);
})}
</Stack>
{/* ============================================= || Search | ============================================= */}
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
sx={{
m: 0,
width: '100%',
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* ============================================= || Table || ============================================= */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
<TableContainer
ref={containerRef}
onScroll={handleScroll}
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
overflow: 'auto',
overflowX: 'auto',
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#C01227'),
borderRadius: '8px',
cursor: 'pointer',
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
{(() => {
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
const showSelect = tabstatus == 'Created';
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
return isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdInventory2 size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No deliveries to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{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'}.`}
</Typography>
</Stack>
)}
{filteredRows.length === 0 && countSourceLoading && (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading deliveries
</Typography>
</Stack>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
const isOpen = productCollapse?.orderid === row?.orderid;
return (
<MobileCard
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
accent={rowStatusMeta.color}
selected={isSelected}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
{showSelect && (
<Checkbox
size="small"
sx={{ p: 0.25 }}
onChange={(e) => {
if (e.target.checked) {
let arr = deliverylist;
arr.push({ ...row, sno: deliverylist.length + 1 });
setDeliverylist([...arr]);
} else {
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
if (res) {
let arr = deliverylist;
arr.splice(res.sno - 1, 1);
arr.map((val, i) => {
val.sno = i + 1;
});
setDeliverylist([...arr]);
}
}
}}
checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
/>
)}
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{ display: 'inline-flex', pl: 0.5, pr: 1, py: 0.25, borderRadius: 999, bgcolor: tint(rowStatusMeta.color), border: `1px solid ${edge(rowStatusMeta.color)}`, color: rowStatusMeta.color }}
>
<AccentAvatar color={rowStatusMeta.color} size={18}>
<RowStatusIcon size={11} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
{rowStatusMeta.label}
</Typography>
</Stack>
</Stack>
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
{row.deliverytype == 'C' && (
<IconButton
size="small"
onClick={() => {
if (productCollapse?.orderid === row.orderid) {
setProductCollapse(null);
setOrderHeaderId(null);
} else {
setProductCollapse(row);
setOrderHeaderId(row.orderheaderid);
}
}}
sx={{ borderRadius: 999, bgcolor: tint('#06b6d4'), color: '#06b6d4', border: `1px solid ${edge('#06b6d4')}`, '&:hover': { bgcolor: soft('#06b6d4') } }}
>
{isOpen ? <KeyboardArrowUpOutlined fontSize="small" /> : <KeyboardArrowDownOutlined fontSize="small" />}
</IconButton>
)}
{showAction && (
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
>
<EditOutlined />
</IconButton>
)}
</Stack>
</Stack>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.bookingreference || `#${row.bookingid}`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Pickup" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.pickupaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Delivery" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.deliveryaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Miler" full>
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={24}>
<MdDirectionsBike size={13} />
</AccentAvatar>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
Unassigned
</Typography>
)}
</MobileField>
</MobileFieldGrid>
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
<Stack spacing={1}>
{orderdetails?.details?.map((product, idx2) => (
<Stack key={idx2} direction="row" alignItems="center" spacing={1.25} sx={{ p: 1, borderRadius: 1.5, bgcolor: '#fff', border: `1px solid ${DT.divider}` }}>
<Box component="img" src={product?.productimage || 'https://via.placeholder.com/40'} alt={product?.productname} sx={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 1.5, border: `1px solid ${DT.divider}`, flexShrink: 0 }} />
<Stack sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }} noWrap>
{product?.productname || 'Unnamed'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
Qty {product?.orderqty || 0} · {product?.price || 0}
</Typography>
</Stack>
<Typography variant="body2" sx={{ fontWeight: 800, color: DT.textPrimary, flexShrink: 0 }}>
{(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
</Typography>
</Stack>
))}
<Stack direction="row" justifyContent="space-between" sx={{ pt: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary }}>Total Amount</Typography>
<Typography variant="body2" sx={{ fontWeight: 800, color: '#10b981' }}>
{orderdetails?.pricedetails?.orderamount?.toFixed(2)}
</Typography>
</Stack>
</Stack>
</Box>
)}
</MobileCard>
);
})}
{countSourceRows?.length != 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{countIsFetchingNext || countHasNext ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
· End of list ·
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: { xs: 1100, lg: 1300, xl: 1400 } }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 2 }
}
}}
>
{showSelect && (
<TableCell sx={{ whiteSpace: 'nowrap' }}>
<Checkbox
indeterminate={deliverylist.length > 0 && deliverylist.length != filteredRows.length}
onChange={(e) => {
if (e.target.checked) {
setDeliverylist([...filteredRows]);
} else {
setDeliverylist([]);
}
}}
checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length}
/>
</TableCell>
)}
<TableCell>#</TableCell>
<TableCell>Status</TableCell>
<TableCell>Booking</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Delivery</TableCell>
<TableCell>Miler</TableCell>
<TableCell>Created</TableCell>
{showAction && <TableCell align="right">Action</TableCell>}
</TableRow>
</TableHead>
{(loading1 || countSourceIsLoading) && <OrdersTableSkeleton col={8} />}
<TableBody>
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdInventory2 size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No deliveries to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{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'}.`}
</Typography>
</Stack>
</TableCell>
</TableRow>
)}
{filteredRows.length == 0 && countSourceLoading && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ textAlign: 'center', py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Loading deliveries
</Typography>
</TableCell>
</TableRow>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
return (
<Fragment key={row.orderheaderid ?? `${row.tenantname}-${index}`}>
<TableRow
role="checkbox"
tabIndex={-1}
sx={{
cursor: 'pointer',
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 }
},
backgroundColor: isSelected ? soft('#8b5cf6') : 'transparent',
'&:hover': {
backgroundColor: isSelected ? soft('#8b5cf6') : DT.surfaceAlt
}
}}
>
{tabstatus == 'Created' && (
<TableCell>
<Checkbox
onChange={(e) => {
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)}
/>
</TableCell>
)}
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
</TableCell>
{/* Status badge */}
<TableCell>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(rowStatusMeta.color),
border: `1px solid ${edge(rowStatusMeta.color)}`,
color: rowStatusMeta.color
}}
>
<AccentAvatar color={rowStatusMeta.color} size={20}>
<RowStatusIcon size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
{rowStatusMeta.label}
</Typography>
</Stack>
</TableCell>
{/* Booking */}
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.bookingreference || `#${row.bookingid}`}
</Typography>
</TableCell>
{/* Pickup */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.pickupaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.pickupaddress || '—'}
</Typography>
</Tooltip>
</TableCell>
{/* Delivery */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.deliveryaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.deliveryaddress || '—'}
</Typography>
</Tooltip>
</TableCell>
{/* Miler */}
<TableCell>
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={28}>
<MdDirectionsBike size={14} />
</AccentAvatar>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
Unassigned
</Typography>
)}
</TableCell>
{/* Created */}
<TableCell>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</TableCell>
{/* Action */}
{tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && (
<TableCell align="right">
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
{row.deliverytype == 'C' && (
<Tooltip title="Product details" placement="top">
<IconButton
size="small"
onClick={() => {
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 ? <KeyboardArrowUpOutlined fontSize="small" /> : <KeyboardArrowDownOutlined fontSize="small" />}
</IconButton>
</Tooltip>
)}
<Tooltip title="Actions" placement="top">
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{
borderRadius: 999,
bgcolor: tint('#C01227'),
color: '#C01227',
border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: soft('#C01227') }
}}
>
<EditOutlined />
</IconButton>
</Tooltip>
</Stack>
</TableCell>
)}
</TableRow>
{productCollapse?.orderid === row?.orderid && (
<TableRow>
<TableCell
colSpan={totalCols}
sx={{
p: 0,
border: 0,
backgroundColor: DT.surfaceAlt
}}
>
<Collapse in={true} timeout="auto" unmountOnExit>
<Box sx={{ width: '100%', p: 2.5 }}>
<Paper
elevation={0}
sx={{
borderRadius: 2,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
<Table size="small" sx={{ minWidth: 800 }}>
<TableHead>
<TableRow
sx={{
'& th': {
fontSize: 11,
fontWeight: 800,
letterSpacing: 0.5,
textTransform: 'uppercase',
color: DT.textSecondary,
borderBottom: `1px solid ${DT.divider}`
},
'&:hover': { backgroundColor: 'transparent !important' }
}}
>
<TableCell>#</TableCell>
<TableCell>Product</TableCell>
<TableCell>Description</TableCell>
<TableCell align="right">Qty</TableCell>
<TableCell align="right">Cost</TableCell>
<TableCell align="right">Price</TableCell>
<TableCell align="right">Tax</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orderdetails?.details?.map((product, idx2) => (
<TableRow
key={idx2}
sx={{ '&:hover': { backgroundColor: DT.surfaceAlt + ' !important' } }}
>
<TableCell sx={{ color: DT.textMuted, fontWeight: 700 }}>{String(idx2 + 1).padStart(2, '0')}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<Box
component="img"
src={product?.productimage || 'https://via.placeholder.com/40'}
alt={product?.productname}
sx={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 1.5, border: `1px solid ${DT.divider}` }}
/>
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }} noWrap>
{product?.productname || 'Unnamed'}
</Typography>
</Stack>
</TableCell>
<TableCell sx={{ color: DT.textSecondary }}>{product?.productdescription || '-'}</TableCell>
<TableCell align="right" sx={{ fontWeight: 700 }}>{product?.orderqty || 0}</TableCell>
<TableCell align="right"> {product?.price || 0}</TableCell>
<TableCell align="right"> {(product?.productsumprice ?? 0).toFixed(2)}</TableCell>
<TableCell align="right"> {(product?.taxamount ?? 0).toFixed(2)}</TableCell>
<TableCell align="right" sx={{ fontWeight: 800, color: DT.textPrimary }}>
{(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
</TableCell>
</TableRow>
))}
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
<TableCell align="right" colSpan={7} sx={{ fontWeight: 700, color: DT.textSecondary, borderTop: `1px solid ${DT.divider}` }}>
Total Amount
</TableCell>
<TableCell align="right" sx={{ fontWeight: 800, color: '#10b981', borderTop: `1px solid ${DT.divider}` }}>
{orderdetails?.pricedetails?.orderamount?.toFixed(2)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Paper>
</Box>
</Collapse>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
{countSourceRows?.length != 0 && (
<TableRow>
<TableCell colSpan={totalCols} rowSpan={3}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{countIsFetchingNext ? <LoaderWithImage /> : countHasNext ? <LoaderWithImage /> : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
· End of list ·
</Typography>
)}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
})()}
</TableContainer>
</Paper>
{/* Shared row-action menu — single instance reused by both the desktop
table rows and the mobile cards (both trigger handleMenuOpen). */}
<Menu
anchorEl={menuAnchorEl}
open={menuOpen}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
elevation: 0,
sx: {
minWidth: 220,
borderRadius: 2,
mt: 0.75,
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden',
'& .MuiMenuItem-root': {
fontSize: 13,
fontWeight: 600,
py: 1.1,
px: 1.5,
gap: 1,
transition: 'background-color 0.15s',
'&:hover': { bgcolor: DT.surfaceAlt }
}
}
}}
>
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
handleMenuClose();
}}
>
<AccentAvatar color="#0ea5e9" size={22}><MdNotificationsActive size={13} /></AccentAvatar>
Notify Rider
</MenuItem>
)}
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
String(selectedRow?.status || '').toLowerCase()
) && (
<MenuItem
onClick={() => {
if (!appId) {
opentoast('Please select a location first!', 'warning');
locationRef.current?.focus();
return;
}
setChangeDialogOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#8b5cf6" size={22}><MdDirectionsBike size={13} /></AccentAvatar>
Change Rider
</MenuItem>
)}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.status || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#10b981" size={22}><MdCheckCircle size={13} /></AccentAvatar>
Update Status
</MenuItem>
)}
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
onClick={() => {
setCancelDeliveryOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#ef4444" size={22}><MdCancel size={13} /></AccentAvatar>
Cancel Delivery
</MenuItem>
)}
</Menu>
{/* =============================== || cancel dialog || =============================== */}
<Dialog
open={cancelDeliveryOpen}
onClose={() => setCancelDeliveryOpen(false)}
maxWidth="xs"
fullWidth
PaperProps={{
elevation: 0,
sx: {
borderRadius: 3,
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden'
}
}}
>
<Box sx={{ height: 4, background: `linear-gradient(90deg, #ef4444 0%, ${soft('#ef4444')} 100%)` }} />
<DialogContent sx={{ p: 3.5 }}>
<Stack alignItems="center" spacing={2.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#ef4444'), color: '#ef4444' }}>
<DeleteFilled style={{ fontSize: 28 }} />
</Avatar>
<Stack spacing={0.5} alignItems="center">
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Cancel Delivery?
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 1 }}>
This action will cancel the delivery and notify the rider. Please share a brief reason.
</Typography>
</Stack>
<TextField
fullWidth
placeholder="Reason for cancellation"
multiline
minRows={2}
maxRows={4}
onChange={(e) => 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')}` }
}
}}
/>
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
<Button
fullWidth
onClick={() => setCancelDeliveryOpen(false)}
variant="outlined"
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 700,
borderColor: DT.borderSubtle,
color: DT.textSecondary,
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
}}
>
Keep Delivery
</Button>
<Button
fullWidth
variant="contained"
onClick={() => {
if (cancelFeed == '') {
opentoast('Enter FeedBack', 'error');
} else {
cancelDelivery({ selectedRow, cancelFeed });
}
cancelridernotification();
}}
autoFocus
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 800,
bgcolor: '#ef4444',
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
'&:hover': { bgcolor: '#dc2626' }
}}
>
Yes, Cancel
</Button>
</Stack>
</Stack>
</DialogContent>
</Dialog>
{/* =============================== || change rider dialog || =============================== */}
<Dialog
open={changeDialogOpen}
onClose={() => setChangeDialogOpen(false)}
maxWidth="sm"
fullWidth
TransitionComponent={PopupTransition}
PaperProps={{
elevation: 0,
sx: {
borderRadius: 3,
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden'
}
}}
>
<Box sx={{ height: 4, background: `linear-gradient(90deg, #8b5cf6 0%, ${soft('#8b5cf6')} 100%)` }} />
<DialogTitle sx={{ p: 3, pb: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
<MdDirectionsBike size={22} />
</Avatar>
<Stack>
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Change Rider
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
Assign this delivery to a different rider
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent sx={{ p: 3, pt: 2 }}>
<Autocomplete
fullWidth
options={ridersList}
PaperComponent={SoftPaper}
getOptionLabel={(option) => `${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) => (
<TextField
{...params}
placeholder="Choose Rider"
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
<AccentAvatar color="#8b5cf6"><MdDirectionsBike size={14} /></AccentAvatar>
</Stack>
)
}}
sx={pillFieldSx('#8b5cf6')}
/>
)}
/>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1 }}>
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
<Button
fullWidth
variant="outlined"
onClick={() => setChangeDialogOpen(false)}
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 700,
borderColor: DT.borderSubtle,
color: DT.textSecondary,
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
}}
>
Cancel
</Button>
<Button
fullWidth
variant="contained"
onClick={() => {
setLoading1(true);
logger.info(`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
changeRiderMutation.mutate({ selectedRider, selectedRow });
}}
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 800,
bgcolor: '#8b5cf6',
boxShadow: `0 6px 18px ${ring('#8b5cf6')}`,
'&:hover': { bgcolor: '#7c3aed' }
}}
>
Change Rider
</Button>
</Stack>
</DialogActions>
</Dialog>
{/* =============================== || Date filter Dialog || =============================== */}
<Dialog
open={open}
onClose={() => setOpen(false)}
fullScreen={isMobile}
PaperProps={{
elevation: 0,
sx: {
borderRadius: { xs: 0, sm: 3 },
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden'
}
}}
>
<Box sx={{ height: 4, background: `linear-gradient(90deg, #f59e0b 0%, ${soft('#f59e0b')} 100%)` }} />
<DialogTitle align="left" sx={{ p: 3, pb: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#f59e0b'), color: '#f59e0b' }}>
<MdOutlineDateRange size={22} />
</Avatar>
<Stack>
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Date Range
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
Pick a window to filter the deliveries
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent sx={{ width: '100%' }} className="datedialog">
<DateRangePicker
open={open}
toggle={() => 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'));
}
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))
}
]}
/>
</DialogContent>
<Stack direction="row" justifyContent="flex-end" sx={{ width: '100%', p: 2.5, pt: 1 }}>
<Button
variant="contained"
onClick={okclicked}
sx={{
borderRadius: 999,
px: 3,
py: 1,
fontWeight: 800,
bgcolor: '#f59e0b',
boxShadow: `0 6px 18px ${ring('#f59e0b')}`,
'&:hover': { bgcolor: '#d97706' }
}}
>
Apply
</Button>
</Stack>
</Dialog>
{/* =============================== || Update Delivery Dialog || =============================== */}
<Dialog
fullWidth
open={dialogopen}
onClose={dialogclose}
scroll="paper"
maxWidth="sm"
fullScreen={isMobile}
TransitionComponent={PopupTransition}
PaperProps={{
elevation: 0,
sx: {
borderRadius: { xs: 0, sm: 3 },
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden'
}
}}
>
<Box sx={{ height: 4, background: `linear-gradient(90deg, #10b981 0%, ${soft('#10b981')} 100%)` }} />
<DialogTitle sx={{ p: 3, pb: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#10b981'), color: '#10b981' }}>
<MdCheckCircle size={22} />
</Avatar>
<Stack>
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Update Delivery Status
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'}
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent dividers sx={{ p: 3, bgcolor: DT.surfaceAlt }}>
<Grid container spacing={2}>
{[
{ 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) => (
<Grid item xs={12} sm={6} key={f.label}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
{f.label}
</Typography>
<TextField
fullWidth
value={f.value}
type="number"
onChange={(e) => 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)}` }
}
}}
/>
</Grid>
))}
<Grid item xs={12}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
Amount
</Typography>
<TextField
fullWidth
type="number"
value={deliveryamount}
onChange={(e) => 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')}` }
}
}}
/>
</Grid>
<Grid item xs={12}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
Status
</Typography>
<TextField
select
fullWidth
value={updateStatus}
onChange={(e) => setUpdateStatus(e.target.value)}
SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 2, mt: 0.5, boxShadow: DT.shadowPop } } } }}
sx={{
mt: 0.5,
'& .MuiOutlinedInput-root': {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
>
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
const Ic = m.icon;
return (
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
<AccentAvatar color={m.color} size={22}><Ic size={13} /></AccentAvatar>
{m.label}
</MenuItem>
);
})}
</TextField>
</Grid>
<Grid item xs={12}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
Notes
</Typography>
<TextField
value={notes}
fullWidth
multiline
minRows={2}
onChange={(e) => setNotes(e.target.value)}
sx={{
mt: 0.5,
'& .MuiOutlinedInput-root': {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
<Button
fullWidth
variant="outlined"
onClick={dialogclose}
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 700,
borderColor: DT.borderSubtle,
color: DT.textSecondary,
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
}}
>
Close
</Button>
<Button
fullWidth
variant="contained"
onClick={() => {
if (!kms) {
opentoast('Fill Kms', 'warning');
} else if (!cumulativekms) {
opentoast('Fill Actual Kms', 'warning');
} else if (!deliverylat) {
opentoast('Fill Delivery Latitude', 'warning');
} else if (!deliverylong) {
opentoast('Fill Delivery Longitude', 'warning');
} else if (!deliveryamount) {
opentoast('Fill Delivery Amount', 'warning');
} else {
updateDeliveryMutation.mutate({
deliveryid: currentorder.deliveryid,
orderheaderid: currentorder.orderheaderid,
orderstatus: updateStatus,
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
deliverylat,
deliverylong,
kms,
cumulativekms,
deliveryamt: parseFloat(deliveryamount),
notes,
userid: +selectedRow.userid
});
}
}}
sx={{
borderRadius: 999,
py: 1.1,
fontWeight: 800,
bgcolor: '#10b981',
boxShadow: `0 6px 18px ${ring('#10b981')}`,
'&:hover': { bgcolor: '#059669' }
}}
>
Update
</Button>
</Stack>
</DialogActions>
</Dialog>
</>
);
};
export default Deliveries;