updates on the fix
This commit is contained in:
@@ -167,7 +167,7 @@ const MobileOrdersList = ({ list, aiMode }) => {
|
||||
<MobileField label="KMS">
|
||||
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
|
||||
<Tooltip title="KMS" placement="top">
|
||||
<Chip size="small" label={`${val.kms} km`} color="error" />
|
||||
<Chip size="small" label={`${Number(val.kms || 0).toFixed(2)} km`} color="error" />
|
||||
</Tooltip>
|
||||
<Tooltip title="Cumulative Kms" placement="right">
|
||||
<Chip size="small" label={`${val.cumulativekms} km`} color="success" />
|
||||
@@ -559,7 +559,7 @@ const OrdersPreview = () => {
|
||||
<TableCell align="center">
|
||||
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
|
||||
<Tooltip title="KMS" placement="top">
|
||||
<Chip size="small" label={`${val.kms} km`} color="error" />
|
||||
<Chip size="small" label={`${Number(val.kms || 0).toFixed(2)} km`} color="error" />
|
||||
</Tooltip>
|
||||
<Tooltip title="Cumulative Kms" placement="right">
|
||||
<Chip size="small" label={`${val.cumulativekms} km`} color="success" />
|
||||
@@ -668,7 +668,7 @@ const OrdersPreview = () => {
|
||||
<TableCell align="center">
|
||||
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
|
||||
<Tooltip title="KMS" placement="top">
|
||||
<Chip size="small" label={`${val.kms} km`} color="error" />
|
||||
<Chip size="small" label={`${Number(val.kms || 0).toFixed(2)} km`} color="error" />
|
||||
</Tooltip>
|
||||
<Tooltip title="Cumulative Kms" placement="right">
|
||||
<Chip size="small" label={`${val.cumulativekms} km`} color="success" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as React from 'react';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState, useEffect, useMemo, useCallback, Fragment, useRef } from 'react';
|
||||
import logger from '../../../utils/logger';
|
||||
import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
@@ -53,7 +54,8 @@ import {
|
||||
MdNotes,
|
||||
MdMyLocation,
|
||||
MdOutlineFlag,
|
||||
MdPerson
|
||||
MdPerson,
|
||||
MdFlashOn
|
||||
} from 'react-icons/md';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import Loader from 'components/Loader';
|
||||
@@ -62,8 +64,9 @@ import CircularLoader from 'components/CircularLoader';
|
||||
import AiImage from '../../../assets/images/aiImage.png';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { fetchPercentageData, createAutomationDeliveries, getallriders } from '../../api/api';
|
||||
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi';
|
||||
import { fetchPercentageData, createAutomationDeliveries, getallriders, buildMilerLookup, notifyRider } from '../../api/api';
|
||||
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack, batchAssignBookings, getMilers } from 'pages/api/doormileApi';
|
||||
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the rest of the redesigned operator pages.
|
||||
@@ -97,13 +100,17 @@ const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
// Semantic per-row status palette, keyed by GET /admin/bookings' real `status`
|
||||
// enum (lowercased — StatusBadge lowercases before lookup). Only
|
||||
// pending_pickup and converted_to_consignment have been observed against the
|
||||
// live API so far; the rest are reasonable guesses at likely sibling states
|
||||
// with a safe unknown-status fallback in StatusBadge below.
|
||||
// enum (lowercased — StatusBadge lowercases before lookup). pending_pickup,
|
||||
// miler_assigned, pickup_scheduled, converted_to_consignment and cancelled are
|
||||
// now confirmed live (see the ORDERS_STATUS_TABS comment above — a real
|
||||
// booking status breakdown logged from an actual assign-miler call); the rest
|
||||
// remain reasonable guesses at likely sibling states with a safe
|
||||
// unknown-status fallback in StatusBadge below.
|
||||
const ROW_STATUS_META = {
|
||||
pending_pickup: { label: 'Pending Pickup', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
pending_assignment: { label: 'Pending Assignment', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
miler_assigned: { label: 'Rider Assigned', color: '#6366f1', icon: MdCheckCircle },
|
||||
pickup_scheduled: { label: 'Pickup Scheduled', color: '#6366f1', icon: MdCheckCircle },
|
||||
converted_to_consignment: { label: 'Converted', color: '#6366f1', icon: MdCheckCircle },
|
||||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
@@ -115,11 +122,42 @@ const ROW_STATUS_META = {
|
||||
// below. `status` values here are the raw, lowercased GET /admin/bookings
|
||||
// enum (matches ROW_STATUS_META's keys), not api.js's separate generic
|
||||
// pending/accepted/delivered mapping used by the Deliveries page.
|
||||
//
|
||||
// The "Assigned" tab originally matched only `converted_to_consignment` — the
|
||||
// ONLY other status this file's author had actually observed live at the
|
||||
// time (see ROW_STATUS_META's comment). Confirmed live now (booking status
|
||||
// breakdown logged from a real assign-miler call): a booking moves
|
||||
// pending_pickup -> miler_assigned immediately on assignment, not straight to
|
||||
// converted_to_consignment (that only happens later, when the rider marks
|
||||
// pickup-complete). `miler_assigned` matched nothing in this tab list, so an
|
||||
// assigned-but-not-yet-picked-up order vanished from every tab.
|
||||
//
|
||||
// Deliberately DIFFERENT rule from api.js's BOOKING_STATUS_TO_DELIVERY_STATUS
|
||||
// (Deliveries page): this Orders page tracks the OPERATOR's own workflow —
|
||||
// "Assigned" means the operator has picked a rider for it, full stop, the
|
||||
// moment assign-miler / batch-assign succeeds. The Deliveries page instead
|
||||
// tracks the RIDER's engagement (its "Accepted" tab specifically means the
|
||||
// rider accepted, not just that one was assigned) — a deliberately later,
|
||||
// narrower bar, which is why that page keeps miler_assigned on 'pending'
|
||||
// while THIS page moves the same status straight to "Assigned". Don't
|
||||
// "fix" this to match Deliveries again — it was tried and reverted per
|
||||
// explicit product direction: on Orders, Assigned = operator action; on
|
||||
// Deliveries, Accepted = rider action. `statuses` (plural) is the actual
|
||||
// match set; each tab's singular `status` stays the tab's
|
||||
// primary/representative value for `currentStatus`, the React key, and the
|
||||
// few `currentStatus === 'pending_pickup'` checks elsewhere in this file.
|
||||
const ORDERS_STATUS_TABS = [
|
||||
{ idx: 0, status: 'pending_pickup', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
{ idx: 1, status: 'converted_to_consignment', label: 'Assigned', color: '#6366f1', icon: MdCheckCircle },
|
||||
{ idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
{ idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
{ idx: 0, status: 'pending_pickup', statuses: ['pending_pickup'], label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
{
|
||||
idx: 1,
|
||||
status: 'converted_to_consignment',
|
||||
statuses: ['converted_to_consignment', 'miler_assigned', 'pickup_scheduled'],
|
||||
label: 'Assigned',
|
||||
color: '#6366f1',
|
||||
icon: MdCheckCircle
|
||||
},
|
||||
{ idx: 2, status: 'delivered', statuses: ['delivered'], label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
{ idx: 3, status: 'cancelled', statuses: ['cancelled'], label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
];
|
||||
|
||||
// Haversine straight-line distance in km — GET /admin/bookings has no `kms`
|
||||
@@ -360,10 +398,30 @@ const Orders = () => {
|
||||
return counts;
|
||||
}, [allBookings]);
|
||||
|
||||
// Diagnostic: a newly-created booking not showing on the Pending tab could
|
||||
// mean either (a) Doormile's background auto-assignment (see
|
||||
// express-console-api.md — fires after commit, 5 retries 2 min apart) has
|
||||
// already moved it out of pending_pickup by the time the operator looks,
|
||||
// which is expected behavior, not a bug, or (b) a brand-new booking's
|
||||
// actual status string doesn't match any of the tabs at all (an unmapped
|
||||
// value BOOKING_STATUS_TO_DELIVERY_STATUS in api.js has never observed).
|
||||
// This makes that distinguishable from the console instead of guessing.
|
||||
useEffect(() => {
|
||||
logger.debug('orders.js: booking status breakdown', statusCounts);
|
||||
}, [statusCounts]);
|
||||
|
||||
// The active tab's full status match set (see ORDERS_STATUS_TABS' comment) —
|
||||
// falls back to [currentStatus] so this still works if currentStatus is
|
||||
// ever set to something outside the tab list.
|
||||
const activeTabStatuses = useMemo(
|
||||
() => ORDERS_STATUS_TABS.find((t) => t.status === currentStatus)?.statuses || [currentStatus],
|
||||
[currentStatus]
|
||||
);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
const q = debouncedSearch.trim().toLowerCase();
|
||||
return allBookings.filter((b) => {
|
||||
if (String(b.status || '').toLowerCase() !== currentStatus) return false;
|
||||
if (!activeTabStatuses.includes(String(b.status || '').toLowerCase())) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
b.bookingno?.toLowerCase().includes(q) ||
|
||||
@@ -372,7 +430,7 @@ const Orders = () => {
|
||||
b.notes?.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}, [allBookings, currentStatus, debouncedSearch]);
|
||||
}, [allBookings, activeTabStatuses, debouncedSearch]);
|
||||
|
||||
// "Load more" now reveals more of the already-fetched, already-filtered
|
||||
// set instead of requesting another server page (the bulk fetch above
|
||||
@@ -473,9 +531,12 @@ const Orders = () => {
|
||||
return {
|
||||
...val,
|
||||
deliveryid: 0,
|
||||
deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD HH:mm:ss'),
|
||||
// Doormile timestamps are naive IST wall-clock (express-console-api.md) —
|
||||
// .utc() was reinterpreting that as local time and shifting it back
|
||||
// 5:30, sending the solver a wrong delivery deadline. Parse bare,
|
||||
// matching assigntime right below (and deliveries.js's own fix for
|
||||
// the same bug class in its date-display cells).
|
||||
deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat).format('YYYY-MM-DD HH:mm:ss'),
|
||||
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
orderstatus: 'pending',
|
||||
orderamount: charge,
|
||||
@@ -498,6 +559,70 @@ const Orders = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Doormile-native alternative to the external solver above — assigns real
|
||||
// milers directly via POST /hub/bookings/batch-assign (Redis GEO + AI
|
||||
// scoring), no external solver rider pool involved, so there's no chance of
|
||||
// the stale-id mismatch that made the "unverified rider" flow on
|
||||
// /dispatch/preview necessary for the solver path. Commits server-side in
|
||||
// this one call — no preview/reconcile step exists for this path because
|
||||
// there is nothing to reconcile. Sequencing (/optimization/doormile/sequence)
|
||||
// isn't deployed yet, so riders with more than one stop come back
|
||||
// unsequenced (step: 0) — an accepted, visible trade-off for this opt-in
|
||||
// "Quick Assign" button; operators who need solver-quality sequencing keep
|
||||
// using "Assign Orders" above. See doormile-flow.md and the plan this was
|
||||
// built from.
|
||||
const quickAssignMutation = useMutation({
|
||||
mutationFn: () => batchAssignBookings(selectedOrders.map((o) => o.bookingid)),
|
||||
onSuccess: async (data) => {
|
||||
const results = data?.results || data?.data?.results || [];
|
||||
const assignedCount = data?.assigned ?? data?.data?.assigned ?? results.filter((r) => r.assigned).length;
|
||||
const skipped = results.filter((r) => !r.assigned);
|
||||
|
||||
enqueueSnackbar(`Quick Assign: ${assignedCount} assigned${skipped.length ? `, ${skipped.length} skipped` : ''}`, {
|
||||
variant: skipped.length ? 'warning' : 'success',
|
||||
autoHideDuration: 3000,
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' }
|
||||
});
|
||||
if (skipped.length) {
|
||||
logger.debug(
|
||||
'quickAssignMutation: skipped bookings',
|
||||
skipped.map((r) => r.bookingid)
|
||||
);
|
||||
}
|
||||
|
||||
// batch-assign returns mileruserid per result — resolve to
|
||||
// milerprofileid (what notifyRider/notify actually needs) the same way
|
||||
// finalCreatedeliveries does, then notify each assigned rider.
|
||||
try {
|
||||
const milers = (await getMilers()) || [];
|
||||
const lookup = buildMilerLookup(milers);
|
||||
const notifiedProfileIds = new Set();
|
||||
results
|
||||
.filter((r) => r.assigned)
|
||||
.forEach((r) => {
|
||||
const rider = lookup.byUserId.get(String(r.mileruserid));
|
||||
if (rider?.milerprofileid && !notifiedProfileIds.has(rider.milerprofileid)) {
|
||||
notifiedProfileIds.add(rider.milerprofileid);
|
||||
notifyRider(rider.milerprofileid).catch((err) => logger.error('quickAssignMutation: notifyRider failed', err.message));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('quickAssignMutation: rider notify resolution failed', err.message);
|
||||
}
|
||||
|
||||
refetchOrders();
|
||||
setSelectedOrders([]);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
|
||||
}
|
||||
});
|
||||
|
||||
const handleQuickAssign = () => {
|
||||
if (selectedOrders.length === 0) return;
|
||||
quickAssignMutation.mutate();
|
||||
};
|
||||
|
||||
// KPI tile definitions.
|
||||
const kpiCards = [
|
||||
{
|
||||
@@ -561,6 +686,39 @@ const Orders = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{selectedOrders.length > 0 && currentStatus === 'pending_pickup' && (
|
||||
<Tooltip title="Assigns real Doormile riders directly (no external solver). Stops aren't sequenced yet — best for single-stop or same-hub batches.">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleQuickAssign}
|
||||
disabled={quickAssignMutation.isPending}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 32,
|
||||
right: 24 + 200,
|
||||
bgcolor: BRAND_LIGHT,
|
||||
color: '#fff',
|
||||
px: 3,
|
||||
py: 1.5,
|
||||
borderRadius: 999,
|
||||
fontWeight: 850,
|
||||
boxShadow: `0 10px 30px ${ring(BRAND_LIGHT)}`,
|
||||
zIndex: 1000,
|
||||
textTransform: 'none',
|
||||
fontSize: '14px',
|
||||
gap: 1,
|
||||
'&:hover': {
|
||||
bgcolor: BRAND,
|
||||
transform: 'translateY(-2px)'
|
||||
}
|
||||
}}
|
||||
startIcon={<MdFlashOn size={18} />}
|
||||
>
|
||||
Quick Assign
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* ============================================= || Header (compact) || ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
@@ -658,7 +816,7 @@ const Orders = () => {
|
||||
{ORDERS_STATUS_TABS.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const active = tabvalue === t.idx;
|
||||
const count = statusCounts[t.status] ?? 0;
|
||||
const count = t.statuses.reduce((sum, s) => sum + (statusCounts[s] ?? 0), 0);
|
||||
return (
|
||||
<Box
|
||||
key={t.status}
|
||||
@@ -903,7 +1061,7 @@ const Orders = () => {
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.125 }}>
|
||||
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
{(() => {
|
||||
const dateObj = dayjs(row.createdat);
|
||||
const dateObj = parseDoormileTimestamp(row.createdat);
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
||||
@@ -1012,28 +1170,6 @@ const Orders = () => {
|
||||
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||
<Tooltip title="Track">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTrackBookingId(row.bookingid);
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: tint('#0ea5e9'),
|
||||
border: `1px solid ${edge('#0ea5e9')}`,
|
||||
color: '#0ea5e9',
|
||||
borderRadius: 999,
|
||||
p: 0.75,
|
||||
'&:hover': {
|
||||
bgcolor: soft('#0ea5e9'),
|
||||
borderColor: '#0ea5e9'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdMyLocation size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{currentStatus === 'pending_pickup' && isCancellable && (
|
||||
<Tooltip title="Cancel Order">
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user