updates on the fix

This commit is contained in:
2026-08-12 12:55:18 +05:30
parent bf28249528
commit fabb74c326
15 changed files with 1478 additions and 528 deletions

View File

@@ -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" />

View File

@@ -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