diff --git a/src/components/nearle_components/LocationAutocomplete.js b/src/components/nearle_components/LocationAutocomplete.js index 1ea3b8c..0c9900c 100644 --- a/src/components/nearle_components/LocationAutocomplete.js +++ b/src/components/nearle_components/LocationAutocomplete.js @@ -110,7 +110,15 @@ const LocationAutocomplete = forwardRef( sx={{ ...sx }} onChange={(event, value, reason) => { if (reason === 'clear') { - setAppId?.(0); + // null (not 0) — 0 is a real, meaningful selection here: the + // synthetic "All" option (fetchAppLocations in api.js appends + // { locationname: 'All', applocationid: 0 }). Callers that gate + // other pickers on "has a zone been chosen yet" (e.g. + // deliveries.js's Tenant/Rider dropdowns) need to tell "nothing + // picked" apart from "deliberately picked All" — both used to + // collapse to the same falsy 0, so choosing All incorrectly + // still tripped the "select a zone first" gate. + setAppId?.(null); setLocoName?.(''); setPage?.(0); } else if (value) { diff --git a/src/pages/api/api.js b/src/pages/api/api.js index 40c8054..3c4154d 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -559,7 +559,14 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { tenantadress: tenant?.primaryemail || '', locationname: tenant?.tenantname || '', locationsuburb: '', - pickupcustomer: '', + // Was hardcoded '' — Dispatch.js's kitchen markers read this as the + // pickup business name (`o.pickupcustomer || o.kitchen_key || 'Unknown'`, + // Dispatch.js:1660), and no booking on this API carries a `kitchen_key` + // field at all, so every kitchen pin fell through to the literal string + // 'Unknown' — rendered as a "U" marker whose hover/popup then showed + // "Unknown". The tenant IS the kitchen for a B2B booking (same value + // already used for tenantname/locationname above), so reuse it here. + pickupcustomer: tenant?.tenantname || '', pickupcontactno: '', Pickupaddress: b.pickupaddress || '', pickupaddress: b.pickupaddress || '', diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index 9075a53..cd32846 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -432,7 +432,10 @@ const Deliveries = () => { } = useQuery({ queryKey: ['tenantlist', appId], queryFn: () => getTenants(appId), // Ensure appId is passed - enabled: appId !== 0 // Ensures query runs only when appId is valid + // appId 0 is a real, deliberate choice — the synthetic "All" zone option + // (fetchAppLocations in api.js) — not "nothing picked yet"; only + // null/undefined means that now. See LocationAutocomplete.js's onChange. + enabled: appId != null }); // ==============================|| gettenantlocations ||============================== // @@ -799,7 +802,7 @@ const Deliveries = () => { // throwing after already toasting there too, which showed this same // error twice) — no onError needed here anymore. queryFn: fetchRidersList, - enabled: Boolean(appId) + enabled: appId != null }); /* ============================================= || updatedelivery | ============================================= */ @@ -814,10 +817,16 @@ const Deliveries = () => { setDialogopen(false); fetchCountRefetch(); countSourceRefetch(); + } else { + // A 200 with success:false (the Doormile API's validation-failure + // shape, same pattern as customers.js's saveMutation) had no else + // branch here before — the dialog just sat open with zero feedback, + // looking exactly like the button didn't work. + opentoast(res.message || 'Failed to update delivery', 'error'); } }, onError: (err) => { - opentoast(err.message, 'success'); + opentoast(err.response?.data?.message || err.message || 'Failed to update delivery', 'error'); } }); @@ -1127,7 +1136,7 @@ const Deliveries = () => { PaperComponent={SoftPaper} sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} onOpen={(event) => { - if (!appId) { + if (appId == null) { event.preventDefault(); OpenToast('Please select your zone first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); @@ -1174,7 +1183,7 @@ const Deliveries = () => { PaperComponent={SoftPaper} sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} onOpen={(event) => { - if (!appId && !tenantid) { + if (appId == null && !tenantid) { event.preventDefault(); OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); @@ -1226,7 +1235,7 @@ const Deliveries = () => { else setRiderid(value.userid); }} onOpen={(event) => { - if (!appId) { + if (appId == null) { event.preventDefault(); OpenToast('Please select your zone first!', 'warning', 3000); setTimeout(() => locationRef.current?.focus(), 0); @@ -1614,7 +1623,7 @@ const Deliveries = () => { {row.transitminutes || 0}m @@ -1920,17 +1929,19 @@ const Deliveries = () => { {/* express-console-api.md: Doormile timestamps are IST - wall-clock with no timezone marker — .utc() was - reinterpreting that naive string as local time and - shifting it back 5:30, showing the wrong time (and - sometimes the wrong day). Parse bare, matching the - already-correct convention in getRowBatchId above. */} + wall-clock with no timezone marker — bare dayjs() was + still being used here despite this comment saying + otherwise (the fix was never actually applied below, + only claimed) — a Z-suffixed response reinterprets as + local time and shifts 5:30, showing the wrong time + (and sometimes the wrong day). parseDoormileTimestamp + strips that, matching getRowBatchId above. */} - {dayjs(row.orderdate).format('DD/MM/YYYY')} + {parseDoormileTimestamp(row.orderdate).format('DD/MM/YYYY')} - {dayjs(row.orderdate).format('hh:mm A')} + {parseDoormileTimestamp(row.orderdate).format('hh:mm A')} @@ -1943,11 +1954,11 @@ const Deliveries = () => { - {dayjs(row.deliverydate).format('DD/MM/YYYY')} + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY')} - {dayjs(row.deliverydate).format('hh:mm A')} + {parseDoormileTimestamp(row.deliverydate).format('hh:mm A')} @@ -2028,7 +2039,7 @@ const Deliveries = () => { {/* Estimated Delivery Time */} - {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} + {row.expecteddeliverytime ? parseDoormileTimestamp(row.expecteddeliverytime).format('hh:mm A') : '—'} {/* Transit Minutes */} @@ -2455,7 +2466,7 @@ const Deliveries = () => { {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && ( { - if (!appId) { + if (appId == null) { opentoast('Please select a location first!', 'warning'); locationRef.current?.focus(); return; @@ -2476,6 +2487,22 @@ const Deliveries = () => { already could. */} { + // PUT /admin/consignments/:id/status is consignment-scoped — it + // only exists once the rider marks pickup-complete (doormile-flow.md). + // Without a real consignmentid, updateDeliveryAPI's `?? deliveryid` + // fallback was sending the BOOKING id where a consignment id is + // required — a different id space, so the call failed against + // the wrong (or nonexistent) resource. Combined with the mutation's + // onSuccess only handling `res.success === true` with no else + // branch, that failure was completely silent — dialog just sat + // there looking like nothing happened. Block it here instead, + // with a message that explains why, rather than letting the + // submit silently no-op. + if (!selectedRow.consignmentid) { + opentoast("This order hasn't been picked up yet — status can only be updated once it becomes a consignment.", 'warning'); + handleMenuClose(); + return; + } setKms(selectedRow.kms); setCumulativeKms(selectedRow.cumulativekms); setDeliverylat(selectedRow.droplat); diff --git a/src/pages/nearle/dispatch/ActiveSection.js b/src/pages/nearle/dispatch/ActiveSection.js index e2a2d92..9284fd2 100644 --- a/src/pages/nearle/dispatch/ActiveSection.js +++ b/src/pages/nearle/dispatch/ActiveSection.js @@ -7,7 +7,7 @@ import { MdAccessTime, MdInventory2 } from 'react-icons/md'; -import dayjs from 'dayjs'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { CircularProgress } from '@mui/material'; import { getStatusStyle, getActiveOrder } from './dispatchShared'; import { OpenToast } from 'components/third-party/OpenToast'; @@ -147,7 +147,9 @@ const ActiveSection = ({ {o.deliverytime && ( - {dayjs(o.deliverytime).isValid() ? dayjs(o.deliverytime).format('HH:mm:ss') : String(o.deliverytime)} + {parseDoormileTimestamp(o.deliverytime).isValid() + ? parseDoormileTimestamp(o.deliverytime).format('HH:mm:ss') + : String(o.deliverytime)} )} diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 7d3b2da..3dae54b 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -1459,11 +1459,16 @@ const Dispatch = ({ const status = String(r.orderstatus ?? r.status ?? '').toLowerCase(); if (SKIPPED_STATUSES.has(status)) return; - // Filter by selectedDate to align with daily slot aggregations + // Filter by selectedDate to align with daily slot aggregations. + // parseDoormileTimestamp (not bare dayjs()) avoids the false-trailing-Z + // parse bug (utils/doormileTimestamp.js) — near a midnight boundary an + // unstripped Z could shift a row +5:30 into the wrong calendar day, + // silently dropping it from (or wrongly adding it to) a rider's daily + // profit total for selectedDate. const dateStr = r.assigntime - ? dayjs(r.assigntime).format('YYYY-MM-DD') + ? parseDoormileTimestamp(r.assigntime).format('YYYY-MM-DD') : r.deliverydate - ? dayjs(r.deliverydate).format('YYYY-MM-DD') + ? parseDoormileTimestamp(r.deliverydate).format('YYYY-MM-DD') : 'unknown'; if (selectedDate && dateStr !== selectedDate) return; @@ -2071,10 +2076,15 @@ const Dispatch = ({ } const kmDelta = actualKm - plannedKm; const kmDeltaPct = plannedKm > 0 ? (kmDelta / plannedKm) * 100 : null; + // parseDoormileTimestamp on both sides (not bare dayjs()) — even though + // a false-Z shift would cancel out in the diff below if BOTH fields + // always had it consistently, that's not guaranteed per-field, and an + // inconsistent shift on just one side would corrupt timeDeltaMin and + // the anomaly flag it drives. const expectedTs = order?.expecteddeliverytime - ? dayjs(order.expecteddeliverytime) + ? parseDoormileTimestamp(order.expecteddeliverytime) : null; - const actualTs = order?.deliverytime ? dayjs(order.deliverytime) : null; + const actualTs = order?.deliverytime ? parseDoormileTimestamp(order.deliverytime) : null; const timeDeltaMin = expectedTs?.isValid() && actualTs?.isValid() ? actualTs.diff(expectedTs, 'minute') @@ -5063,7 +5073,9 @@ const Dispatch = ({ Last Seen {' '} - {dayjs(r.logdate).isValid() ? dayjs(r.logdate).format('hh:mm:ss A') : r.logdate} + {parseDoormileTimestamp(r.logdate).isValid() + ? parseDoormileTimestamp(r.logdate).format('hh:mm:ss A') + : r.logdate} )} @@ -5339,7 +5351,7 @@ const Dispatch = ({
{t.deliverytime - ? `Delivered ${dayjs(t.deliverytime).format('hh:mm A')}` + ? `Delivered ${parseDoormileTimestamp(t.deliverytime).format('hh:mm A')}` : `${t.coords.length} GPS pings`}
diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.js b/src/pages/nearle/dispatch/ProfitabilitySection.js index c1769a8..2f607d0 100644 --- a/src/pages/nearle/dispatch/ProfitabilitySection.js +++ b/src/pages/nearle/dispatch/ProfitabilitySection.js @@ -1,6 +1,7 @@ import React, { useState, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { MdTrendingUp, MdTrendingDown, @@ -93,10 +94,13 @@ function calcRiderMetrics(rider, selectedDate, batches) { // context. Only the money/km math below excludes them. const orders = (rider.orders ?? []).filter((o) => { if (!selectedDate) return true; + // parseDoormileTimestamp (not bare dayjs()) avoids the false-trailing-Z + // parse bug (utils/doormileTimestamp.js) — near a midnight boundary an + // unstripped Z could shift a row +5:30 out of selectedDate entirely. const dateStr = o.assigntime - ? dayjs(o.assigntime).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.assigntime).format('YYYY-MM-DD') : o.deliverydate - ? dayjs(o.deliverydate).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.deliverydate).format('YYYY-MM-DD') : 'unknown'; return dateStr === selectedDate; }); @@ -116,9 +120,9 @@ function calcRiderMetrics(rider, selectedDate, batches) { if (!slot) continue; const dateStr = o.assigntime - ? dayjs(o.assigntime).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.assigntime).format('YYYY-MM-DD') : o.deliverydate - ? dayjs(o.deliverydate).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.deliverydate).format('YYYY-MM-DD') : null; if (!dateStr) continue; diff --git a/src/pages/nearle/orders/OrdersPreview.js b/src/pages/nearle/orders/OrdersPreview.js index c7a6cba..62f8930 100644 --- a/src/pages/nearle/orders/OrdersPreview.js +++ b/src/pages/nearle/orders/OrdersPreview.js @@ -24,6 +24,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { useLocation, useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import MainCard from 'components/MainCard'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../../api/api'; @@ -103,10 +104,10 @@ const MobileOrdersList = ({ list, aiMode }) => { - {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.orderdate).format('DD/MM/YYYY')} - {dayjs(val.orderdate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.orderdate).format('hh:mm A')} @@ -114,10 +115,10 @@ const MobileOrdersList = ({ list, aiMode }) => { - {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.deliverydate).format('DD/MM/YYYY')} - {dayjs(val.deliverydate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.deliverydate).format('hh:mm A')} @@ -492,20 +493,20 @@ const OrdersPreview = () => { - {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.orderdate).format('DD/MM/YYYY')} - {dayjs(val.orderdate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.orderdate).format('hh:mm A')} - - {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.deliverydate).format('DD/MM/YYYY')} - {dayjs(val.deliverydate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.deliverydate).format('hh:mm A')} @@ -607,20 +608,20 @@ const OrdersPreview = () => { - {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.orderdate).format('DD/MM/YYYY')} - {dayjs(val.orderdate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.orderdate).format('hh:mm A')} - - {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + {parseDoormileTimestamp(val.deliverydate).format('DD/MM/YYYY')} - {dayjs(val.deliverydate).utc().format('hh:mm A')} + {parseDoormileTimestamp(val.deliverydate).format('hh:mm A')} diff --git a/src/pages/nearle/reports/mapWithRoute.js b/src/pages/nearle/reports/mapWithRoute.js index c450dee..5db77b6 100644 --- a/src/pages/nearle/reports/mapWithRoute.js +++ b/src/pages/nearle/reports/mapWithRoute.js @@ -3,6 +3,7 @@ import { MapContainer, TileLayer, Marker, Polyline, Tooltip } from 'react-leafle import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import dayjs from 'dayjs'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { Chip, Stack, Typography, Box } from '@mui/material'; import { CloseCircleOutlined } from '@ant-design/icons'; import CircularLoader from 'components/CircularLoader'; @@ -160,12 +161,12 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { {/* START MARKER */} - {`Pickup: ${dayjs(additionalProps.riderStart).format('DD-MM-YYYY hh:mm A')}`} + {`Pickup: ${parseDoormileTimestamp(additionalProps.riderStart).format('DD-MM-YYYY hh:mm A')}`} {/* END MARKER */} - {`Drop: ${dayjs(additionalProps.riderEnd).format('DD-MM-YYYY hh:mm A')}`} + {`Drop: ${parseDoormileTimestamp(additionalProps.riderEnd).format('DD-MM-YYYY hh:mm A')}`} {/* REAL OSRM ROUTE */} diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js index db54fc7..a3ec9de 100644 --- a/src/pages/nearle/reports/ordersDetails.js +++ b/src/pages/nearle/reports/ordersDetails.js @@ -80,6 +80,7 @@ import dayjs from 'dayjs'; import { OpenToast } from 'components/third-party/OpenToast'; import TableLoader from 'components/nearle_components/TableLoader'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); @@ -448,7 +449,10 @@ export default function OrdersDetails() { } = useQuery({ queryKey: ['tenantlist', appId], queryFn: () => getTenants(appId), - enabled: appId !== 0 + // appId 0 is a real, deliberate choice — the synthetic "All" zone option + // (fetchAppLocations in api.js) — not "nothing picked yet"; only + // null/undefined means that now. See LocationAutocomplete.js's onChange. + enabled: appId != null }); const { @@ -459,7 +463,7 @@ export default function OrdersDetails() { } = useQuery({ queryKey: ['fetchRidersList', appId], queryFn: fetchRidersList, - enabled: appId !== 0 + enabled: appId != null }); const { @@ -627,7 +631,7 @@ export default function OrdersDetails() { getOptionLabel={(option) => option?.tenantname || ''} PaperComponent={SoftPaper} onOpen={(event) => { - if (!appId) { + if (appId == null) { event.preventDefault(); OpenToast('Please select a your app location first!', 'warning', 3000); setTimeout(() => { @@ -677,7 +681,7 @@ export default function OrdersDetails() { value={locationValue} PaperComponent={SoftPaper} onOpen={(event) => { - if (!appId && !tenantid) { + if (appId == null && !tenantid) { event.preventDefault(); OpenToast('Please select a your Location and Tenant first!', 'warning', 3000); setTimeout(() => { @@ -731,7 +735,7 @@ export default function OrdersDetails() { }} PaperComponent={SoftPaper} onOpen={() => { - if (!appId) { + if (appId == null) { OpenToast('Select App Location First', 'warning', 2000); } }} @@ -1105,7 +1109,7 @@ export default function OrdersDetails() { )} - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')} + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} @@ -1376,7 +1380,7 @@ export default function OrdersDetails() { - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')} + {parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')} diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js index 6a72123..58cb71b 100644 --- a/src/pages/nearle/reports/profitability.js +++ b/src/pages/nearle/reports/profitability.js @@ -6,6 +6,7 @@ import { Box, Paper, TextField, Typography } from '@mui/material'; import { MdCalendarMonth } from 'react-icons/md'; import dayjs from 'dayjs'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { fetchDeliveries } from 'pages/api/api'; import Loader from 'components/Loader'; @@ -111,10 +112,12 @@ const Profitability = () => { rider.orders.forEach((o) => { const status = String(o.orderstatus || '').toLowerCase(); if (SKIPPED_STATUSES.has(status)) return; + // parseDoormileTimestamp (not bare dayjs()) avoids the false-trailing-Z + // parse bug (utils/doormileTimestamp.js) — see ProfitabilitySection.js. const dateStr = o.assigntime - ? dayjs(o.assigntime).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.assigntime).format('YYYY-MM-DD') : o.deliverydate - ? dayjs(o.deliverydate).format('YYYY-MM-DD') + ? parseDoormileTimestamp(o.deliverydate).format('YYYY-MM-DD') : null; if (dateStr !== selectedDate) return; const km = parseFloat(o.riderkms || 0);