Files
nearle_console/src/pages/nearle/reports/ordersDetails.js

1821 lines
67 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 { React, useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
// material-ui
import {
Avatar,
Backdrop,
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid,
IconButton,
List,
ListItem,
Paper,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
Autocomplete
} from '@mui/material';
import {
MdAssignment,
MdMyLocation,
MdGroups,
MdPlace,
MdDirectionsBike,
MdCalendarMonth,
MdFileDownload,
MdHourglassEmpty,
MdPersonPin,
MdLocationOn,
MdInventory2,
MdRoute,
MdSkipNext,
MdCheckCircle,
MdCancel,
MdList,
MdLocalShipping,
MdStraighten,
MdCurrencyRupee,
MdMap,
MdNoteAlt,
MdClose
} from 'react-icons/md';
import { FaCircleCheck } from 'react-icons/fa6';
import MapWithRoute from './mapWithRoute';
import CircularLoader from 'components/CircularLoader';
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
import { CSVExport } from 'components/third-party/ReactTable';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
import DateFilterDialog from 'components/DateFilterDialog';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import dayjs from 'dayjs';
import { OpenToast } from 'components/third-party/OpenToast';
import TableLoader from 'components/nearle_components/TableLoader';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
const opentoast = (message, variant, time) => {
enqueueSnackbar(message, {
variant: variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 1500
});
};
// ============================================================================
// Design tokens — shared with deliveries / tenants / customers / pricing 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 a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
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 }
}
});
// Status visual meta — semantic colours, NOT brand. Each lifecycle state has
// its own colour so operators can recognise it at a glance.
const STATUS_META = {
all: { label: 'All', color: BRAND, icon: MdList },
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
const STATUS_TABS = ['all', 'pending', 'accepted', 'arrived', 'picked', 'active', 'delivered', 'skipped', 'cancelled'];
// Soft pill used for metric cells (km, charges) inside the table.
const MetricPill = ({ color, icon, label, tooltip }) => (
<Tooltip title={tooltip || ''} placement="top">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint(color),
border: `1px solid ${edge(color)}`,
color,
fontSize: 11,
fontWeight: 800,
minWidth: 80,
justifyContent: 'center',
whiteSpace: 'nowrap'
}}
>
{icon}
{label}
</Box>
</Tooltip>
);
// Stamp cell — date + time stack with skeleton fallback for empty timestamps.
const StampCell = ({ value, formatDate, formatTime, success }) => {
if (!value) {
return (
<Stack spacing={0.5}>
<Skeleton animation={false} variant="text" width={70} height={12} />
<Skeleton animation={false} variant="text" width={55} height={14} />
</Stack>
);
}
return (
<Stack spacing={0.25}>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600, whiteSpace: 'nowrap' }}>
{formatDate(value)}
</Typography>
<Stack direction="row" alignItems="center" spacing={0.5}>
<Typography variant="caption" sx={{ color: DT.textPrimary, fontWeight: 800, whiteSpace: 'nowrap' }}>
{formatTime(value)}
</Typography>
{success && <FaCircleCheck style={{ color: '#10b981', fontSize: 12 }} />}
</Stack>
</Stack>
);
};
// ==============================|| Orders Details ||============================== //
// Haversine distance between two [lat, lng] points in kilometers.
function haversineKm(a, b) {
const R = 6371; // km
const toRad = (d) => (d * Math.PI) / 180;
const lat1 = toRad(a[0]);
const lat2 = toRad(b[0]);
const dLat = toRad(b[0] - a[0]);
const dLon = toRad(b[1] - a[1]);
const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
}
function kalmanSmoothGps(pings, options = {}) {
if (!Array.isArray(pings) || pings.length === 0) return [];
// 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN)
const cleanedPings = pings.filter(p =>
Number.isFinite(p.lat) &&
Number.isFinite(p.lng) &&
(Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1)
);
if (cleanedPings.length === 0) return [];
if (cleanedPings.length === 1) {
return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }];
}
const processNoise =
options.processNoise != null ? options.processNoise : 1e-10;
const measurementNoise =
options.measurementNoise != null ? options.measurementNoise : 2e-9;
const outlierGate =
options.outlierGate != null ? options.outlierGate : 9.0;
const maxSpeedKmh =
options.maxSpeedKmh != null ? options.maxSpeedKmh : 120;
const tsOf = (p) =>
p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0);
// 2. Scan forward to find the first valid starting anchor
let startIdx = 0;
while (startIdx < cleanedPings.length - 1) {
const p0 = cleanedPings[startIdx];
const p1 = cleanedPings[startIdx + 1];
const ts0 = tsOf(p0);
const ts1 = tsOf(p1) || ts0 + 1000;
const dtSec = Math.max(0.001, (ts1 - ts0) / 1000);
const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]);
const speedKmh = (km / dtSec) * 3600;
if (speedKmh <= maxSpeedKmh) {
break;
} else {
// Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier)
if (startIdx + 2 < cleanedPings.length) {
const p2 = cleanedPings[startIdx + 2];
const ts2 = tsOf(p2) || ts1 + 1000;
const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000);
const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]);
const speedKmh12 = (km12 / dtSec12) * 3600;
if (speedKmh12 <= maxSpeedKmh) {
startIdx = startIdx + 1;
continue;
}
}
startIdx++;
}
}
// 3. Teleport filter starting from the valid anchor
const accepted = [cleanedPings[startIdx]];
let lastTs = tsOf(cleanedPings[startIdx]);
for (let i = startIdx + 1; i < cleanedPings.length; i++) {
const p = cleanedPings[i];
const ts = tsOf(p) || lastTs + 1000;
const dtSec = Math.max(0.001, (ts - lastTs) / 1000);
const prev = accepted[accepted.length - 1];
const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]);
const speedKmh = (km / dtSec) * 3600;
if (speedKmh > maxSpeedKmh) continue;
accepted.push(p);
lastTs = ts;
}
if (accepted.length < 2) {
return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts }));
}
// Run a 1D Kalman + RTS smoother over one axis. Returns smoothed
// positions parallel to `accepted`.
const smoothAxis = (axisKey) => {
const N = accepted.length;
const xPost = new Array(N);
const pPost = new Array(N);
const xPrior = new Array(N);
const pPrior = new Array(N);
const dtArr = new Array(N);
const ts0 = tsOf(accepted[0]);
const ts1 = tsOf(accepted[1]);
const dt01 = Math.max(0.1, (ts1 - ts0) / 1000);
const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01;
xPost[0] = [accepted[0][axisKey], v0];
pPost[0] = [measurementNoise, 0, 0, 1];
xPrior[0] = xPost[0].slice();
pPrior[0] = pPost[0].slice();
dtArr[0] = 0;
let prevTs = ts0;
for (let i = 1; i < N; i++) {
const ts = tsOf(accepted[i]) || prevTs + 1000;
const dt = Math.max(0.1, (ts - prevTs) / 1000);
prevTs = ts;
dtArr[i] = dt;
// Predict
const [xPrev, vPrev] = xPost[i - 1];
const xPredPos = xPrev + vPrev * dt;
const xPredVel = vPrev;
const [pp00, pp01, pp10, pp11] = pPost[i - 1];
const dt2 = dt * dt;
const dt3 = dt2 * dt;
const dt4 = dt3 * dt;
const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise;
const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise;
const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise;
const np11 = pp11 + dt2 * processNoise;
xPrior[i] = [xPredPos, xPredVel];
pPrior[i] = [np00, np01, np10, np11];
// Update
const z = accepted[i][axisKey];
const y = z - xPredPos;
const S = np00 + measurementNoise;
const mahal2 = (y * y) / S;
if (mahal2 > outlierGate) {
xPost[i] = [xPredPos, xPredVel];
pPost[i] = [np00, np01, np10, np11];
continue;
}
const K0 = np00 / S;
const K1 = np10 / S;
const newPos = xPredPos + K0 * y;
const newVel = xPredVel + K1 * y;
xPost[i] = [newPos, newVel];
pPost[i] = [
(1 - K0) * np00,
(1 - K0) * np01,
np10 - K1 * np00,
np11 - K1 * np01
];
}
// RTS backward smoother
const xSmooth = new Array(N);
xSmooth[N - 1] = xPost[N - 1].slice();
for (let i = N - 2; i >= 0; i--) {
const dt = dtArr[i + 1];
const [pp00, pp01, pp10, pp11] = pPost[i];
const a = pp00 + dt * pp01;
const b = pp01;
const c = pp10 + dt * pp11;
const d = pp11;
const [q00, q01, q10, q11] = pPrior[i + 1];
const det = q00 * q11 - q01 * q10;
if (!Number.isFinite(det) || Math.abs(det) < 1e-30) {
xSmooth[i] = xPost[i].slice();
continue;
}
const inv00 = q11 / det;
const inv01 = -q01 / det;
const inv10 = -q10 / det;
const inv11 = q00 / det;
const c00 = a * inv00 + b * inv10;
const c01 = a * inv01 + b * inv11;
const c10 = c * inv00 + d * inv10;
const c11 = c * inv01 + d * inv11;
const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0];
const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1];
xSmooth[i] = [
xPost[i][0] + c00 * dxPos + c01 * dxVel,
xPost[i][1] + c10 * dxPos + c11 * dxVel
];
}
return xSmooth.map((s) => s[0]);
};
const lats = smoothAxis('lat');
const lngs = smoothAxis('lng');
return accepted.map((p, i) => ({
lat: lats[i],
lng: lngs[i],
logdate: p.logdate,
_ts: p._ts
}));
}
export default function OrdersDetails() {
const loadMoreRef = useRef();
const containerRef = useRef();
const locationRef = useRef(null);
const tenantRef = useRef(null);
const userid = localStorage.getItem('userid');
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(50);
const [locaName, setLocoName] = useState('All');
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [open, setOpen] = useState(false);
const [mapOpen, setMapOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [appId, setAppId] = useState(0);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [riderCoordinates, setRiderCoordinates] = useState([]);
const [riderStart, setRiderStart] = useState();
const [riderEnd, setRiderEnd] = useState();
const [mapTenant, setMapTenant] = useState({});
const [isLoading, setIsLoading] = useState(false);
let [total, settotal] = useState(0);
let [deliveredLenght, setDeliveredLenght] = useState(0);
let [pendingLenght, setPendingLenght] = useState(0);
let [cancelLenght, setCancelLenght] = useState(0);
let [assignLenght, setAssignLenght] = useState(0);
let [pickedLenght, setPickedLenght] = useState(0);
let [activeLenght, setActiveLenght] = useState(0);
let [arrivesLenght, setArrivedLenght] = useState(0);
let [skippedLenght, setSkippedLenght] = useState(0);
const [currentStatus, setCurrentStatus] = useState('All');
const [locationid, setLocationid] = useState(0);
const [tenantid, setTenantid] = useState(0);
const [tenantValue, setTenantValue] = useState(null);
const [locationValue, setLocationValue] = useState(null);
const [selectedRider, setSelectedRider] = useState();
const [riderValue, setRiderValue] = useState(null);
const [reportDialog, setReportDialog] = useState(false);
const [logsLoading, setLogsLoading] = useState(false);
// Map status key (lowercase) → count value from the summary endpoint.
const statusCountByKey = {
all: total,
pending: pendingLenght,
accepted: assignLenght,
arrived: arrivesLenght,
picked: pickedLenght,
active: activeLenght,
delivered: deliveredLenght,
skipped: skippedLenght,
cancelled: cancelLenght
};
// Cascading clears so changing a parent filter resets its children.
useEffect(() => {
setTenantid(0);
setTenantValue(null);
setLocationid(0);
setLocationValue(null);
setSelectedRider(null);
setRiderValue(null);
}, [appId]);
useEffect(() => {
setLocationid(0);
setLocationValue(null);
setRiderValue(null);
}, [tenantid]);
useEffect(() => {
setRiderValue(null);
}, [locationid]);
// ============== Haversine distance calculation for the map route ==============
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function calculateTotalDistance(routeCoordinates) {
let totalDistance = 0;
for (let i = 0; i < routeCoordinates.length - 1; i++) {
const { lat: lat1, lng: lon1 } = routeCoordinates[i];
const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1];
totalDistance += calculateDistance(lat1, lon1, lat2, lon2);
}
return totalDistance;
}
const getdeliverylogs = async (id) => {
setLogsLoading(true);
try {
const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`);
const datas = res.data.details;
if (Array.isArray(datas) && datas.length !== 0) {
// Sort chronologically by logdate
const sorted = datas
.map((r) => {
const ts = r?.logdate ? dayjs(r.logdate) : null;
return {
lat: parseFloat(r?.latitude ?? r?.lat),
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
logdate: r?.logdate,
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
};
})
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
.sort((a, b) => a._ts - b._ts);
if (sorted.length !== 0) {
setRiderStart(sorted[0].logdate);
setRiderEnd(sorted[sorted.length - 1].logdate);
// Apply Kalman filter
const smoothed = kalmanSmoothGps(sorted);
const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng }));
setRiderCoordinates(coData);
calculateTotalDistance(coData);
setMapOpen(true);
} else {
opentoast('No Valid Logs Found', 'error', 2000);
}
} else {
opentoast('No Logs Found ', 'error', 2000);
}
} catch (error) {
console.log('getdeliverylogs', error);
} finally {
setLogsLoading(false);
}
};
// ==============================|| fetchDeliveries (infinite) ||============================== //
const {
data: deliveriesData,
isLoading: fetchDeliveriesIsLoading,
isError: fetchDeliveriesIsError,
error: fetchDeliveriesError,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: [
'fetchdeliveries',
appId,
userid,
currentStatus,
startdate,
enddate,
rowsPerPage,
debouncedSearch,
tenantid,
locationid,
selectedRider?.userid || 0
],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true
});
const rows = deliveriesData?.pages.flatMap((page) => page.rows) || [];
useEffect(() => {
if (!hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
fetchNextPage();
}
},
{
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);
};
}, [hasNextPage, fetchNextPage]);
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}
};
// ==============================|| Tenant / Location / Rider lookups ||============================== //
const {
data: tenantlist,
isLoading: fetchtenantsIsLoading,
isError: fetchtenantsIsError,
error: fetchtenantsError
} = useQuery({
queryKey: ['tenantlist', appId],
queryFn: () => getTenants(appId),
enabled: appId !== 0
});
const {
data: ridersList,
isLoading: getriderbydeliveryIsLoading,
isError: getriderbydeliveryIsError,
error: getriderbydeliveryError
} = useQuery({
queryKey: ['fetchRidersList', appId],
queryFn: fetchRidersList,
enabled: appId !== 0
});
const {
data: locationlist,
isLoading: fetchlocationsIsLoading,
isError: fetchlocationsIsError,
error: fetchlocationsError
} = useQuery({
queryKey: ['gettenantlocations', tenantid],
queryFn: () => gettenantlocations(tenantid),
enabled: tenantid !== 0
});
// ==============================|| status summary counts ||============================== //
const fetchcount = async () => {
setIsLoading(true);
try {
await axios
.get(
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
: `${
process.env.REACT_APP_URL
}/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${
selectedRider?.userid || 0
}`
)
.then((res) => {
settotal(res.data.details.total);
setPendingLenght(res.data.details.pending);
setAssignLenght(res.data.details.accepted);
setArrivedLenght(res.data.details.arrived);
setPickedLenght(res.data.details.picked);
setActiveLenght(res.data.details.active);
setDeliveredLenght(res.data.details.delivered);
setSkippedLenght(res.data.details.skipped);
setCancelLenght(res.data.details.cancelled);
})
.catch((err) => {
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchcount();
}, [appId, startdate, enddate, currentStatus, tenantid, locationid, selectedRider]);
// CSV export payload — flat schema preserved for backwards compatibility.
const csvData = rows?.map((order) => ({
tenantname: order.tenantname,
tenantcity: order.tenantcity,
tenantcontactno: order.tenantcontactno,
rider: order.ridername,
orderid: order.orderid,
paymenttype: order.paymenttype == 64 ? 'Pay Later' : order.paymenttype == 42 ? 'Pay on Delivery' : 'Digital',
deliverydate: order.deliverydate,
orderstatus: order.orderstatus,
ordernotes: order.ordernotes,
kms: order.kms,
cumulativekms: order.cumulativekms,
assigntime: order.assigntime,
starttime: order.starttime,
arrivaltime: order.arrivaltime,
pickuptime: order.pickuptime,
deliverytime: order.deliverytime,
canceltime: order.canceltime,
deliverycharge: order.deliverycharges,
deliveryamt: order.deliveryamt,
pickupcustomer: order.pickupcustomer,
pickupcontactno: order.pickupcontactno,
Pickupaddress: order.pickupaddress,
pickupsuburb: order.pickupsuburb,
pickupcity: order.applocation,
pickuplat: order.pickuplat,
pickuplong: order.pickuplon,
deliverycustomer: order.deliverycustomer,
deliverycontactno: order.deliverycontactno,
deliveryaddress: order.deliveryaddress,
deliverysuburb: order.locationsuburb,
deliverylat: order.deliverylat,
deliverylong: order.deliverylong,
locationname: order.locationname,
locationsuburb: order.pickuplocation,
deliverylocation: order.deliverylocation,
locationcontactno: order.locationcontactno
}));
function formatDate(dateString) {
return dayjs(dateString).format('DD/MM/YYYY ');
}
function formatTime(dateString) {
return dayjs(dateString).format(' hh:mm A');
}
const errormessage = fetchDeliveriesIsError
? `An error has occurred: (fetchDeliveries) ${fetchDeliveriesError.message}`
: fetchtenantsIsError
? `An error has occurred: (getTenants) ${fetchtenantsError.message}`
: fetchlocationsIsError
? `An error has occurred: (gettenantlocations) ${fetchlocationsError.message}`
: getriderbydeliveryIsError
? `An error has occurred: (getriderbydelivery) ${getriderbydeliveryError.message}`
: null;
useEffect(() => {
if (errormessage) {
opentoast(errormessage, 'warning', 2000);
}
}, [errormessage]);
const KPI_META = [
{ key: 'total', label: 'Total Orders', color: BRAND, icon: MdLocalShipping, value: total },
{ key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, value: deliveredLenght },
{ key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, value: pendingLenght },
{ key: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, value: cancelLenght }
];
return (
<>
{(isLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
logsLoading ||
getriderbydeliveryIsLoading ||
isFetchingNextPage) && (
<div>
<Loader />
</div>
)}
{fetchDeliveriesIsLoading && (
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
open={fetchDeliveriesIsLoading}
/>
)}
{/* ============================================= || Header || ============================================= */}
<Paper
elevation={0}
sx={{
mb: { xs: 1.5, md: 2 },
p: { xs: 1.5, sm: 2, md: 2.5 },
borderRadius: DT.radiusCard / 8,
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.5, sm: 2 }}
>
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
<Avatar
sx={{
width: { xs: 40, sm: 48 },
height: { xs: 40, sm: 48 },
bgcolor: BRAND,
color: '#fff',
boxShadow: `0 6px 18px ${ring(BRAND)}`
}}
>
<MdAssignment size={22} />
</Avatar>
<Stack>
<Typography
variant="h3"
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.1,
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
}}
>
Orders Details
</Typography>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.5 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: '#10b981',
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
}}
/>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
Live · {locaName || 'All Zones'} · {datestatus}
</Typography>
</Stack>
</Stack>
</Stack>
<LocationAutocomplete
ref={locationRef}
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
</Stack>
</Paper>
{/* ============================================= || KPI Cards || ============================================= */}
<Grid container spacing={{ xs: 1.25, sm: 1.5, md: 2 }} sx={{ mt: '1px' }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<Paper
elevation={0}
sx={{
position: 'relative',
overflow: 'hidden',
p: { xs: 1.25, sm: 1.75, md: 2.25 },
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
'&:hover': {
transform: 'translateY(-3px)',
boxShadow: DT.shadowMd,
borderColor: edge(item.color)
}
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)`
}}
/>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack spacing={0.5} sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
fontWeight: 700,
letterSpacing: 0.4,
textTransform: 'uppercase',
fontSize: { xs: 10, sm: 11 },
whiteSpace: 'nowrap'
}}
>
{item.label}
</Typography>
<Typography
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.1,
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
}}
>
{item.value}
</Typography>
</Stack>
<Avatar
sx={{
width: { xs: 36, sm: 42, md: 48 },
height: { xs: 36, sm: 42, md: 48 },
bgcolor: soft(item.color),
color: item.color,
boxShadow: `inset 0 0 0 1px ${edge(item.color)}`,
flexShrink: 0
}}
>
<Icon size={20} />
</Avatar>
</Stack>
</Paper>
</Grid>
);
})}
</Grid>
{/* ============================================= || Filter Bar (tenant, location, rider, date, export) || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1.25, md: 1.75 },
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
boxShadow: DT.shadowSoft
}}
>
<Grid container spacing={1.25} alignItems="center">
<Grid item xs={12} sm={6} md={3}>
<Autocomplete
options={tenantlist || []}
value={tenantValue}
getOptionLabel={(option) => option?.tenantname || ''}
PaperComponent={SoftPaper}
onOpen={(event) => {
if (!appId) {
event.preventDefault();
OpenToast('Please select a your app location first!', 'warning', 3000);
setTimeout(() => {
locationRef.current?.focus();
}, 0);
}
}}
onChange={(e, val, reason) => {
if (reason === 'clear') {
setTenantid(0);
setTenantValue(null);
setLocationid(0);
setLocationValue(null);
} else {
setTenantid(val?.tenantid || 0);
setTenantValue(val);
setLocationid(val.locationid);
setLocationValue(null);
}
}}
renderInput={(params) => (
<TextField
{...params}
inputRef={tenantRef}
placeholder="Select Tenant"
size="small"
sx={pillFieldSx('#0ea5e9')}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color="#0ea5e9" size={22} selected>
<MdGroups size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Autocomplete
options={locationlist || []}
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb})` : '')}
value={locationValue}
PaperComponent={SoftPaper}
onOpen={(event) => {
if (!appId && !tenantid) {
event.preventDefault();
OpenToast('Please select a your Location and Tenant first!', 'warning', 3000);
setTimeout(() => {
locationRef.current?.focus();
}, 0);
} else if (!tenantid) {
event.preventDefault();
OpenToast('Please select a your Tenant first!', 'warning', 3000);
setTimeout(() => {
tenantRef.current?.focus();
}, 0);
}
}}
onChange={(e, val, reason) => {
if (reason === 'clear') {
setLocationid(0);
setLocationValue(null);
} else {
setLocationid(val.locationid || 0);
setLocationValue(val);
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Location"
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>
<Grid item xs={12} sm={6} md={3}>
<Autocomplete
options={ridersList || []}
value={riderValue}
getOptionLabel={(option) => `${option.firstname} ${option.lastname}`}
PaperComponent={SoftPaper}
onOpen={() => {
if (!appId) {
OpenToast('Select App Location First', 'warning', 2000);
}
}}
onChange={(event, value, reason) => {
if (reason === 'clear') {
setSelectedRider(null);
setRiderValue(null);
} else {
setSelectedRider(value);
setRiderValue(value);
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Rider"
size="small"
sx={pillFieldSx('#8b5cf6')}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color="#8b5cf6" size={22} selected>
<MdDirectionsBike size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Stack direction="row" spacing={1} alignItems="center" justifyContent={{ xs: 'flex-start', md: 'flex-end' }}>
<Tooltip title="Date Filter" placement="top">
<Box
onClick={() => setOpen(true)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.75,
borderRadius: 999,
cursor: 'pointer',
bgcolor: tint('#f59e0b'),
border: `1.5px solid ${edge('#f59e0b')}`,
color: '#f59e0b',
fontWeight: 800,
fontSize: 12,
transition: 'all 0.18s',
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
}}
>
<MdCalendarMonth size={14} />
{dayjs(startdate).format('DD/MM/YY')} {dayjs(enddate).format('DD/MM/YY')}
</Box>
</Tooltip>
<Button
variant="contained"
size="small"
startIcon={<MdFileDownload />}
onClick={() => {
setReportDialog(true);
setTimeout(() => {
setRowsPerPage('');
}, 0);
}}
sx={{
borderRadius: 999,
px: 1.5,
py: 0.75,
fontWeight: 800,
fontSize: 12,
textTransform: 'none',
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
boxShadow: `0 6px 18px ${ring(BRAND)}`,
'&:hover': {
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
boxShadow: `0 8px 22px ${ring(BRAND)}`
}
}}
>
Export
</Button>
</Stack>
</Grid>
</Grid>
</Paper>
{/* ============================================= || Status Tabs + Search || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
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((key) => {
const meta = STATUS_META[key];
const Icon = meta.icon;
const active = currentStatus.toLowerCase() === key;
const count = statusCountByKey[key] ?? 0;
return (
<Box
key={key}
onClick={() => setCurrentStatus(key === 'all' ? 'All' : key)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: { xs: 0.625, md: 0.875 },
pl: 0.5,
pr: { xs: 1, md: 1.25 },
py: 0.5,
flexShrink: 0,
cursor: 'pointer',
borderRadius: 999,
border: `1.5px solid ${active ? meta.color : edge(meta.color)}`,
bgcolor: active ? meta.color : tint(meta.color),
color: active ? '#fff' : meta.color,
fontWeight: 700,
boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : 'none',
transition: 'all 0.18s',
'&:hover': {
borderColor: meta.color,
boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : `0 0 0 3px ${ring(meta.color)}`
}
}}
>
<Avatar
sx={{
width: { xs: 22, md: 26 },
height: { xs: 22, md: 26 },
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
color: active ? '#fff' : meta.color
}}
>
<Icon size={13} />
</Avatar>
<Typography
variant="caption"
sx={{
fontWeight: 800,
fontSize: { xs: 11.5, md: 13 },
lineHeight: 1
}}
>
{meta.label}
</Typography>
<Box
sx={{
minWidth: { xs: 22, md: 26 },
height: { xs: 18, md: 22 },
px: 0.625,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
bgcolor: active ? 'rgba(255,255,255,0.22)' : '#fff',
color: active ? '#fff' : meta.color,
border: active ? 'none' : `1px solid ${edge(meta.color)}`
}}
>
{count}
</Box>
</Box>
);
})}
</Stack>
<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}
placeholder="Search orders (ctrl+k)"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: tint(BRAND),
'& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(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',
'&::-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 sx={{ minWidth: 1600 }}>
<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: 1.5 }
}
}}
>
<TableCell>#</TableCell>
<TableCell>Map</TableCell>
<TableCell>Client</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell>Status / Rider</TableCell>
<TableCell>Assigned</TableCell>
<TableCell>Accepted</TableCell>
<TableCell>Arrived</TableCell>
<TableCell>Picked</TableCell>
<TableCell>Active</TableCell>
<TableCell>Delivered</TableCell>
<TableCell>Cancelled</TableCell>
<TableCell>Notes</TableCell>
<TableCell align="center">KMS</TableCell>
<TableCell align="center">Charges</TableCell>
</TableRow>
</TableHead>
<TableBody>
{fetchDeliveriesIsLoading ? (
<TableLoader rows={16} columns={16} height={15} />
) : rows?.length == 0 ? (
<TableRow>
<TableCell colSpan={16} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdAssignment size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No orders to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
rows?.map((row, index) => {
const statusKey = String(row.orderstatus || '').toLowerCase();
const rowStatusMeta = STATUS_META[statusKey] || {
label: row.orderstatus || '—',
color: BRAND,
icon: MdAssignment
};
const StatusIcon = rowStatusMeta.icon;
const cancelled = statusKey === 'cancelled';
return (
<TableRow
key={row.deliveryid || `${row.orderid}-${index}`}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 1.5 },
verticalAlign: 'top'
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
</TableCell>
{/* ====================== Map button ====================== */}
<TableCell>
<Tooltip
title={row.orderstatus === 'delivered' ? 'View rider route' : 'Available for delivered orders'}
placement="top"
>
<span>
<IconButton
size="small"
disabled={row.orderstatus !== 'delivered'}
onClick={() => {
if (row.orderstatus === 'delivered') {
getdeliverylogs(row.deliveryid);
setMapTenant(row);
}
}}
sx={{
bgcolor: row.orderstatus === 'delivered' ? soft(BRAND) : soft('#94a3b8'),
color: row.orderstatus === 'delivered' ? BRAND : DT.textMuted,
border: `1px solid ${row.orderstatus === 'delivered' ? edge(BRAND) : edge('#94a3b8')}`,
'&:hover': {
bgcolor: row.orderstatus === 'delivered' ? BRAND : soft('#94a3b8'),
color: row.orderstatus === 'delivered' ? '#fff' : DT.textMuted
}
}}
>
<MdMap size={14} />
</IconButton>
</span>
</Tooltip>
</TableCell>
{/* ====================== Client ====================== */}
<TableCell>
<Stack direction="row" alignItems="flex-start" spacing={1}>
<AccentAvatar color={BRAND} size={32}>
<MdGroups size={16} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.tenantname}
</Typography>
<Tooltip title="Order ID" placement="top">
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 700 }}>
#{row.orderid}
</Typography>
</Tooltip>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')}
</Typography>
</Stack>
</Stack>
</TableCell>
{/* ====================== Pickup ====================== */}
<TableCell>
<Stack spacing={0.25}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.pickupcontactno}
</Typography>
<Tooltip title={row.Pickupaddress || row.pickupsuburb || ''}>
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
cursor: 'pointer',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')}
</Typography>
</Tooltip>
{row.applocation && (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.75,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 10,
fontWeight: 800,
width: 'fit-content'
}}
>
<MdPlace size={11} /> {row.applocation}
</Box>
)}
</Stack>
</TableCell>
{/* ====================== Drop ====================== */}
<TableCell>
<Stack spacing={0.25}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.deliverycontactno}
</Typography>
<Tooltip title={row.deliveryaddress || ''}>
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
cursor: 'pointer',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* ====================== Status / Rider ====================== */}
<TableCell>
<Stack spacing={0.5}>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint(rowStatusMeta.color),
border: `1px solid ${edge(rowStatusMeta.color)}`,
color: rowStatusMeta.color,
fontSize: 11,
fontWeight: 800,
width: 'fit-content'
}}
>
<StatusIcon size={12} /> {rowStatusMeta.label}
</Box>
{row.ridername && (
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdDirectionsBike size={12} color={DT.textMuted} />
<Tooltip title={`Rider ID: ${row.userid}`}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.ridername}
</Typography>
</Tooltip>
</Stack>
)}
</Stack>
</TableCell>
{/* ====================== Timestamps ====================== */}
<TableCell>
<StampCell value={row.assigntime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
<TableCell>
<StampCell value={row.acceptedtime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
<TableCell>
<StampCell value={row.arrivaltime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
<TableCell>
<StampCell
value={row.pickuptime}
formatDate={formatDate}
formatTime={formatTime}
success={row.deliverystatus === 'active'}
/>
</TableCell>
<TableCell>
<StampCell value={row.starttime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
<TableCell>
<StampCell value={row.deliverytime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
<TableCell>
<StampCell value={row.canceltime} formatDate={formatDate} formatTime={formatTime} />
</TableCell>
{/* ====================== Notes ====================== */}
<TableCell sx={{ maxWidth: 180 }}>
{row.ordernotes ? (
<Stack direction="row" spacing={0.5} alignItems="flex-start">
<MdNoteAlt size={12} color={DT.textMuted} style={{ marginTop: 2, flexShrink: 0 }} />
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{row.ordernotes}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
{/* ====================== KMS ====================== */}
<TableCell align="center">
<Stack spacing={0.5} alignItems="center">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={11} />}
label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
tooltip="KMS"
/>
<MetricPill
color="#10b981"
icon={<MdStraighten size={11} />}
label={`${row.cumulativekms ?? 0} km`}
tooltip="Actual KMS"
/>
<MetricPill
color="#0ea5e9"
icon={<MdStraighten size={11} />}
label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
tooltip="Rider KMS"
/>
</Stack>
</TableCell>
{/* ====================== Charges ====================== */}
<TableCell align="center">
<Stack spacing={0.5} alignItems="center">
<MetricPill
color="#ef4444"
icon={<MdCurrencyRupee size={11} />}
label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`}
tooltip="Delivery Charge"
/>
<MetricPill
color="#10b981"
icon={<MdCurrencyRupee size={11} />}
label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`}
tooltip="Delivery Amount"
/>
</Stack>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<Divider />
{rows?.length !== 0 && (
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}>
<Stack ref={loadMoreRef} style={{ textAlign: 'center', width: '100%' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more orders
</Typography>
)}
</Stack>
</Stack>
)}
</TableContainer>
</Paper>
{/* ============================================= || Export Dialog || ============================================= */}
<Dialog
open={reportDialog}
onClose={() => setReportDialog(false)}
fullWidth
maxWidth="sm"
PaperProps={{ sx: { borderRadius: 2.5, overflow: 'hidden' } }}
>
<DialogTitle
sx={{
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
color: '#fff',
py: 2
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 36, height: 36, bgcolor: 'rgba(255,255,255,0.22)', color: '#fff' }}>
<MdFileDownload size={20} />
</Avatar>
<Stack>
<Typography sx={{ fontSize: 11, fontWeight: 700, opacity: 0.85, letterSpacing: 0.6, textTransform: 'uppercase', lineHeight: 1 }}>
Report
</Typography>
<Typography sx={{ fontWeight: 800, fontSize: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
Export Orders
</Typography>
</Stack>
</Stack>
<IconButton
size="small"
onClick={() => setReportDialog(false)}
sx={{ color: '#fff', bgcolor: 'rgba(255,255,255,0.18)', '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }}
>
<MdClose size={16} />
</IconButton>
</Stack>
</DialogTitle>
<DialogContent sx={{ p: 2 }}>
{fetchDeliveriesIsLoading && <CircularLoader />}
{[
{ label: 'App Location', value: locaName, color: BRAND },
{ label: 'Tenant', value: tenantValue?.tenantname, color: '#0ea5e9' },
{ label: 'Business Location', value: locationValue?.locationname, color: '#10b981' },
{ label: 'Status', value: currentStatus, color: '#f59e0b' },
{ label: 'Rider', value: riderValue ? `${riderValue.firstname} ${riderValue.lastname}` : null, color: '#8b5cf6' },
{ label: 'Keyword', value: searchword, color: '#06b6d4' },
{ label: 'Start Date', value: startdate, color: '#14b8a6' },
{ label: 'End Date', value: enddate, color: '#ef4444' }
].map((item, idx) => (
<Stack
key={idx}
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{
px: 1.5,
py: 1,
borderRadius: 2,
bgcolor: tint(item.color),
border: `1px solid ${edge(item.color)}`,
mb: 1
}}
>
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{item.label}
</Typography>
<Chip
size="small"
label={item.value || 'None'}
sx={{
fontWeight: 700,
bgcolor: item.value ? item.color : soft('#94a3b8'),
color: item.value ? '#fff' : DT.textMuted,
border: 'none'
}}
/>
</Stack>
))}
</DialogContent>
<DialogActions sx={{ p: 2, bgcolor: DT.surfaceAlt, gap: 1 }}>
<Button
color="inherit"
variant="outlined"
onClick={() => setReportDialog(false)}
sx={{
borderRadius: 999,
px: 2,
textTransform: 'none',
fontWeight: 700,
borderColor: DT.borderSubtle,
color: DT.textSecondary
}}
>
Cancel
</Button>
<CSVExport
data={csvData}
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
label="Download CSV"
btnLoading={fetchDeliveriesIsLoading}
onClick={() => {
setTimeout(() => setReportDialog(false), 0);
}}
/>
</DialogActions>
</Dialog>
{/* ============================================= || Date Filter Dialog || ============================================= */}
<DateFilterDialog
open={open}
onClose={() => setOpen(false)}
onSelect={(range) => {
setStartdate(range.startDate);
setEnddate(range.endDate);
setDatestatus(range.label);
}}
/>
{/* ============================================= || Map Dialog || ============================================= */}
<Dialog
open={mapOpen}
onClose={() => {
setMapOpen(false);
}}
fullScreen
fullWidth
>
{riderCoordinates && (
<div>
<MapWithRoute
coordinates={riderCoordinates}
additionalProps={{ riderStart, riderEnd }}
order={mapTenant}
setMapOpen={setMapOpen}
/>
</div>
)}
</Dialog>
</>
);
}