updates on the deliveries page and the order creation page
This commit is contained in:
@@ -152,13 +152,27 @@ export const finalCreatedeliveries = async (deliveryData) => {
|
||||
export const createAutomationDeliveries = async (variables) => {
|
||||
console.log('variables', variables);
|
||||
|
||||
// optimse the orders and auto rider assign
|
||||
const response = await axios.post(
|
||||
const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : [];
|
||||
|
||||
// Bike mode (routes.workolik) historically accepted just the deliveries
|
||||
// array as the body. To carry the operator's "Absent Riders" picks
|
||||
// through to the AI assignment, we now wrap the body as
|
||||
// { deliveries: [...], absent_riders: [...] }
|
||||
// for that endpoint. Auto mode (routemate) already uses a structured
|
||||
// body via `variables.data`, so we just merge absent_riders into it.
|
||||
const url =
|
||||
variables.selectedMode.value == 1
|
||||
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
|
||||
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`,
|
||||
variables.selectedMode.value == 1 ? variables.deliveries : variables.data
|
||||
);
|
||||
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
|
||||
|
||||
const body =
|
||||
variables.selectedMode.value == 1
|
||||
? { deliveries: variables.deliveries, absent_riders: absentRiders }
|
||||
: { ...(variables.data || {}), absent_riders: absentRiders };
|
||||
|
||||
console.log('createAutomationDeliveries body', body);
|
||||
|
||||
const response = await axios.post(url, body);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logger from '../../../utils/logger';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { DeleteFilled, EditOutlined } from '@ant-design/icons';
|
||||
import { useState, useEffect, Fragment, useRef } from 'react';
|
||||
import { useState, useEffect, Fragment, useRef, useMemo } from 'react';
|
||||
import { Empty } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
@@ -9,9 +9,15 @@ dayjs.extend(utc);
|
||||
import axios from 'axios';
|
||||
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { PiMapPinLineDuotone } from 'react-icons/pi';
|
||||
import { MdOutlineDateRange } from 'react-icons/md';
|
||||
import { MdOutlineDeliveryDining } from 'react-icons/md';
|
||||
import {
|
||||
MdOutlineDateRange,
|
||||
MdAccessTime,
|
||||
MdAllInclusive,
|
||||
MdLightMode,
|
||||
MdWbSunny,
|
||||
MdNightsStay,
|
||||
MdCheck
|
||||
} from 'react-icons/md';
|
||||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
@@ -50,7 +56,8 @@ import {
|
||||
CircularProgress,
|
||||
Backdrop,
|
||||
MenuItem,
|
||||
Menu
|
||||
Menu,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
|
||||
import MainCard from 'components/MainCard';
|
||||
@@ -98,6 +105,84 @@ 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 (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;
|
||||
};
|
||||
|
||||
// ================================================= || deliveries (initial point)|| =================================================
|
||||
const Deliveries = () => {
|
||||
const userid = localStorage.getItem('userid');
|
||||
@@ -146,6 +231,10 @@ const Deliveries = () => {
|
||||
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(() => {
|
||||
@@ -182,6 +271,7 @@ const Deliveries = () => {
|
||||
setCancelDeliveryOpen(false);
|
||||
fetchCountRefetch(); // Refresh count data
|
||||
fetchDeliveriesRefetch(); // Refresh deliveries
|
||||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||||
},
|
||||
onError: (error) => {
|
||||
opentoast(error.message, 'error');
|
||||
@@ -268,6 +358,7 @@ const Deliveries = () => {
|
||||
}
|
||||
fetchCountRefetch(); // Refresh count data
|
||||
fetchDeliveriesRefetch(); // Refresh deliveries
|
||||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||||
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
|
||||
},
|
||||
onError: (err, { selectedRider, selectedRow }) => {
|
||||
@@ -382,16 +473,177 @@ const Deliveries = () => {
|
||||
});
|
||||
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 (!hasNextPage) return;
|
||||
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:1198–1206`) — 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) {
|
||||
fetchNextPage();
|
||||
countFetchNext();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
|
||||
root: document.querySelector('.MuiTableContainer-root'),
|
||||
rootMargin: '0px',
|
||||
threshold: 1.0
|
||||
}
|
||||
@@ -400,13 +652,13 @@ const Deliveries = () => {
|
||||
return () => {
|
||||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||||
};
|
||||
}, [hasNextPage, fetchNextPage]);
|
||||
}, [countHasNext, countFetchNext]);
|
||||
|
||||
const handleScroll = (event) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
if (countHasNext && !countIsFetchingNext) {
|
||||
countFetchNext();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -476,6 +728,7 @@ const Deliveries = () => {
|
||||
setDialogopen(false);
|
||||
fetchDeliveriesRefetch();
|
||||
fetchCountRefetch();
|
||||
countSourceRefetch();
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -508,7 +761,7 @@ const Deliveries = () => {
|
||||
<>
|
||||
{(fetchCountIsLoading ||
|
||||
fetchPercentageIsLoading ||
|
||||
fetchDeliveriesIsLoading ||
|
||||
countSourceIsLoading ||
|
||||
fetchtenantsIsLoading ||
|
||||
fetchlocationsIsLoading ||
|
||||
riderListIsLoading) && (
|
||||
@@ -589,24 +842,197 @@ const Deliveries = () => {
|
||||
{/* ============================================= || orderFilter | ============================================= */}
|
||||
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={3} sx={{ flexWrap: 'wrap', my: 2 }}>
|
||||
{startdate && enddate ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap" // ✅ allow wrapping
|
||||
gap={1.5} // ✅ space between items when wrapped
|
||||
alignItems="center" // optional, for vertical alignment
|
||||
>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
||||
<MdOutlineDeliveryDining fontSize="30px" style={{ color: 'red' }} />
|
||||
</Avatar>
|
||||
}
|
||||
label={`Deliveries-${datestatus}`}
|
||||
color="error"
|
||||
variant="combined"
|
||||
/>
|
||||
<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' }}>
|
||||
@@ -621,24 +1047,11 @@ const Deliveries = () => {
|
||||
onClick={() => setOpen(true)}
|
||||
variant="combined"
|
||||
color="warning"
|
||||
sx={{ maxWidth: '100%', cursor: 'pointer' }} // to avoid overflow
|
||||
sx={{ maxWidth: '100%', cursor: 'pointer' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
||||
<PiMapPinLineDuotone fontSize="30px" style={{ color: '#00bcd4' }} />
|
||||
</Avatar>
|
||||
}
|
||||
label={locaName}
|
||||
color="info"
|
||||
variant="combined"
|
||||
sx={{ maxWidth: '100%' }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<Chip label="Orders-All" color="primary" variant="light" size="small" />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
@@ -754,7 +1167,7 @@ const Deliveries = () => {
|
||||
label="Assigned"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.uncoveredLength || 0}
|
||||
label={batchCounts.uncoveredLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -767,7 +1180,7 @@ const Deliveries = () => {
|
||||
label="Accepted"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.assignedLength || 0}
|
||||
label={batchCounts.assignedLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -780,7 +1193,7 @@ const Deliveries = () => {
|
||||
label="Arrived"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.arrivedLength || 0}
|
||||
label={batchCounts.arrivedLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -794,7 +1207,7 @@ const Deliveries = () => {
|
||||
label="Picked"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.pickedLength || 0}
|
||||
label={batchCounts.pickedLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -807,7 +1220,7 @@ const Deliveries = () => {
|
||||
label="Active"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.activeLength || 0}
|
||||
label={batchCounts.activeLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -820,7 +1233,7 @@ const Deliveries = () => {
|
||||
label="Skipped"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.skippedLength || 0}
|
||||
label={batchCounts.skippedLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -834,7 +1247,7 @@ const Deliveries = () => {
|
||||
label="Delivered"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.coveredLength || 0}
|
||||
label={batchCounts.coveredLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -848,7 +1261,7 @@ const Deliveries = () => {
|
||||
label="Cancelled"
|
||||
icon={
|
||||
<Chip
|
||||
label={countData?.cancelLength || 0}
|
||||
label={batchCounts.cancelLength}
|
||||
color="primary"
|
||||
variant="light"
|
||||
size="small"
|
||||
@@ -901,15 +1314,15 @@ const Deliveries = () => {
|
||||
{tabstatus == 'Created' && (
|
||||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
||||
<Checkbox
|
||||
indeterminate={deliverylist.length > 0 && deliverylist.length != rows.length}
|
||||
indeterminate={deliverylist.length > 0 && deliverylist.length != filteredRows.length}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setDeliverylist([...rows]);
|
||||
setDeliverylist([...filteredRows]);
|
||||
} else {
|
||||
setDeliverylist([]);
|
||||
}
|
||||
}}
|
||||
checked={deliverylist.length == rows.length}
|
||||
checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
@@ -944,17 +1357,33 @@ const Deliveries = () => {
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
{(loading1 || fetchDeliveriesIsLoading) && <OrdersTableSkeleton col={8} />}
|
||||
{(loading1 || countSourceIsLoading) && <OrdersTableSkeleton col={8} />}
|
||||
<TableBody>
|
||||
{rows.length == 0 && !loading1 && (
|
||||
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
|
||||
<>
|
||||
<TableCell colSpan={14}>
|
||||
{/* <Stack width={'100%'} direction={'row'} justifyContent={'center'}> */}
|
||||
<Empty description={`No ${tabstatus} Orders`} styles={{ description: { color: theme.palette.error.main } }} />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{rows.map((row, index) => {
|
||||
{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
|
||||
@@ -1399,11 +1828,11 @@ const Deliveries = () => {
|
||||
</>
|
||||
);
|
||||
})}
|
||||
{rows?.length != 0 && (
|
||||
{countSourceRows?.length != 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={15} rowSpan={3}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage ? <LoaderWithImage /> : hasNextPage ? <LoaderWithImage /> : 'No More Deliveries'}
|
||||
{countIsFetchingNext ? <LoaderWithImage /> : countHasNext ? <LoaderWithImage /> : 'No More Deliveries'}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -13,7 +13,7 @@ import Loader from 'components/Loader';
|
||||
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
|
||||
|
||||
import { PiMapPinLineDuotone } from 'react-icons/pi';
|
||||
import { MdOutlineDateRange } from 'react-icons/md';
|
||||
import { MdOutlineDateRange, MdPersonOff, MdEventBusy } from 'react-icons/md';
|
||||
import { VscArchive } from 'react-icons/vsc';
|
||||
import DateFilterDialog from 'components/DateFilterDialog';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
@@ -29,11 +29,7 @@ import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlin
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import { ArrowRightAltOutlined } from '@mui/icons-material';
|
||||
import { DashboardFilled } from '@ant-design/icons';
|
||||
import { MdDirectionsBike } from 'react-icons/md';
|
||||
import { FaMapLocationDot } from 'react-icons/fa6';
|
||||
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||
import { GiProfit } from 'react-icons/gi';
|
||||
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
|
||||
@@ -74,7 +70,8 @@ import {
|
||||
Divider,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Accordion
|
||||
Accordion,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
@@ -152,6 +149,10 @@ const Orders = () => {
|
||||
{ label: 'Auto', value: 2 }
|
||||
];
|
||||
const [selectedMode, setSelectedMode] = useState(null);
|
||||
// Riders the operator has marked as absent for today — passed to the AI
|
||||
// assign API so its allocation skips them. Optional: the AI run still
|
||||
// works with an empty list.
|
||||
const [absentRiders, setAbsentRiders] = useState([]);
|
||||
|
||||
// when orders are selected to show popup to stop unwanted reload
|
||||
|
||||
@@ -549,6 +550,18 @@ const Orders = () => {
|
||||
deliverylocation: val.deliverysuburb
|
||||
}));
|
||||
console.log('deliveryData', deliveryData);
|
||||
// Normalize the operator's "Absent Riders" picks into the {userid, username}
|
||||
// shape the AI assign API expects. The API returns firstname/lastname, so
|
||||
// we join them into a single `username` string (with a fallback to the
|
||||
// raw `username` field when names are missing).
|
||||
const absentRidersPayload = (absentRiders || []).map((r) => ({
|
||||
userid: r.userid,
|
||||
username:
|
||||
`${r.firstname || ''} ${r.lastname || ''}`.trim() ||
|
||||
r.username ||
|
||||
`Rider ${r.userid}`
|
||||
}));
|
||||
|
||||
if (aiModeRef.current == 0) {
|
||||
// manual assign
|
||||
createDeliveryMutation.mutate({
|
||||
@@ -561,7 +574,8 @@ const Orders = () => {
|
||||
// mode 1 -> bike , 2 -> auto
|
||||
deliveries: selectedMode.value == 1 ? deliveryData : { body: deliveryData },
|
||||
hypertuning_params: tune || null,
|
||||
selectedMode
|
||||
selectedMode,
|
||||
absent_riders: absentRidersPayload
|
||||
// reshuffle: 'joshi'
|
||||
});
|
||||
} else {
|
||||
@@ -574,7 +588,8 @@ const Orders = () => {
|
||||
pay_type: 'hourly', // options: "hourly", "daily", or "orders"
|
||||
base_pay: 300.0,
|
||||
strategy: 'multi_trip'
|
||||
}
|
||||
},
|
||||
absent_riders: absentRidersPayload
|
||||
},
|
||||
selectedMode
|
||||
});
|
||||
@@ -634,6 +649,8 @@ const Orders = () => {
|
||||
setAssignDialog(false);
|
||||
setRider(null);
|
||||
setPayment(null);
|
||||
setSelectedMode(null);
|
||||
setAbsentRiders([]);
|
||||
};
|
||||
|
||||
const errorMessage = fetchpercentageIsError
|
||||
@@ -1716,6 +1733,235 @@ const Orders = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Absent Riders multi-select — optional. Operator picks the
|
||||
riders who are off today; the AI assign API receives their
|
||||
{userid, username} under `absent_riders` and skips them
|
||||
during allocation. Leaving it empty is valid: the AI run
|
||||
proceeds against every rider in the location. */}
|
||||
{aiModeRef.current == 1 && (
|
||||
<Autocomplete
|
||||
multiple
|
||||
fullWidth={false}
|
||||
options={ridersList || []}
|
||||
loading={ridersListLoading}
|
||||
disableCloseOnSelect
|
||||
limitTags={2}
|
||||
value={absentRiders}
|
||||
onChange={(event, newValue) => setAbsentRiders(newValue)}
|
||||
getOptionLabel={(option) => {
|
||||
const name = `${option.firstname || ''} ${option.lastname || ''}`.trim();
|
||||
return name || `Rider #${option.userid}`;
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => option.userid === value.userid}
|
||||
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: 340 } }}
|
||||
noOptionsText={
|
||||
<Stack alignItems="center" py={2} spacing={1}>
|
||||
<MdEventBusy size={26} color="#94a3b8" />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
No riders to show for this location
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const name =
|
||||
`${option.firstname || ''} ${option.lastname || ''}`.trim() ||
|
||||
`Rider #${option.userid}`;
|
||||
const initials =
|
||||
(name.match(/\b\w/g) || []).slice(0, 2).join('').toUpperCase() || '?';
|
||||
const tagProps = getTagProps({ index });
|
||||
return (
|
||||
<Chip
|
||||
{...tagProps}
|
||||
key={option.userid}
|
||||
size="small"
|
||||
avatar={
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: '#fed7aa',
|
||||
color: '#9a3412',
|
||||
fontSize: 10,
|
||||
fontWeight: 800
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
</Avatar>
|
||||
}
|
||||
label={name}
|
||||
sx={{
|
||||
height: 26,
|
||||
bgcolor: '#fff7ed',
|
||||
color: '#9a3412',
|
||||
border: '1px solid',
|
||||
borderColor: '#fdba74',
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
'& .MuiChip-label': { px: 0.75 },
|
||||
'& .MuiChip-deleteIcon': {
|
||||
color: '#c2410c',
|
||||
fontSize: 16,
|
||||
'&:hover': { color: '#7c2d12' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
renderOption={(props, option, { selected }) => {
|
||||
const name = `${option.firstname || ''} ${option.lastname || ''}`.trim();
|
||||
const initials =
|
||||
(name.match(/\b\w/g) || []).slice(0, 2).join('').toUpperCase() || '?';
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
key={option.userid}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 12px',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
backgroundColor: selected ? '#fff7ed' : 'transparent'
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selected}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
color: '#fb923c',
|
||||
'&.Mui-checked': { color: '#ea580c' }
|
||||
}}
|
||||
/>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
bgcolor: selected ? '#fb923c' : '#f1f5f9',
|
||||
color: selected ? '#fff' : '#475569',
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
transition: 'background-color 0.15s, color 0.15s'
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
</Avatar>
|
||||
<Stack direction="column" spacing={0} flex={1} minWidth={0}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={700}
|
||||
color="#0f172a"
|
||||
noWrap
|
||||
>
|
||||
{name || `Rider #${option.userid}`}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
ID #{option.userid}
|
||||
{option.contactno ? ` · ${option.contactno}` : ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{selected && (
|
||||
<Chip
|
||||
size="small"
|
||||
label="Absent"
|
||||
sx={{
|
||||
height: 18,
|
||||
fontSize: 9,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.3,
|
||||
bgcolor: '#ea580c',
|
||||
color: '#fff',
|
||||
'& .MuiChip-label': { px: 0.75 }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Absent Riders"
|
||||
placeholder={absentRiders.length ? '' : 'Pick riders unavailable today'}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.75}
|
||||
sx={{ pl: 0.5, mr: 0.25 }}
|
||||
>
|
||||
<MdPersonOff size={18} color="#ea580c" />
|
||||
{absentRiders.length > 0 && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={absentRiders.length}
|
||||
sx={{
|
||||
height: 18,
|
||||
minWidth: 22,
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
bgcolor: '#ea580c',
|
||||
color: '#fff',
|
||||
'& .MuiChip-label': { px: 0.5 }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{params.InputProps.startAdornment}
|
||||
</Stack>
|
||||
)
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 300,
|
||||
maxWidth: 440,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#fffbf5',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
'& fieldset': {
|
||||
borderColor: '#fdba74',
|
||||
borderWidth: 1.5
|
||||
},
|
||||
'&:hover fieldset': { borderColor: '#fb923c' },
|
||||
'&.Mui-focused': {
|
||||
boxShadow: '0 0 0 3px rgba(251, 146, 60, 0.18)'
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#ea580c',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
fontWeight: 700,
|
||||
fontSize: 13,
|
||||
color: '#9a3412',
|
||||
'&.Mui-focused': { color: '#9a3412' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
sx={{
|
||||
'& .MuiAutocomplete-tag': { my: 0.25 },
|
||||
'& .MuiAutocomplete-endAdornment': { right: 8 }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color={'primary'}
|
||||
disabled={!selectedMode && aiModeRef.current == 1}
|
||||
@@ -2108,7 +2354,7 @@ const Orders = () => {
|
||||
<CircularLoader color="inherit" />
|
||||
</Backdrop>
|
||||
}
|
||||
<DialogTitle>
|
||||
<DialogTitle sx={{ py: 1.25 }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{}}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip title="Back to orders" placement="top">
|
||||
@@ -2190,51 +2436,13 @@ const Orders = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack sx={{ my: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={6} md={3}>
|
||||
<HoverSocialCard
|
||||
secondary={metaData?.total_orders}
|
||||
primary={'Orders'}
|
||||
percentage={<DashboardFilled />}
|
||||
color={theme.palette.success.main}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={6} md={3}>
|
||||
<HoverSocialCard
|
||||
secondary={metaData?.utilized_riders}
|
||||
primary={'Riders'}
|
||||
percentage={<MdDirectionsBike />}
|
||||
color={theme.palette.warning.main}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={6} md={3}>
|
||||
<HoverSocialCard
|
||||
secondary={zoneData?.length}
|
||||
primary={'Zones'}
|
||||
percentage={<FaMapLocationDot />}
|
||||
color={theme.palette.info.main}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={6} md={3}>
|
||||
<HoverSocialCard
|
||||
secondary={metaData?.total_profit}
|
||||
primary={'Profit'}
|
||||
percentage={<GiProfit />}
|
||||
color={theme.palette.error.main}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
||||
{dispatchPreviewData && <Dispatch data={dispatchPreviewData} embedded />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack display={'flex'} flexDirection={'row'} gap={2} alignItems={'center'} justifyContent={'end'} sx={{ p: 2 }}>
|
||||
<DialogActions sx={{ px: 2, py: 1.25 }}>
|
||||
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
|
||||
<Button
|
||||
sx={{}}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
@@ -2245,7 +2453,7 @@ const Orders = () => {
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button sx={{ my: 2 }} variant="contained" onClick={handleFinalCreateDelivery}>
|
||||
<Button variant="contained" onClick={handleFinalCreateDelivery}>
|
||||
Assign Orders
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user