1653 lines
64 KiB
JavaScript
1653 lines
64 KiB
JavaScript
import React, { useState, useEffect, useRef, Fragment } from 'react';
|
||
import axios from 'axios';
|
||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||
import { useTheme } from '@mui/material/styles';
|
||
import { enqueueSnackbar } from 'notistack';
|
||
|
||
import {
|
||
Avatar,
|
||
Box,
|
||
Button,
|
||
CircularProgress,
|
||
Dialog,
|
||
DialogContent,
|
||
Grid,
|
||
IconButton,
|
||
InputBase,
|
||
Paper,
|
||
Stack,
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableContainer,
|
||
TableHead,
|
||
TableRow,
|
||
Tooltip,
|
||
Typography,
|
||
Autocomplete,
|
||
TextField,
|
||
Skeleton,
|
||
Menu,
|
||
Switch
|
||
} from '@mui/material';
|
||
import {
|
||
MdLocalShipping,
|
||
MdHourglassEmpty,
|
||
MdCheckCircle,
|
||
MdCancel,
|
||
MdMyLocation,
|
||
MdPlace,
|
||
MdSearch,
|
||
MdClear,
|
||
MdCalendarMonth,
|
||
MdReceiptLong,
|
||
MdStraighten,
|
||
MdCurrencyRupee,
|
||
MdInventory2,
|
||
MdHistoryToggleOff,
|
||
MdAccessTime,
|
||
MdInsights,
|
||
MdLocalOffer,
|
||
MdAssignmentTurnedIn,
|
||
MdFilterList,
|
||
MdViewWeek,
|
||
MdRestartAlt,
|
||
MdLock,
|
||
MdDoneAll
|
||
} from 'react-icons/md';
|
||
|
||
import dayjs from 'dayjs';
|
||
var utc = require('dayjs/plugin/utc');
|
||
dayjs.extend(utc);
|
||
import { DateRangePicker } from 'mui-daterange-picker';
|
||
import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns';
|
||
|
||
import { fetchDeliverySummary, fetchorderdetails } from '../api/api';
|
||
import { CSVExport } from 'components/third-party/ReactTable';
|
||
import Loader from 'components/Loader';
|
||
import { useDebounce } from 'components/nearle_components/useDebounce';
|
||
import MapWithRoute from './mapWithRoute';
|
||
|
||
// ============================================================================
|
||
// Design tokens — shared with the rest of the redesigned operator pages.
|
||
// ============================================================================
|
||
const DT = {
|
||
radiusPill: 999,
|
||
radiusCard: 16,
|
||
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||
textPrimary: '#0f172a',
|
||
textSecondary: '#64748b',
|
||
textMuted: '#94a3b8',
|
||
borderSubtle: '#e2e8f0',
|
||
divider: '#f1f5f9',
|
||
surface: '#ffffff',
|
||
surfaceAlt: '#f8fafc'
|
||
};
|
||
const dtA = (c, suffix) => `${c}${suffix}`;
|
||
const tint = (c) => dtA(c, '08');
|
||
const soft = (c) => dtA(c, '18');
|
||
const ring = (c) => dtA(c, '26');
|
||
const edge = (c) => dtA(c, '55');
|
||
|
||
const BRAND = '#662582';
|
||
const BRAND_LIGHT = '#9255AB';
|
||
|
||
// Semantic per-status palette — drives both status badges and the timeline cells.
|
||
// Colors per brand standard: green=delivered, amber=pending, blue=processing,
|
||
// red=cancelled, dark-red=failed, purple=on-hold.
|
||
const STATUS_META = {
|
||
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
|
||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||
accepted: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
|
||
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdCheckCircle },
|
||
picked: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
|
||
active: { label: 'Active', color: '#0ea5e9', icon: MdLocalShipping },
|
||
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
|
||
onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
||
'on hold': { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||
skipped: { label: 'Skipped', color: '#f97316', icon: MdCancel },
|
||
failed: { label: 'Failed', color: '#991b1b', icon: MdCancel },
|
||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||
};
|
||
|
||
const STATUS_OPTIONS = [
|
||
{ id: 0, status: 'All', statusLow: 'All', color: BRAND, icon: MdInsights },
|
||
{ id: 1, status: 'Pending', statusLow: 'pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||
{ id: 2, status: 'Accepted', statusLow: 'accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
|
||
{ id: 3, status: 'Arrived', statusLow: 'arrived', color: '#06b6d4', icon: MdCheckCircle },
|
||
{ id: 4, status: 'Picked', statusLow: 'picked', color: '#8b5cf6', icon: MdLocalShipping },
|
||
{ id: 5, status: 'Active', statusLow: 'active', color: '#14b8a6', icon: MdLocalShipping },
|
||
{ id: 6, status: 'Delivered', statusLow: 'delivered', color: '#10b981', icon: MdCheckCircle },
|
||
{ id: 7, status: 'Skipped', statusLow: 'skipped', color: '#f97316', icon: MdCancel },
|
||
{ id: 8, status: 'Cancelled', statusLow: 'cancelled', color: '#ef4444', icon: MdCancel }
|
||
];
|
||
|
||
const SoftPaper = (props) => (
|
||
<Paper
|
||
{...props}
|
||
sx={{
|
||
mt: 0.75,
|
||
borderRadius: 2,
|
||
boxShadow: DT.shadowPop,
|
||
border: '1px solid',
|
||
borderColor: 'divider',
|
||
overflow: 'hidden'
|
||
}}
|
||
/>
|
||
);
|
||
|
||
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>
|
||
);
|
||
|
||
const pillFieldSx = (color) => ({
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: DT.radiusPill + 'px',
|
||
bgcolor: tint(color),
|
||
fontWeight: 600,
|
||
'& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
|
||
'&:hover fieldset': { borderColor: color },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||
'&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
|
||
}
|
||
});
|
||
|
||
// Filled status badge — high-contrast pill (white text on solid color).
|
||
const StatusBadge = ({ status }) => {
|
||
const meta = STATUS_META[String(status || '').toLowerCase()] || {
|
||
label: status || '—',
|
||
color: DT.textMuted,
|
||
icon: MdHistoryToggleOff
|
||
};
|
||
const Icon = meta.icon;
|
||
return (
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.5,
|
||
px: 1.125,
|
||
py: 0.375,
|
||
borderRadius: 999,
|
||
bgcolor: meta.color,
|
||
color: '#fff',
|
||
fontSize: 11,
|
||
fontWeight: 700,
|
||
letterSpacing: 0.2,
|
||
whiteSpace: 'nowrap',
|
||
minWidth: 86,
|
||
justifyContent: 'center',
|
||
boxShadow: `0 1px 2px ${ring(meta.color)}`
|
||
}}
|
||
>
|
||
<Icon size={12} /> {meta.label}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
const MetricPill = ({ value, color, icon, isMoney = false }) => {
|
||
const n = Number(value);
|
||
const display = isMoney ? formatNumberToRupees(n) : Number.isFinite(n) ? n : value || 0;
|
||
const isZero = !Number.isFinite(n) || n === 0;
|
||
if (isZero) {
|
||
return (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
|
||
{display}
|
||
</Typography>
|
||
);
|
||
}
|
||
return (
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.5,
|
||
px: 0.875,
|
||
py: 0.25,
|
||
borderRadius: 999,
|
||
bgcolor: tint(color),
|
||
border: `1px solid ${edge(color)}`,
|
||
color,
|
||
fontSize: 12,
|
||
fontWeight: 800,
|
||
whiteSpace: 'nowrap'
|
||
}}
|
||
>
|
||
{icon}
|
||
{display}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// Timeline cell — time dominant (bold/high-contrast), date secondary (muted).
|
||
// No decorative dot; the column header already names the event.
|
||
const TimelineCell = ({ value }) => {
|
||
if (!value) {
|
||
return (
|
||
<Typography sx={{ fontSize: 12, color: DT.textMuted, fontWeight: 700 }}>
|
||
—
|
||
</Typography>
|
||
);
|
||
}
|
||
return (
|
||
<Stack spacing={0} sx={{ lineHeight: 1.1 }}>
|
||
<Typography
|
||
sx={{
|
||
fontSize: 12.5,
|
||
fontWeight: 800,
|
||
color: DT.textPrimary,
|
||
letterSpacing: 0.1,
|
||
lineHeight: 1.15
|
||
}}
|
||
noWrap
|
||
>
|
||
{dayjs(value).format('hh:mm A')}
|
||
</Typography>
|
||
<Typography
|
||
sx={{
|
||
fontSize: 10.5,
|
||
fontWeight: 600,
|
||
color: DT.textMuted,
|
||
lineHeight: 1.2
|
||
}}
|
||
noWrap
|
||
>
|
||
{dayjs(value).format('DD MMM YYYY')}
|
||
</Typography>
|
||
</Stack>
|
||
);
|
||
};
|
||
|
||
function formatNumberToRupees(value) {
|
||
return new Intl.NumberFormat('en-IN', {
|
||
style: 'currency',
|
||
currency: 'INR',
|
||
minimumFractionDigits: 2
|
||
}).format(value || 0);
|
||
}
|
||
|
||
const opentoast = (message, variant, time) => {
|
||
enqueueSnackbar(message, {
|
||
variant: variant,
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: time ? time : 1500
|
||
});
|
||
};
|
||
|
||
// ==============================|| OrdersDetails ||============================== //
|
||
|
||
export default function OrdersDetails() {
|
||
const theme = useTheme();
|
||
const tenId = localStorage.getItem('tenantid');
|
||
const textFieldRef = useRef(null);
|
||
const loadMoreRef = useRef();
|
||
const containerRef = useRef();
|
||
|
||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [open, setOpen] = useState(false);
|
||
const [datestatus, setDatestatus] = useState('Today');
|
||
const [totalCharge, settotalCharge] = useState(0);
|
||
const [totalAmount, settotalAmount] = useState(0);
|
||
const [searchword, setSearchword] = useState('');
|
||
|
||
// Density toggle — operators choose how many rows to surface per screen.
|
||
// Default 'compact' (rows ≈ 32px) so the table starts dense like Linear / Vercel.
|
||
const [density, setDensity] = useState(() => {
|
||
try { return localStorage.getItem('ordersDetails.density') || 'compact'; } catch { return 'compact'; }
|
||
});
|
||
const isCompact = density === 'compact';
|
||
const rowPadY = isCompact ? 0.5 : 1;
|
||
useEffect(() => {
|
||
try { localStorage.setItem('ordersDetails.density', density); } catch {}
|
||
}, [density]);
|
||
|
||
// Column visibility — eliminates horizontal scroll on smaller desktops by letting
|
||
// operators hide columns they don't need. Persists per browser.
|
||
const ALL_COLUMNS = [
|
||
{ key: 'index', label: '#', group: 'Core', required: true, defaultVisible: true, width: 36 },
|
||
{ key: 'location', label: 'Location / Order', group: 'Core', required: true, defaultVisible: true, minWidth: 150 },
|
||
{ key: 'pickup', label: 'Pickup', group: 'Core', defaultVisible: true, minWidth: 140 },
|
||
{ key: 'drop', label: 'Drop', group: 'Core', defaultVisible: true, minWidth: 140 },
|
||
{ key: 'status', label: 'Status', group: 'Core', required: true, defaultVisible: true, width: 110 },
|
||
{ key: 'assigned', label: 'Assigned', group: 'Lifecycle', defaultVisible: true, minWidth: 110 },
|
||
{ key: 'accepted', label: 'Accepted', group: 'Lifecycle', defaultVisible: false, minWidth: 90 },
|
||
{ key: 'arrived', label: 'Arrived', group: 'Lifecycle', defaultVisible: false, minWidth: 90 },
|
||
{ key: 'picked', label: 'Picked', group: 'Lifecycle', defaultVisible: true, minWidth: 90 },
|
||
{ key: 'delivered', label: 'Delivered', group: 'Lifecycle', defaultVisible: true, minWidth: 90 },
|
||
{ key: 'cancelled', label: 'Cancelled', group: 'Lifecycle', defaultVisible: false, minWidth: 90 },
|
||
{ key: 'kms', label: 'Kms', group: 'Metrics', defaultVisible: true, width: 70, align: 'center' },
|
||
{ key: 'charges', label: 'Charges', group: 'Metrics', defaultVisible: true, width: 100, align: 'right' }
|
||
];
|
||
const COLUMN_GROUPS = ['Core', 'Lifecycle', 'Metrics'];
|
||
const COLUMN_DEFAULTS = ALL_COLUMNS.reduce((acc, c) => ({ ...acc, [c.key]: c.defaultVisible }), {});
|
||
const [colVis, setColVis] = useState(() => {
|
||
try {
|
||
const saved = JSON.parse(localStorage.getItem('ordersDetails.cols') || 'null');
|
||
return saved && typeof saved === 'object' ? { ...COLUMN_DEFAULTS, ...saved } : COLUMN_DEFAULTS;
|
||
} catch { return COLUMN_DEFAULTS; }
|
||
});
|
||
useEffect(() => {
|
||
try { localStorage.setItem('ordersDetails.cols', JSON.stringify(colVis)); } catch {}
|
||
}, [colVis]);
|
||
const isVisible = (key) => {
|
||
const col = ALL_COLUMNS.find((c) => c.key === key);
|
||
return col?.required || colVis[key] !== false;
|
||
};
|
||
const visibleCount = ALL_COLUMNS.filter((c) => isVisible(c.key)).length;
|
||
const hiddenCount = ALL_COLUMNS.length - visibleCount;
|
||
|
||
const [colMenuAnchor, setColMenuAnchor] = useState(null);
|
||
const toggleCol = (key) => setColVis((prev) => ({ ...prev, [key]: !(prev[key] !== false) }));
|
||
const resetCols = () => setColVis(COLUMN_DEFAULTS);
|
||
const showAllCols = () => setColVis(ALL_COLUMNS.reduce((acc, c) => ({ ...acc, [c.key]: true }), {}));
|
||
const isDefaultCols = ALL_COLUMNS.every((c) => isVisible(c.key) === !!c.defaultVisible);
|
||
|
||
const [riderCoordinates, setRiderCoordinates] = useState([]);
|
||
const [riderStart, setRiderStart] = useState();
|
||
const [riderEnd, setRiderEnd] = useState();
|
||
const [mapOpen, setMapOpen] = useState(false);
|
||
const [mapTenant, setMapTenant] = useState({});
|
||
|
||
const [currentStatus, setCurrentStatus] = useState('All');
|
||
const [statusCount, setStatusCount] = useState(0);
|
||
const [statusValue, setStatusValue] = useState(STATUS_OPTIONS[0]);
|
||
|
||
const [tenantLocations, setTenantlocations] = useState([]);
|
||
const [locationId, setLocationId] = useState(0);
|
||
const [locoName, setLocoName] = useState('All Locations');
|
||
const [selectedLocation, setSelectedLocation] = useState(null);
|
||
|
||
const debouncedSearch = useDebounce(searchword, 500);
|
||
|
||
// ============================================= || gettenantlocations || =============================================
|
||
const gettenantlocations = async (id) => {
|
||
try {
|
||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
|
||
setTenantlocations(res.data.details || []);
|
||
} catch (err) {
|
||
console.log('gettenantlocations', err);
|
||
}
|
||
};
|
||
useEffect(() => {
|
||
gettenantlocations(tenId);
|
||
}, []);
|
||
|
||
// ============================================= || getdeliverylogs (for map) || =============================================
|
||
const getdeliverylogs = async (id) => {
|
||
try {
|
||
const res = await axios.get(`${process.env.REACT_APP_URL}/deliveries/getdeliverylogs/?deliveryid=${id}`);
|
||
const datas = res.data.details;
|
||
if (datas.length != 0) {
|
||
setRiderStart(datas[0].logdate);
|
||
setRiderEnd(datas[datas.length - 1].logdate);
|
||
const coData = datas.map((data) => ({ lat: data.latitude, lng: data.longitude }));
|
||
setRiderCoordinates(coData);
|
||
} else {
|
||
opentoast('No Logs Found ', 'error', 2000);
|
||
}
|
||
} catch (error) {
|
||
console.log('getdeliverylogs', error);
|
||
}
|
||
};
|
||
|
||
// ============================================= || ctrl/cmd+k focuses search || =============================================
|
||
useEffect(() => {
|
||
const handleKeyPress = (event) => {
|
||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||
event.preventDefault();
|
||
textFieldRef.current && textFieldRef.current.focus();
|
||
}
|
||
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
|
||
textFieldRef.current.blur();
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleKeyPress);
|
||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||
}, []);
|
||
|
||
// ============================================= || fetchorderdetails (infinite) || =============================================
|
||
const {
|
||
data: rowdata,
|
||
isError: isErrorOrderDetails,
|
||
error: orderDetailsError,
|
||
fetchNextPage,
|
||
isLoading: isLoadingOrderDetails,
|
||
hasNextPage,
|
||
isFetchingNextPage
|
||
} = useInfiniteQuery({
|
||
queryKey: [startdate, enddate, currentStatus, locationId, debouncedSearch],
|
||
queryFn: fetchorderdetails,
|
||
getNextPageParam: (lastPage) => lastPage.nextPage
|
||
});
|
||
|
||
const rows = rowdata?.pages.flatMap((page) => page.details) || [];
|
||
|
||
useEffect(() => {
|
||
if (!hasNextPage) return;
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
if (entries[0].isIntersecting) {
|
||
fetchNextPage();
|
||
}
|
||
},
|
||
{
|
||
// The page (viewport) is now the scroll container, not the table.
|
||
// Prefetch the next page ~400px before the sentinel reaches the bottom.
|
||
root: null,
|
||
rootMargin: '0px 0px 400px 0px',
|
||
threshold: 0
|
||
}
|
||
);
|
||
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
||
return () => {
|
||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||
};
|
||
}, [hasNextPage, fetchNextPage]);
|
||
|
||
const handleScroll = (event) => {
|
||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||
if (hasNextPage && !isFetchingNextPage) {
|
||
fetchNextPage();
|
||
}
|
||
}
|
||
};
|
||
|
||
// ============================================= || fetchDeliverySummary || =============================================
|
||
const { data: deliverycount } = useQuery({
|
||
queryKey: ['deliverycount', startdate, enddate, currentStatus, locationId],
|
||
queryFn: fetchDeliverySummary
|
||
});
|
||
|
||
useEffect(() => {
|
||
const map = {
|
||
All: deliverycount?.total,
|
||
pending: deliverycount?.pending,
|
||
accepted: deliverycount?.accepted,
|
||
arrived: deliverycount?.arrived,
|
||
picked: deliverycount?.picked,
|
||
active: deliverycount?.active,
|
||
delivered: deliverycount?.delivered,
|
||
cancelled: deliverycount?.cancelled
|
||
};
|
||
setStatusCount(map[currentStatus] ?? 0);
|
||
}, [currentStatus, deliverycount]);
|
||
|
||
// ============================================= || calculate totals || =============================================
|
||
useEffect(() => {
|
||
let totalC = 0;
|
||
let totalA = 0;
|
||
rows &&
|
||
rows.forEach((row) => {
|
||
totalC += row.deliverycharges;
|
||
totalA += row.deliveryamt;
|
||
});
|
||
settotalCharge(totalC);
|
||
settotalAmount(totalA);
|
||
}, [rows]);
|
||
|
||
if (isErrorOrderDetails) return 'An error has occurred:(isErrorOrderDetails) ' + orderDetailsError.message;
|
||
|
||
// CSV export rows.
|
||
const csvData = rows.map((order) => ({
|
||
tenantname: order.tenantname,
|
||
tenantcity: order.tenantcity,
|
||
tenantcontactno: order.tenantcontactno,
|
||
rider: order.rider,
|
||
orderid: order.orderid,
|
||
orderdate: order.orderdate,
|
||
deliverydate: order.deliverydate,
|
||
orderstatus: order.orderstatus,
|
||
deliverystatus: order.deliverystatus,
|
||
ordernotes: order.ordernotes,
|
||
kms: order.kms,
|
||
actualkms: order.actualkms,
|
||
assigntime: order.assigntime,
|
||
starttime: order.starttime,
|
||
arrivaltime: order.arrivaltime,
|
||
pickuptime: order.pickuptime,
|
||
deliverytime: order.deliverytime,
|
||
canceltime: order.canceltime,
|
||
deliverycharge: order.deliverycharge,
|
||
deliveryamt: order.deliveryamt,
|
||
pickupcustomer: order.pickupcustomer,
|
||
pickupcontactno: order.pickupcontactno,
|
||
pickupaddress: order.pickupaddress,
|
||
pickupsuburb: order.pickupsuburb,
|
||
pickupcity: order.pickupcity,
|
||
pickuplat: order.pickuplat,
|
||
pickuplong: order.pickuplong,
|
||
deliverycustomer: order.deliverycustomer,
|
||
deliverycontactno: order.deliverycontactno,
|
||
deliveryaddress: order.deliveryaddress,
|
||
deliverysuburb: order.deliverysuburb,
|
||
deliverylat: order.deliverylat,
|
||
deliverylong: order.deliverylong,
|
||
locationname: order.locationname,
|
||
locationsuburb: order.locationsuburb,
|
||
locationcity: order.locationcity,
|
||
locationcontactno: order.locationcontactno
|
||
}));
|
||
|
||
// KPI tiles row — clearer financial terminology with tooltips.
|
||
const kpiCards = [
|
||
{ key: 'total', label: `${currentStatus === 'All' ? 'Total' : currentStatus} Orders`, color: BRAND, icon: MdInsights, value: statusCount, hint: 'Orders matching the current filters.' },
|
||
{ key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, value: deliverycount?.pending ?? 0, hint: 'Orders awaiting rider assignment or pickup.' },
|
||
{ key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, value: deliverycount?.delivered ?? 0, hint: 'Successfully delivered orders.' },
|
||
{ key: 'charges', label: 'Delivery Charges', color: '#6366f1', icon: MdLocalOffer, value: totalCharge, isMoney: true, hint: 'Total platform / delivery fees billed across the filtered orders.' },
|
||
{ key: 'amount', label: 'Order Value', color: '#10b981', icon: MdCurrencyRupee, value: totalAmount, isMoney: true, hint: 'Total customer order value across the filtered orders.' }
|
||
];
|
||
|
||
return (
|
||
<>
|
||
{isLoadingOrderDetails && <Loader />}
|
||
|
||
{/* ============================================= || Header (compact, standardized actions) || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
mb: { xs: 1, md: 1.25 },
|
||
px: { xs: 1.5, sm: 2 },
|
||
py: { xs: 1, sm: 1.25 },
|
||
borderRadius: 2,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||
boxShadow: DT.shadowMd
|
||
}}
|
||
>
|
||
<Stack
|
||
direction={{ xs: 'column', sm: 'row' }}
|
||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||
justifyContent="space-between"
|
||
spacing={{ xs: 1, sm: 1.5 }}
|
||
>
|
||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||
<Avatar
|
||
variant="rounded"
|
||
sx={{
|
||
width: 36,
|
||
height: 36,
|
||
bgcolor: BRAND,
|
||
color: '#fff',
|
||
borderRadius: 1.5,
|
||
boxShadow: `0 4px 12px ${ring(BRAND)}`
|
||
}}
|
||
>
|
||
<MdReceiptLong size={19} />
|
||
</Avatar>
|
||
<Stack spacing={0.125}>
|
||
<Typography
|
||
variant="h3"
|
||
sx={{
|
||
fontWeight: 800,
|
||
color: DT.textPrimary,
|
||
lineHeight: 1.1,
|
||
fontSize: { xs: '1.1rem', sm: '1.25rem', md: '1.375rem' }
|
||
}}
|
||
>
|
||
Orders Details
|
||
</Typography>
|
||
<Stack direction="row" alignItems="center" spacing={0.75}>
|
||
<Box
|
||
sx={{
|
||
width: 7,
|
||
height: 7,
|
||
borderRadius: '50%',
|
||
bgcolor: '#10b981',
|
||
boxShadow: '0 0 0 3px rgba(16,185,129,0.18)'
|
||
}}
|
||
/>
|
||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600 }}>
|
||
Live · {locoName} · {datestatus}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</Stack>
|
||
|
||
{/* Action bar — unified button system: 32px height, 8px radius, identical typography */}
|
||
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap>
|
||
<Tooltip title={isCompact ? 'Switch to comfortable density' : 'Switch to compact density'} arrow>
|
||
<Button
|
||
onClick={() => setDensity((d) => (d === 'compact' ? 'comfortable' : 'compact'))}
|
||
disableElevation
|
||
variant="outlined"
|
||
size="small"
|
||
startIcon={<MdFilterList size={15} style={{ transform: isCompact ? 'none' : 'scaleY(1.4)' }} />}
|
||
sx={{
|
||
height: 32,
|
||
px: 1.25,
|
||
borderRadius: 1.5,
|
||
textTransform: 'none',
|
||
fontSize: 12.5,
|
||
fontWeight: 700,
|
||
bgcolor: '#fff',
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textPrimary,
|
||
'&:hover': {
|
||
bgcolor: '#fff',
|
||
borderColor: BRAND,
|
||
color: BRAND,
|
||
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||
}
|
||
}}
|
||
>
|
||
{isCompact ? 'Compact' : 'Comfortable'}
|
||
</Button>
|
||
</Tooltip>
|
||
|
||
<Tooltip title="Show or hide columns" arrow>
|
||
<Button
|
||
onClick={(e) => setColMenuAnchor(e.currentTarget)}
|
||
disableElevation
|
||
variant="outlined"
|
||
size="small"
|
||
startIcon={<MdViewWeek size={15} />}
|
||
endIcon={
|
||
hiddenCount > 0 ? (
|
||
<Box
|
||
component="span"
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
minWidth: 30,
|
||
height: 18,
|
||
px: 0.5,
|
||
borderRadius: 999,
|
||
bgcolor: soft(BRAND),
|
||
color: BRAND,
|
||
fontSize: 10.5,
|
||
fontWeight: 800,
|
||
lineHeight: 1
|
||
}}
|
||
>
|
||
{visibleCount}/{ALL_COLUMNS.length}
|
||
</Box>
|
||
) : null
|
||
}
|
||
sx={{
|
||
height: 32,
|
||
px: 1.25,
|
||
borderRadius: 1.5,
|
||
textTransform: 'none',
|
||
fontSize: 12.5,
|
||
fontWeight: 700,
|
||
bgcolor: hiddenCount > 0 ? tint(BRAND) : '#fff',
|
||
borderColor: hiddenCount > 0 ? edge(BRAND) : DT.borderSubtle,
|
||
color: hiddenCount > 0 ? BRAND : DT.textPrimary,
|
||
'&:hover': {
|
||
bgcolor: hiddenCount > 0 ? tint(BRAND) : '#fff',
|
||
borderColor: BRAND,
|
||
color: BRAND,
|
||
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||
}
|
||
}}
|
||
>
|
||
Columns
|
||
</Button>
|
||
</Tooltip>
|
||
|
||
<Tooltip title="Filter by date range" arrow>
|
||
<Button
|
||
onClick={() => setOpen(true)}
|
||
disableElevation
|
||
variant="outlined"
|
||
size="small"
|
||
startIcon={<MdCalendarMonth size={15} />}
|
||
sx={{
|
||
height: 32,
|
||
px: 1.25,
|
||
borderRadius: 1.5,
|
||
textTransform: 'none',
|
||
fontSize: 12.5,
|
||
fontWeight: 700,
|
||
letterSpacing: 0.1,
|
||
bgcolor: '#fff',
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textPrimary,
|
||
'&:hover': {
|
||
bgcolor: '#fff',
|
||
borderColor: BRAND,
|
||
color: BRAND,
|
||
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||
},
|
||
'&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||
}}
|
||
>
|
||
{startdate && enddate
|
||
? `${dayjs(startdate).format('DD MMM')} – ${dayjs(enddate).format('DD MMM')}`
|
||
: 'All time'}
|
||
</Button>
|
||
</Tooltip>
|
||
|
||
{/* Download — CSVExport already renders its own Button; style it directly so
|
||
only ONE button paints in the action bar. Matches the contained brand look. */}
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
'& a': { textDecoration: 'none', lineHeight: 0 }
|
||
}}
|
||
>
|
||
<CSVExport
|
||
data={csvData}
|
||
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
|
||
label="Download"
|
||
style={{
|
||
height: 32,
|
||
minHeight: 32,
|
||
px: 1.5,
|
||
borderRadius: 1.5,
|
||
bgcolor: BRAND,
|
||
color: '#fff',
|
||
textTransform: 'none',
|
||
fontSize: 12.5,
|
||
fontWeight: 700,
|
||
letterSpacing: 0.1,
|
||
boxShadow: `0 2px 6px ${ring(BRAND)}`,
|
||
'&:hover': { bgcolor: BRAND_LIGHT, boxShadow: `0 4px 12px ${ring(BRAND)}` },
|
||
'&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` },
|
||
'& .MuiButton-startIcon': { mr: 0.75, '& svg': { fontSize: 16 } }
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Stack>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
{/* ============================================= || KPI Cards (compact) || ============================================= */}
|
||
<Grid container spacing={{ xs: 1, sm: 1.25, md: 1.5 }}>
|
||
{kpiCards.map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<Grid item key={item.key} xs={6} sm={4} md={item.isMoney ? 3 : 2}>
|
||
<Tooltip title={item.hint || ''} placement="top" arrow>
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
px: { xs: 1.25, sm: 1.5 },
|
||
py: { xs: 0.875, sm: 1.125 },
|
||
borderRadius: 2,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
background: '#fff',
|
||
transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s',
|
||
'&:hover': {
|
||
transform: 'translateY(-1px)',
|
||
boxShadow: DT.shadowMd,
|
||
borderColor: edge(item.color)
|
||
}
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
bottom: 0,
|
||
width: 3,
|
||
background: item.color
|
||
}}
|
||
/>
|
||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1} sx={{ pl: 0.5 }}>
|
||
<Stack spacing={0.125} sx={{ minWidth: 0, flex: 1 }}>
|
||
<Typography
|
||
sx={{
|
||
color: DT.textSecondary,
|
||
fontWeight: 700,
|
||
letterSpacing: 0.4,
|
||
textTransform: 'uppercase',
|
||
fontSize: 10.5,
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
lineHeight: 1.2
|
||
}}
|
||
>
|
||
{item.label}
|
||
</Typography>
|
||
<Typography
|
||
sx={{
|
||
fontWeight: 800,
|
||
color: DT.textPrimary,
|
||
lineHeight: 1.15,
|
||
fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' }
|
||
}}
|
||
noWrap
|
||
>
|
||
{isLoadingOrderDetails && !item.value ? (
|
||
<Skeleton sx={{ width: 40 }} animation="wave" />
|
||
) : item.isMoney ? (
|
||
formatNumberToRupees(item.value)
|
||
) : (
|
||
item.value ?? 0
|
||
)}
|
||
</Typography>
|
||
</Stack>
|
||
<Avatar
|
||
variant="rounded"
|
||
sx={{
|
||
width: 30,
|
||
height: 30,
|
||
bgcolor: soft(item.color),
|
||
color: item.color,
|
||
borderRadius: 1.25,
|
||
flexShrink: 0
|
||
}}
|
||
>
|
||
<Icon size={15} />
|
||
</Avatar>
|
||
</Stack>
|
||
</Paper>
|
||
</Tooltip>
|
||
</Grid>
|
||
);
|
||
})}
|
||
</Grid>
|
||
|
||
{/* ============================================= || Filter Bar (compact) || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
mt: { xs: 1, md: 1.25 },
|
||
p: { xs: 1, md: 1.125 },
|
||
borderRadius: 2,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
background: '#fff',
|
||
boxShadow: DT.shadowSoft
|
||
}}
|
||
>
|
||
<Grid container spacing={1} alignItems="center">
|
||
{/* Location filter */}
|
||
<Grid item xs={12} sm={6} md={4}>
|
||
{tenantLocations.length === 1 ? (
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.75,
|
||
px: 1.5,
|
||
py: 0.875,
|
||
borderRadius: 999,
|
||
bgcolor: tint(BRAND),
|
||
border: `1.5px solid ${edge(BRAND)}`,
|
||
color: BRAND,
|
||
fontWeight: 800,
|
||
fontSize: 13,
|
||
width: '100%'
|
||
}}
|
||
>
|
||
<MdMyLocation size={14} /> {tenantLocations[0].locationname}
|
||
</Box>
|
||
) : (
|
||
<Autocomplete
|
||
fullWidth
|
||
options={tenantLocations || []}
|
||
value={selectedLocation}
|
||
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
|
||
PaperComponent={SoftPaper}
|
||
onChange={(event, value, reason) => {
|
||
if (value) {
|
||
setSelectedLocation(value);
|
||
setLocationId(value.locationid);
|
||
setLocoName(value.locationname);
|
||
}
|
||
if (reason === 'clear') {
|
||
setSelectedLocation(null);
|
||
setLocationId(0);
|
||
setLocoName('All Locations');
|
||
}
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="All Locations"
|
||
size="small"
|
||
sx={pillFieldSx('#10b981')}
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
|
||
<AccentAvatar color="#10b981" size={22} selected>
|
||
<MdPlace size={13} />
|
||
</AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
)}
|
||
</Grid>
|
||
|
||
{/* Status filter */}
|
||
<Grid item xs={12} sm={6} md={4}>
|
||
<Autocomplete
|
||
fullWidth
|
||
options={STATUS_OPTIONS}
|
||
value={statusValue}
|
||
getOptionLabel={(option) => option?.status || ''}
|
||
PaperComponent={SoftPaper}
|
||
onChange={(event, value, reason) => {
|
||
if (reason === 'clear') {
|
||
setCurrentStatus('All');
|
||
setStatusValue(STATUS_OPTIONS[0]);
|
||
} else {
|
||
setCurrentStatus(value.statusLow);
|
||
setStatusValue(value);
|
||
}
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="Status"
|
||
size="small"
|
||
sx={pillFieldSx(statusValue?.color || BRAND)}
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
|
||
<AccentAvatar color={statusValue?.color || BRAND} size={22} selected>
|
||
{statusValue?.icon ? React.createElement(statusValue.icon, { size: 13 }) : <MdFilterList size={13} />}
|
||
</AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
</Grid>
|
||
|
||
{/* Search */}
|
||
<Grid item xs={12} sm={12} md={4}>
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 0.75,
|
||
px: 1.25,
|
||
py: 0.75,
|
||
borderRadius: 999,
|
||
bgcolor: tint(BRAND),
|
||
border: `1.5px solid ${edge(BRAND)}`,
|
||
transition: 'all 0.18s',
|
||
'&:focus-within': {
|
||
borderColor: BRAND,
|
||
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||
}
|
||
}}
|
||
>
|
||
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
||
<InputBase
|
||
inputRef={textFieldRef}
|
||
placeholder="Search (ctrl+k)"
|
||
value={searchword}
|
||
onChange={(e) => setSearchword(e.target.value)}
|
||
autoComplete="off"
|
||
sx={{
|
||
flex: 1,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
color: DT.textPrimary,
|
||
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
|
||
}}
|
||
/>
|
||
{searchword && (
|
||
<IconButton size="small" onClick={() => setSearchword('')} sx={{ p: 0.25, color: BRAND }}>
|
||
<MdClear size={14} />
|
||
</IconButton>
|
||
)}
|
||
</Box>
|
||
</Grid>
|
||
</Grid>
|
||
</Paper>
|
||
|
||
{/* ============================================= || Table (dense, sticky header, no h-scroll) || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
mt: { xs: 1, md: 1.25 },
|
||
borderRadius: 2,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
background: '#fff'
|
||
}}
|
||
>
|
||
<TableContainer
|
||
ref={containerRef}
|
||
onScroll={handleScroll}
|
||
sx={{
|
||
// Single page scroll: the table is NOT height-capped, so it renders at its
|
||
// full height and the whole page scrolls as one. Scrolling down moves the
|
||
// KPI cards + header + filter bar off-screen and reveals the full table.
|
||
// Only horizontal overflow scrolls inside the container (for wide column sets).
|
||
overflowX: 'auto',
|
||
overflowY: 'visible',
|
||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||
'&::-webkit-scrollbar-thumb': {
|
||
backgroundColor: edge(BRAND),
|
||
borderRadius: 8,
|
||
'&:hover': { backgroundColor: BRAND }
|
||
},
|
||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
<Table
|
||
stickyHeader
|
||
size="small"
|
||
sx={{
|
||
// minWidth shrinks as columns are hidden, so horizontal scroll disappears entirely
|
||
// once the visible column set fits the viewport. Sum of declared widths.
|
||
minWidth: ALL_COLUMNS.filter((c) => isVisible(c.key))
|
||
.reduce((acc, c) => acc + (c.width || c.minWidth || 100), 0),
|
||
tableLayout: 'auto'
|
||
}}
|
||
>
|
||
<TableHead>
|
||
<TableRow
|
||
sx={{
|
||
'& th': {
|
||
backgroundColor: DT.surfaceAlt,
|
||
color: DT.textSecondary,
|
||
fontSize: 10.5,
|
||
fontWeight: 800,
|
||
letterSpacing: 0.5,
|
||
textTransform: 'uppercase',
|
||
whiteSpace: 'nowrap',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||
py: 0.75,
|
||
px: 1
|
||
}
|
||
}}
|
||
>
|
||
{isVisible('index') && <TableCell sx={{ width: 36 }}>#</TableCell>}
|
||
{isVisible('location') && <TableCell sx={{ minWidth: 150 }}>Location / Order</TableCell>}
|
||
{isVisible('pickup') && <TableCell sx={{ minWidth: 140 }}>Pickup</TableCell>}
|
||
{isVisible('drop') && <TableCell sx={{ minWidth: 140 }}>Drop</TableCell>}
|
||
{isVisible('status') && <TableCell sx={{ width: 110 }}>Status</TableCell>}
|
||
{isVisible('assigned') && <TableCell sx={{ minWidth: 110 }}>Assigned</TableCell>}
|
||
{isVisible('accepted') && <TableCell sx={{ minWidth: 90 }}>Accepted</TableCell>}
|
||
{isVisible('arrived') && <TableCell sx={{ minWidth: 90 }}>Arrived</TableCell>}
|
||
{isVisible('picked') && <TableCell sx={{ minWidth: 90 }}>Picked</TableCell>}
|
||
{isVisible('delivered') && <TableCell sx={{ minWidth: 90 }}>Delivered</TableCell>}
|
||
{isVisible('cancelled') && <TableCell sx={{ minWidth: 90 }}>Cancelled</TableCell>}
|
||
{isVisible('kms') && <TableCell align="center" sx={{ width: 70 }}>Kms</TableCell>}
|
||
{isVisible('charges') && <TableCell align="right" sx={{ width: 100 }}>Charges</TableCell>}
|
||
</TableRow>
|
||
</TableHead>
|
||
|
||
<TableBody>
|
||
{rows?.length === 0 && !isLoadingOrderDetails && (
|
||
<TableRow>
|
||
<TableCell colSpan={visibleCount} sx={{ py: 7, borderBottom: 'none' }}>
|
||
<Stack alignItems="center" spacing={1.25}>
|
||
<Avatar
|
||
sx={{
|
||
width: 56,
|
||
height: 56,
|
||
bgcolor: soft('#94a3b8'),
|
||
color: DT.textMuted,
|
||
borderRadius: 2
|
||
}}
|
||
variant="rounded"
|
||
>
|
||
<MdReceiptLong size={26} />
|
||
</Avatar>
|
||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>
|
||
No orders match these filters
|
||
</Typography>
|
||
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
||
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
|
||
</Typography>
|
||
{searchword && (
|
||
<Button
|
||
size="small"
|
||
onClick={() => setSearchword('')}
|
||
sx={{
|
||
mt: 0.5,
|
||
height: 28,
|
||
textTransform: 'none',
|
||
fontSize: 12,
|
||
fontWeight: 700,
|
||
color: BRAND,
|
||
borderRadius: 1.25,
|
||
'&:hover': { bgcolor: tint(BRAND) }
|
||
}}
|
||
startIcon={<MdClear size={14} />}
|
||
>
|
||
Clear search
|
||
</Button>
|
||
)}
|
||
</Stack>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
|
||
{isLoadingOrderDetails &&
|
||
rows.length === 0 &&
|
||
Array.from({ length: 10 }).map((_, idx) => (
|
||
<TableRow key={`sk-${idx}`}>
|
||
{Array.from({ length: visibleCount }).map((__, ci) => (
|
||
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
||
<Skeleton animation="wave" height={20} />
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))}
|
||
|
||
{rows.map((row, index) => (
|
||
<TableRow
|
||
key={`${row.orderid}-${index}`}
|
||
sx={{
|
||
cursor: 'pointer',
|
||
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||
'& td': {
|
||
borderBottom: `1px solid ${DT.divider}`,
|
||
py: rowPadY,
|
||
px: 1,
|
||
verticalAlign: 'top'
|
||
},
|
||
'&:hover': {
|
||
backgroundColor: tint(BRAND),
|
||
boxShadow: `inset 3px 0 0 ${BRAND}`
|
||
}
|
||
}}
|
||
>
|
||
{isVisible('index') && (
|
||
<TableCell>
|
||
<Typography sx={{ fontWeight: 700, fontSize: 12, color: DT.textMuted }}>{index + 1}</Typography>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Location */}
|
||
{isVisible('location') && (
|
||
<TableCell>
|
||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||
{row.locationname}
|
||
</Typography>
|
||
<Tooltip title="Order Id">
|
||
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||
{row.orderid}
|
||
</Typography>
|
||
</Tooltip>
|
||
<Tooltip title="Delivery Date">
|
||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.125 }}>
|
||
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
||
</Typography>
|
||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
||
· {dayjs(row.deliverydate).utc().format('DD MMM YY')}
|
||
</Typography>
|
||
</Stack>
|
||
</Tooltip>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Pickup */}
|
||
{isVisible('pickup') && (
|
||
<TableCell>
|
||
<Stack spacing={0.125}>
|
||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||
{row.pickupcustomer}
|
||
</Typography>
|
||
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||
{row.pickupcontactno}
|
||
</Typography>
|
||
<Tooltip title={row.Pickupaddress || row.pickupaddress}>
|
||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||
{row.pickupsuburb ||
|
||
row.pickuplocation ||
|
||
(row.Pickupaddress ? `${row.Pickupaddress.slice(0, 20)}…` : '—')}
|
||
</Typography>
|
||
</Tooltip>
|
||
</Stack>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Drop */}
|
||
{isVisible('drop') && (
|
||
<TableCell>
|
||
<Stack spacing={0.125}>
|
||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||
{row.deliverycustomer}
|
||
</Typography>
|
||
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||
{row.deliverycontactno}
|
||
</Typography>
|
||
<Tooltip title={row.deliveryaddress}>
|
||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||
{row.deliverysuburb ||
|
||
row.deliverylocation ||
|
||
(row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')}
|
||
</Typography>
|
||
</Tooltip>
|
||
</Stack>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Order Status */}
|
||
{isVisible('status') && (
|
||
<TableCell>
|
||
<StatusBadge status={row.orderstatus} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Assigned */}
|
||
{isVisible('assigned') && (
|
||
<TableCell>
|
||
<Stack spacing={0.25}>
|
||
{row.ridername && (
|
||
<Typography sx={{ fontSize: 11, fontWeight: 800, color: BRAND }} noWrap>
|
||
{row.ridername}
|
||
</Typography>
|
||
)}
|
||
<TimelineCell value={row.assigntime} />
|
||
</Stack>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Accepted */}
|
||
{isVisible('accepted') && (
|
||
<TableCell>
|
||
<TimelineCell value={row.starttime} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Arrived */}
|
||
{isVisible('arrived') && (
|
||
<TableCell>
|
||
<TimelineCell value={row.arrivaltime} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Picked */}
|
||
{isVisible('picked') && (
|
||
<TableCell>
|
||
<TimelineCell value={row.pickuptime} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Delivered */}
|
||
{isVisible('delivered') && (
|
||
<TableCell>
|
||
<TimelineCell value={row.deliverytime} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Cancelled */}
|
||
{isVisible('cancelled') && (
|
||
<TableCell>
|
||
<TimelineCell value={row.canceltime} />
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Kms */}
|
||
{isVisible('kms') && (
|
||
<TableCell align="center">
|
||
<MetricPill
|
||
value={
|
||
row.orderstatus === 'cancelled' || row.kms === ''
|
||
? 0
|
||
: Number(parseFloat(row.kms || 0).toFixed(1))
|
||
}
|
||
color="#f59e0b"
|
||
icon={<MdStraighten size={11} />}
|
||
/>
|
||
</TableCell>
|
||
)}
|
||
|
||
{/* Charges */}
|
||
{isVisible('charges') && (
|
||
<TableCell align="right">
|
||
<MetricPill
|
||
value={
|
||
row.orderstatus === 'cancelled' || row.deliverycharges === ''
|
||
? 0
|
||
: Number(row.deliverycharges)
|
||
}
|
||
color={BRAND}
|
||
icon={<MdCurrencyRupee size={11} />}
|
||
isMoney
|
||
/>
|
||
</TableCell>
|
||
)}
|
||
</TableRow>
|
||
))}
|
||
|
||
{rows.length !== 0 && (
|
||
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
||
<TableCell colSpan={visibleCount} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
||
<Stack
|
||
ref={loadMoreRef}
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
spacing={1}
|
||
sx={{ minHeight: 32 }}
|
||
>
|
||
{isFetchingNextPage || hasNextPage ? (
|
||
<>
|
||
<CircularProgress size={14} sx={{ color: BRAND }} />
|
||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>
|
||
Loading more orders…
|
||
</Typography>
|
||
</>
|
||
) : (
|
||
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
|
||
{rows.length} order{rows.length === 1 ? '' : 's'} · End of list
|
||
</Typography>
|
||
)}
|
||
</Stack>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
</Paper>
|
||
|
||
{/* ============================================= || Columns Visibility Menu || ============================================= */}
|
||
<Menu
|
||
anchorEl={colMenuAnchor}
|
||
open={Boolean(colMenuAnchor)}
|
||
onClose={() => setColMenuAnchor(null)}
|
||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||
slotProps={{
|
||
paper: {
|
||
sx: {
|
||
mt: 0.75,
|
||
width: 300,
|
||
borderRadius: 2.5,
|
||
border: `1px solid ${DT.borderSubtle}`,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden'
|
||
}
|
||
}
|
||
}}
|
||
MenuListProps={{ sx: { py: 0 } }}
|
||
>
|
||
{/* Header — title, live count, quick actions */}
|
||
<Box
|
||
sx={{
|
||
px: 1.5,
|
||
pt: 1.25,
|
||
pb: 1.125,
|
||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||
borderBottom: `1px solid ${DT.borderSubtle}`
|
||
}}
|
||
>
|
||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<Avatar variant="rounded" sx={{ width: 28, height: 28, bgcolor: BRAND, color: '#fff', borderRadius: 1.25 }}>
|
||
<MdViewWeek size={15} />
|
||
</Avatar>
|
||
<Stack spacing={0}>
|
||
<Typography sx={{ fontSize: 13, fontWeight: 800, color: DT.textPrimary, lineHeight: 1.15 }}>
|
||
Columns
|
||
</Typography>
|
||
<Typography sx={{ fontSize: 10.5, fontWeight: 700, color: DT.textSecondary, lineHeight: 1.2 }}>
|
||
{visibleCount} of {ALL_COLUMNS.length} shown
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</Stack>
|
||
|
||
<Stack direction="row" spacing={0.75} sx={{ mt: 1.125 }}>
|
||
<Button
|
||
size="small"
|
||
onClick={showAllCols}
|
||
disabled={hiddenCount === 0}
|
||
startIcon={<MdDoneAll size={13} />}
|
||
sx={{
|
||
flex: 1,
|
||
height: 28,
|
||
px: 1,
|
||
fontSize: 11.5,
|
||
fontWeight: 800,
|
||
textTransform: 'none',
|
||
borderRadius: 1.25,
|
||
color: BRAND,
|
||
bgcolor: '#fff',
|
||
border: `1px solid ${edge(BRAND)}`,
|
||
'&:hover': { bgcolor: tint(BRAND), borderColor: BRAND },
|
||
'&.Mui-disabled': { color: DT.textMuted, bgcolor: '#fff', borderColor: DT.borderSubtle, opacity: 0.7 }
|
||
}}
|
||
>
|
||
Show all
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
onClick={resetCols}
|
||
disabled={isDefaultCols}
|
||
startIcon={<MdRestartAlt size={14} />}
|
||
sx={{
|
||
flex: 1,
|
||
height: 28,
|
||
px: 1,
|
||
fontSize: 11.5,
|
||
fontWeight: 800,
|
||
textTransform: 'none',
|
||
borderRadius: 1.25,
|
||
color: DT.textSecondary,
|
||
bgcolor: '#fff',
|
||
border: `1px solid ${DT.borderSubtle}`,
|
||
'&:hover': { bgcolor: DT.surfaceAlt, borderColor: DT.textMuted, color: DT.textPrimary },
|
||
'&.Mui-disabled': { color: DT.textMuted, bgcolor: '#fff', borderColor: DT.borderSubtle, opacity: 0.7 }
|
||
}}
|
||
>
|
||
Reset
|
||
</Button>
|
||
</Stack>
|
||
</Box>
|
||
|
||
{/* Grouped, switch-driven column list */}
|
||
<Box sx={{ maxHeight: 340, overflowY: 'auto', py: 0.5 }}>
|
||
{COLUMN_GROUPS.map((group) => {
|
||
const cols = ALL_COLUMNS.filter((c) => c.group === group);
|
||
const shownInGroup = cols.filter((c) => isVisible(c.key)).length;
|
||
return (
|
||
<Box key={group} sx={{ px: 0.5, pb: 0.25 }}>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="space-between"
|
||
sx={{ px: 1, pt: 0.875, pb: 0.375 }}
|
||
>
|
||
<Typography
|
||
sx={{ fontSize: 10, fontWeight: 800, color: DT.textMuted, letterSpacing: 0.7, textTransform: 'uppercase' }}
|
||
>
|
||
{group}
|
||
</Typography>
|
||
<Typography sx={{ fontSize: 10, fontWeight: 700, color: DT.textMuted }}>
|
||
{shownInGroup}/{cols.length}
|
||
</Typography>
|
||
</Stack>
|
||
|
||
{cols.map((c) => {
|
||
const checked = isVisible(c.key);
|
||
const disabled = !!c.required;
|
||
return (
|
||
<Box
|
||
key={c.key}
|
||
onClick={() => !disabled && toggleCol(c.key)}
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: 1,
|
||
mx: 0.5,
|
||
px: 1,
|
||
py: 0.375,
|
||
borderRadius: 1.5,
|
||
cursor: disabled ? 'default' : 'pointer',
|
||
bgcolor: checked ? tint(BRAND) : 'transparent',
|
||
border: `1px solid ${checked ? edge(BRAND) : 'transparent'}`,
|
||
transition: 'background-color 0.15s, border-color 0.15s',
|
||
'&:hover': { bgcolor: disabled ? (checked ? tint(BRAND) : 'transparent') : soft(BRAND) }
|
||
}}
|
||
>
|
||
<Stack direction="row" alignItems="center" spacing={0.625} sx={{ minWidth: 0 }}>
|
||
<Typography
|
||
noWrap
|
||
sx={{ fontSize: 13, fontWeight: 700, color: checked ? DT.textPrimary : DT.textSecondary }}
|
||
>
|
||
{c.label}
|
||
</Typography>
|
||
{disabled && (
|
||
<Tooltip title="Always visible" arrow>
|
||
<Box sx={{ display: 'inline-flex', color: DT.textMuted }}>
|
||
<MdLock size={11} />
|
||
</Box>
|
||
</Tooltip>
|
||
)}
|
||
</Stack>
|
||
<Switch
|
||
size="small"
|
||
checked={checked}
|
||
disabled={disabled}
|
||
onClick={(e) => e.stopPropagation()}
|
||
onChange={() => !disabled && toggleCol(c.key)}
|
||
sx={{
|
||
'& .MuiSwitch-switchBase.Mui-checked': { color: BRAND },
|
||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: BRAND, opacity: 1 },
|
||
'& .MuiSwitch-switchBase.Mui-disabled.Mui-checked': { color: BRAND_LIGHT },
|
||
'& .MuiSwitch-switchBase.Mui-disabled.Mui-checked + .MuiSwitch-track': { backgroundColor: BRAND_LIGHT, opacity: 0.6 }
|
||
}}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
</Menu>
|
||
|
||
{/* ============================================= || Date Filter Dialog || ============================================= */}
|
||
<Dialog open={open} onClose={() => setOpen(false)} PaperProps={{ sx: { borderRadius: 3 } }}>
|
||
<Box
|
||
sx={{
|
||
p: 2.5,
|
||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||
borderBottom: `1px solid ${DT.borderSubtle}`
|
||
}}
|
||
>
|
||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ bgcolor: BRAND, color: '#fff', width: 40, height: 40, boxShadow: `0 6px 18px ${ring(BRAND)}` }}>
|
||
<MdCalendarMonth size={20} />
|
||
</Avatar>
|
||
<Stack>
|
||
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
Select Date Range
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||
Filter the order details by a date range or preset
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</Box>
|
||
<DialogContent sx={{ width: '100%' }} className="datedialog">
|
||
<DateRangePicker
|
||
open={open}
|
||
toggle={() => setOpen(!open)}
|
||
id="daterange1"
|
||
onChange={(range) => {
|
||
if (range.label === 'All') {
|
||
setStartdate('');
|
||
setEnddate('');
|
||
setDatestatus('All');
|
||
setOpen(false);
|
||
} else {
|
||
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
|
||
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
|
||
setDatestatus(range.label || '');
|
||
}
|
||
}}
|
||
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) }
|
||
]}
|
||
/>
|
||
</DialogContent>
|
||
<Stack direction="row" justifyContent="flex-end" spacing={1} sx={{ width: '100%', p: 2, borderTop: `1px solid ${DT.divider}` }}>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={() => setOpen(false)}
|
||
sx={{
|
||
borderRadius: 999,
|
||
px: 2.5,
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textSecondary,
|
||
fontWeight: 700,
|
||
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
onClick={() => setOpen(false)}
|
||
sx={{
|
||
borderRadius: 999,
|
||
px: 3,
|
||
bgcolor: BRAND,
|
||
fontWeight: 700,
|
||
boxShadow: `0 6px 18px ${ring(BRAND)}`,
|
||
'&:hover': { bgcolor: '#4D1C61' }
|
||
}}
|
||
>
|
||
Apply
|
||
</Button>
|
||
</Stack>
|
||
</Dialog>
|
||
|
||
{/* ============================================= || MapWithRoute Dialog || ============================================= */}
|
||
<Dialog
|
||
open={mapOpen}
|
||
onClose={() => setMapOpen(false)}
|
||
maxWidth="lg"
|
||
fullWidth
|
||
PaperProps={{ sx: { borderRadius: 3, overflow: 'hidden' } }}
|
||
>
|
||
{riderCoordinates && (
|
||
<Box>
|
||
<MapWithRoute
|
||
coordinates={riderCoordinates}
|
||
additionalProps={{ riderStart, riderEnd }}
|
||
order={mapTenant}
|
||
setMapOpen={setMapOpen}
|
||
/>
|
||
</Box>
|
||
)}
|
||
</Dialog>
|
||
</>
|
||
);
|
||
}
|