updates on the fix
This commit is contained in:
@@ -38,15 +38,33 @@ import {
|
||||
// lifecycle keys (pending/accepted/arrived/picked/active/skipped/delivered/
|
||||
// cancelled) for its tabs, chip counts, and row badges. The new booking
|
||||
// status enum uses different strings entirely (Pending_Pickup,
|
||||
// Converted_To_Consignment, Out_for_Delivery, ...) — only Pending_Pickup,
|
||||
// Converted_To_Consignment (from a real sample) and Out_for_Delivery (named
|
||||
// in express-console-api.md's CityGate note) are confirmed; the rest of this
|
||||
// mapping is a best-effort guess. Unmapped statuses pass through lowercased,
|
||||
// which the page's own fallback renders as an "unknown" badge rather than
|
||||
// crashing. There's no confirmed equivalent for arrived/picked/skipped at
|
||||
// all, so those tabs will show a 0 count until the real enum is confirmed.
|
||||
// Miler_Assigned, Pickup_Scheduled, Converted_To_Consignment, Out_for_Delivery,
|
||||
// ...) — Pending_Pickup, Converted_To_Consignment, Out_for_Delivery (named in
|
||||
// express-console-api.md's CityGate note), and now Miler_Assigned /
|
||||
// Pickup_Scheduled (confirmed live via a real booking-status breakdown logged
|
||||
// right after an assign-miler call — see orders.js's ORDERS_STATUS_TABS
|
||||
// comment) are confirmed; the rest of this mapping is still a best-effort
|
||||
// guess. Unmapped statuses pass through lowercased, which the page's own
|
||||
// fallback renders as an "unknown" badge rather than crashing. There's no
|
||||
// confirmed equivalent for arrived/picked/skipped at all, so those tabs will
|
||||
// show a 0 count until the real enum is confirmed.
|
||||
//
|
||||
// Miler_Assigned is deliberately kept on 'pending', NOT bumped to 'accepted'.
|
||||
// Assigning a rider is an OPERATOR action (POST /admin/bookings/:id/assign-miler
|
||||
// from this console); "Accepted" is meant to reflect the RIDER's own action
|
||||
// (doormile-flow.md's POST /miler/assignments/:id/accept). Conflating the two
|
||||
// made a just-assigned, not-yet-acknowledged order look already-accepted —
|
||||
// wrong from an ops standpoint (per explicit product requirement: stays
|
||||
// "Pending" in the operator's eyes until the rider actually accepts).
|
||||
// Pickup_Scheduled — the status that appears once a rider has accepted and
|
||||
// the pickup is on their route — is the best available proxy for "rider
|
||||
// accepted" in the currently-confirmed enum, so that one maps to 'accepted'.
|
||||
// If the backend turns out to have a distinct status specifically for the
|
||||
// accept action, add it here rather than reusing Miler_Assigned for it.
|
||||
const BOOKING_STATUS_TO_DELIVERY_STATUS = {
|
||||
pending_pickup: 'pending',
|
||||
miler_assigned: 'pending',
|
||||
pickup_scheduled: 'accepted',
|
||||
converted_to_consignment: 'accepted',
|
||||
out_for_delivery: 'active',
|
||||
delivered: 'delivered',
|
||||
@@ -101,6 +119,19 @@ export const getRiderPeriodicLogs = async (userid) => {
|
||||
// ==============================|| fetchAppLocations (zone/location picker) ||============================== //
|
||||
// The new API has no "zones" resource — applocationid only exists as a field
|
||||
// on Hubs. Derive a zone picker list from the distinct cities in GET /admin/hubs.
|
||||
//
|
||||
// Scoped per tenant: Hub has no tenantid field at all (confirmed against
|
||||
// express-console-api.md), so GET /admin/hubs always returns every hub
|
||||
// nationwide with no server-side tenant filtering possible. A client-tenant
|
||||
// login should only see their own city's hub(s) — inferred from the
|
||||
// tenant's own GET /admin/tenants/:id/locations (city is free text there,
|
||||
// and on the hub side too, so matched via normalized exact-then-substring
|
||||
// comparison, not a clean foreign key). Staff logins (tenantid falsy) keep
|
||||
// seeing every hub, unchanged. Fails open (full list) whenever the tenant's
|
||||
// city can't be determined or nothing matches, rather than ever locking an
|
||||
// operator out with an empty picker.
|
||||
const normCity = (s) => String(s || '').trim().toLowerCase();
|
||||
|
||||
export const fetchAppLocations = async () => {
|
||||
try {
|
||||
const hubs = await getHubs();
|
||||
@@ -110,7 +141,36 @@ export const fetchAppLocations = async () => {
|
||||
seen.set(hub.applocationid, { applocationid: hub.applocationid, locationname: hub.city || hub.hubname });
|
||||
}
|
||||
});
|
||||
return [...seen.values(), { locationname: 'All', applocationid: 0 }];
|
||||
const allLocations = [...seen.values()];
|
||||
|
||||
const tenantid = localStorage.getItem('tenantid');
|
||||
const isStaff = !tenantid || tenantid === '0';
|
||||
if (isStaff) {
|
||||
return [...allLocations, { locationname: 'All', applocationid: 0 }];
|
||||
}
|
||||
|
||||
const tenantLocations = await gettenantlocations(tenantid);
|
||||
const tenantCities = new Set((tenantLocations || []).map((loc) => normCity(loc.city)).filter(Boolean));
|
||||
|
||||
if (tenantCities.size === 0) {
|
||||
OpenToast("Could not determine your tenant's city — showing all zones.", 'warning', 3000);
|
||||
return [...allLocations, { locationname: 'All', applocationid: 0 }];
|
||||
}
|
||||
|
||||
let matched = allLocations.filter((loc) => tenantCities.has(normCity(loc.locationname)));
|
||||
if (matched.length === 0) {
|
||||
matched = allLocations.filter((loc) => {
|
||||
const hubNorm = normCity(loc.locationname);
|
||||
return [...tenantCities].some((cityNorm) => hubNorm.includes(cityNorm) || cityNorm.includes(hubNorm));
|
||||
});
|
||||
}
|
||||
|
||||
if (matched.length === 0) {
|
||||
OpenToast("No zones matched your tenant's city — showing all zones.", 'warning', 3000);
|
||||
return [...allLocations, { locationname: 'All', applocationid: 0 }];
|
||||
}
|
||||
|
||||
return matched;
|
||||
} catch (err) {
|
||||
OpenToast(err.message, 'error', 2000);
|
||||
return [{ locationname: 'All', applocationid: 0 }];
|
||||
@@ -175,10 +235,18 @@ export const fetchPaymentType = async () => [];
|
||||
export const fetchRidersList = async () => {
|
||||
try {
|
||||
const milers = await getMilers();
|
||||
return (milers || []).map((val) => ({
|
||||
...val,
|
||||
label: `${val.displayname || val.authname || ''} | ${val.contactno || ''}`
|
||||
}));
|
||||
return (milers || []).map((val) => {
|
||||
const name = val.displayname || val.authname || '';
|
||||
return {
|
||||
...val,
|
||||
// Only append " | phone" when a phone actually exists — an
|
||||
// unconditional template literal left a dangling " | " on every
|
||||
// rider missing contactno, rendering literally in every dropdown
|
||||
// that falls back to this default label (e.g. OrdersPreview.js,
|
||||
// Preview.js's Change Rider dialog).
|
||||
label: val.contactno ? `${name} | ${val.contactno}` : name
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
// Was `throw`ing after already toasting here — deliveries.js also wires
|
||||
// its own onError toast on this same query, so a real failure showed
|
||||
@@ -201,7 +269,16 @@ export const createOptimisationDeliveries = async (deliveryData) => {
|
||||
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
|
||||
|
||||
export const reconcileSteps = async ({ riders }) => {
|
||||
logger.debug(`reconcileSteps: posting ${riders?.length ?? 0} rider(s)`, riders);
|
||||
const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/reconcile-steps`, { riders });
|
||||
// Diagnostic: this is an external, unverified solver contract (see this
|
||||
// area's CLAUDE.md) — Preview.js's reconcileMutation only clears the
|
||||
// dirty-rider set (which is what re-enables Assign Orders) when
|
||||
// response.data.riders is an array. If the solver's real response shape
|
||||
// is different (wrapped in an envelope, a different key name, etc.),
|
||||
// that check silently fails every time and Assign Orders can never
|
||||
// re-enable — logging the exact raw shape here settles it either way.
|
||||
logger.debug('reconcileSteps: raw response.data', response.data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -232,6 +309,49 @@ export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
|
||||
// (that one creates a brand-new booking from scratch; wrong here, since
|
||||
// every order already exists as a Doormile booking pulled from
|
||||
// GET /admin/bookings).
|
||||
// Shared by finalCreatedeliveries (here) and Preview.js's pre-commit
|
||||
// verification UI, so both use the IDENTICAL matching rule rather than two
|
||||
// copies that could silently drift apart. userid/milerprofileid matching is
|
||||
// tried first (the solver's rider pool is fed full miler objects for
|
||||
// Auto/multi-trip mode — see Preview.js's handleCreateDelivery -> `riders:
|
||||
// autoRiders`, raw GET /admin/milers data carrying both fields per rider)
|
||||
// but confirmed live that for Bike hypertuning mode NEITHER matches: that
|
||||
// solver never receives a rider pool at all and assigns from its own
|
||||
// internal roster, seeded against jupiter rider ids when the integration
|
||||
// was first built — disconnected from Doormile's id space entirely (see
|
||||
// jupiter2doormile.md comparison). Falls back to matching the rider's NAME
|
||||
// (also echoed by the solver, see flattenRiders' rider_name) against each
|
||||
// miler's displayname/authname — the only other correlatable field.
|
||||
const normMilerName = (s) => String(s || '').trim().toLowerCase();
|
||||
|
||||
export const buildMilerLookup = (milers) => {
|
||||
const byUserId = new Map((milers || []).map((m) => [String(m.userid), m]));
|
||||
const byProfileId = new Map((milers || []).map((m) => [String(m.milerprofileid), m]));
|
||||
const byName = new Map();
|
||||
(milers || []).forEach((m) => {
|
||||
[m.displayname, m.authname].forEach((n) => {
|
||||
const key = normMilerName(n);
|
||||
if (key && !byName.has(key)) byName.set(key, m);
|
||||
});
|
||||
});
|
||||
return { byUserId, byProfileId, byName };
|
||||
};
|
||||
|
||||
export const resolveMilerForOrder = (order, lookup) => {
|
||||
const riderUserId = order.rider_id ?? order.userid;
|
||||
const riderName = order.rider_name ?? order.rider;
|
||||
const matchedVia = lookup.byUserId.has(String(riderUserId))
|
||||
? 'userid'
|
||||
: lookup.byProfileId.has(String(riderUserId))
|
||||
? 'milerprofileid'
|
||||
: lookup.byName.has(normMilerName(riderName))
|
||||
? 'name'
|
||||
: null;
|
||||
const rider =
|
||||
lookup.byUserId.get(String(riderUserId)) ?? lookup.byProfileId.get(String(riderUserId)) ?? lookup.byName.get(normMilerName(riderName));
|
||||
return rider?.milerprofileid ? { rider, matchedVia } : null;
|
||||
};
|
||||
|
||||
export const finalCreatedeliveries = async (deliveryData) => {
|
||||
const deliveries = deliveryData.deliveries || [];
|
||||
logger.debug(`finalCreatedeliveries: ${deliveries.length} order(s) to assign`);
|
||||
@@ -253,19 +373,6 @@ export const finalCreatedeliveries = async (deliveryData) => {
|
||||
});
|
||||
});
|
||||
|
||||
// assign-miler needs a milerprofileid. userid/milerprofileid matching was
|
||||
// tried first (the solver's rider pool is fed full miler objects — see
|
||||
// Preview.js's handleCreateDelivery -> `riders: autoRiders`, raw
|
||||
// GET /admin/milers data carrying both fields per rider) but confirmed
|
||||
// live that NEITHER matches: the solver's rider_id/userid (e.g. "883")
|
||||
// isn't in GET /admin/milers under either field. This matches
|
||||
// Dispatch.js's own Analysis-panel comment — "the workolik solver doesn't
|
||||
// have our auth/users table" — so its numeric rider ids are its own
|
||||
// internal numbering, disconnected from Doormile's id space entirely.
|
||||
// Falls back to matching the rider's NAME (also echoed by the solver,
|
||||
// see flattenRiders' rider_name) against each miler's displayname/
|
||||
// authname — the only other correlatable field. Logs the full roster
|
||||
// alongside so a further mismatch is fully diagnosable from one run.
|
||||
let milers = [];
|
||||
try {
|
||||
milers = (await getMilers()) || [];
|
||||
@@ -276,47 +383,62 @@ export const finalCreatedeliveries = async (deliveryData) => {
|
||||
`finalCreatedeliveries: ${milers.length} miler(s) available for rider resolution`,
|
||||
milers.map((m) => ({ userid: m.userid, milerprofileid: m.milerprofileid, name: m.displayname || m.authname }))
|
||||
);
|
||||
const milerByUserId = new Map(milers.map((m) => [String(m.userid), m]));
|
||||
const milerByProfileId = new Map(milers.map((m) => [String(m.milerprofileid), m]));
|
||||
const normName = (s) => String(s || '').trim().toLowerCase();
|
||||
const milerByName = new Map();
|
||||
milers.forEach((m) => {
|
||||
[m.displayname, m.authname].forEach((n) => {
|
||||
const key = normName(n);
|
||||
if (key && !milerByName.has(key)) milerByName.set(key, m);
|
||||
});
|
||||
});
|
||||
const lookup = buildMilerLookup(milers);
|
||||
|
||||
// Booking id resolution: confirmed live that guessing a single field name
|
||||
// (bookingid first, on the assumption the solver passes unknown fields
|
||||
// through untouched) sends assign-miler a wrong id — a small, sequential-
|
||||
// looking number that 404s. The AI solver is a separate, unverified
|
||||
// service (see this area's CLAUDE.md); there's no reliable way to know
|
||||
// which candidate field it actually preserves. Instead of guessing,
|
||||
// fetch the tenant's real current booking list once and VALIDATE each
|
||||
// candidate against it, using whichever one actually matches a real
|
||||
// booking — this is correct regardless of which field the solver happens
|
||||
// to preserve, and regardless of which one changes in a future solver
|
||||
// update.
|
||||
let realBookingIds = new Set();
|
||||
try {
|
||||
const realBookings = (await getBookings(1, 1000)) || [];
|
||||
realBookingIds = new Set(realBookings.map((b) => String(b.bookingid)));
|
||||
logger.debug(`finalCreatedeliveries: ${realBookingIds.size} real booking id(s) fetched for validation`);
|
||||
} catch (err) {
|
||||
logger.error('finalCreatedeliveries: GET /admin/bookings failed — cannot validate booking ids', err.response?.status, err.response?.data || err.message);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
deliveries.map(async (d) => {
|
||||
// Booking id: the solver is a separate, unverified service (see this
|
||||
// area's CLAUDE.md) — falls back through every plausible field name
|
||||
// an order might carry it under, `bookingid` first since that's what
|
||||
// orders.js actually sends in and most solvers pass unknown fields
|
||||
// through untouched.
|
||||
const bookingId = d.bookingid ?? d.orderheaderid ?? d.deliveryid ?? d.orderid;
|
||||
const candidates = [d.bookingid, d.orderheaderid, d.deliveryid, d.orderid];
|
||||
const bookingId = candidates.find((c) => c != null && realBookingIds.has(String(c)));
|
||||
const riderUserId = d.rider_id ?? d.userid;
|
||||
const riderName = d.rider_name ?? d.rider;
|
||||
const matchedVia = milerByUserId.has(String(riderUserId))
|
||||
? 'userid'
|
||||
: milerByProfileId.has(String(riderUserId))
|
||||
? 'milerprofileid'
|
||||
: milerByName.has(normName(riderName))
|
||||
? 'name'
|
||||
: null;
|
||||
const rider =
|
||||
milerByUserId.get(String(riderUserId)) ?? milerByProfileId.get(String(riderUserId)) ?? milerByName.get(normName(riderName));
|
||||
if (bookingId == null || !rider?.milerprofileid) {
|
||||
const resolved = resolveMilerForOrder(d, lookup);
|
||||
if (bookingId == null || !resolved) {
|
||||
const reason =
|
||||
bookingId == null
|
||||
? 'no booking id resolved on the order object'
|
||||
? `no candidate id matched a real booking (tried bookingid=${d.bookingid}, orderheaderid=${d.orderheaderid}, deliveryid=${d.deliveryid}, orderid=${d.orderid})`
|
||||
: `no miler found for rider id ${riderUserId} / name "${riderName}" (checked userid, milerprofileid, name)`;
|
||||
logger.error(`finalCreatedeliveries: skipping order — ${reason}`);
|
||||
throw new Error(`order ${d.orderid ?? bookingId ?? '?'}: ${reason}`);
|
||||
}
|
||||
const { rider, matchedVia } = resolved;
|
||||
logger.debug(`finalCreatedeliveries: order booking ${bookingId} -> rider ${riderUserId} ("${riderName}") matched via ${matchedVia}`);
|
||||
try {
|
||||
return await assignMilerToBooking(bookingId, { milerid: rider.milerprofileid });
|
||||
// doormile-flow.md (confirmed current, authoritative): assign-miler's
|
||||
// body key is `mileruserid`, and it's the miler's userid — a
|
||||
// DIFFERENT identity space than milerprofileid, which is what admin
|
||||
// miler endpoints (notify, block, etc.) key on instead. Sending
|
||||
// { milerid: rider.milerprofileid } was wrong on both the key name
|
||||
// and the value's identity space — the actual root cause of the
|
||||
// persistent 404s on this call.
|
||||
await assignMilerToBooking(bookingId, { mileruserid: rider.userid });
|
||||
// Return the REAL resolved milerprofileid, not the solver's own
|
||||
// rider_id/userid — the caller (Preview.js) needs this for
|
||||
// notifyRider, which takes a milerprofileid specifically. Before
|
||||
// this, Preview.js notified using the raw solver id directly, which
|
||||
// is neither a real userid nor a milerprofileid (see the matching
|
||||
// comment above) — so rider push notifications were going out with
|
||||
// a bogus id and very likely silently failing server-side.
|
||||
return { milerprofileid: rider.milerprofileid };
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`finalCreatedeliveries: assign-miler failed for booking ${bookingId}`,
|
||||
@@ -337,7 +459,12 @@ export const finalCreatedeliveries = async (deliveryData) => {
|
||||
if (failed.length) {
|
||||
OpenToast(`${failed.length} of ${deliveries.length} order(s) couldn't be assigned — check Orders/Deliveries`, 'warning', 4000);
|
||||
}
|
||||
return { success: true, assigned: deliveries.length - failed.length, failed: failed.length };
|
||||
const resolvedMilerProfileIds = [
|
||||
...new Set(
|
||||
results.filter((r) => r.status === 'fulfilled').map((r) => r.value.milerprofileid)
|
||||
)
|
||||
];
|
||||
return { success: true, assigned: deliveries.length - failed.length, failed: failed.length, resolvedMilerProfileIds };
|
||||
};
|
||||
// ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== //
|
||||
// Also part of the optimiser pipeline (routes.workolik.com / routemate.workolik.com) — untouched.
|
||||
@@ -486,7 +613,16 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
|
||||
|
||||
return {
|
||||
rows,
|
||||
nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
// Whether to fetch another page must be based on the RAW bookings page
|
||||
// (bookings.length), not the post-filter `rows.length` — most bookings on
|
||||
// any given page are still pending, not dispatched, so the filtered count
|
||||
// almost never equals rowsPerPage. Comparing the filtered count against
|
||||
// rowsPerPage (the previous logic) made pagination stop after page 1 in
|
||||
// virtually every real dataset, silently hiding dispatched/assigned
|
||||
// orders that live beyond the first `rowsPerPage` bookings — e.g. a
|
||||
// booking just created and assigned wouldn't show on the Deliveries page
|
||||
// at all once the tenant has more than one page's worth of bookings.
|
||||
nextPage: (bookings || []).length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
|
||||
@@ -541,19 +677,27 @@ export const cancelDeliveryAPI = async (selectedRow, cancelFeed) =>
|
||||
export const getorderdetails = async (orderHeaderid) => getBooking(orderHeaderid);
|
||||
|
||||
// ==============================|| changeRiderAPI (deliveries) ||============================== //
|
||||
// POST /admin/bookings/:id/assign-miler has no documented body — `milerid` as
|
||||
// the JSON key is a guess (unverified, this is a write endpoint we didn't
|
||||
// test live), but the VALUE must be the miler's milerprofileid regardless of
|
||||
// key name: selectedRider comes straight from getMilers(), whose own .userid
|
||||
// field is a different resource (confirmed live via GET /admin/milers/:id).
|
||||
// doormile-flow.md (confirmed current, authoritative) settles this: the body
|
||||
// is { "mileruserid": <miler's userid> } — the previous guess here (`milerid`
|
||||
// key, `milerprofileid` value) was wrong on both counts. Admin miler
|
||||
// endpoints (notify, block, etc.) key on milerprofileid; assign-miler is the
|
||||
// one exception that wants userid instead — "different identity spaces on
|
||||
// adjacent endpoints," per that doc's own wording. selectedRider comes
|
||||
// straight from getMilers(), which carries both fields on the same object.
|
||||
export const changeRiderAPI = async (selectedRider, selectedRow) =>
|
||||
assignMilerToBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { milerid: selectedRider.milerprofileid });
|
||||
assignMilerToBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { mileruserid: selectedRider.userid });
|
||||
|
||||
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
|
||||
// No amount/notes field exists on PUT /admin/consignments/:id/status — closest
|
||||
// available write is a status update. Free-text amount/notes edits have no home
|
||||
// in the new API yet.
|
||||
export const updateDeliveryAPI = async (orderData) => updateConsignmentStatus(orderData.deliveryid ?? orderData.consignmentid, orderData);
|
||||
// Target endpoint is consignment-scoped (/admin/consignments/:id/status), so
|
||||
// this needs the real consignmentid, not a booking id. deliveryid on a
|
||||
// deliveries-page row is always b.bookingid (see fetchDeliveries) — always
|
||||
// truthy, so `deliveryid ?? consignmentid` never actually fell through to
|
||||
// consignmentid even when it was present, silently calling the endpoint
|
||||
// with the wrong kind of id on every Update Status submit.
|
||||
export const updateDeliveryAPI = async (orderData) => updateConsignmentStatus(orderData.consignmentid ?? orderData.deliveryid, orderData);
|
||||
|
||||
// ==============================|| getalltenants (tenants) ||============================== //
|
||||
|
||||
|
||||
Reference in New Issue
Block a user