1821 lines
67 KiB
JavaScript
1821 lines
67 KiB
JavaScript
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>
|
||
</>
|
||
);
|
||
}
|