updates on bugs on the dispatch and ai button integrations
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
106
src/lib/locationScope.js
Normal 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;
|
||||
};
|
||||
};
|
||||
@@ -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) ??
|
||||
|
||||
Reference in New Issue
Block a user