updates on bugs on the dispatch and ai button integrations

This commit is contained in:
2026-09-24 11:50:04 +05:30
parent 5eb4a6466b
commit cbce47f949
18 changed files with 1190 additions and 124 deletions

View File

@@ -1,6 +1,9 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useHubs, useTenantLocations } from '@/lib/doormileHooks';
import { useAuth } from '@/lib/AuthContext';
// Shared with Control X's city filter. Two radii would mean the Orders page and
// the dispatch board disagreeing about which city an order is in.
import { haversineKm, ZONE_RADIUS_KM } from '@/lib/locationScope';
const ZONE_STORAGE_KEY = 'doormile_active_zone_id';
@@ -13,17 +16,6 @@ const ALL_ZONE = Object.freeze({
status: 'Active',
});
const haversineKm = (lat1, lon1, lat2, lon2) => {
if (![lat1, lon1, lat2, lon2].every((v) => Number.isFinite(Number(v)))) return undefined;
const R = 6371;
const toRad = (deg) => (Number(deg) * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
const ZoneContext = createContext(null);
export function ZoneProvider({ children }) {
@@ -147,21 +139,40 @@ export function ZoneProvider({ children }) {
// 1. Direct hub/location id match
//
// `servicinghubid` is queries.js's answer to "which hub OWNS this order",
// which is a different question from "where is it collected from" and the
// only one that stays answerable once the pickup is a customer's doorstep.
// It is consulted first, and only for staff sessions: a tenant's zones are
// its own locations, so a hub id compared against a tenantlocationid would
// match on a bare numeric collision and file the order under a zone that
// has nothing to do with it.
const itemHubId =
(!isTenantUser ? item.servicinghubid : null) ??
item.hubid ??
item.sourcehubid ??
item.applocationid ??
item.tenantlocationid ??
item.locationid;
if (itemHubId != null && String(itemHubId) === targetHubId) {
// The hub ids a booking can actually carry. `pickuphubid` and
// `nearesthubid` are the only hub foreign keys that exist on the
// pickupbookings row; `servicinghubid` is synthesised by queries.js and so
// reaches this function on Deliveries but never on Orders; `hubid`,
// `sourcehubid`, `applocationid` and `locationid` are not columns on a
// booking at all and are kept only for the other row shapes that pass
// through here.
//
// Until `pickuphubid` was added, every one of those was undefined on the
// Orders page, so this whole branch fell through and the 35km radius below
// was the only matcher left running — which means an order that reached the
// database without coordinates disappeared from every zone at once, with
// nowhere to see it and nothing to say it had been hidden.
//
// Each branch compares ONE id space against itself, and this is the whole
// reason the list is split.
//
// A staff zone is a hub, so only hub ids may be tested against it.
// `applocationid` is a city, `tenantlocationid` and `locationid` are a
// client's own sites — different numbering entirely, and a delivery row
// carries several of them at once (queries.js sets servicinghubid,
// applocationid and tenantlocationid on the same row). Booking DM-626241 is
// the live example: servicinghubid 5 (Bangalore Earth Hub) alongside
// tenantlocationid 3, which as a hub id is Hyderabad Mars Hub. Testing them
// all would file a Bangalore run under Hyderabad on a bare numeric
// collision.
//
// The old `??` chain hid this by stopping at the first non-null field, so
// the collision only fired when the earlier ones happened to be absent. It
// was a bug waiting on its inputs, not a safe design.
const itemHubIds = isTenantUser
? [item.tenantlocationid, item.locationid]
: [item.servicinghubid, item.pickuphubid, item.nearesthubid, item.hubid, item.sourcehubid];
if (itemHubIds.some((id) => id != null && String(id) === targetHubId)) {
return true;
}
@@ -184,7 +195,7 @@ export function ZoneProvider({ children }) {
return true;
}
// 3. Coordinate distance match (within 35km radius of hub)
// 3. Coordinate distance match, within ZONE_RADIUS_KM of the hub
const hubLat = Number(currentHub.latitude);
const hubLon = Number(currentHub.longitude);
if (Number.isFinite(hubLat) && Number.isFinite(hubLon)) {
@@ -196,8 +207,8 @@ export function ZoneProvider({ children }) {
const pickDist = haversineKm(hubLat, hubLon, pickLat, pickLon);
const dropDist = haversineKm(hubLat, hubLon, dropLat, dropLon);
if (pickDist !== undefined && pickDist <= 35) return true;
if (dropDist !== undefined && dropDist <= 35) return true;
if (pickDist !== undefined && pickDist <= ZONE_RADIUS_KM) return true;
if (dropDist !== undefined && dropDist <= ZONE_RADIUS_KM) return true;
}
return false;

View File

@@ -20,3 +20,32 @@ export const toDateRange = (value) => {
if (typeof value !== 'object') return { from: '', to: '' };
return { from: value.from || '', to: value.to || '' };
};
/**
* Keep the rows whose `createdat` falls inside the DatePicker's current value.
*
* Lives here rather than inline on each page because Orders and Bookings read
* the same `/admin/bookings` rows through the same picker, and a page that
* wrote its own comparison would eventually disagree with the other about what
* "today" contains — the kind of difference nobody notices until two screens
* show different counts for the same day.
*
* A row whose timestamp will not parse is dropped from a date-scoped view. That
* is the same call `fetchBookingsInRange` makes: a date filter cannot honestly
* claim a row it could not place in time. Nothing is dropped when the value is
* 'all', which is the case that has to keep showing everything.
*/
export const filterByCreatedAt = (rows, value, parse) => {
if (!value || value === 'all') return rows || [];
const { from, to } = toDateRange(value);
if (!from && !to) return rows || [];
return (rows || []).filter((row) => {
const at = parse(row?.createdat);
if (!at?.isValid?.()) return false;
const day = at.format('YYYY-MM-DD');
if (from && day < from) return false;
if (to && day > to) return false;
return true;
});
};

106
src/lib/locationScope.js Normal file
View File

@@ -0,0 +1,106 @@
// ==============================|| Which city a row belongs to ||============================== //
//
// Control X carries a hub/city picker in its header, keyed on `applocationid`.
// It rebuilt the query key and refetched, and then filtered nothing: the id was
// destructured out of the key in `fetchDeliveries` and never read, exactly as
// startdate/enddate once were. The chip said "Coimbatore" while the map drew a
// Bangalore rider on a Bangalore road, which is worse than having no picker —
// an operator reads a labelled screen as scoped.
//
// `/admin/bookings` takes only pageno/pagesize, so this is a client-side pass,
// the same shape the date range uses.
/** How far from a hub a stop can be and still belong to that hub's city. */
export const ZONE_RADIUS_KM = 35;
export const haversineKm = (lat1, lon1, lat2, lon2) => {
if (![lat1, lon1, lat2, lon2].every((v) => Number.isFinite(Number(v)))) return undefined;
const R = 6371;
const toRad = (deg) => (Number(deg) * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
/** A coordinate pair is only usable if it is real. 0,0 is the Gulf of Guinea,
* and it is what this system writes when nothing geocoded — both for a hub
* (`Coimbatore Test Hub`) and for an order saved without a pin. Treating it as
* a place puts orders in the sea; treating it as absent is the truth. */
const usable = (lat, lon) =>
Number.isFinite(Number(lat)) &&
Number.isFinite(Number(lon)) &&
!(Number(lat) === 0 && Number(lon) === 0);
const hasCoords = (h) => usable(h?.latitude, h?.longitude);
/** Every hub id a row might name. */
const NAMED_HUB_KEYS = ['servicinghubid', 'pickuphubid', 'nearesthubid', 'hubid', 'sourcehubid'];
/**
* A predicate for "this row belongs to the selected city", or `null` for
* "everything" — which is what the caller wants when no city is selected.
*
* Returns null rather than a match-all function so the caller can skip the pass
* entirely, and so "All locations" is one branch rather than a filter that
* happens to be true.
*
* An unrecognised `applocationId`, or one with no hubs, also returns null. A
* picker pointing at a city we cannot resolve must not empty the board: a blank
* screen reads as "no work today", and that is the more expensive mistake.
*/
export const makeLocationFilter = (hubs, applocationId) => {
const id = Number(applocationId);
if (!Number.isFinite(id) || id === 0) return null;
const all = hubs || [];
const here = all.filter((h) => Number(h?.applocationid) === id);
if (!here.length) return null;
const hubIds = new Set(here.map((h) => String(h.hubid)));
// Every known hub, so a row naming one somewhere ELSE counts as located —
// and is therefore excluded here rather than treated as unplaceable.
const knownHubIds = new Set(all.map((h) => String(h?.hubid)));
const centres = here.filter(hasCoords);
return (row) => {
if (!row) return false;
const named = NAMED_HUB_KEYS.map((k) => row[k]).filter((v) => v != null).map(String);
// 1. The hub the row itself names, when that hub is one of ours.
if (named.some((v) => hubIds.has(v))) return true;
// 2. Either end of the journey inside the city's footprint. A run that
// collects here and delivers elsewhere is still this city's work, and so
// is one that merely arrives — so either end qualifying is enough.
const ends = [
[row.pickuplatitude ?? row.latitude, row.pickuplongitude ?? row.longitude],
[row.deliverylatitude ?? row.droplat, row.deliverylongitude ?? row.droplon],
];
let located = false;
for (const [lat, lon] of ends) {
if (!usable(lat, lon)) continue;
for (const hub of centres) {
const d = haversineKm(hub.latitude, hub.longitude, lat, lon);
if (d === undefined) continue;
located = true;
if (d <= ZONE_RADIUS_KM) return true;
}
}
// 3. It names a hub we know, and that hub is not in this city.
if (named.some((v) => knownHubIds.has(v))) return false;
// 4. Its coordinates put it somewhere, and somewhere else. Drop it — that
// is the whole point of the picker.
if (located) return false;
// 5. Not locatable at all: no hub we recognise, no usable coordinates. Keep
// it. A row that cannot be placed has to stay visible under every city
// rather than disappear from all of them with nothing to say it was
// hidden — the exact failure this area keeps producing.
return true;
};
};

View File

@@ -435,6 +435,12 @@ export const classifyBooking = (booking, anchors = []) => {
const servicingHubId =
b.sourcehubid ??
b.hubid ??
// The column the backend actually writes when the collection point is a
// base (`createExpressBooking` sets it from the request, and the hub
// console reads it back). It outranks the anchor match below because it is
// what the order RECORDS, where the anchor is only what its coordinates
// resolve to — and those two can disagree once a hub is moved or retired.
b.pickuphubid ??
(originAnchor?.kind === 'hub' ? originAnchor.hubid : null) ??
originAnchor?.hubid ??
(destAnchor?.kind === 'hub' ? destAnchor.hubid : null) ??