Part A: remaining api.js legacy functions replaced - fetchRidersList, cancelOrder/Delivery, changeRiderAPI - fetchPercentageAPI, fetchCountAPI → Doormile status counts - getpricinglist, fetchLocations, fetchRidersLogs, getusers - fetchPaymentType, getorderdetails Part B: deliveries.js status tabs → Doormile statuses Part C: 3 new pages - pages/nearle/hubs/Hubs.js (hub CRUD + miler counts) - pages/nearle/bookings/BookingDetail.js (booking detail + timeline) - pages/nearle/dashboard/Dashboard.js (live KPIs + city breakdown) Part D: routes + menu updated
2547 lines
107 KiB
JavaScript
2547 lines
107 KiB
JavaScript
import logger from '../../../utils/logger';
|
||
import { enqueueSnackbar } from 'notistack';
|
||
import { DeleteFilled, EditOutlined } from '@ant-design/icons';
|
||
import { useState, useEffect, Fragment, useRef, useMemo } from 'react';
|
||
import dayjs from 'dayjs';
|
||
var utc = require('dayjs/plugin/utc');
|
||
dayjs.extend(utc);
|
||
import axios from 'axios';
|
||
import { useTheme } from '@mui/material/styles';
|
||
import {
|
||
MdOutlineDateRange,
|
||
MdAccessTime,
|
||
MdAllInclusive,
|
||
MdLightMode,
|
||
MdWbSunny,
|
||
MdNightsStay,
|
||
MdCheck,
|
||
MdStorefront,
|
||
MdLocationOn,
|
||
MdDirectionsBike,
|
||
MdNotificationsActive,
|
||
MdPersonPin,
|
||
MdHistoryToggleOff,
|
||
MdCheckCircle,
|
||
MdCancel,
|
||
MdInventory2,
|
||
MdHourglassEmpty,
|
||
MdTune,
|
||
MdMyLocation,
|
||
MdOutlineLocalShipping,
|
||
MdOutlinePendingActions,
|
||
MdOutlineCheckCircle,
|
||
MdOutlineCancel
|
||
} from 'react-icons/md';
|
||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||
|
||
import {
|
||
Avatar,
|
||
Box,
|
||
Button,
|
||
Chip,
|
||
Grid,
|
||
IconButton,
|
||
Stack,
|
||
Typography,
|
||
Table,
|
||
TableCell,
|
||
TableBody,
|
||
TableHead,
|
||
Collapse,
|
||
Dialog,
|
||
TableRow,
|
||
DialogContent,
|
||
DialogTitle,
|
||
Tooltip,
|
||
DialogActions,
|
||
Checkbox,
|
||
Autocomplete,
|
||
TextField,
|
||
TableContainer,
|
||
Backdrop,
|
||
MenuItem,
|
||
Menu,
|
||
Paper,
|
||
useMediaQuery
|
||
} from '@mui/material';
|
||
|
||
import { PopupTransition } from 'components/@extended/Transitions';
|
||
import {
|
||
addDays,
|
||
addMonths,
|
||
addWeeks,
|
||
endOfMonth,
|
||
endOfWeek,
|
||
startOfMonth,
|
||
startOfWeek
|
||
} from 'date-fns';
|
||
import { DateRangePicker } from 'mui-daterange-picker';
|
||
import * as React from 'react';
|
||
import Loader from 'components/Loader';
|
||
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
|
||
|
||
import {
|
||
cancelDeliveryAPI,
|
||
changeRiderAPI,
|
||
fetchCountAPI,
|
||
fetchDeliveries,
|
||
fetchRidersList,
|
||
notifyRider,
|
||
updateDeliveryAPI,
|
||
getorderdetails,
|
||
gettenantlocations,
|
||
getTenants
|
||
} from 'pages/api/api';
|
||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||
import { OpenToast } from 'components/third-party/OpenToast';
|
||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||
import PageHeader from 'components/nearle_components/PageHeader';
|
||
import StatCard from 'components/nearle_components/StatCard';
|
||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||
|
||
// ============================================================================
|
||
// Design tokens — extracted from the polished "Batch" dropdown so every
|
||
// surface on this page (filters, KPIs, tabs, status badges, dialogs) shares
|
||
// the same visual language. All helpers take a color and emit MUI sx values.
|
||
// ============================================================================
|
||
const DT = {
|
||
radiusPill: 999,
|
||
radiusCard: 14,
|
||
radiusInner: 10,
|
||
radiusField: 10,
|
||
// Restrained, low-contrast elevation — corporate (Linear/Stripe), not flashy.
|
||
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
|
||
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
|
||
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
|
||
textPrimary: '#0f172a',
|
||
textSecondary: '#64748b',
|
||
textMuted: '#94a3b8',
|
||
borderSubtle: '#e2e8f0',
|
||
borderHover: '#cbd5e1',
|
||
divider: '#f1f5f9',
|
||
surface: '#ffffff',
|
||
surfaceAlt: '#f8fafc',
|
||
brand: '#C01227'
|
||
};
|
||
|
||
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
|
||
// pattern (`${color}08`, `${color}18`, `${color}55`, `${color}26`).
|
||
const a = (c, suffix) => `${c}${suffix}`;
|
||
const tint = (c) => a(c, '08'); // very subtle surface tint
|
||
const soft = (c) => a(c, '18'); // soft chip / avatar bg
|
||
const ring = (c) => a(c, '26'); // focus ring color
|
||
const edge = (c) => a(c, '55'); // resting border
|
||
|
||
// Pill input sx — used by every filter Autocomplete/TextField on the page.
|
||
// Accepts the accent color and returns sx for the outer TextField. Width is
|
||
// driven by parent flex/grid so this helper stays width-agnostic.
|
||
// Neutral, corporate filter field: white surface, hairline border, brand focus
|
||
// ring. Colour is no longer used to tint the whole control (that produced the
|
||
// "rainbow" filter bar) — accent now lives only in the small start-adornment
|
||
// icon, which aids scanning without flooding the surface.
|
||
const pillFieldSx = () => ({
|
||
cursor: 'pointer',
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: DT.radiusField + 'px',
|
||
bgcolor: DT.surface,
|
||
fontWeight: 600,
|
||
color: DT.textPrimary,
|
||
paddingRight: '8px',
|
||
cursor: 'pointer',
|
||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
|
||
'&:hover fieldset': { borderColor: DT.borderHover },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` },
|
||
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 }
|
||
},
|
||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
|
||
});
|
||
|
||
// Status palette — drives tab pills, row status badges, dialogs. Keys are
|
||
// Doormile's real booking statuses, lowercased (row.status is compared
|
||
// lowercased everywhere below so exact backend casing doesn't matter).
|
||
const STATUS_META = {
|
||
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
|
||
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
|
||
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
|
||
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
|
||
picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
|
||
at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
|
||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||
};
|
||
|
||
// Ordered status list driving the tabs row (left → right). `status` is the
|
||
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
|
||
// casing Doormile's /admin/bookings status filter expects.
|
||
const STATUS_TABS = [
|
||
{ status: 'all', apiStatus: 'all', countKey: 'all' },
|
||
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
|
||
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
|
||
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
|
||
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
|
||
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
|
||
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
|
||
{ status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
|
||
{ status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
|
||
];
|
||
|
||
// KPI palette + icons — mirrors the four cards across the top of the page.
|
||
const KPI_META = [
|
||
{ key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping },
|
||
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions },
|
||
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle },
|
||
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel }
|
||
];
|
||
|
||
// Batches mirror the dispatch page's slot definitions so an operator who
|
||
// segments the day there sees the same buckets here. Hours are 24h, half-open
|
||
// [startHour, endHour) — a delivery at exactly endHour falls into the *next*
|
||
// batch (or none, if the gap isn't covered).
|
||
const BATCH_OPTIONS = [
|
||
{ id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' },
|
||
{ id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 },
|
||
{ id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12:30 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12.5 },
|
||
{ id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 }
|
||
];
|
||
|
||
// Per-batch icon components — kept separate from BATCH_OPTIONS so the option
|
||
// objects stay serializable / printable. Looked up at render time by iconKey.
|
||
const BATCH_ICONS = {
|
||
all: MdAllInclusive,
|
||
morning: MdLightMode,
|
||
afternoon: MdWbSunny,
|
||
evening: MdNightsStay
|
||
};
|
||
|
||
// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the
|
||
// page lands them on the slot they're most likely curious about. Falls back
|
||
// to 'all' when the current hour lies in a configured gap (8–9 AM, 12 PM–4
|
||
// 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
|
||
// 8–9 AM / 12 PM–4 PM gap and silently disappeared from every batch on this
|
||
// page while still showing up on dispatch's Morning Batch. Rows without an
|
||
// `assigntime` (unassigned pending orders) return null and intentionally
|
||
// don't bucket — matches dispatch.
|
||
const BATCH_TIME_KEYS = ['assigntime'];
|
||
|
||
// Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null
|
||
// when the row has no usable timestamp / falls in a gap between batches.
|
||
const getRowBatchId = (row) => {
|
||
let t = null;
|
||
for (const k of BATCH_TIME_KEYS) {
|
||
if (row?.[k]) {
|
||
t = row[k];
|
||
break;
|
||
}
|
||
}
|
||
if (!t) return null;
|
||
const str = String(t).trim();
|
||
// Bare YYYY-MM-DD with no time component would always parse to midnight and
|
||
// get mis-bucketed into Morning — skip those.
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
|
||
// Parse in LOCAL time to match the dispatch page's bucketing exactly
|
||
// (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment,
|
||
// so a row that lands in Evening Batch on the dispatch page must also land
|
||
// in Evening Batch here — otherwise an operator running in IST/PST/etc.
|
||
// sees different totals on the two pages for the same underlying rows.
|
||
const d = dayjs(t);
|
||
if (!d.isValid()) return null;
|
||
const h = d.hour() + d.minute() / 60;
|
||
for (const b of BATCH_OPTIONS) {
|
||
if (b.id === 'all') continue;
|
||
if (h >= b.startHour && h < b.endHour) return b.id;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// Shared Paper used by every Autocomplete popup on the page. Matches the
|
||
// batch dropdown's soft elevated look.
|
||
const SoftPaper = (props) => (
|
||
<Paper
|
||
{...props}
|
||
sx={{
|
||
mt: 0.75,
|
||
borderRadius: 2,
|
||
boxShadow: DT.shadowPop,
|
||
border: '1px solid',
|
||
borderColor: 'divider',
|
||
overflow: 'hidden'
|
||
}}
|
||
/>
|
||
);
|
||
|
||
// Renders a colored avatar with an icon — used in pill input adornments and
|
||
// in dropdown rows. Tracks `selected` so it can flip between filled and
|
||
// soft variants identically to the batch dropdown.
|
||
const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||
<Avatar
|
||
sx={{
|
||
width: size,
|
||
height: size,
|
||
bgcolor: selected ? color : soft(color),
|
||
color: selected ? '#fff' : color,
|
||
transition: 'background-color 0.15s, color 0.15s'
|
||
}}
|
||
>
|
||
{children}
|
||
</Avatar>
|
||
);
|
||
|
||
// ================================================= || deliveries (initial point)|| =================================================
|
||
const Deliveries = () => {
|
||
const userid = localStorage.getItem('userid');
|
||
const theme = useTheme();
|
||
// Below `md` we swap the wide data table for an app-style card list. Desktop
|
||
// (md and up) keeps the exact same table — no behaviour change either way.
|
||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||
const loadMoreRef = useRef();
|
||
const containerRef = useRef();
|
||
const [deliverylist, setDeliverylist] = useState([]);
|
||
const [dialogopen, setDialogopen] = useState(false);
|
||
const [locaName, setLocoName] = useState('All');
|
||
const [appId, setAppId] = useState(0);
|
||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [tabstatus, setTabstatus] = useState('All');
|
||
const [tabvalue, setTabvalue] = useState(0);
|
||
const [open, setOpen] = useState(false);
|
||
const [kms, setKms] = useState('');
|
||
const [cumulativekms, setCumulativeKms] = useState();
|
||
const [deliveryamount, setDeliveryamount] = useState();
|
||
const [notes, setNotes] = useState('');
|
||
const [currentorder, setCurrentorder] = useState({});
|
||
const [deliverylat, setDeliverylat] = useState('');
|
||
const [deliverylong, setDeliverylong] = useState('');
|
||
const [currentStatus, setCurrentStatus] = useState('all');
|
||
const [updateStatus, setUpdateStatus] = useState('delivered');
|
||
const locationRef = useRef(null);
|
||
const tenantRef = useRef(null);
|
||
const [page, setPage] = React.useState(0);
|
||
const [rowsPerPage, setRowsPerPage] = React.useState(50);
|
||
const [productCollapse, setProductCollapse] = useState(null);
|
||
const [orderHeaderid, setOrderHeaderId] = useState(null);
|
||
const [searchword, setSearchword] = useState('');
|
||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
|
||
const [selectedRow, setSelectedRow] = useState(null);
|
||
const [loading1, setLoading1] = useState(false);
|
||
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
|
||
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
||
const [cancelFeed, setCancelFeed] = useState('');
|
||
const [selectedRider, setSelectedRider] = useState(null);
|
||
const [tenantid, setTenantid] = useState(0);
|
||
const [locationid, setLocationid] = useState(0);
|
||
const [tenantValue, setTenantValue] = useState(null);
|
||
const [locationValue, setLocationValue] = useState(null);
|
||
const [riderid, setRiderid] = useState(0);
|
||
// Selected batch id — drives the client-side row filter. Defaults to the
|
||
// batch matching the current UTC hour (the operator is most likely curious
|
||
// about "now"); never `null` since there's no longer an "All" option.
|
||
const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId);
|
||
const roleid = localStorage.getItem('roleid');
|
||
|
||
useEffect(() => {
|
||
setTenantid(0);
|
||
setTenantValue(null);
|
||
setLocationid(0);
|
||
setLocationValue(null);
|
||
}, [appId]);
|
||
// to clear the location autocomplete
|
||
useEffect(() => {
|
||
setLocationid(0);
|
||
setLocationValue(null);
|
||
}, [tenantid]);
|
||
|
||
const menuOpen = Boolean(menuAnchorEl);
|
||
|
||
const handleMenuOpen = (event, row) => {
|
||
setSelectedRow(row);
|
||
console.log('selectedRow', row);
|
||
|
||
setMenuAnchorEl(event.currentTarget);
|
||
};
|
||
|
||
const handleMenuClose = () => {
|
||
setMenuAnchorEl(null);
|
||
};
|
||
|
||
// =========================================== || cancelDelivery || ===========================================
|
||
|
||
const { mutate: cancelDelivery } = useMutation({
|
||
mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed),
|
||
onSuccess: () => {
|
||
opentoast('Delivery Cancelled Successfully', 'success');
|
||
setCancelDeliveryOpen(false);
|
||
fetchCountRefetch(); // Refresh count data
|
||
fetchDeliveriesRefetch(); // Refresh deliveries
|
||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||
},
|
||
onError: (error) => {
|
||
opentoast(error.message, 'error');
|
||
}
|
||
});
|
||
|
||
// ==============================|| cancelridernotification ||============================== //
|
||
|
||
const cancelridernotification = async () => {
|
||
console.log('cancelridernotification', selectedRow);
|
||
try {
|
||
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
|
||
token: selectedRow.userfcmtoken,
|
||
notification: {
|
||
title: 'NearleXpress',
|
||
body: `${selectedRow.orderid} have been Cancelled`,
|
||
sound: 'ring',
|
||
image: ''
|
||
},
|
||
data: {
|
||
type: 'cancel'
|
||
}
|
||
});
|
||
return response.data;
|
||
} catch (err) {
|
||
opentoast(err.message, 'error', 2000);
|
||
console.log(err);
|
||
}
|
||
};
|
||
|
||
// ==============================|| getTenants ||============================== //
|
||
|
||
const {
|
||
data: tenantlist,
|
||
isLoading: fetchtenantsIsLoading,
|
||
isError: fetchtenantsIsError,
|
||
error: fetchtenantsError
|
||
} = useQuery({
|
||
queryKey: ['tenantlist', appId],
|
||
queryFn: () => getTenants(appId), // Ensure appId is passed
|
||
enabled: appId !== 0 // Ensures query runs only when appId is valid
|
||
});
|
||
// ==============================|| gettenantlocations ||============================== //
|
||
|
||
const {
|
||
data: locationlist,
|
||
isLoading: fetchlocationsIsLoading,
|
||
isError: fetchlocationsIsError,
|
||
error: fetchlocationsError
|
||
} = useQuery({
|
||
queryKey: ['gettenantlocations', tenantid],
|
||
queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed
|
||
enabled: tenantid !== 0 // Ensures query runs only when appId is valid
|
||
});
|
||
// =========================================== || notifyrider || ===========================================
|
||
|
||
const notifyRiderMutation = useMutation({
|
||
mutationFn: notifyRider, // Using the separate function
|
||
onSuccess: () => {
|
||
enqueueSnackbar('Notification sent Successfully', {
|
||
variant: 'success',
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
},
|
||
onError: (error) => {
|
||
enqueueSnackbar(error.message, {
|
||
variant: 'error',
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
}
|
||
});
|
||
|
||
// =========================================== || changerider || ===========================================
|
||
const changeRiderMutation = useMutation({
|
||
mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow),
|
||
onSuccess: (res, { selectedRider, selectedRow }) => {
|
||
setLoading1(false);
|
||
setChangeDialogOpen(false);
|
||
if (res.data.message === 'Success') {
|
||
logger.info(`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
|
||
opentoast('Rider Changed Successfully', 'success');
|
||
}
|
||
fetchCountRefetch(); // Refresh count data
|
||
fetchDeliveriesRefetch(); // Refresh deliveries
|
||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
|
||
},
|
||
onError: (err, { selectedRow }) => {
|
||
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
|
||
opentoast(err.message, 'error');
|
||
setLoading1(false);
|
||
}
|
||
});
|
||
|
||
/* ============================================= || opentoast || ============================================= */
|
||
|
||
const opentoast = (message, variant) => {
|
||
enqueueSnackbar(message, {
|
||
variant,
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
};
|
||
|
||
// ==============================|| getorderdetails ||============================== //
|
||
|
||
const { data: orderdetails } = useQuery({
|
||
queryKey: ['orderdetails', orderHeaderid],
|
||
queryFn: () => getorderdetails(orderHeaderid),
|
||
enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined
|
||
});
|
||
|
||
const dialogclose = () => {
|
||
setDialogopen(false);
|
||
};
|
||
|
||
const handleChangetab = (e, i) => {
|
||
setPage(0);
|
||
setTabvalue(i);
|
||
setRowsPerPage(50);
|
||
const tab = STATUS_TABS[i];
|
||
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
|
||
setCurrentStatus(tab.apiStatus);
|
||
setSearchword('');
|
||
};
|
||
|
||
const okclicked = () => {
|
||
setOpen(false);
|
||
};
|
||
|
||
/* ============================================= || fetchDeliveries | ============================================= */
|
||
|
||
const {
|
||
isLoading: fetchDeliveriesIsLoading,
|
||
isError: fetchDeliveriesIsError,
|
||
error: fetchDeliveriesError,
|
||
refetch: fetchDeliveriesRefetch
|
||
} = useInfiniteQuery({
|
||
queryKey: [
|
||
'fetchdeliveries',
|
||
appId,
|
||
userid,
|
||
currentStatus,
|
||
startdate,
|
||
enddate,
|
||
rowsPerPage,
|
||
debouncedSearch,
|
||
tenantid,
|
||
locationid,
|
||
riderid
|
||
],
|
||
queryFn: fetchDeliveries,
|
||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
|
||
});
|
||
|
||
// (filteredRows is defined below, after countSourceRows / countSourceLoading
|
||
// are in scope — they're the single source of truth for both the table and
|
||
// the per-status chip counts.)
|
||
|
||
// Parallel "all-statuses" query used ONLY to derive batch-aware tab counts.
|
||
// The primary `fetchdeliveries` query above is keyed by `currentStatus`, so
|
||
// its rows are scoped to the active tab — they can't tell us how many
|
||
// delivered/cancelled/etc. exist in the same batch. We fetch the full day
|
||
// once (status='all'), keep it cached, and re-derive the counts per status
|
||
// whenever the operator switches batches. Search keyword is intentionally
|
||
// dropped from this key so the chip counts reflect the batch totals rather
|
||
// than the search-filtered subset.
|
||
const {
|
||
data: countSourceData,
|
||
fetchNextPage: countFetchNext,
|
||
hasNextPage: countHasNext,
|
||
isFetchingNextPage: countIsFetchingNext,
|
||
isLoading: countSourceIsLoading,
|
||
refetch: countSourceRefetch
|
||
} = useInfiniteQuery({
|
||
queryKey: [
|
||
'fetchdeliveries-batchcounts',
|
||
appId,
|
||
userid,
|
||
'all',
|
||
startdate,
|
||
enddate,
|
||
200,
|
||
'',
|
||
tenantid,
|
||
locationid,
|
||
riderid
|
||
],
|
||
queryFn: fetchDeliveries,
|
||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
|
||
});
|
||
|
||
const countSourceRows = useMemo(
|
||
() => (countSourceData?.pages || []).flatMap((p) => p.rows || []),
|
||
[countSourceData]
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (countHasNext && !countIsFetchingNext) countFetchNext();
|
||
}, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]);
|
||
|
||
// Loading flag for the table empty state — true while we're still streaming
|
||
// pages, so we don't show "No <status> Orders" prematurely before the full
|
||
// day's rows have arrived. Goes false once auto-pagination drains.
|
||
const countSourceLoading = countHasNext || countIsFetchingNext;
|
||
|
||
// The table now reads from the same source as the chip counts. That way the
|
||
// "74" in the Delivered chip and the rows shown in the table can never
|
||
// disagree (they used to, because the table consumed a separate per-status
|
||
// paginated query while the chips read from the full all-statuses dataset).
|
||
// Three filter steps:
|
||
// 1. Batch — selected via the dropdown. 'all' bypasses this filter.
|
||
// 2. Tab status — currentStatus comes from handleChangetab.
|
||
// 3. Search — client-side substring match across customer/address/order
|
||
// fields. Server-side search isn't needed since every row is loaded.
|
||
const filteredRows = useMemo(() => {
|
||
const wantStatus = String(currentStatus || '').toLowerCase();
|
||
const q = String(debouncedSearch || '').trim().toLowerCase();
|
||
return countSourceRows.filter((r) => {
|
||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
|
||
const s = String(r.status || '').toLowerCase();
|
||
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
|
||
if (q) {
|
||
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
|
||
.map((v) => String(v || '').toLowerCase())
|
||
.join(' ');
|
||
if (!hay.includes(q)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]);
|
||
|
||
// Counts per status, scoped to the selected batch. Keys mirror the legacy
|
||
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
|
||
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
|
||
const batchCounts = useMemo(() => {
|
||
const c = {};
|
||
countSourceRows.forEach((r) => {
|
||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
|
||
const s = String(r.status || '').toLowerCase();
|
||
c[s] = (c[s] || 0) + 1;
|
||
c.all = (c.all || 0) + 1;
|
||
});
|
||
return c;
|
||
}, [countSourceRows, selectedBatch]);
|
||
|
||
// Per-batch total (any status) for the dropdown badges. Computed once for
|
||
// every batch + 'all' so each option in the menu can show its own count
|
||
// without re-walking the list. Mirrors dispatch's batchCounts shape
|
||
// (`Dispatch.js:1198–1206`) — totals here should match the per-batch numbers
|
||
// visible on the dispatch page for the same date.
|
||
const batchTotals = useMemo(() => {
|
||
// `all` is the SUM of the three batch buckets — not the raw row count.
|
||
// Rows that fall in a gap (8–9 AM, 12:30–4 PM, after 7 PM) or have no
|
||
// `assigntime` belong to no batch, so they're excluded here too. This keeps
|
||
// the "All Batches" badge equal to Morning + Afternoon + Evening
|
||
// (e.g. 36 + 89 + 53 = 178) instead of over-reporting the un-bucketed total.
|
||
const totals = { all: 0 };
|
||
BATCH_OPTIONS.forEach((b) => {
|
||
if (b.id === 'all') return;
|
||
totals[b.id] = 0;
|
||
});
|
||
countSourceRows.forEach((r) => {
|
||
const id = getRowBatchId(r);
|
||
if (id) {
|
||
totals[id] = (totals[id] || 0) + 1;
|
||
totals.all += 1;
|
||
}
|
||
});
|
||
return totals;
|
||
}, [countSourceRows]);
|
||
|
||
// IntersectionObserver now watches the count-source query — that's the
|
||
// single source of truth for both the table and the chip counts. When the
|
||
// sentinel scrolls into view it nudges the next page of all-statuses
|
||
// deliveries to keep the dataset complete.
|
||
useEffect(() => {
|
||
if (!countHasNext) return;
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
if (entries[0].isIntersecting) {
|
||
countFetchNext();
|
||
}
|
||
},
|
||
{
|
||
root: document.querySelector('.MuiTableContainer-root'),
|
||
rootMargin: '0px',
|
||
threshold: 1.0
|
||
}
|
||
);
|
||
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
||
return () => {
|
||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||
};
|
||
}, [countHasNext, countFetchNext]);
|
||
|
||
const handleScroll = (event) => {
|
||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||
if (countHasNext && !countIsFetchingNext) {
|
||
countFetchNext();
|
||
}
|
||
}
|
||
};
|
||
|
||
/* ============================================= || fetchPercentageAPI | ============================================= */
|
||
|
||
/* ============================================= || fetchcount | ============================================= */
|
||
|
||
const {
|
||
data: countData = {}, // Default to empty object
|
||
isLoading: fetchCountIsLoading,
|
||
isError: fetchCountIsError,
|
||
error: fetchCountError,
|
||
refetch: fetchCountRefetch
|
||
} = useQuery({
|
||
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
|
||
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
|
||
});
|
||
// ==============================|| fetchRidersList ||============================== //
|
||
|
||
const {
|
||
data: ridersList = [],
|
||
isLoading: riderListIsLoading,
|
||
isError: ridersListIsError,
|
||
error: ridersListError
|
||
} = useQuery({
|
||
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
|
||
queryFn: fetchRidersList,
|
||
enabled: Boolean(appId),
|
||
onError: (err) => {
|
||
OpenToast(err.message, 'error', 2000);
|
||
}
|
||
});
|
||
|
||
/* ============================================= || updatedelivery | ============================================= */
|
||
|
||
const updateDeliveryMutation = useMutation({
|
||
mutationFn: (orderData) => updateDeliveryAPI(orderData),
|
||
onSuccess: (res) => {
|
||
console.log(res);
|
||
if (res.data.status) {
|
||
opentoast('Updated Successfully', 'success');
|
||
setDeliveryamount('');
|
||
setNotes('');
|
||
setDialogopen(false);
|
||
fetchDeliveriesRefetch();
|
||
fetchCountRefetch();
|
||
countSourceRefetch();
|
||
}
|
||
},
|
||
onError: (err) => {
|
||
console.log(err);
|
||
opentoast(err.message, 'success');
|
||
}
|
||
});
|
||
|
||
const errorMessage = fetchDeliveriesIsError
|
||
? `Error fetching deliveries: ${fetchDeliveriesError?.message}`
|
||
: fetchCountIsError
|
||
? `Error fetching count summary: ${fetchCountError?.message}`
|
||
: ridersListIsError
|
||
? `Error fetching riders: ${ridersListError?.message}`
|
||
: fetchtenantsIsError
|
||
? `Error tenant list: ${fetchtenantsError?.message}`
|
||
: fetchlocationsIsError
|
||
? `Error location list: ${fetchlocationsError?.message}`
|
||
: null;
|
||
|
||
if (errorMessage) {
|
||
console.log('errorMessage', errorMessage);
|
||
OpenToast(errorMessage, 'error', 2000);
|
||
return null; // or return <></> if inside a component
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{(fetchCountIsLoading ||
|
||
countSourceIsLoading ||
|
||
fetchtenantsIsLoading ||
|
||
fetchlocationsIsLoading ||
|
||
riderListIsLoading) && (
|
||
<>
|
||
<Loader />
|
||
{/* <CircularLoader /> */}
|
||
</>
|
||
)}
|
||
{
|
||
<Backdrop
|
||
sx={{
|
||
color: '#fff',
|
||
zIndex: (theme) => theme.zIndex.drawer + 1
|
||
}}
|
||
open={
|
||
fetchCountIsLoading ||
|
||
fetchDeliveriesIsLoading ||
|
||
fetchtenantsIsLoading ||
|
||
fetchlocationsIsLoading ||
|
||
riderListIsLoading
|
||
}
|
||
>
|
||
{/* <CircularLoader color="inherit" /> */}
|
||
</Backdrop>
|
||
}
|
||
{/* ============================================= || Header | ============================================= */}
|
||
<PageHeader
|
||
title="Deliveries"
|
||
subtitle={`Live · ${locaName || 'All Zones'}`}
|
||
live
|
||
action={
|
||
<LocationAutocomplete
|
||
ref={locationRef}
|
||
locaName={locaName}
|
||
setAppId={setAppId}
|
||
setLocoName={setLocoName}
|
||
setPage={setPage}
|
||
pill
|
||
accentColor="#C01227"
|
||
icon={<MdMyLocation size={14} />}
|
||
placeholder="Select Zone"
|
||
paperComponent={SoftPaper}
|
||
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
|
||
/>
|
||
}
|
||
/>
|
||
|
||
{/* ============================================= || KPI Cards | ============================================= */}
|
||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||
{[
|
||
{ ...KPI_META[0], value: batchTotals.all, percentage: null },
|
||
{ ...KPI_META[1], value: countData?.uncoveredLength, percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 },
|
||
{ ...KPI_META[2], value: countData?.coveredLength, percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 },
|
||
{ ...KPI_META[3], value: countData?.cancelLength, percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 }
|
||
].map((item) => {
|
||
const Icon = item.icon;
|
||
const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage);
|
||
const hasPct = !Number.isNaN(pct) && item.percentage !== undefined && item.percentage !== null;
|
||
return (
|
||
<Grid item key={item.key} xs={6} sm={6} md={3}>
|
||
<StatCard
|
||
title={item.label}
|
||
value={item.value ?? 0}
|
||
icon={<Icon size={20} />}
|
||
color={item.color}
|
||
loading={fetchCountIsLoading}
|
||
caption={hasPct ? `${Math.abs(pct)}% of total` : undefined}
|
||
/>
|
||
</Grid>
|
||
);
|
||
})}
|
||
</Grid>
|
||
|
||
{/* ============================================= || Filter Bar | ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
mt: { xs: 1.5, md: 2 },
|
||
mb: { xs: 1.5, md: 2 },
|
||
p: { xs: 1.5, md: 2 },
|
||
borderRadius: DT.radiusCard / 8,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
background: '#fff',
|
||
boxShadow: DT.shadowMd
|
||
}}
|
||
>
|
||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}>
|
||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
|
||
<MdTune size={16} />
|
||
</Avatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
|
||
Filters
|
||
</Typography>
|
||
</Stack>
|
||
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={1.25} sx={{ flexWrap: 'wrap' }}>
|
||
<Stack
|
||
direction="row"
|
||
flexWrap="wrap"
|
||
gap={1.5}
|
||
alignItems="center"
|
||
>
|
||
{/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and
|
||
"Orders-All" pills. Filters the loaded rows by the slot the
|
||
operator picks; defaults to 'all'. Mirrors the dispatch page's
|
||
batch model. UI mirrors the polished Absent Riders dropdown from
|
||
the orders page — custom Paper, per-batch color/icon, selected
|
||
indicator. */}
|
||
{(() => {
|
||
const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0];
|
||
const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime;
|
||
return (
|
||
<Autocomplete
|
||
disableClearable
|
||
size="small"
|
||
// Click-only dropdown: typing is disabled (readOnly input below),
|
||
// but the field still opens the menu on click. openOnFocus +
|
||
// selectOnFocus give the operator one-click access; the search
|
||
// filter built into Autocomplete is suppressed since there's
|
||
// nothing to search across only 4 options.
|
||
openOnFocus
|
||
selectOnFocus
|
||
filterOptions={(opts) => opts}
|
||
options={BATCH_OPTIONS}
|
||
value={activeBatch}
|
||
onChange={(e, val) => val && setSelectedBatch(val.id)}
|
||
getOptionLabel={(option) => option.label}
|
||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||
PaperComponent={SoftPaper}
|
||
ListboxProps={{ sx: { py: 0, maxHeight: 360 } }}
|
||
renderOption={(props, option, { selected }) => {
|
||
const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime;
|
||
const total = batchTotals[option.id] ?? 0;
|
||
return (
|
||
<li
|
||
{...props}
|
||
key={option.id}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
padding: '10px 14px',
|
||
borderBottom: '1px solid #f1f5f9',
|
||
backgroundColor: selected ? `${option.color}10` : 'transparent',
|
||
transition: 'background-color 0.15s'
|
||
}}
|
||
>
|
||
<Avatar
|
||
sx={{
|
||
width: 36,
|
||
height: 36,
|
||
bgcolor: selected ? option.color : `${option.color}18`,
|
||
color: selected ? '#fff' : option.color,
|
||
transition: 'background-color 0.15s, color 0.15s'
|
||
}}
|
||
>
|
||
<Icon size={20} />
|
||
</Avatar>
|
||
<Stack direction="column" spacing={0} flex={1} minWidth={0}>
|
||
<Typography
|
||
variant="body2"
|
||
fontWeight={700}
|
||
color="#0f172a"
|
||
noWrap
|
||
>
|
||
{option.label}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary" noWrap>
|
||
{option.range}
|
||
</Typography>
|
||
</Stack>
|
||
{/* Per-batch total — same number you see next to each
|
||
batch on the dispatch page. Lets the operator
|
||
eyeball-compare without summing the per-status
|
||
chip counts on the tabs. */}
|
||
<Chip
|
||
size="small"
|
||
label={total}
|
||
sx={{
|
||
minWidth: 36,
|
||
height: 22,
|
||
fontWeight: 800,
|
||
fontSize: 11,
|
||
bgcolor: total > 0 ? `${option.color}18` : '#f1f5f9',
|
||
color: total > 0 ? option.color : '#94a3b8',
|
||
border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`,
|
||
'& .MuiChip-label': { px: 0.75 }
|
||
}}
|
||
/>
|
||
{selected && (
|
||
<Avatar
|
||
sx={{
|
||
width: 22,
|
||
height: 22,
|
||
bgcolor: option.color,
|
||
color: '#fff'
|
||
}}
|
||
>
|
||
<MdCheck size={14} />
|
||
</Avatar>
|
||
)}
|
||
</li>
|
||
);
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="Batch"
|
||
// readOnly disables typing; the input still receives focus
|
||
// and clicks, so the popup still opens. caretColor:transparent
|
||
// hides the blinking caret that focus would normally show.
|
||
inputProps={{
|
||
...params.inputProps,
|
||
readOnly: true,
|
||
style: { ...(params.inputProps?.style || {}), cursor: 'pointer', caretColor: 'transparent' }
|
||
}}
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={0.75}
|
||
sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}
|
||
>
|
||
<Avatar
|
||
sx={{
|
||
width: 24,
|
||
height: 24,
|
||
bgcolor: `${activeBatch.color}18`,
|
||
color: activeBatch.color
|
||
}}
|
||
>
|
||
<ActiveIcon size={14} />
|
||
</Avatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
sx={{
|
||
minWidth: { xs: 180, sm: 220, md: 240 },
|
||
cursor: 'pointer',
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: '10px',
|
||
bgcolor: '#ffffff',
|
||
fontWeight: 600,
|
||
color: '#0f172a',
|
||
paddingRight: '8px',
|
||
cursor: 'pointer',
|
||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
|
||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||
},
|
||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
);
|
||
})()}
|
||
|
||
{/* Date range pill — opens the date picker. Stays visible only when
|
||
a date range is set. Uses the same pill aesthetic as Batch. */}
|
||
{startdate && enddate && (
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={0.75}
|
||
onClick={() => setOpen(true)}
|
||
sx={{
|
||
pl: 0.5,
|
||
pr: 1.25,
|
||
py: 0.5,
|
||
borderRadius: '10px',
|
||
cursor: 'pointer',
|
||
bgcolor: '#ffffff',
|
||
border: '1px solid #e2e8f0',
|
||
transition: 'border-color 0.15s',
|
||
'&:hover': { borderColor: '#cbd5e1' }
|
||
}}
|
||
>
|
||
<AccentAvatar color="#94a3b8">
|
||
<MdOutlineDateRange size={14} />
|
||
</AccentAvatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', whiteSpace: 'nowrap' }}>
|
||
{dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')}
|
||
</Typography>
|
||
</Stack>
|
||
)}
|
||
|
||
</Stack>
|
||
|
||
{/* Tenant */}
|
||
<Autocomplete
|
||
disablePortal
|
||
options={tenantlist || []}
|
||
value={tenantValue}
|
||
PaperComponent={SoftPaper}
|
||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
|
||
onOpen={(event) => {
|
||
if (!appId) {
|
||
event.preventDefault();
|
||
OpenToast('Please select your zone first!', 'warning', 3000);
|
||
setTimeout(() => locationRef.current?.focus(), 0);
|
||
}
|
||
}}
|
||
onChange={(e, val, reason) => {
|
||
if (reason === 'clear') {
|
||
setTenantid(0);
|
||
setTenantValue(null);
|
||
setLocationid(0);
|
||
setLocationValue(null);
|
||
} else {
|
||
setTenantid(val?.tenantid || 0);
|
||
setTenantValue(val);
|
||
setLocationid(0);
|
||
setLocationValue(null);
|
||
}
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
inputRef={tenantRef}
|
||
placeholder="Select Tenant"
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
|
||
<AccentAvatar color="#94a3b8"><MdStorefront size={14} /></AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
sx={pillFieldSx('#0ea5e9')}
|
||
/>
|
||
)}
|
||
/>
|
||
{/* Location */}
|
||
<Autocomplete
|
||
disablePortal
|
||
options={locationlist || []}
|
||
getOptionLabel={(option) => `${option.locationname} (${option.suburb})` || ''}
|
||
value={locationValue}
|
||
PaperComponent={SoftPaper}
|
||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
|
||
onOpen={(event) => {
|
||
if (!appId && !tenantid) {
|
||
event.preventDefault();
|
||
OpenToast('Please select your Zone and Tenant first!', 'warning', 3000);
|
||
setTimeout(() => locationRef.current?.focus(), 0);
|
||
} else if (!tenantid) {
|
||
event.preventDefault();
|
||
OpenToast('Please select your Tenant first!', 'warning', 3000);
|
||
setTimeout(() => tenantRef.current?.focus(), 0);
|
||
}
|
||
}}
|
||
onChange={(e, val, reason) => {
|
||
if (reason === 'clear') {
|
||
setLocationid(0);
|
||
setLocationValue(null);
|
||
} else {
|
||
setLocationid(val.locationid || 0);
|
||
setLocationValue(val);
|
||
}
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="Select Location"
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
|
||
<AccentAvatar color="#94a3b8"><MdLocationOn size={14} /></AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
sx={pillFieldSx('#14b8a6')}
|
||
/>
|
||
)}
|
||
/>
|
||
{/* Rider */}
|
||
<Autocomplete
|
||
disabled={riderListIsLoading}
|
||
options={ridersList}
|
||
PaperComponent={SoftPaper}
|
||
getOptionLabel={(option) => `${option.firstname} ${option.lastname} (${option.contactno})`}
|
||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
|
||
onChange={(e, value, reason) => {
|
||
if (reason === 'clear') setRiderid(0);
|
||
else setRiderid(value.userid);
|
||
}}
|
||
onOpen={(event) => {
|
||
if (!appId) {
|
||
event.preventDefault();
|
||
OpenToast('Please select your zone first!', 'warning', 3000);
|
||
setTimeout(() => locationRef.current?.focus(), 0);
|
||
}
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="Choose Rider"
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
|
||
<AccentAvatar color="#94a3b8"><MdDirectionsBike size={14} /></AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
sx={pillFieldSx('#8b5cf6')}
|
||
/>
|
||
)}
|
||
/>
|
||
</Stack>
|
||
</Paper>
|
||
{/* ============================================= || Status Tabs + Search || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
p: { xs: 1, md: 1.5 },
|
||
borderTopLeftRadius: DT.radiusCard / 8,
|
||
borderTopRightRadius: DT.radiusCard / 8,
|
||
borderBottomLeftRadius: 0,
|
||
borderBottomRightRadius: 0,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
borderBottom: 0,
|
||
background: '#fff'
|
||
}}
|
||
>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="space-between"
|
||
gap={1.5}
|
||
sx={{ flexWrap: 'wrap-reverse' }}
|
||
>
|
||
<Stack
|
||
direction="row"
|
||
spacing={0.75}
|
||
sx={{
|
||
flex: 1,
|
||
overflowX: 'auto',
|
||
py: 0.5,
|
||
px: 0.25,
|
||
'&::-webkit-scrollbar': { height: 6 },
|
||
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
|
||
}}
|
||
>
|
||
{STATUS_TABS.map((t, idx) => {
|
||
const meta = STATUS_META[t.status];
|
||
const Icon = meta.icon;
|
||
const active = tabvalue === idx;
|
||
const count = batchCounts[t.countKey] ?? 0;
|
||
return (
|
||
<Box
|
||
key={t.status}
|
||
onClick={(e) => handleChangetab(e, idx)}
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: { xs: 0.625, md: 0.875 },
|
||
pl: 0.5,
|
||
pr: { xs: 1, md: 1.25 },
|
||
py: 0.5,
|
||
flexShrink: 0,
|
||
cursor: 'pointer',
|
||
borderRadius: DT.radiusField + 'px',
|
||
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
|
||
bgcolor: active ? meta.color : DT.surface,
|
||
color: active ? '#fff' : DT.textSecondary,
|
||
fontWeight: 600,
|
||
boxShadow: 'none',
|
||
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
|
||
'&:hover': {
|
||
borderColor: active ? meta.color : DT.borderHover,
|
||
bgcolor: active ? meta.color : DT.surfaceAlt
|
||
}
|
||
}}
|
||
>
|
||
<Avatar
|
||
sx={{
|
||
width: { xs: 20, md: 22 },
|
||
height: { xs: 20, md: 22 },
|
||
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
|
||
color: active ? '#fff' : meta.color
|
||
}}
|
||
>
|
||
<Icon size={12} />
|
||
</Avatar>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontWeight: 600,
|
||
fontSize: { xs: 11.5, md: 13 },
|
||
lineHeight: 1
|
||
}}
|
||
>
|
||
{meta.label}
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
minWidth: { xs: 20, md: 24 },
|
||
height: { xs: 18, md: 20 },
|
||
px: 0.625,
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderRadius: 999,
|
||
fontSize: { xs: 10, md: 11 },
|
||
fontWeight: 700,
|
||
bgcolor: active ? 'rgba(255,255,255,0.22)' : DT.surfaceAlt,
|
||
color: active ? '#fff' : DT.textSecondary,
|
||
border: 'none'
|
||
}}
|
||
>
|
||
{count}
|
||
</Box>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Stack>
|
||
|
||
{/* ============================================= || Search | ============================================= */}
|
||
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
|
||
<DebounceSearchBar
|
||
value={searchword}
|
||
onChange={setSearchword}
|
||
onDebouncedChange={setDebouncedSearch}
|
||
sx={{
|
||
m: 0,
|
||
width: '100%',
|
||
borderRadius: DT.radiusField + 'px',
|
||
bgcolor: DT.surface,
|
||
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
|
||
'&:hover fieldset': { borderColor: DT.borderHover },
|
||
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` }
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Stack>
|
||
</Paper>
|
||
{/* ============================================= || Table || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
borderTopLeftRadius: 0,
|
||
borderTopRightRadius: 0,
|
||
borderBottomLeftRadius: DT.radiusCard / 8,
|
||
borderBottomRightRadius: DT.radiusCard / 8,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
background: '#fff'
|
||
}}
|
||
>
|
||
<TableContainer
|
||
ref={containerRef}
|
||
onScroll={handleScroll}
|
||
sx={{
|
||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||
overflow: 'auto',
|
||
overflowX: 'auto',
|
||
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
|
||
'&::-webkit-scrollbar-thumb': {
|
||
backgroundColor: edge('#C01227'),
|
||
borderRadius: '8px',
|
||
cursor: 'pointer',
|
||
'&:hover': { backgroundColor: '#C01227' }
|
||
},
|
||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
{(() => {
|
||
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
|
||
const showSelect = tabstatus == 'Created';
|
||
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
|
||
return isMobile ? (
|
||
/* ===================== MOBILE: card list ===================== */
|
||
<MobileCardList sx={{ p: 1.25 }}>
|
||
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
|
||
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
|
||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||
<MdInventory2 size={28} />
|
||
</Avatar>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||
No deliveries to show
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
|
||
{selectedBatch === 'all'
|
||
? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.`
|
||
: `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`}
|
||
</Typography>
|
||
</Stack>
|
||
)}
|
||
{filteredRows.length === 0 && countSourceLoading && (
|
||
<Stack alignItems="center" sx={{ py: 6 }}>
|
||
<LoaderWithImage />
|
||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
||
Loading deliveries…
|
||
</Typography>
|
||
</Stack>
|
||
)}
|
||
{filteredRows.map((row, index) => {
|
||
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||
label: row.status || '—',
|
||
color: '#94a3b8',
|
||
icon: MdHistoryToggleOff
|
||
};
|
||
const RowStatusIcon = rowStatusMeta.icon;
|
||
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
const isOpen = productCollapse?.orderid === row?.orderid;
|
||
return (
|
||
<MobileCard
|
||
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
|
||
accent={rowStatusMeta.color}
|
||
selected={isSelected}
|
||
header={
|
||
<Stack spacing={1.25}>
|
||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
|
||
{showSelect && (
|
||
<Checkbox
|
||
size="small"
|
||
sx={{ p: 0.25 }}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
let arr = deliverylist;
|
||
arr.push({ ...row, sno: deliverylist.length + 1 });
|
||
setDeliverylist([...arr]);
|
||
} else {
|
||
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
if (res) {
|
||
let arr = deliverylist;
|
||
arr.splice(res.sno - 1, 1);
|
||
arr.map((val, i) => {
|
||
val.sno = i + 1;
|
||
});
|
||
setDeliverylist([...arr]);
|
||
}
|
||
}
|
||
}}
|
||
checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
|
||
/>
|
||
)}
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
|
||
</Typography>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={0.5}
|
||
sx={{ display: 'inline-flex', pl: 0.5, pr: 1, py: 0.25, borderRadius: 999, bgcolor: tint(rowStatusMeta.color), border: `1px solid ${edge(rowStatusMeta.color)}`, color: rowStatusMeta.color }}
|
||
>
|
||
<AccentAvatar color={rowStatusMeta.color} size={18}>
|
||
<RowStatusIcon size={11} />
|
||
</AccentAvatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
|
||
{rowStatusMeta.label}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
|
||
{row.deliverytype == 'C' && (
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
if (productCollapse?.orderid === row.orderid) {
|
||
setProductCollapse(null);
|
||
setOrderHeaderId(null);
|
||
} else {
|
||
setProductCollapse(row);
|
||
setOrderHeaderId(row.orderheaderid);
|
||
}
|
||
}}
|
||
sx={{ borderRadius: 999, bgcolor: tint('#06b6d4'), color: '#06b6d4', border: `1px solid ${edge('#06b6d4')}`, '&:hover': { bgcolor: soft('#06b6d4') } }}
|
||
>
|
||
{isOpen ? <KeyboardArrowUpOutlined fontSize="small" /> : <KeyboardArrowDownOutlined fontSize="small" />}
|
||
</IconButton>
|
||
)}
|
||
{showAction && (
|
||
<IconButton
|
||
size="small"
|
||
onClick={(e) => handleMenuOpen(e, row)}
|
||
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
|
||
>
|
||
<EditOutlined />
|
||
</IconButton>
|
||
)}
|
||
</Stack>
|
||
</Stack>
|
||
<Box sx={{ minWidth: 0 }}>
|
||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
||
{row.bookingreference || `#${row.bookingid}`}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||
</Typography>
|
||
</Box>
|
||
</Stack>
|
||
}
|
||
>
|
||
<MobileFieldGrid>
|
||
<MobileField label="Pickup" full>
|
||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||
{row.pickupaddress || '—'}
|
||
</Typography>
|
||
</MobileField>
|
||
<MobileField label="Delivery" full>
|
||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||
{row.deliveryaddress || '—'}
|
||
</Typography>
|
||
</MobileField>
|
||
<MobileField label="Miler" full>
|
||
{row.assignedmileruserid ? (
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<AccentAvatar color="#8b5cf6" size={24}>
|
||
<MdDirectionsBike size={13} />
|
||
</AccentAvatar>
|
||
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||
#{row.assignedmileruserid}
|
||
</Typography>
|
||
</Stack>
|
||
) : (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||
Unassigned
|
||
</Typography>
|
||
)}
|
||
</MobileField>
|
||
</MobileFieldGrid>
|
||
{isOpen && (
|
||
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
|
||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||
Product Details
|
||
</Typography>
|
||
</Stack>
|
||
<Stack spacing={1}>
|
||
{orderdetails?.details?.map((product, idx2) => (
|
||
<Stack key={idx2} direction="row" alignItems="center" spacing={1.25} sx={{ p: 1, borderRadius: 1.5, bgcolor: '#fff', border: `1px solid ${DT.divider}` }}>
|
||
<Box component="img" src={product?.productimage || 'https://via.placeholder.com/40'} alt={product?.productname} sx={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 1.5, border: `1px solid ${DT.divider}`, flexShrink: 0 }} />
|
||
<Stack sx={{ minWidth: 0, flex: 1 }}>
|
||
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||
{product?.productname || 'Unnamed'}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
Qty {product?.orderqty || 0} · ₹ {product?.price || 0}
|
||
</Typography>
|
||
</Stack>
|
||
<Typography variant="body2" sx={{ fontWeight: 800, color: DT.textPrimary, flexShrink: 0 }}>
|
||
₹ {(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
|
||
</Typography>
|
||
</Stack>
|
||
))}
|
||
<Stack direction="row" justifyContent="space-between" sx={{ pt: 0.5 }}>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary }}>Total Amount</Typography>
|
||
<Typography variant="body2" sx={{ fontWeight: 800, color: '#10b981' }}>
|
||
₹ {orderdetails?.pricedetails?.orderamount?.toFixed(2)}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</Box>
|
||
)}
|
||
</MobileCard>
|
||
);
|
||
})}
|
||
{countSourceRows?.length != 0 && (
|
||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||
{countIsFetchingNext || countHasNext ? (
|
||
<LoaderWithImage />
|
||
) : (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||
· End of list ·
|
||
</Typography>
|
||
)}
|
||
</div>
|
||
)}
|
||
</MobileCardList>
|
||
) : (
|
||
<Table stickyHeader sx={{ minWidth: { xs: 1100, lg: 1300, xl: 1400 } }}>
|
||
<TableHead>
|
||
<TableRow
|
||
sx={{
|
||
'& th': {
|
||
backgroundColor: DT.surfaceAlt,
|
||
color: DT.textSecondary,
|
||
fontSize: { xs: 10, md: 11 },
|
||
fontWeight: 800,
|
||
letterSpacing: 0.6,
|
||
textTransform: 'uppercase',
|
||
whiteSpace: 'nowrap',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||
py: { xs: 1, md: 1.25 },
|
||
px: { xs: 1, md: 2 }
|
||
}
|
||
}}
|
||
>
|
||
{showSelect && (
|
||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
||
<Checkbox
|
||
indeterminate={deliverylist.length > 0 && deliverylist.length != filteredRows.length}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
setDeliverylist([...filteredRows]);
|
||
} else {
|
||
setDeliverylist([]);
|
||
}
|
||
}}
|
||
checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length}
|
||
/>
|
||
</TableCell>
|
||
)}
|
||
<TableCell>#</TableCell>
|
||
<TableCell>Status</TableCell>
|
||
<TableCell>Booking</TableCell>
|
||
<TableCell>Pickup</TableCell>
|
||
<TableCell>Delivery</TableCell>
|
||
<TableCell>Miler</TableCell>
|
||
<TableCell>Created</TableCell>
|
||
{showAction && <TableCell align="right">Action</TableCell>}
|
||
</TableRow>
|
||
</TableHead>
|
||
{(loading1 || countSourceIsLoading) && <OrdersTableSkeleton col={8} />}
|
||
<TableBody>
|
||
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
|
||
<TableRow>
|
||
<TableCell colSpan={totalCols} sx={{ py: 6 }}>
|
||
<Stack alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||
<MdInventory2 size={28} />
|
||
</Avatar>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||
No deliveries to show
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
{selectedBatch === 'all'
|
||
? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.`
|
||
: `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`}
|
||
</Typography>
|
||
</Stack>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
{filteredRows.length == 0 && countSourceLoading && (
|
||
<TableRow>
|
||
<TableCell colSpan={totalCols} sx={{ textAlign: 'center', py: 6 }}>
|
||
<LoaderWithImage />
|
||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
||
Loading deliveries…
|
||
</Typography>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
{filteredRows.map((row, index) => {
|
||
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||
label: row.status || '—',
|
||
color: '#94a3b8',
|
||
icon: MdHistoryToggleOff
|
||
};
|
||
const RowStatusIcon = rowStatusMeta.icon;
|
||
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
return (
|
||
<Fragment key={row.orderheaderid ?? `${row.tenantname}-${index}`}>
|
||
<TableRow
|
||
role="checkbox"
|
||
tabIndex={-1}
|
||
sx={{
|
||
cursor: 'pointer',
|
||
transition: 'background-color 0.15s',
|
||
'& td': {
|
||
borderBottom: `1px solid ${DT.divider}`,
|
||
py: { xs: 1, md: 1.5 },
|
||
px: { xs: 1, md: 2 }
|
||
},
|
||
backgroundColor: isSelected ? soft('#8b5cf6') : 'transparent',
|
||
'&:hover': {
|
||
backgroundColor: isSelected ? soft('#8b5cf6') : DT.surfaceAlt
|
||
}
|
||
}}
|
||
>
|
||
{tabstatus == 'Created' && (
|
||
<TableCell>
|
||
<Checkbox
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
let arr = deliverylist;
|
||
arr.push({ ...row, sno: deliverylist.length + 1 });
|
||
setDeliverylist([...arr]);
|
||
} else {
|
||
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
if (res) {
|
||
let arr = deliverylist;
|
||
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
arr.splice(res.sno - 1, 1);
|
||
arr.map((val, i) => {
|
||
val.sno = i + 1;
|
||
});
|
||
setDeliverylist([...arr]);
|
||
}
|
||
}
|
||
console.log('list', deliverylist);
|
||
}}
|
||
checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
|
||
/>
|
||
</TableCell>
|
||
)}
|
||
<TableCell>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
|
||
</Typography>
|
||
</TableCell>
|
||
{/* Status badge */}
|
||
<TableCell>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={0.5}
|
||
sx={{
|
||
display: 'inline-flex',
|
||
pl: 0.5,
|
||
pr: 1,
|
||
py: 0.25,
|
||
borderRadius: 999,
|
||
bgcolor: tint(rowStatusMeta.color),
|
||
border: `1px solid ${edge(rowStatusMeta.color)}`,
|
||
color: rowStatusMeta.color
|
||
}}
|
||
>
|
||
<AccentAvatar color={rowStatusMeta.color} size={20}>
|
||
<RowStatusIcon size={12} />
|
||
</AccentAvatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
|
||
{rowStatusMeta.label}
|
||
</Typography>
|
||
</Stack>
|
||
</TableCell>
|
||
{/* Booking */}
|
||
<TableCell>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||
{row.bookingreference || `#${row.bookingid}`}
|
||
</Typography>
|
||
</TableCell>
|
||
{/* Pickup */}
|
||
<TableCell sx={{ maxWidth: 220 }}>
|
||
<Tooltip title={row.pickupaddress || ''} placement="top">
|
||
<Typography variant="body2" noWrap>
|
||
{row.pickupaddress || '—'}
|
||
</Typography>
|
||
</Tooltip>
|
||
</TableCell>
|
||
{/* Delivery */}
|
||
<TableCell sx={{ maxWidth: 220 }}>
|
||
<Tooltip title={row.deliveryaddress || ''} placement="top">
|
||
<Typography variant="body2" noWrap>
|
||
{row.deliveryaddress || '—'}
|
||
</Typography>
|
||
</Tooltip>
|
||
</TableCell>
|
||
{/* Miler */}
|
||
<TableCell>
|
||
{row.assignedmileruserid ? (
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<AccentAvatar color="#8b5cf6" size={28}>
|
||
<MdDirectionsBike size={14} />
|
||
</AccentAvatar>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||
#{row.assignedmileruserid}
|
||
</Typography>
|
||
</Stack>
|
||
) : (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||
Unassigned
|
||
</Typography>
|
||
)}
|
||
</TableCell>
|
||
{/* Created */}
|
||
<TableCell>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||
</Typography>
|
||
</TableCell>
|
||
{/* Action */}
|
||
{tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && (
|
||
<TableCell align="right">
|
||
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
|
||
{row.deliverytype == 'C' && (
|
||
<Tooltip title="Product details" placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
if (productCollapse?.orderid === row.orderid) {
|
||
setProductCollapse(null);
|
||
setOrderHeaderId(null);
|
||
} else {
|
||
setProductCollapse(row);
|
||
setOrderHeaderId(row.orderheaderid);
|
||
}
|
||
}}
|
||
sx={{
|
||
borderRadius: 999,
|
||
bgcolor: tint('#06b6d4'),
|
||
color: '#06b6d4',
|
||
border: `1px solid ${edge('#06b6d4')}`,
|
||
'&:hover': { bgcolor: soft('#06b6d4') }
|
||
}}
|
||
>
|
||
{productCollapse?.orderid === row.orderid ? <KeyboardArrowUpOutlined fontSize="small" /> : <KeyboardArrowDownOutlined fontSize="small" />}
|
||
</IconButton>
|
||
</Tooltip>
|
||
)}
|
||
|
||
<Tooltip title="Actions" placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={(e) => handleMenuOpen(e, row)}
|
||
sx={{
|
||
borderRadius: 999,
|
||
bgcolor: tint('#C01227'),
|
||
color: '#C01227',
|
||
border: `1px solid ${edge('#C01227')}`,
|
||
'&:hover': { bgcolor: soft('#C01227') }
|
||
}}
|
||
>
|
||
<EditOutlined />
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Stack>
|
||
</TableCell>
|
||
)}
|
||
</TableRow>
|
||
{productCollapse?.orderid === row?.orderid && (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={totalCols}
|
||
sx={{
|
||
p: 0,
|
||
border: 0,
|
||
backgroundColor: DT.surfaceAlt
|
||
}}
|
||
>
|
||
<Collapse in={true} timeout="auto" unmountOnExit>
|
||
<Box sx={{ width: '100%', p: 2.5 }}>
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
borderRadius: 2,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
background: '#fff'
|
||
}}
|
||
>
|
||
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
|
||
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
|
||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||
Product Details
|
||
</Typography>
|
||
</Stack>
|
||
<Table size="small" sx={{ minWidth: 800 }}>
|
||
<TableHead>
|
||
<TableRow
|
||
sx={{
|
||
'& th': {
|
||
fontSize: 11,
|
||
fontWeight: 800,
|
||
letterSpacing: 0.5,
|
||
textTransform: 'uppercase',
|
||
color: DT.textSecondary,
|
||
borderBottom: `1px solid ${DT.divider}`
|
||
},
|
||
'&:hover': { backgroundColor: 'transparent !important' }
|
||
}}
|
||
>
|
||
<TableCell>#</TableCell>
|
||
<TableCell>Product</TableCell>
|
||
<TableCell>Description</TableCell>
|
||
<TableCell align="right">Qty</TableCell>
|
||
<TableCell align="right">Cost</TableCell>
|
||
<TableCell align="right">Price</TableCell>
|
||
<TableCell align="right">Tax</TableCell>
|
||
<TableCell align="right">Amount</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{orderdetails?.details?.map((product, idx2) => (
|
||
<TableRow
|
||
key={idx2}
|
||
sx={{ '&:hover': { backgroundColor: DT.surfaceAlt + ' !important' } }}
|
||
>
|
||
<TableCell sx={{ color: DT.textMuted, fontWeight: 700 }}>{String(idx2 + 1).padStart(2, '0')}</TableCell>
|
||
<TableCell>
|
||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||
<Box
|
||
component="img"
|
||
src={product?.productimage || 'https://via.placeholder.com/40'}
|
||
alt={product?.productname}
|
||
sx={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 1.5, border: `1px solid ${DT.divider}` }}
|
||
/>
|
||
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||
{product?.productname || 'Unnamed'}
|
||
</Typography>
|
||
</Stack>
|
||
</TableCell>
|
||
<TableCell sx={{ color: DT.textSecondary }}>{product?.productdescription || '-'}</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 700 }}>{product?.orderqty || 0}</TableCell>
|
||
<TableCell align="right">₹ {product?.price || 0}</TableCell>
|
||
<TableCell align="right">₹ {(product?.productsumprice ?? 0).toFixed(2)}</TableCell>
|
||
<TableCell align="right">₹ {(product?.taxamount ?? 0).toFixed(2)}</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
₹ {(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
||
<TableCell align="right" colSpan={7} sx={{ fontWeight: 700, color: DT.textSecondary, borderTop: `1px solid ${DT.divider}` }}>
|
||
Total Amount
|
||
</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 800, color: '#10b981', borderTop: `1px solid ${DT.divider}` }}>
|
||
₹ {orderdetails?.pricedetails?.orderamount?.toFixed(2)}
|
||
</TableCell>
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
</Paper>
|
||
</Box>
|
||
</Collapse>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</Fragment>
|
||
);
|
||
})}
|
||
{countSourceRows?.length != 0 && (
|
||
<TableRow>
|
||
<TableCell colSpan={totalCols} rowSpan={3}>
|
||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||
{countIsFetchingNext ? <LoaderWithImage /> : countHasNext ? <LoaderWithImage /> : (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||
· End of list ·
|
||
</Typography>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
);
|
||
})()}
|
||
</TableContainer>
|
||
</Paper>
|
||
|
||
{/* Shared row-action menu — single instance reused by both the desktop
|
||
table rows and the mobile cards (both trigger handleMenuOpen). */}
|
||
<Menu
|
||
anchorEl={menuAnchorEl}
|
||
open={menuOpen}
|
||
onClose={handleMenuClose}
|
||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||
PaperProps={{
|
||
elevation: 0,
|
||
sx: {
|
||
minWidth: 220,
|
||
borderRadius: 2,
|
||
mt: 0.75,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden',
|
||
'& .MuiMenuItem-root': {
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
py: 1.1,
|
||
px: 1.5,
|
||
gap: 1,
|
||
transition: 'background-color 0.15s',
|
||
'&:hover': { bgcolor: DT.surfaceAlt }
|
||
}
|
||
}
|
||
}}
|
||
>
|
||
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
|
||
<MenuItem
|
||
onClick={() => {
|
||
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
|
||
handleMenuClose();
|
||
}}
|
||
>
|
||
<AccentAvatar color="#0ea5e9" size={22}><MdNotificationsActive size={13} /></AccentAvatar>
|
||
Notify Rider
|
||
</MenuItem>
|
||
)}
|
||
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
|
||
String(selectedRow?.status || '').toLowerCase()
|
||
) && (
|
||
<MenuItem
|
||
onClick={() => {
|
||
if (!appId) {
|
||
opentoast('Please select a location first!', 'warning');
|
||
locationRef.current?.focus();
|
||
return;
|
||
}
|
||
setChangeDialogOpen(true);
|
||
handleMenuClose();
|
||
}}
|
||
>
|
||
<AccentAvatar color="#8b5cf6" size={22}><MdDirectionsBike size={13} /></AccentAvatar>
|
||
Change Rider
|
||
</MenuItem>
|
||
)}
|
||
{(roleid == 1 || roleid == 2) && (
|
||
<MenuItem
|
||
onClick={() => {
|
||
setKms(selectedRow.kms);
|
||
setCumulativeKms(selectedRow.cumulativekms);
|
||
setDeliverylat(selectedRow.droplat);
|
||
setDeliverylong(selectedRow.droplon);
|
||
setNotes(selectedRow.notes);
|
||
setDeliveryamount(selectedRow.deliveryamount);
|
||
setUpdateStatus(selectedRow.status || 'delivered');
|
||
setCurrentorder(selectedRow);
|
||
setDialogopen(true);
|
||
handleMenuClose();
|
||
}}
|
||
>
|
||
<AccentAvatar color="#10b981" size={22}><MdCheckCircle size={13} /></AccentAvatar>
|
||
Update Status
|
||
</MenuItem>
|
||
)}
|
||
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
|
||
<MenuItem
|
||
sx={{ color: '#ef4444 !important' }}
|
||
onClick={() => {
|
||
setCancelDeliveryOpen(true);
|
||
handleMenuClose();
|
||
}}
|
||
>
|
||
<AccentAvatar color="#ef4444" size={22}><MdCancel size={13} /></AccentAvatar>
|
||
Cancel Delivery
|
||
</MenuItem>
|
||
)}
|
||
</Menu>
|
||
|
||
{/* =============================== || cancel dialog || =============================== */}
|
||
<Dialog
|
||
open={cancelDeliveryOpen}
|
||
onClose={() => setCancelDeliveryOpen(false)}
|
||
maxWidth="xs"
|
||
fullWidth
|
||
PaperProps={{
|
||
elevation: 0,
|
||
sx: {
|
||
borderRadius: 3,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden'
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ height: 4, background: `linear-gradient(90deg, #ef4444 0%, ${soft('#ef4444')} 100%)` }} />
|
||
<DialogContent sx={{ p: 3.5 }}>
|
||
<Stack alignItems="center" spacing={2.5}>
|
||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#ef4444'), color: '#ef4444' }}>
|
||
<DeleteFilled style={{ fontSize: 28 }} />
|
||
</Avatar>
|
||
<Stack spacing={0.5} alignItems="center">
|
||
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
Cancel Delivery?
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 1 }}>
|
||
This action will cancel the delivery and notify the rider. Please share a brief reason.
|
||
</Typography>
|
||
</Stack>
|
||
<TextField
|
||
fullWidth
|
||
placeholder="Reason for cancellation"
|
||
multiline
|
||
minRows={2}
|
||
maxRows={4}
|
||
onChange={(e) => setCancelFeed(e.target.value)}
|
||
sx={{
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: 2,
|
||
bgcolor: DT.surfaceAlt,
|
||
'& fieldset': { borderColor: DT.borderSubtle },
|
||
'&:hover fieldset': { borderColor: '#ef4444' },
|
||
'&.Mui-focused fieldset': { borderColor: '#ef4444', borderWidth: 2 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#ef4444')}` }
|
||
}
|
||
}}
|
||
/>
|
||
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
||
<Button
|
||
fullWidth
|
||
onClick={() => setCancelDeliveryOpen(false)}
|
||
variant="outlined"
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 700,
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textSecondary,
|
||
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
Keep Delivery
|
||
</Button>
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={() => {
|
||
if (cancelFeed == '') {
|
||
opentoast('Enter FeedBack', 'error');
|
||
} else {
|
||
cancelDelivery({ selectedRow, cancelFeed });
|
||
}
|
||
cancelridernotification();
|
||
}}
|
||
autoFocus
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 800,
|
||
bgcolor: '#ef4444',
|
||
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
|
||
'&:hover': { bgcolor: '#dc2626' }
|
||
}}
|
||
>
|
||
Yes, Cancel
|
||
</Button>
|
||
</Stack>
|
||
</Stack>
|
||
</DialogContent>
|
||
</Dialog>
|
||
{/* =============================== || change rider dialog || =============================== */}
|
||
<Dialog
|
||
open={changeDialogOpen}
|
||
onClose={() => setChangeDialogOpen(false)}
|
||
maxWidth="sm"
|
||
fullWidth
|
||
TransitionComponent={PopupTransition}
|
||
PaperProps={{
|
||
elevation: 0,
|
||
sx: {
|
||
borderRadius: 3,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden'
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ height: 4, background: `linear-gradient(90deg, #8b5cf6 0%, ${soft('#8b5cf6')} 100%)` }} />
|
||
<DialogTitle sx={{ p: 3, pb: 1.5 }}>
|
||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
|
||
<MdDirectionsBike size={22} />
|
||
</Avatar>
|
||
<Stack>
|
||
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
Change Rider
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
Assign this delivery to a different rider
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</DialogTitle>
|
||
<DialogContent sx={{ p: 3, pt: 2 }}>
|
||
<Autocomplete
|
||
fullWidth
|
||
options={ridersList}
|
||
PaperComponent={SoftPaper}
|
||
getOptionLabel={(option) => `${option.firstname} ${option.lastname} (${option.contactno})`}
|
||
onChange={(e, value) => {
|
||
setSelectedRider(value);
|
||
logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None');
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder="Choose Rider"
|
||
InputProps={{
|
||
...params.InputProps,
|
||
startAdornment: (
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5, mr: 0.25, flexShrink: 0 }}>
|
||
<AccentAvatar color="#8b5cf6"><MdDirectionsBike size={14} /></AccentAvatar>
|
||
</Stack>
|
||
)
|
||
}}
|
||
sx={pillFieldSx('#8b5cf6')}
|
||
/>
|
||
)}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions sx={{ p: 3, pt: 1 }}>
|
||
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
||
<Button
|
||
fullWidth
|
||
variant="outlined"
|
||
onClick={() => setChangeDialogOpen(false)}
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 700,
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textSecondary,
|
||
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={() => {
|
||
setLoading1(true);
|
||
logger.info(`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
|
||
changeRiderMutation.mutate({ selectedRider, selectedRow });
|
||
}}
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 800,
|
||
bgcolor: '#8b5cf6',
|
||
boxShadow: `0 6px 18px ${ring('#8b5cf6')}`,
|
||
'&:hover': { bgcolor: '#7c3aed' }
|
||
}}
|
||
>
|
||
Change Rider
|
||
</Button>
|
||
</Stack>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* =============================== || Date filter Dialog || =============================== */}
|
||
<Dialog
|
||
open={open}
|
||
onClose={() => setOpen(false)}
|
||
fullScreen={isMobile}
|
||
PaperProps={{
|
||
elevation: 0,
|
||
sx: {
|
||
borderRadius: { xs: 0, sm: 3 },
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden'
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ height: 4, background: `linear-gradient(90deg, #f59e0b 0%, ${soft('#f59e0b')} 100%)` }} />
|
||
<DialogTitle align="left" sx={{ p: 3, pb: 1.5 }}>
|
||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#f59e0b'), color: '#f59e0b' }}>
|
||
<MdOutlineDateRange size={22} />
|
||
</Avatar>
|
||
<Stack>
|
||
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
Date Range
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
Pick a window to filter the deliveries
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</DialogTitle>
|
||
<DialogContent sx={{ width: '100%' }} className="datedialog">
|
||
<DateRangePicker
|
||
open={open}
|
||
toggle={() => setOpen(!open)}
|
||
id="daterange1"
|
||
onChange={(range) => {
|
||
if (range.label === 'All') {
|
||
setStartdate('');
|
||
setEnddate('');
|
||
|
||
setOpen(false);
|
||
} else {
|
||
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
|
||
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
|
||
}
|
||
console.log(range);
|
||
}}
|
||
definedRanges={[
|
||
{
|
||
label: 'Today',
|
||
startDate: new Date(),
|
||
endDate: new Date()
|
||
},
|
||
{
|
||
label: 'Yesterday',
|
||
startDate: addDays(new Date(), -1),
|
||
endDate: addDays(new Date(), -1)
|
||
},
|
||
{
|
||
label: 'Tomorrow',
|
||
startDate: addDays(new Date(), +1),
|
||
endDate: addDays(new Date(), +1)
|
||
},
|
||
{
|
||
label: 'This Week',
|
||
startDate: startOfWeek(new Date()),
|
||
endDate: endOfWeek(new Date())
|
||
},
|
||
{
|
||
label: 'Last Week',
|
||
startDate: startOfWeek(addWeeks(new Date(), -1)),
|
||
endDate: endOfWeek(addWeeks(new Date(), -1))
|
||
},
|
||
{
|
||
label: 'Last 7 Days',
|
||
startDate: addWeeks(new Date(), -1),
|
||
endDate: new Date()
|
||
},
|
||
{
|
||
label: 'This Month',
|
||
startDate: startOfMonth(new Date()),
|
||
endDate: endOfMonth(new Date())
|
||
},
|
||
{
|
||
label: 'Last Month',
|
||
startDate: startOfMonth(addMonths(new Date(), -1)),
|
||
endDate: endOfMonth(addMonths(new Date(), -1))
|
||
}
|
||
]}
|
||
/>
|
||
</DialogContent>
|
||
<Stack direction="row" justifyContent="flex-end" sx={{ width: '100%', p: 2.5, pt: 1 }}>
|
||
<Button
|
||
variant="contained"
|
||
onClick={okclicked}
|
||
sx={{
|
||
borderRadius: 999,
|
||
px: 3,
|
||
py: 1,
|
||
fontWeight: 800,
|
||
bgcolor: '#f59e0b',
|
||
boxShadow: `0 6px 18px ${ring('#f59e0b')}`,
|
||
'&:hover': { bgcolor: '#d97706' }
|
||
}}
|
||
>
|
||
Apply
|
||
</Button>
|
||
</Stack>
|
||
</Dialog>
|
||
|
||
{/* =============================== || Update Delivery Dialog || =============================== */}
|
||
<Dialog
|
||
fullWidth
|
||
open={dialogopen}
|
||
onClose={dialogclose}
|
||
scroll="paper"
|
||
maxWidth="sm"
|
||
fullScreen={isMobile}
|
||
TransitionComponent={PopupTransition}
|
||
PaperProps={{
|
||
elevation: 0,
|
||
sx: {
|
||
borderRadius: { xs: 0, sm: 3 },
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
boxShadow: DT.shadowPop,
|
||
overflow: 'hidden'
|
||
}
|
||
}}
|
||
>
|
||
<Box sx={{ height: 4, background: `linear-gradient(90deg, #10b981 0%, ${soft('#10b981')} 100%)` }} />
|
||
<DialogTitle sx={{ p: 3, pb: 1.5 }}>
|
||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#10b981'), color: '#10b981' }}>
|
||
<MdCheckCircle size={22} />
|
||
</Avatar>
|
||
<Stack>
|
||
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||
Update Delivery Status
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
{currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</DialogTitle>
|
||
<DialogContent dividers sx={{ p: 3, bgcolor: DT.surfaceAlt }}>
|
||
<Grid container spacing={2}>
|
||
{[
|
||
{ label: 'KMs', value: kms, set: (v) => setKms(v), color: '#ef4444' },
|
||
{ label: 'Actual KMs', value: cumulativekms, set: (v) => setCumulativeKms(+v), color: '#10b981' },
|
||
{ label: 'Delivery Latitude', value: deliverylat, set: (v) => setDeliverylat(v), color: '#06b6d4' },
|
||
{ label: 'Delivery Longitude', value: deliverylong, set: (v) => setDeliverylong(v), color: '#06b6d4' }
|
||
].map((f) => (
|
||
<Grid item xs={12} sm={6} key={f.label}>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||
{f.label}
|
||
</Typography>
|
||
<TextField
|
||
fullWidth
|
||
value={f.value}
|
||
type="number"
|
||
onChange={(e) => f.set(e.target.value)}
|
||
sx={{
|
||
mt: 0.5,
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: 2,
|
||
bgcolor: '#fff',
|
||
'& fieldset': { borderColor: DT.borderSubtle },
|
||
'&:hover fieldset': { borderColor: f.color },
|
||
'&.Mui-focused fieldset': { borderColor: f.color, borderWidth: 2 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(f.color)}` }
|
||
}
|
||
}}
|
||
/>
|
||
</Grid>
|
||
))}
|
||
<Grid item xs={12}>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||
Amount
|
||
</Typography>
|
||
<TextField
|
||
fullWidth
|
||
type="number"
|
||
value={deliveryamount}
|
||
onChange={(e) => setDeliveryamount(+e.target.value)}
|
||
sx={{
|
||
mt: 0.5,
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: 2,
|
||
bgcolor: '#fff',
|
||
'& fieldset': { borderColor: DT.borderSubtle },
|
||
'&:hover fieldset': { borderColor: '#10b981' },
|
||
'&.Mui-focused fieldset': { borderColor: '#10b981', borderWidth: 2 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#10b981')}` }
|
||
}
|
||
}}
|
||
/>
|
||
</Grid>
|
||
<Grid item xs={12}>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||
Status
|
||
</Typography>
|
||
<TextField
|
||
select
|
||
fullWidth
|
||
value={updateStatus}
|
||
onChange={(e) => setUpdateStatus(e.target.value)}
|
||
SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 2, mt: 0.5, boxShadow: DT.shadowPop } } } }}
|
||
sx={{
|
||
mt: 0.5,
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: 2,
|
||
bgcolor: '#fff',
|
||
'& fieldset': { borderColor: DT.borderSubtle },
|
||
'&:hover fieldset': { borderColor: '#C01227' },
|
||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||
}
|
||
}}
|
||
>
|
||
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
|
||
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
|
||
const Ic = m.icon;
|
||
return (
|
||
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
|
||
<AccentAvatar color={m.color} size={22}><Ic size={13} /></AccentAvatar>
|
||
{m.label}
|
||
</MenuItem>
|
||
);
|
||
})}
|
||
</TextField>
|
||
</Grid>
|
||
<Grid item xs={12}>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||
Notes
|
||
</Typography>
|
||
<TextField
|
||
value={notes}
|
||
fullWidth
|
||
multiline
|
||
minRows={2}
|
||
onChange={(e) => setNotes(e.target.value)}
|
||
sx={{
|
||
mt: 0.5,
|
||
'& .MuiOutlinedInput-root': {
|
||
borderRadius: 2,
|
||
bgcolor: '#fff',
|
||
'& fieldset': { borderColor: DT.borderSubtle },
|
||
'&:hover fieldset': { borderColor: '#C01227' },
|
||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||
}
|
||
}}
|
||
/>
|
||
</Grid>
|
||
</Grid>
|
||
</DialogContent>
|
||
<DialogActions sx={{ p: 3 }}>
|
||
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
||
<Button
|
||
fullWidth
|
||
variant="outlined"
|
||
onClick={dialogclose}
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 700,
|
||
borderColor: DT.borderSubtle,
|
||
color: DT.textSecondary,
|
||
'&:hover': { borderColor: DT.textMuted, bgcolor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
Close
|
||
</Button>
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={() => {
|
||
if (!kms) {
|
||
opentoast('Fill Kms', 'warning');
|
||
} else if (!cumulativekms) {
|
||
opentoast('Fill Actual Kms', 'warning');
|
||
} else if (!deliverylat) {
|
||
opentoast('Fill Delivery Latitude', 'warning');
|
||
} else if (!deliverylong) {
|
||
opentoast('Fill Delivery Longitude', 'warning');
|
||
} else if (!deliveryamount) {
|
||
opentoast('Fill Delivery Amount', 'warning');
|
||
} else {
|
||
updateDeliveryMutation.mutate({
|
||
deliveryid: currentorder.deliveryid,
|
||
orderheaderid: currentorder.orderheaderid,
|
||
orderstatus: updateStatus,
|
||
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
deliverylat,
|
||
deliverylong,
|
||
kms,
|
||
cumulativekms,
|
||
deliveryamt: parseFloat(deliveryamount),
|
||
notes,
|
||
userid: +selectedRow.userid
|
||
});
|
||
}
|
||
}}
|
||
sx={{
|
||
borderRadius: 999,
|
||
py: 1.1,
|
||
fontWeight: 800,
|
||
bgcolor: '#10b981',
|
||
boxShadow: `0 6px 18px ${ring('#10b981')}`,
|
||
'&:hover': { bgcolor: '#059669' }
|
||
}}
|
||
>
|
||
Update
|
||
</Button>
|
||
</Stack>
|
||
</DialogActions>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Deliveries;
|