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) => {
|
export const createAutomationDeliveries = async (variables) => {
|
||||||
console.log('variables', variables);
|
console.log('variables', variables);
|
||||||
|
|
||||||
// optimse the orders and auto rider assign
|
const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : [];
|
||||||
const response = await axios.post(
|
|
||||||
|
// 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
|
variables.selectedMode.value == 1
|
||||||
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
|
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
|
||||||
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`,
|
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
|
||||||
variables.selectedMode.value == 1 ? variables.deliveries : variables.data
|
|
||||||
);
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logger from '../../../utils/logger';
|
import logger from '../../../utils/logger';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { DeleteFilled, EditOutlined } from '@ant-design/icons';
|
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 { Empty } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
var utc = require('dayjs/plugin/utc');
|
var utc = require('dayjs/plugin/utc');
|
||||||
@@ -9,9 +9,15 @@ dayjs.extend(utc);
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
|
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { PiMapPinLineDuotone } from 'react-icons/pi';
|
import {
|
||||||
import { MdOutlineDateRange } from 'react-icons/md';
|
MdOutlineDateRange,
|
||||||
import { MdOutlineDeliveryDining } from 'react-icons/md';
|
MdAccessTime,
|
||||||
|
MdAllInclusive,
|
||||||
|
MdLightMode,
|
||||||
|
MdWbSunny,
|
||||||
|
MdNightsStay,
|
||||||
|
MdCheck
|
||||||
|
} from 'react-icons/md';
|
||||||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -50,7 +56,8 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Backdrop,
|
Backdrop,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Menu
|
Menu,
|
||||||
|
Paper
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import MainCard from 'components/MainCard';
|
import MainCard from 'components/MainCard';
|
||||||
@@ -98,6 +105,84 @@ import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
|||||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
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)|| =================================================
|
// ================================================= || deliveries (initial point)|| =================================================
|
||||||
const Deliveries = () => {
|
const Deliveries = () => {
|
||||||
const userid = localStorage.getItem('userid');
|
const userid = localStorage.getItem('userid');
|
||||||
@@ -146,6 +231,10 @@ const Deliveries = () => {
|
|||||||
const [tenantValue, setTenantValue] = useState(null);
|
const [tenantValue, setTenantValue] = useState(null);
|
||||||
const [locationValue, setLocationValue] = useState(null);
|
const [locationValue, setLocationValue] = useState(null);
|
||||||
const [riderid, setRiderid] = useState(0);
|
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');
|
const roleid = localStorage.getItem('roleid');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,6 +271,7 @@ const Deliveries = () => {
|
|||||||
setCancelDeliveryOpen(false);
|
setCancelDeliveryOpen(false);
|
||||||
fetchCountRefetch(); // Refresh count data
|
fetchCountRefetch(); // Refresh count data
|
||||||
fetchDeliveriesRefetch(); // Refresh deliveries
|
fetchDeliveriesRefetch(); // Refresh deliveries
|
||||||
|
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
opentoast(error.message, 'error');
|
opentoast(error.message, 'error');
|
||||||
@@ -268,6 +358,7 @@ const Deliveries = () => {
|
|||||||
}
|
}
|
||||||
fetchCountRefetch(); // Refresh count data
|
fetchCountRefetch(); // Refresh count data
|
||||||
fetchDeliveriesRefetch(); // Refresh deliveries
|
fetchDeliveriesRefetch(); // Refresh deliveries
|
||||||
|
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||||||
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
|
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
|
||||||
},
|
},
|
||||||
onError: (err, { selectedRider, selectedRow }) => {
|
onError: (err, { selectedRider, selectedRow }) => {
|
||||||
@@ -382,16 +473,177 @@ const Deliveries = () => {
|
|||||||
});
|
});
|
||||||
const rows = deliveriesData?.pages.flatMap((page) => page.rows) || [];
|
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(() => {
|
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(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting) {
|
if (entries[0].isIntersecting) {
|
||||||
fetchNextPage();
|
countFetchNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
|
root: document.querySelector('.MuiTableContainer-root'),
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
threshold: 1.0
|
threshold: 1.0
|
||||||
}
|
}
|
||||||
@@ -400,13 +652,13 @@ const Deliveries = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||||||
};
|
};
|
||||||
}, [hasNextPage, fetchNextPage]);
|
}, [countHasNext, countFetchNext]);
|
||||||
|
|
||||||
const handleScroll = (event) => {
|
const handleScroll = (event) => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||||
if (hasNextPage && !isFetchingNextPage) {
|
if (countHasNext && !countIsFetchingNext) {
|
||||||
fetchNextPage();
|
countFetchNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -476,6 +728,7 @@ const Deliveries = () => {
|
|||||||
setDialogopen(false);
|
setDialogopen(false);
|
||||||
fetchDeliveriesRefetch();
|
fetchDeliveriesRefetch();
|
||||||
fetchCountRefetch();
|
fetchCountRefetch();
|
||||||
|
countSourceRefetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -508,7 +761,7 @@ const Deliveries = () => {
|
|||||||
<>
|
<>
|
||||||
{(fetchCountIsLoading ||
|
{(fetchCountIsLoading ||
|
||||||
fetchPercentageIsLoading ||
|
fetchPercentageIsLoading ||
|
||||||
fetchDeliveriesIsLoading ||
|
countSourceIsLoading ||
|
||||||
fetchtenantsIsLoading ||
|
fetchtenantsIsLoading ||
|
||||||
fetchlocationsIsLoading ||
|
fetchlocationsIsLoading ||
|
||||||
riderListIsLoading) && (
|
riderListIsLoading) && (
|
||||||
@@ -589,24 +842,197 @@ const Deliveries = () => {
|
|||||||
{/* ============================================= || orderFilter | ============================================= */}
|
{/* ============================================= || orderFilter | ============================================= */}
|
||||||
|
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={3} sx={{ flexWrap: 'wrap', my: 2 }}>
|
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={3} sx={{ flexWrap: 'wrap', my: 2 }}>
|
||||||
{startdate && enddate ? (
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
flexWrap="wrap" // ✅ allow wrapping
|
flexWrap="wrap"
|
||||||
gap={1.5} // ✅ space between items when wrapped
|
gap={1.5}
|
||||||
alignItems="center" // optional, for vertical alignment
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Chip
|
{/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and
|
||||||
avatar={
|
"Orders-All" pills. Filters the loaded rows by the slot the
|
||||||
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
operator picks; defaults to 'all'. Mirrors the dispatch page's
|
||||||
<MdOutlineDeliveryDining fontSize="30px" style={{ color: 'red' }} />
|
batch model. UI mirrors the polished Absent Riders dropdown from
|
||||||
</Avatar>
|
the orders page — custom Paper, per-batch color/icon, selected
|
||||||
}
|
indicator. */}
|
||||||
label={`Deliveries-${datestatus}`}
|
{(() => {
|
||||||
color="error"
|
const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0];
|
||||||
variant="combined"
|
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
|
<Chip
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
||||||
@@ -621,25 +1047,12 @@ const Deliveries = () => {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
variant="combined"
|
variant="combined"
|
||||||
color="warning"
|
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
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
options={tenantlist || []}
|
options={tenantlist || []}
|
||||||
@@ -754,7 +1167,7 @@ const Deliveries = () => {
|
|||||||
label="Assigned"
|
label="Assigned"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.uncoveredLength || 0}
|
label={batchCounts.uncoveredLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -767,7 +1180,7 @@ const Deliveries = () => {
|
|||||||
label="Accepted"
|
label="Accepted"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.assignedLength || 0}
|
label={batchCounts.assignedLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -780,7 +1193,7 @@ const Deliveries = () => {
|
|||||||
label="Arrived"
|
label="Arrived"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.arrivedLength || 0}
|
label={batchCounts.arrivedLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -794,7 +1207,7 @@ const Deliveries = () => {
|
|||||||
label="Picked"
|
label="Picked"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.pickedLength || 0}
|
label={batchCounts.pickedLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -807,7 +1220,7 @@ const Deliveries = () => {
|
|||||||
label="Active"
|
label="Active"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.activeLength || 0}
|
label={batchCounts.activeLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -820,7 +1233,7 @@ const Deliveries = () => {
|
|||||||
label="Skipped"
|
label="Skipped"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.skippedLength || 0}
|
label={batchCounts.skippedLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -834,7 +1247,7 @@ const Deliveries = () => {
|
|||||||
label="Delivered"
|
label="Delivered"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.coveredLength || 0}
|
label={batchCounts.coveredLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -848,7 +1261,7 @@ const Deliveries = () => {
|
|||||||
label="Cancelled"
|
label="Cancelled"
|
||||||
icon={
|
icon={
|
||||||
<Chip
|
<Chip
|
||||||
label={countData?.cancelLength || 0}
|
label={batchCounts.cancelLength}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -901,15 +1314,15 @@ const Deliveries = () => {
|
|||||||
{tabstatus == 'Created' && (
|
{tabstatus == 'Created' && (
|
||||||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
indeterminate={deliverylist.length > 0 && deliverylist.length != rows.length}
|
indeterminate={deliverylist.length > 0 && deliverylist.length != filteredRows.length}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
setDeliverylist([...rows]);
|
setDeliverylist([...filteredRows]);
|
||||||
} else {
|
} else {
|
||||||
setDeliverylist([]);
|
setDeliverylist([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
checked={deliverylist.length == rows.length}
|
checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
@@ -944,17 +1357,33 @@ const Deliveries = () => {
|
|||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{(loading1 || fetchDeliveriesIsLoading) && <OrdersTableSkeleton col={8} />}
|
{(loading1 || countSourceIsLoading) && <OrdersTableSkeleton col={8} />}
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.length == 0 && !loading1 && (
|
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
|
||||||
<>
|
<>
|
||||||
<TableCell colSpan={14}>
|
<TableCell colSpan={14}>
|
||||||
{/* <Stack width={'100%'} direction={'row'} justifyContent={'center'}> */}
|
<Empty
|
||||||
<Empty description={`No ${tabstatus} Orders`} styles={{ description: { color: theme.palette.error.main } }} />
|
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>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -1399,11 +1828,11 @@ const Deliveries = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{rows?.length != 0 && (
|
{countSourceRows?.length != 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={15} rowSpan={3}>
|
<TableCell colSpan={15} rowSpan={3}>
|
||||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||||
{isFetchingNextPage ? <LoaderWithImage /> : hasNextPage ? <LoaderWithImage /> : 'No More Deliveries'}
|
{countIsFetchingNext ? <LoaderWithImage /> : countHasNext ? <LoaderWithImage /> : 'No More Deliveries'}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Loader from 'components/Loader';
|
|||||||
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
|
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
|
||||||
|
|
||||||
import { PiMapPinLineDuotone } from 'react-icons/pi';
|
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 { VscArchive } from 'react-icons/vsc';
|
||||||
import DateFilterDialog from 'components/DateFilterDialog';
|
import DateFilterDialog from 'components/DateFilterDialog';
|
||||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
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 WarningIcon from '@mui/icons-material/Warning';
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import { ArrowRightAltOutlined } from '@mui/icons-material';
|
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 { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||||
import { GiProfit } from 'react-icons/gi';
|
|
||||||
|
|
||||||
import { IoReload } from 'react-icons/io5';
|
import { IoReload } from 'react-icons/io5';
|
||||||
|
|
||||||
@@ -74,7 +70,8 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Accordion
|
Accordion,
|
||||||
|
Paper
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -152,6 +149,10 @@ const Orders = () => {
|
|||||||
{ label: 'Auto', value: 2 }
|
{ label: 'Auto', value: 2 }
|
||||||
];
|
];
|
||||||
const [selectedMode, setSelectedMode] = useState(null);
|
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
|
// when orders are selected to show popup to stop unwanted reload
|
||||||
|
|
||||||
@@ -549,6 +550,18 @@ const Orders = () => {
|
|||||||
deliverylocation: val.deliverysuburb
|
deliverylocation: val.deliverysuburb
|
||||||
}));
|
}));
|
||||||
console.log('deliveryData', deliveryData);
|
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) {
|
if (aiModeRef.current == 0) {
|
||||||
// manual assign
|
// manual assign
|
||||||
createDeliveryMutation.mutate({
|
createDeliveryMutation.mutate({
|
||||||
@@ -561,7 +574,8 @@ const Orders = () => {
|
|||||||
// mode 1 -> bike , 2 -> auto
|
// mode 1 -> bike , 2 -> auto
|
||||||
deliveries: selectedMode.value == 1 ? deliveryData : { body: deliveryData },
|
deliveries: selectedMode.value == 1 ? deliveryData : { body: deliveryData },
|
||||||
hypertuning_params: tune || null,
|
hypertuning_params: tune || null,
|
||||||
selectedMode
|
selectedMode,
|
||||||
|
absent_riders: absentRidersPayload
|
||||||
// reshuffle: 'joshi'
|
// reshuffle: 'joshi'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -574,7 +588,8 @@ const Orders = () => {
|
|||||||
pay_type: 'hourly', // options: "hourly", "daily", or "orders"
|
pay_type: 'hourly', // options: "hourly", "daily", or "orders"
|
||||||
base_pay: 300.0,
|
base_pay: 300.0,
|
||||||
strategy: 'multi_trip'
|
strategy: 'multi_trip'
|
||||||
}
|
},
|
||||||
|
absent_riders: absentRidersPayload
|
||||||
},
|
},
|
||||||
selectedMode
|
selectedMode
|
||||||
});
|
});
|
||||||
@@ -634,6 +649,8 @@ const Orders = () => {
|
|||||||
setAssignDialog(false);
|
setAssignDialog(false);
|
||||||
setRider(null);
|
setRider(null);
|
||||||
setPayment(null);
|
setPayment(null);
|
||||||
|
setSelectedMode(null);
|
||||||
|
setAbsentRiders([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMessage = fetchpercentageIsError
|
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
|
<Button
|
||||||
color={'primary'}
|
color={'primary'}
|
||||||
disabled={!selectedMode && aiModeRef.current == 1}
|
disabled={!selectedMode && aiModeRef.current == 1}
|
||||||
@@ -2108,7 +2354,7 @@ const Orders = () => {
|
|||||||
<CircularLoader color="inherit" />
|
<CircularLoader color="inherit" />
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
}
|
}
|
||||||
<DialogTitle>
|
<DialogTitle sx={{ py: 1.25 }}>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{}}>
|
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{}}>
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<Tooltip title="Back to orders" placement="top">
|
<Tooltip title="Back to orders" placement="top">
|
||||||
@@ -2190,51 +2436,13 @@ const Orders = () => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</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>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
<DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
||||||
{dispatchPreviewData && <Dispatch data={dispatchPreviewData} embedded />}
|
{dispatchPreviewData && <Dispatch data={dispatchPreviewData} embedded />}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions sx={{ px: 2, py: 1.25 }}>
|
||||||
<Stack display={'flex'} flexDirection={'row'} gap={2} alignItems={'center'} justifyContent={'end'} sx={{ p: 2 }}>
|
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
|
||||||
<Button
|
<Button
|
||||||
sx={{}}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={<ArrowBackIcon />}
|
startIcon={<ArrowBackIcon />}
|
||||||
@@ -2245,7 +2453,7 @@ const Orders = () => {
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button sx={{ my: 2 }} variant="contained" onClick={handleFinalCreateDelivery}>
|
<Button variant="contained" onClick={handleFinalCreateDelivery}>
|
||||||
Assign Orders
|
Assign Orders
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user