79 lines
3.4 KiB
JavaScript
79 lines
3.4 KiB
JavaScript
// ==============================|| Routing decision, in words ||============================== //
|
|
//
|
|
// `GET /admin/bookings/:id` returns a `routing` block: the decision the backend
|
|
// took about where a parcel goes, and the inputs it took it from. This turns
|
|
// that into the sentences the order drawer shows.
|
|
//
|
|
// It exists so support can answer "why does the rider's screen say hand over at
|
|
// a base instead of deliver to the customer?" as a lookup instead of
|
|
// reconstructing a pincode rule by hand on the phone.
|
|
//
|
|
// TWO RULES THIS FILE EXISTS TO HOLD:
|
|
//
|
|
// 1. A PROJECTION IS NOT A DECISION. Before pickup, nothing has been decided —
|
|
// `routing.decided` is false and what is shown is what WILL happen. Saying
|
|
// "this went via a base" about a parcel nobody has collected yet is a
|
|
// wrong answer delivered confidently, which is worse than no answer.
|
|
//
|
|
// 2. THE WIRE SAYS HUB, PEOPLE SAY BASE. The translation happens here, at the
|
|
// edge, and never travels back up: every value sent to the backend keeps
|
|
// its wire spelling.
|
|
|
|
/** What the rider does next with the parcel. Keys are the backend's wire
|
|
* values; an unrecognised one is shown as itself rather than swallowed —
|
|
* the backend may add actions, and a blank row is worse than a raw word. */
|
|
export const NEXT_ACTION_LABEL = Object.freeze({
|
|
pickup: 'Collect from the pickup point',
|
|
inward_at_hub: 'Carry to a base and hand over',
|
|
start_delivery: 'Start the delivery run',
|
|
deliver: 'Deliver to the receiver',
|
|
handed_to_hub: 'Handed over at the base',
|
|
none: 'Nothing further for the rider'
|
|
});
|
|
|
|
export const nextActionLabel = (action) => NEXT_ACTION_LABEL[action] || action || '—';
|
|
|
|
/** Why the parcel is going the way it is, in one sentence an operator can read
|
|
* to a rider over the phone. */
|
|
export const routingReason = (routing) => {
|
|
if (!routing) return '';
|
|
return routing.is_hyperlocal
|
|
? 'Same postal area — the collecting rider carries it straight to the receiver.'
|
|
: 'Different postal area — it goes through a base rather than direct to the receiver.';
|
|
};
|
|
|
|
/** The from → to line, with the honesty caveat when nothing has been decided
|
|
* yet. A booking not yet collected has no routing FACT, only a forecast. */
|
|
export const routingRoute = (routing) => {
|
|
if (!routing) return '';
|
|
const from = routing.from_pincode || '—';
|
|
const to = routing.destination_pincode || '—';
|
|
const line = `${from} → ${to}`;
|
|
return routing.decided ? line : `${line} · not yet collected, so this is what will happen, not what has`;
|
|
};
|
|
|
|
/** The base a parcel is routed to, as one readable line. Null when no base is
|
|
* involved — which is the correct answer for a hyperlocal parcel, not a gap. */
|
|
export const baseLine = (base) => {
|
|
if (!base || !base.name) return null;
|
|
return base.pincode ? `${base.name} · ${base.pincode}` : base.name;
|
|
};
|
|
|
|
/**
|
|
* Everything the drawer renders, in one call, so the component holds no
|
|
* decisions of its own.
|
|
*/
|
|
export const summariseRouting = (routing) => {
|
|
if (!routing) return null;
|
|
return {
|
|
decided: Boolean(routing.decided),
|
|
reason: routingReason(routing),
|
|
route: routingRoute(routing),
|
|
nextAction: nextActionLabel(routing.next_action),
|
|
base: baseLine(routing.next_hub),
|
|
baseAddress: routing.next_hub?.address || null,
|
|
consignmentState: routing.consignment_state || '—',
|
|
inwardedAt: routing.inwardedat || null
|
|
};
|
|
};
|