Files
Doormilexpress_console/src/pages/nearle/deliveries/deliveries.js

2174 lines
86 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logger from '../../../utils/logger';
import { enqueueSnackbar } from 'notistack';
import { DeleteFilled, EditOutlined } from '@ant-design/icons';
import { useState, useEffect, Fragment, useRef, useMemo } from 'react';
import { Empty } from 'antd';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import axios from 'axios';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { useTheme } from '@mui/material/styles';
import {
MdOutlineDateRange,
MdAccessTime,
MdAllInclusive,
MdLightMode,
MdWbSunny,
MdNightsStay,
MdCheck
} from 'react-icons/md';
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import {
Avatar,
Box,
Button,
Grid,
Tabs,
Tab,
IconButton,
Stack,
Chip,
Typography,
Table,
TableCell,
TableBody,
TableHead,
Collapse,
Dialog,
TableRow,
DialogContent,
DialogTitle,
Tooltip,
DialogActions,
Popper,
ClickAwayListener,
Checkbox,
Autocomplete,
TextField,
FormLabel,
TablePagination,
TableContainer,
Skeleton,
useMediaQuery,
Divider,
CircularProgress,
Backdrop,
MenuItem,
Menu,
Paper
} from '@mui/material';
import MainCard from 'components/MainCard';
import {
MoreOutlined,
CloseOutlined
// NotificationOutlined
} from '@ant-design/icons';
import { PopupTransition } from 'components/@extended/Transitions';
import {
addDays,
addMonths,
addWeeks,
// addYears,
endOfMonth,
endOfWeek,
// endOfYear,
startOfMonth,
startOfWeek
// startOfYear,
} from 'date-fns';
import { DateRangePicker } from 'mui-daterange-picker';
import * as React from 'react';
import Loader from 'components/Loader';
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
import CircularLoader from 'components/CircularLoader';
import {
cancelDeliveryAPI,
changeRiderAPI,
fetchCountAPI,
fetchDeliveries,
fetchPercentageAPI,
fetchRidersList,
notifyRider,
updateDeliveryAPI,
getorderdetails,
gettenantlocations,
getTenants
} from 'pages/api/api';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import TitleCard from 'components/nearle_components/TitleCard';
import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
// Batches mirror the dispatch page's slot definitions so an operator who
// segments the day there sees the same buckets here. Hours are 24h, half-open
// [startHour, endHour) — a delivery at exactly endHour falls into the *next*
// batch (or none, if the gap isn't covered).
const BATCH_OPTIONS = [
{ id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' },
{ id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 },
{ id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12 },
{ id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 }
];
// Per-batch icon components — kept separate from BATCH_OPTIONS so the option
// objects stay serializable / printable. Looked up at render time by iconKey.
const BATCH_ICONS = {
all: MdAllInclusive,
morning: MdLightMode,
afternoon: MdWbSunny,
evening: MdNightsStay
};
// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the
// page lands them on the slot they're most likely curious about. Falls back
// to 'all' when the current hour lies in a configured gap (89 AM, 12 PM4
// PM, after 7 PM). Local time, not UTC, to match the dispatch page's
// bucketing — both pages must agree on which batch a given row belongs to.
const detectInitialBatchId = () => {
const now = dayjs();
const h = now.hour() + now.minute() / 60;
for (const b of BATCH_OPTIONS) {
if (b.id === 'all') continue;
if (h >= b.startHour && h < b.endHour) return b.id;
}
return 'all';
};
// Bucket by `assigntime` only — matches the spec ("bucketed by assigntime")
// and the dispatch page's default time-field selection (`Dispatch.js:763`
// initialises `selectedTimeField` to `'assigned'`, whose `keys` is just
// `['assigntime']`). The previous priority cascade started with
// `deliverytime`, which mis-bucketed delivered orders into the wave they
// were *completed* in rather than the wave they were *assigned to* — so an
// order assigned at 07:00 (Morning) but delivered at 11:00 fell into the
// 89 AM / 12 PM4 PM gap and silently disappeared from every batch on this
// page while still showing up on dispatch's Morning Batch. Rows without an
// `assigntime` (unassigned pending orders) return null and intentionally
// don't bucket — matches dispatch.
const BATCH_TIME_KEYS = ['assigntime'];
// Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null
// when the row has no usable timestamp / falls in a gap between batches.
const getRowBatchId = (row) => {
let t = null;
for (const k of BATCH_TIME_KEYS) {
if (row?.[k]) {
t = row[k];
break;
}
}
if (!t) return null;
const str = String(t).trim();
// Bare YYYY-MM-DD with no time component would always parse to midnight and
// get mis-bucketed into Morning — skip those.
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
// Parse in LOCAL time to match the dispatch page's bucketing exactly
// (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment,
// so a row that lands in Evening Batch on the dispatch page must also land
// in Evening Batch here — otherwise an operator running in IST/PST/etc.
// sees different totals on the two pages for the same underlying rows.
const d = dayjs(t);
if (!d.isValid()) return null;
const h = d.hour() + d.minute() / 60;
for (const b of BATCH_OPTIONS) {
if (b.id === 'all') continue;
if (h >= b.startHour && h < b.endHour) return b.id;
}
return null;
};
// ================================================= || deliveries (initial point)|| =================================================
const Deliveries = () => {
const userid = localStorage.getItem('userid');
const theme = useTheme();
const loadMoreRef = useRef();
const containerRef = useRef();
const [deliverylist, setDeliverylist] = useState([]);
const [dialogopen, setDialogopen] = useState(false);
const [locaName, setLocoName] = useState('All');
const [appId, setAppId] = useState(0);
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [tabstatus, setTabstatus] = useState('Pending');
const [tabvalue, setTabvalue] = useState(0);
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [kms, setKms] = useState('');
const [cumulativekms, setCumulativeKms] = useState();
const [deliveryamount, setDeliveryamount] = useState();
const [notes, setNotes] = useState('');
const [currentorder, setCurrentorder] = useState({});
const [deliverylat, setDeliverylat] = useState('');
const [deliverylong, setDeliverylong] = useState('');
const [currentStatus, setCurrentStatus] = useState('pending');
const [updateStatus, setUpdateStatus] = useState('delivered');
const locationRef = useRef(null);
const tenantRef = useRef(null);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(50);
const [totalCount, setTotalCount] = React.useState();
const [productCollapse, setProductCollapse] = useState(null);
const [orderHeaderid, setOrderHeaderId] = useState(null);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
const [selectedRow, setSelectedRow] = useState(null);
const [loading1, setLoading1] = useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const [open2, setOpen2] = useState('');
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
const [cancelFeed, setCancelFeed] = useState('');
const [selectedRider, setSelectedRider] = useState(null);
const [tenantid, setTenantid] = useState(0);
const [locationid, setLocationid] = useState(0);
const [tenantValue, setTenantValue] = useState(null);
const [locationValue, setLocationValue] = useState(null);
const [riderid, setRiderid] = useState(0);
// Selected batch id — drives the client-side row filter. Defaults to the
// batch matching the current UTC hour (the operator is most likely curious
// about "now"); never `null` since there's no longer an "All" option.
const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId);
const roleid = localStorage.getItem('roleid');
useEffect(() => {
setTenantid(0);
setTenantValue(null);
setLocationid(0);
setLocationValue(null);
}, [appId]);
// to clear the location autocomplete
useEffect(() => {
setLocationid(0);
setLocationValue(null);
}, [tenantid]);
const menuOpen = Boolean(menuAnchorEl);
const handleMenuOpen = (event, row) => {
setSelectedRow(row);
console.log('selectedRow', row);
setMenuAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
};
// =========================================== || cancelDelivery || ===========================================
const { mutate: cancelDelivery } = useMutation({
mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed),
onSuccess: () => {
opentoast('Delivery Cancelled Successfully', 'success');
setCancelDeliveryOpen(false);
fetchCountRefetch(); // Refresh count data
fetchDeliveriesRefetch(); // Refresh deliveries
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
},
onError: (error) => {
opentoast(error.message, 'error');
}
});
// ==============================|| cancelridernotification ||============================== //
const cancelridernotification = async () => {
console.log('cancelridernotification', selectedRow);
try {
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: selectedRow.userfcmtoken,
notification: {
title: 'NearleXpress',
body: `${selectedRow.orderid} have been Cancelled`,
sound: 'ring',
image: ''
},
data: {
type: 'cancel'
}
});
return response.data;
} catch (err) {
opentoast(err.message, 'error', 2000);
console.log(err);
}
};
// ==============================|| getTenants ||============================== //
const {
data: tenantlist,
isLoading: fetchtenantsIsLoading,
isError: fetchtenantsIsError,
error: fetchtenantsError
} = useQuery({
queryKey: ['tenantlist', appId],
queryFn: () => getTenants(appId), // Ensure appId is passed
enabled: appId !== 0 // Ensures query runs only when appId is valid
});
// ==============================|| gettenantlocations ||============================== //
const {
data: locationlist,
isLoading: fetchlocationsIsLoading,
isError: fetchlocationsIsError,
error: fetchlocationsError
} = useQuery({
queryKey: ['gettenantlocations', tenantid],
queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed
enabled: tenantid !== 0 // Ensures query runs only when appId is valid
});
// =========================================== || notifyrider || ===========================================
const notifyRiderMutation = useMutation({
mutationFn: notifyRider, // Using the separate function
onSuccess: () => {
enqueueSnackbar('Notification sent Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
},
onError: (error) => {
enqueueSnackbar(error.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
});
// =========================================== || changerider || ===========================================
const changeRiderMutation = useMutation({
mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow),
onSuccess: (res, { selectedRider, selectedRow }) => {
setLoading1(false);
setChangeDialogOpen(false);
if (res.data.message === 'Success') {
logger.info(`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
opentoast('Rider Changed Successfully', 'success');
}
fetchCountRefetch(); // Refresh count data
fetchDeliveriesRefetch(); // Refresh deliveries
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
},
onError: (err, { selectedRider, selectedRow }) => {
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
opentoast(err.message, 'error');
setLoading1(false);
}
});
/* ============================================= || opentoast || ============================================= */
const opentoast = (message, variant) => {
enqueueSnackbar(message, {
variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
};
// ==============================|| getorderdetails ||============================== //
const { data: orderdetails } = useQuery({
queryKey: ['orderdetails', orderHeaderid],
queryFn: () => getorderdetails(orderHeaderid),
enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined
});
const dialogclose = () => {
setDialogopen(false);
};
const handleChangetab = (e, i) => {
setPage(0);
setTabvalue(i);
setRowsPerPage(50);
if (i === 0) {
setTabstatus('Pending');
setCurrentStatus('pending');
setTotalCount(countData?.uncoveredLength);
}
if (i === 1) {
setTabstatus('Assigned');
setCurrentStatus('accepted');
setTotalCount(countData?.assignedLength);
}
if (i === 2) {
setTabstatus('Arrived');
setCurrentStatus('arrived');
setTotalCount(countData?.arrivedLength);
}
if (i === 3) {
setTabstatus('Picked');
setCurrentStatus('picked');
setTotalCount(countData?.pickedLength);
}
if (i === 4) {
setTabstatus('Active');
setCurrentStatus('active');
setTotalCount(countData?.activeLength);
}
if (i === 5) {
setTabstatus('Skipped');
setCurrentStatus('skipped');
setTotalCount(countData?.skippedLength);
}
if (i === 6) {
setTabstatus('Delivered');
setCurrentStatus('delivered');
setTotalCount(countData?.coveredLength);
}
if (i === 7) {
setTabstatus('Cancelled');
setCurrentStatus('cancelled');
setTotalCount(countData?.cancelLength);
}
console.log(i);
setSearchword('');
};
const okclicked = () => {
setOpen(false);
};
/* ============================================= || fetchDeliveries | ============================================= */
const {
data: deliveriesData,
isLoading: fetchDeliveriesIsLoading,
isError: fetchDeliveriesIsError,
error: fetchDeliveriesError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch: fetchDeliveriesRefetch
} = useInfiniteQuery({
queryKey: [
'fetchdeliveries',
appId,
userid,
currentStatus,
startdate,
enddate,
rowsPerPage,
debouncedSearch,
tenantid,
locationid,
riderid
],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
});
const rows = deliveriesData?.pages.flatMap((page) => page.rows) || [];
// (filteredRows is defined below, after countSourceRows / countSourceLoading
// are in scope — they're the single source of truth for both the table and
// the per-status chip counts.)
// Parallel "all-statuses" query used ONLY to derive batch-aware tab counts.
// The primary `fetchdeliveries` query above is keyed by `currentStatus`, so
// its rows are scoped to the active tab — they can't tell us how many
// delivered/cancelled/etc. exist in the same batch. We fetch the full day
// once (status='all'), keep it cached, and re-derive the counts per status
// whenever the operator switches batches. Search keyword is intentionally
// dropped from this key so the chip counts reflect the batch totals rather
// than the search-filtered subset.
const {
data: countSourceData,
fetchNextPage: countFetchNext,
hasNextPage: countHasNext,
isFetchingNextPage: countIsFetchingNext,
isLoading: countSourceIsLoading,
refetch: countSourceRefetch
} = useInfiniteQuery({
queryKey: [
'fetchdeliveries-batchcounts',
appId,
userid,
'all',
startdate,
enddate,
200,
'',
tenantid,
locationid,
riderid
],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
});
const countSourceRows = useMemo(
() => (countSourceData?.pages || []).flatMap((p) => p.rows || []),
[countSourceData]
);
useEffect(() => {
if (countHasNext && !countIsFetchingNext) countFetchNext();
}, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]);
// Loading flag for the table empty state — true while we're still streaming
// pages, so we don't show "No <status> Orders" prematurely before the full
// day's rows have arrived. Goes false once auto-pagination drains.
const countSourceLoading = countHasNext || countIsFetchingNext;
// The table now reads from the same source as the chip counts. That way the
// "74" in the Delivered chip and the rows shown in the table can never
// disagree (they used to, because the table consumed a separate per-status
// paginated query while the chips read from the full all-statuses dataset).
// Three filter steps:
// 1. Batch — selected via the dropdown. 'all' bypasses this filter.
// 2. Tab status — currentStatus comes from handleChangetab.
// 3. Search — client-side substring match across customer/address/order
// fields. Server-side search isn't needed since every row is loaded.
const filteredRows = useMemo(() => {
const wantStatus = String(currentStatus || '').toLowerCase();
const q = String(debouncedSearch || '').trim().toLowerCase();
return countSourceRows.filter((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
const s = String(r.orderstatus || '').toLowerCase();
if (wantStatus && s !== wantStatus) return false;
if (q) {
const hay = [
r.deliverycustomer,
r.deliveryaddress,
r.deliverysuburb,
r.pickupcustomer,
r.pickupaddress,
r.pickupsuburb,
r.orderid,
r.tenantname,
r.ridername,
r.username
]
.map((v) => String(v || '').toLowerCase())
.join(' ');
if (!hay.includes(q)) return false;
}
return true;
});
}, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]);
// Counts per status, scoped to the selected batch. Keys mirror the legacy
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
const batchCounts = useMemo(() => {
const c = {
uncoveredLength: 0,
assignedLength: 0,
arrivedLength: 0,
pickedLength: 0,
activeLength: 0,
skippedLength: 0,
coveredLength: 0,
cancelLength: 0
};
countSourceRows.forEach((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
const s = String(r.orderstatus || '').toLowerCase();
switch (s) {
case 'pending':
c.uncoveredLength += 1;
break;
case 'accepted':
case 'assigned':
c.assignedLength += 1;
break;
case 'arrived':
c.arrivedLength += 1;
break;
case 'picked':
c.pickedLength += 1;
break;
case 'active':
c.activeLength += 1;
break;
case 'skipped':
c.skippedLength += 1;
break;
case 'delivered':
c.coveredLength += 1;
break;
case 'cancelled':
case 'canceled':
c.cancelLength += 1;
break;
default:
break;
}
});
return c;
}, [countSourceRows, selectedBatch]);
// Per-batch total (any status) for the dropdown badges. Computed once for
// every batch + 'all' so each option in the menu can show its own count
// without re-walking the list. Mirrors dispatch's batchCounts shape
// (`Dispatch.js:11981206`) — totals here should match the per-batch numbers
// visible on the dispatch page for the same date.
const batchTotals = useMemo(() => {
const totals = { all: countSourceRows.length };
BATCH_OPTIONS.forEach((b) => {
if (b.id === 'all') return;
totals[b.id] = 0;
});
countSourceRows.forEach((r) => {
const id = getRowBatchId(r);
if (id) totals[id] = (totals[id] || 0) + 1;
});
return totals;
}, [countSourceRows]);
// IntersectionObserver now watches the count-source query — that's the
// single source of truth for both the table and the chip counts. When the
// sentinel scrolls into view it nudges the next page of all-statuses
// deliveries to keep the dataset complete.
useEffect(() => {
if (!countHasNext) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
countFetchNext();
}
},
{
root: document.querySelector('.MuiTableContainer-root'),
rootMargin: '0px',
threshold: 1.0
}
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
};
}, [countHasNext, countFetchNext]);
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (countHasNext && !countIsFetchingNext) {
countFetchNext();
}
}
};
/* ============================================= || fetchPercentageAPI | ============================================= */
const {
data: percentageData,
isLoading: fetchPercentageIsLoading,
isError: fetchPercentageIsError,
error: fetchPercentageError
} = useQuery({
queryKey: ['fetchpercentageaPI', appId],
queryFn: () => fetchPercentageAPI(appId)
});
useEffect(() => {
if (percentageData) {
console.log('percentageData', percentageData);
}
}, [percentageData]);
/* ============================================= || fetchcount | ============================================= */
const {
data: countData = {}, // Default to empty object
isLoading: fetchCountIsLoading,
isError: fetchCountIsError,
error: fetchCountError,
refetch: fetchCountRefetch
} = useQuery({
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
});
useEffect(() => {
console.log('countData', countData);
if (tabvalue === 0 && countData) {
setTotalCount(countData.uncoveredLength);
}
}, [countData]);
// ==============================|| fetchRidersList ||============================== //
const {
data: ridersList = [],
isLoading: riderListIsLoading,
isError: ridersListIsError,
error: ridersListError
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList,
enabled: Boolean(appId),
onError: (err) => {
OpenToast(err.message, 'error', 2000);
}
});
/* ============================================= || updatedelivery | ============================================= */
const updateDeliveryMutation = useMutation({
mutationFn: (orderData) => updateDeliveryAPI(orderData),
onSuccess: (res) => {
console.log(res);
if (res.data.status) {
opentoast('Updated Successfully', 'success');
setDeliveryamount('');
setNotes('');
setDialogopen(false);
fetchDeliveriesRefetch();
fetchCountRefetch();
countSourceRefetch();
}
},
onError: (err) => {
console.log(err);
opentoast(err.message, 'success');
}
});
const errorMessage = fetchDeliveriesIsError
? `Error fetching percentages: ${fetchDeliveriesError?.message}`
: fetchPercentageIsError
? `Error fetching percentages: ${fetchPercentageError?.message}`
: fetchCountIsError
? `Error fetching percentages: ${fetchCountError?.message}`
: ridersListIsError
? `Error fetching percentages: ${ridersListError?.message}`
: fetchtenantsIsError
? `Error tenant list: ${fetchtenantsError?.message}`
: fetchlocationsIsError
? `Error location list: ${fetchlocationsError?.message}`
: null;
if (errorMessage) {
console.log('errorMessage', errorMessage);
OpenToast(errorMessage, 'error', 2000);
return null; // or return <></> if inside a component
}
return (
<>
{(fetchCountIsLoading ||
fetchPercentageIsLoading ||
countSourceIsLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
riderListIsLoading) && (
<>
<Loader />
{/* <CircularLoader /> */}
</>
)}
{
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
open={
fetchCountIsLoading ||
fetchPercentageIsLoading ||
fetchDeliveriesIsLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
riderListIsLoading
}
>
{/* <CircularLoader color="inherit" /> */}
</Backdrop>
}
{/* ============================================= || TitleCard | ============================================= */}
<TitleCard title={'Deliveries'}>
<LocationAutocomplete
ref={locationRef}
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
setPage={setPage}
sx={{ width: { xs: '100%', custom450: 300 }, zIndex: '100' }}
/>
</TitleCard>
{/* ============================================= || hoverCard | ============================================= */}
<Grid container spacing={2} sx={{ mt: '1px' }}>
{[
{
label: 'Created orders',
value: percentageData?.uncoveredOrders,
percentage: percentageData?.percentage1,
color: theme.palette.info.main
},
{
label: 'Pending orders',
value: percentageData?.assignedOrders,
percentage: percentageData?.percentage2,
color: theme.palette.warning.main
},
{
label: 'Delivered orders',
value: percentageData?.pickedOrders,
percentage: percentageData?.percentage3,
color: theme.palette.success.main
},
{
label: 'Cancelled Orders',
value: percentageData?.coveredOrders,
percentage: percentageData?.percentage4,
color: theme.palette.error.main
}
].map((item, index) => (
<Grid item key={index} xs={12} custom400={6} sm={3} sx={{ cursor: 'pointer' }}>
<HoverSocialCard
primary={item.label}
secondary={fetchPercentageIsLoading ? <Skeleton sx={{ width: '30px' }} animation="wave" /> : item.value}
percentage={item.percentage?.toString()}
color={item.color}
/>
</Grid>
))}
</Grid>
{/* ============================================= || orderFilter | ============================================= */}
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={3} sx={{ flexWrap: 'wrap', my: 2 }}>
<Stack
direction="row"
flexWrap="wrap"
gap={1.5}
alignItems="center"
>
{/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and
"Orders-All" pills. Filters the loaded rows by the slot the
operator picks; defaults to 'all'. Mirrors the dispatch page's
batch model. UI mirrors the polished Absent Riders dropdown from
the orders page — custom Paper, per-batch color/icon, selected
indicator. */}
{(() => {
const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0];
const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime;
return (
<Autocomplete
disableClearable
size="small"
// Click-only dropdown: typing is disabled (readOnly input below),
// but the field still opens the menu on click. openOnFocus +
// selectOnFocus give the operator one-click access; the search
// filter built into Autocomplete is suppressed since there's
// nothing to search across only 4 options.
openOnFocus
selectOnFocus
filterOptions={(opts) => opts}
options={BATCH_OPTIONS}
value={activeBatch}
onChange={(e, val) => val && setSelectedBatch(val.id)}
getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id}
PaperComponent={(paperProps) => (
<Paper
{...paperProps}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.18)',
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
)}
ListboxProps={{ sx: { py: 0, maxHeight: 360 } }}
renderOption={(props, option, { selected }) => {
const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime;
const total = batchTotals[option.id] ?? 0;
return (
<li
{...props}
key={option.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '10px 14px',
borderBottom: '1px solid #f1f5f9',
backgroundColor: selected ? `${option.color}10` : 'transparent',
transition: 'background-color 0.15s'
}}
>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: selected ? option.color : `${option.color}18`,
color: selected ? '#fff' : option.color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
<Icon size={20} />
</Avatar>
<Stack direction="column" spacing={0} flex={1} minWidth={0}>
<Typography
variant="body2"
fontWeight={700}
color="#0f172a"
noWrap
>
{option.label}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{option.range}
</Typography>
</Stack>
{/* Per-batch total — same number you see next to each
batch on the dispatch page. Lets the operator
eyeball-compare without summing the per-status
chip counts on the tabs. */}
<Chip
size="small"
label={total}
sx={{
minWidth: 36,
height: 22,
fontWeight: 800,
fontSize: 11,
bgcolor: total > 0 ? `${option.color}18` : '#f1f5f9',
color: total > 0 ? option.color : '#94a3b8',
border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`,
'& .MuiChip-label': { px: 0.75 }
}}
/>
{selected && (
<Avatar
sx={{
width: 22,
height: 22,
bgcolor: option.color,
color: '#fff'
}}
>
<MdCheck size={14} />
</Avatar>
)}
</li>
);
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Batch"
// readOnly disables typing; the input still receives focus
// and clicks, so the popup still opens. caretColor:transparent
// hides the blinking caret that focus would normally show.
inputProps={{
...params.inputProps,
readOnly: true,
style: { ...(params.inputProps?.style || {}), cursor: 'pointer', caretColor: 'transparent' }
}}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack
direction="row"
alignItems="center"
spacing={0.75}
sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}
>
<Avatar
sx={{
width: 24,
height: 24,
bgcolor: `${activeBatch.color}18`,
color: activeBatch.color
}}
>
<ActiveIcon size={14} />
</Avatar>
</Stack>
)
}}
sx={{
minWidth: 240,
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: '999px',
bgcolor: `${activeBatch.color}08`,
fontWeight: 700,
color: activeBatch.color,
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
'& fieldset': {
borderColor: `${activeBatch.color}55`,
borderWidth: 1.5
},
'&:hover fieldset': { borderColor: activeBatch.color },
'&.Mui-focused': {
boxShadow: `0 0 0 3px ${activeBatch.color}26`
},
'&.Mui-focused fieldset': {
borderColor: activeBatch.color,
borderWidth: 2
}
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': {
color: activeBatch.color
}
}}
/>
)}
/>
);
})()}
{/* Date range chip — opens the date picker. Stays visible only when
a date range is set (matches prior behavior). */}
{startdate && enddate && (
<Chip
avatar={
<Avatar sx={{ backgroundColor: 'transparent' }}>
<MdOutlineDateRange fontSize="30px" style={{ color: '#fbc02d' }} />
</Avatar>
}
label={
<Typography noWrap color="secondary">
{dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')}
</Typography>
}
onClick={() => setOpen(true)}
variant="combined"
color="warning"
sx={{ maxWidth: '100%', cursor: 'pointer' }}
/>
)}
</Stack>
<Autocomplete
disablePortal
options={tenantlist || []}
value={tenantValue}
sx={{ minWidth: 200, flex: 1 }}
onOpen={(event) => {
if (!appId) {
event.preventDefault();
OpenToast('Please select a your location first!', 'warning', 3000);
setTimeout(() => {
locationRef.current?.focus();
}, 0);
}
}}
onChange={(e, val, reason) => {
if (reason === 'clear') {
setTenantid(0);
setTenantValue(null);
setLocationid(0);
setLocationValue(null);
} else {
setTenantid(val?.tenantid || 0);
setTenantValue(val);
setLocationid(0);
setLocationValue(null);
}
}}
renderInput={(params) => <TextField {...params} inputRef={tenantRef} label="Select Tenant" />}
/>
<Autocomplete
disablePortal
options={locationlist || []}
getOptionLabel={(option) => `${option.locationname} (${option.suburb})` || ''}
value={locationValue}
sx={{ minWidth: 200, flex: 1 }}
onOpen={(event) => {
if (!appId && !tenantid) {
event.preventDefault();
OpenToast('Please select a your Location and Tenant first!', 'warning', 3000);
setTimeout(() => {
locationRef.current?.focus();
}, 0);
} else if (!tenantid) {
event.preventDefault();
OpenToast('Please select a your Tenant first!', 'warning', 3000);
setTimeout(() => {
tenantRef.current?.focus();
}, 0);
}
}}
onChange={(e, val, reason) => {
if (reason === 'clear') {
setLocationid(0);
setLocationValue(null);
} else {
setLocationid(val.locationid || 0);
setLocationValue(val);
}
}}
renderInput={(params) => <TextField {...params} label="Select Location" />}
/>
<Autocomplete
sx={{ minWidth: 200, flex: 1 }}
fullWidth
disabled={riderListIsLoading}
options={ridersList}
getOptionLabel={(option) => `${option.firstname} ${option.lastname} (${option.contactno})`}
renderInput={(params) => <TextField {...params} label="Choose Rider" />}
onChange={(e, value, reason) => {
if (reason === 'clear') {
setRiderid(0);
} else {
setRiderid(value.userid);
console.log('selected rider', value);
}
}}
onOpen={(event) => {
if (!appId) {
event.preventDefault();
OpenToast('Please select a your location first!', 'warning', 3000);
setTimeout(() => {
locationRef.current?.focus();
}, 0);
}
}}
/>
</Stack>
{/* ============================================= || Tabs || ============================================= */}
<Stack
display={'flex'}
flexDirection={'row'}
justifyContent={'space-between'}
alignItems={'center'}
flexWrap={'wrap-reverse'}
gap={2}
sx={{
border: '1px solid ',
borderBottom: 0,
borderColor: 'bg.main',
p: 1.5
}}
>
<Tabs
value={tabvalue}
onChange={handleChangetab}
variant="scrollable"
// eslint-disable-next-line react-hooks/rules-of-hooks
scrollButtons={useMediaQuery(theme.breakpoints.down('custom1000')) ? true : false}
allowScrollButtonsMobile
>
<Tab
label="Assigned"
icon={
<Chip
label={batchCounts.uncoveredLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Accepted"
icon={
<Chip
label={batchCounts.assignedLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Arrived"
icon={
<Chip
label={batchCounts.arrivedLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Picked"
icon={
<Chip
label={batchCounts.pickedLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Active"
icon={
<Chip
label={batchCounts.activeLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Skipped"
icon={
<Chip
label={batchCounts.skippedLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Delivered"
icon={
<Chip
label={batchCounts.coveredLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
<Tab
label="Cancelled"
icon={
<Chip
label={batchCounts.cancelLength}
color="primary"
variant="light"
size="small"
sx={{ minWidth: 40, justifyContent: 'center' }}
/>
}
iconPosition="end"
/>
</Tabs>
{/* ============================================= || searchOutlined | ============================================= */}
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
sx={{ m: 0, width: 280, flex: 1 }}
/>
</Stack>
{/* ============================================= || MainCard || ============================================= */}
<MainCard content={false}>
<TableContainer
ref={containerRef}
onScroll={handleScroll}
sx={{
maxHeight: 'calc(100vh - 190px)',
overflow: 'auto',
overflowX: 'auto',
'&::-webkit-scrollbar': {
width: '12px', // scroll bar width
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main, // thumb color
borderRadius: '8px',
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.primary.dark, // hover color
cursor: 'pointer'
},
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.primary.lighter,
cursor: 'pointer'
}
}}
>
<Table stickyHeader sx={{ minWidth: 1400 }}>
<TableHead>
<TableRow>
{tabstatus == 'Created' && (
<TableCell sx={{ whiteSpace: 'nowrap' }}>
<Checkbox
indeterminate={deliverylist.length > 0 && deliverylist.length != filteredRows.length}
onChange={(e) => {
if (e.target.checked) {
setDeliverylist([...filteredRows]);
} else {
setDeliverylist([]);
}
}}
checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length}
/>
</TableCell>
)}
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>S.No </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Tenant </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>
Order Location{' '}
</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Pickup </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Drop </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Rider </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Est. Delivery Time</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Transit</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}> Kms</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Amount </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Notes </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Steps</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>Qty</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light, whiteSpace: 'nowrap', border: 'none' }}>COD </TableCell>
{tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && (
<TableCell
align="right"
sx={{
position: 'sticky !important',
backgroundColor: theme.palette.secondary.light,
whiteSpace: 'nowrap',
border: 'none'
}}
>
Action
</TableCell>
)}
</TableRow>
</TableHead>
{(loading1 || countSourceIsLoading) && <OrdersTableSkeleton col={8} />}
<TableBody>
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
<>
<TableCell colSpan={14}>
<Empty
description={
selectedBatch === 'all'
? `No ${tabstatus} Orders`
: `No ${tabstatus} Orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}`
}
styles={{ description: { color: theme.palette.error.main } }}
/>
</TableCell>
</>
)}
{filteredRows.length == 0 && countSourceLoading && (
<TableRow>
<TableCell colSpan={14} sx={{ textAlign: 'center', py: 4 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Loading deliveries
</Typography>
</TableCell>
</TableRow>
)}
{filteredRows.map((row, index) => {
return (
<>
<TableRow
role="checkbox"
tabIndex={-1}
key={row.tenantname}
sx={{
cursor: 'pointer',
backgroundColor: deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid) ? '#e1bee7' : '',
'&:hover': {
backgroundColor: deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid) ? '#e1bee7 !important' : ''
}
}}
>
{tabstatus == 'Created' && (
<TableCell>
<Checkbox
onChange={(e) => {
if (e.target.checked) {
let arr = deliverylist;
arr.push({ ...row, sno: deliverylist.length + 1 });
setDeliverylist([...arr]);
} else {
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
if (res) {
let arr = deliverylist;
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
arr.splice(res.sno - 1, 1);
arr.map((val, i) => {
val.sno = i + 1;
});
setDeliverylist([...arr]);
}
}
console.log('list', deliverylist);
}}
checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
/>
</TableCell>
)}
<TableCell>
{/* <Typography> {row.sno}</Typography> */}
<Typography> {page * rowsPerPage + index + 1}</Typography>
</TableCell>
{/* Tenants */}
<TableCell>
<Tooltip title={row.tenantadress}>
<Typography noWrap variant="subtitle1">
{row.tenantname}
</Typography>
<Typography noWrap variant="body2">
{row.tenantsuburb}
</Typography>
<Typography noWrap variant="body2">
{row.applocation}
</Typography>
</Tooltip>
</TableCell>
{/* order details */}
<TableCell align="left">
<Tooltip title="Location Name-Suburb" placement="top">
<Typography variant="subtitle1" noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
</Tooltip>
<Stack display={'flex'} flexDirection={'row'} gap={3}>
<Stack>
<Tooltip title="Order Id" placement="top">
<Typography variant="body2" noWrap>
{row.orderid}
</Typography>
</Tooltip>
<Tooltip title="Ordered date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.orderdate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
-
<Stack>
<Tooltip title="Delivery Id" placement="top">
<Typography variant="body2" noWrap>
{row.deliveryid}
</Typography>
</Tooltip>
<Tooltip title="Delivery date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.deliverydate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* pickup */}
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<Stack direction="column">
<Typography variant="subtitle1" sx={{ whiteSpace: 'nowrap' }}>
{row.pickupcustomer}
</Typography>
<Typography variant="body2">{row.pickupcontactno}</Typography>
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
<Typography variant="body2">{row.pickuplocation || row.Pickupaddress.slice(0, 12) + '...'}</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* drop */}
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<Stack direction="column">
<Typography variant="subtitle1" sx={{ whiteSpace: 'nowrap' }}>
{row.deliverycustomer}
</Typography>
<Typography variant="body2">{row.deliverycontactno}</Typography>
<Tooltip title={row.deliveryaddress}>
<Typography variant="body2" sx={{ whiteSpace: 'nowrap' }}>
{`${row.deliverylocation || row.deliveryaddress.slice(0, 12) + '...'}`}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* rider */}
<TableCell>
<Typography variant="subtitle1" noWrap>
{row.ridername}
</Typography>
<Typography variant="subtitle2"> {`ID: ${row.userid}`}</Typography>
<Typography variant="subtitle2"> {row.ridercontact}</Typography>
</TableCell>
{/* Estimated Delivery Time */}
<TableCell align="left">
<Typography variant="subtitle1" noWrap>
{row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : 'N/A'}
</Typography>
</TableCell>
{/* Transit Minutes */}
<TableCell align="left">
<Typography variant="subtitle1" noWrap>
{row.transitminutes || 0} min
</Typography>
</TableCell>
{/* kms */}
<TableCell>
<Stack direction={'column'} spacing={1}>
<Tooltip title=" KMS" placement="top">
<Chip label={row.kms || 0} size="small" variant="light" color="error" />
</Tooltip>
<Tooltip title="Actual KMS">
<Chip label={row.cumulativekms || 0} size="small" color="success" variant="light" />
</Tooltip>
</Stack>
</TableCell>
{/* amount */}
<TableCell align="left">
<Stack direction={'column'} spacing={1}>
<Tooltip title=" Delivery Charge" placement="top">
<Chip label={`${row.deliverycharges?.toFixed(2)}` || 0} size="small" variant="light" color="error" />
</Tooltip>
<Tooltip title="Delivery Amount">
<Chip label={`${row.deliveryamt?.toFixed(2)}` || 0} size="small" color="success" variant="light" />
</Tooltip>
</Stack>
</TableCell>
{/* notes */}
<TableCell>
<Typography>{row.notes}</Typography>
</TableCell>
{/* step */}
<TableCell align="center">
{row.step ? (
<Chip
label={row.step}
size="small"
color="primary"
variant="light"
sx={{ fontWeight: 700, minWidth: 28 }}
/>
) : (
<Typography variant="subtitle2" color="text.disabled">
</Typography>
)}
</TableCell>
{/* qty */}
<TableCell>
{row.Quantity ? (
<Typography variant="subtitle1" color={'error'} sx={{ whiteSpace: 'nowrap' }}>
{row.Quantity}
</Typography>
) : (
<Typography variant="subtitle1" sx={{ whiteSpace: 'nowrap' }}>
{' '}
{row.Quantity}
</Typography>
)}
</TableCell>
{/* {cash} */}
<TableCell>
{row.collectionamt ? (
<Typography variant="subtitle1" color={'error'} sx={{ whiteSpace: 'nowrap' }}>
{' '}
{`${row.collectionamt.toFixed(2)}`}{' '}
</Typography>
) : (
<Typography variant="subtitle1" sx={{ whiteSpace: 'nowrap' }}>
{' '}
{`${row.collectionamt.toFixed(2)}`}{' '}
</Typography>
)}
</TableCell>
{/* Action */}
{tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && (
<TableCell>
<Stack display={'flex'} flexDirection={'row'} justifyContent={'end'}>
{row.deliverytype == 'C' && (
<Tooltip title="Product details" placement="top">
<IconButton
color="primary"
onClick={() => {
if (productCollapse?.orderid === row.orderid) {
setProductCollapse(null); // Collapse if already open
setOrderHeaderId(null);
} else {
setProductCollapse(row); // Open if different
setOrderHeaderId(row.orderheaderid); // Open if different
}
}}
>
{productCollapse?.orderid === row.orderid ? <KeyboardArrowUpOutlined /> : <KeyboardArrowDownOutlined />}
</IconButton>
</Tooltip>
)}
<IconButton
color="primary"
size="small"
sx={{ bgcolor: 'primary.100' }}
onClick={(e) => handleMenuOpen(e, row)}
>
<EditOutlined />
</IconButton>
<Menu
anchorEl={menuAnchorEl}
open={menuOpen}
onClose={handleMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
PaperProps={{
elevation: 4,
sx: {
minWidth: 200,
borderRadius: 1
}
}}
>
{selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
handleMenuClose();
}}
>
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
<MenuItem
onClick={() => {
if (!appId) {
opentoast('Please select a location first!', 'warning');
locationRef.current?.focus();
return;
}
setChangeDialogOpen(true);
handleMenuClose();
}}
>
Change Rider
</MenuItem>
)}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
Update Delivery Status
</MenuItem>
)}
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
sx={{ color: 'error.main' }}
onClick={() => {
setCancelDeliveryOpen(true);
handleMenuClose();
}}
>
Cancel Delivery
</MenuItem>
)}
</Menu>
</Stack>
</TableCell>
)}
</TableRow>
{productCollapse?.orderid === row?.orderid && (
<TableRow>
<TableCell
colSpan={11}
sx={{
p: 0,
border: 0,
'&:hover': {
backgroundColor: 'white' // or use 'inherit'
}
}}
>
<Collapse in={true} timeout="auto" unmountOnExit>
<Box
sx={{
width: '100%',
px: 2,
py: 2
}}
>
<Table size="small" sx={{ minWidth: 800 }}>
<TableHead>
<TableRow
sx={{
'&:hover': {
backgroundColor: 'transparent !important'
}
}}
>
<TableCell sx={{ fontWeight: 600 }}>S.No</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Product Name</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Description</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
Quantity
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
Cost
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
Price
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
Tax
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>
Amount
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orderdetails?.details?.map((product, index) => (
<TableRow
key={index}
sx={{
'&:hover': {
backgroundColor: 'transparent !important'
}
}}
>
<TableCell>{index + 1}</TableCell>
<TableCell>
<Stack direction="row" spacing={1} alignItems="center">
<img
src={product?.productimage || 'https://via.placeholder.com/40'} // fallback image
alt={product?.productname}
style={{
width: 40,
height: 40,
objectFit: 'cover',
borderRadius: 4
}}
/>
<Typography variant="body2" noWrap>
{product?.productname || 'Unnamed'}
</Typography>
</Stack>
</TableCell>
<TableCell>{product?.productdescription || '-'}</TableCell>
<TableCell align="right">{product?.orderqty || 0}</TableCell>
<TableCell align="right"> {product?.price || 0}</TableCell>
<TableCell align="right"> {(product?.productsumprice ?? 0).toFixed(2)}</TableCell>
<TableCell align="right"> {(product?.taxamount ?? 0).toFixed(2)}</TableCell>
<TableCell align="right">
{(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
</TableCell>
</TableRow>
))}
<TableRow
sx={{
'&:hover': {
backgroundColor: 'transparent !important'
}
}}
>
<TableCell align="right" colSpan={7} sx={{}}>
Total Amount :
</TableCell>
<TableCell align="right" sx={{}}>
{orderdetails?.pricedetails?.orderamount.toFixed(2)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
)}
</>
);
})}
{countSourceRows?.length != 0 && (
<TableRow>
<TableCell colSpan={15} rowSpan={3}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{countIsFetchingNext ? <LoaderWithImage /> : countHasNext ? <LoaderWithImage /> : 'No More Deliveries'}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</MainCard>
{/* =============================== || cancel dialog || =============================== */}
<Dialog open={cancelDeliveryOpen} onClose={() => setCancelDeliveryOpen(false)} maxWidth="xs">
<DialogContent sx={{ mt: 2, my: 1 }}>
<Stack alignItems="center" spacing={3.5}>
<Avatar color="error" sx={{ width: 72, height: 72, fontSize: '1.75rem' }}>
<DeleteFilled />
</Avatar>
<Stack spacing={2}>
<Typography variant="h5" align="center">
Are you sure you want to cancel this delivery?
</Typography>
</Stack>
<TextField
fullWidth
label="Enter Feedback"
multiline
maxRows={4}
onChange={(e) => {
setCancelFeed(e.target.value);
}}
/>
<Stack direction="row" spacing={2} sx={{ width: 1 }}>
<Button
// disabled={cancelFeed == '' ? true : false}
fullWidth
color="error"
variant="contained"
onClick={() => {
{
cancelFeed == '' ? opentoast('Enter FeedBack', 'error') : cancelDelivery({ selectedRow, cancelFeed });
}
cancelridernotification();
}}
autoFocus
sx={{ border: '1px solid red' }}
>
Yes, Cancel
</Button>
<Button
fullWidth
onClick={() => {
setCancelDeliveryOpen(false);
}}
color="secondary"
variant="outlined"
>
No
</Button>
</Stack>
</Stack>
</DialogContent>
</Dialog>
{/* =============================== || change dialog || =============================== */}
<Dialog
open={changeDialogOpen}
onClose={() => setChangeDialogOpen(false)}
maxWidth="sm"
fullWidth
TransitionComponent={PopupTransition}
>
<DialogTitle sx={{ bgcolor: theme.palette.primary.main }}>
<Typography variant="h3" sx={{ color: 'white' }}>
Change Rider
</Typography>
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
<Grid item xs={12} sx={{ m: 2 }}>
<Autocomplete
fullWidth
options={ridersList}
// getOptionLabel={(option) => `${option.firstname} ${option.lastname} | ${option.contactno}`}
renderInput={(params) => <TextField {...params} label="Choose Rider" />}
onChange={(e, value) => {
setSelectedRider(value);
logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None');
}}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Stack direction={'row'} spacing={3}>
<Button
variant="outlined"
color="secondary"
onClick={() => {
setChangeDialogOpen(false);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
setLoading1(true);
logger.info(`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
changeRiderMutation.mutate({ selectedRider, selectedRow });
}}
>
Change Rider
</Button>
</Stack>
</DialogActions>
</Dialog>
{/* =============================== || Date filter Dialog || =============================== */}
<Dialog open={open}>
<DialogTitle align="left">
<Typography variant="h4">Select Filter Options</Typography>
</DialogTitle>
<DialogContent sx={{ width: '100%' }} className="datedialog">
<DateRangePicker
open={open}
toggle={() => setOpen(!open)}
id="daterange1"
onChange={(range) => {
if (range.label === 'All') {
setStartdate('');
setEnddate('');
setOpen(false);
} else {
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
if (range.label) {
setDatestatus(range.label);
} else {
setDatestatus('');
}
}
console.log(range);
}}
definedRanges={[
{
label: 'Today',
startDate: new Date(),
endDate: new Date()
},
{
label: 'Yesterday',
startDate: addDays(new Date(), -1),
endDate: addDays(new Date(), -1)
},
{
label: 'Tomorrow',
startDate: addDays(new Date(), +1),
endDate: addDays(new Date(), +1)
},
{
label: 'This Week',
startDate: startOfWeek(new Date()),
endDate: endOfWeek(new Date())
},
{
label: 'Last Week',
startDate: startOfWeek(addWeeks(new Date(), -1)),
endDate: endOfWeek(addWeeks(new Date(), -1))
},
{
label: 'Last 7 Days',
startDate: addWeeks(new Date(), -1),
endDate: new Date()
},
{
label: 'This Month',
startDate: startOfMonth(new Date()),
endDate: endOfMonth(new Date())
},
{
label: 'Last Month',
startDate: startOfMonth(addMonths(new Date(), -1)),
endDate: endOfMonth(addMonths(new Date(), -1))
}
// {
// label: 'All',
// startDate: new Date(),
// endDate: addDays(new Date(), -1),
// },
]}
/>
</DialogContent>
<Stack direction="row" justifyContent="flex-end" sx={{ width: '100%', p: 2 }}>
<Button variant="contained" size="small" onClick={okclicked}>
ok
</Button>
</Stack>
</Dialog>
{/* =============================== || Update Delivery Dialog || =============================== */}
<Dialog fullWidth={true} open={dialogopen} onClose={dialogclose} scroll={'paper'} maxWidth="sm" TransitionComponent={PopupTransition}>
<DialogTitle
sx={{
bgcolor: '#662582',
color: '#fff'
}}
>
Update Delivery Status
</DialogTitle>
<DialogContent dividers={true}>
<MainCard sx={{ height: '100%' }}>
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6}>
<FormLabel>Kms</FormLabel>
<TextField
fullWidth
value={kms}
type="number"
onChange={(e) => {
setKms(e.target.value);
console.log(e);
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Actual Kms</FormLabel>
<TextField value={cumulativekms} type="number" onChange={(e) => setCumulativeKms(+e.target.value)} fullWidth />
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Delivery lat</FormLabel>
<TextField
fullWidth
type="number"
value={deliverylat}
onChange={(e) => {
setDeliverylat(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Delivery Long</FormLabel>
<TextField
value={deliverylong}
type="number"
onChange={(e) => {
setDeliverylong(e.target.value);
}}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<FormLabel>Amount</FormLabel>
<TextField fullWidth type="number" value={deliveryamount} onChange={(e) => setDeliveryamount(+e.target.value)} />
</Grid>
<Grid item xs={12}>
<FormLabel>Status</FormLabel>
<TextField
select
fullWidth
value={updateStatus}
onChange={(e) => {
setUpdateStatus(e.target.value);
}}
>
<MenuItem value="pending">Pending</MenuItem>
<MenuItem value="accepted">Accepted</MenuItem>
<MenuItem value="started">Started</MenuItem>
<MenuItem value="arrived">Arrived</MenuItem>
<MenuItem value="delivered">Delivered</MenuItem>
<MenuItem value="cancelled">Cancelled</MenuItem>
</TextField>
</Grid>
<Grid item xs={12}>
<FormLabel>Notes</FormLabel>
<TextField value={notes} fullWidth onChange={(e) => setNotes(e.target.value)} />
</Grid>
</Grid>
</MainCard>
</DialogContent>
<DialogActions>
<Grid container>
<Grid item xs={12}>
<Stack direction={'row'} justifyContent={'flex-end'} spacing={2} sx={{ p: 2 }}>
<Button
variant="contained"
onClick={() => {
dialogclose();
}}
color="error"
>
Close
</Button>{' '}
<Button
variant="contained"
onClick={() => {
if (!kms) {
opentoast('Fill Kms', 'warning');
} else if (!cumulativekms) {
opentoast('Fill Actual Kms', 'warning');
} else if (!deliverylat) {
opentoast('Fill Delivery Latitude', 'warning');
} else if (!deliverylong) {
opentoast('Fill Delivery Longitude', 'warning');
} else if (!deliveryamount) {
opentoast('Fill Delivery Amount', 'warning');
} else {
updateDeliveryMutation.mutate({
deliveryid: currentorder.deliveryid,
orderheaderid: currentorder.orderheaderid,
orderstatus: updateStatus,
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
deliverylat,
deliverylong,
kms,
cumulativekms,
deliveryamt: parseFloat(deliveryamount),
notes,
userid: +selectedRow.userid
});
}
}}
>
Update
</Button>
</Stack>
</Grid>
</Grid>
</DialogActions>
</Dialog>
</>
);
};
export default Deliveries;