updates on the ui changes and changed into doormile
This commit is contained in:
270
src/lib/dispatchPreview.js
Normal file
270
src/lib/dispatchPreview.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* The dispatch preview's data model.
|
||||
*
|
||||
* The optimiser answers with a zoned tree — `zones[].riders[].orders[]` — plus
|
||||
* a flat `details[]` list, and every edit on the preview page has to keep both
|
||||
* in step: the board renders from the tree, and the commit payload is built
|
||||
* from the flat list. Doing that in one place is what stops an order being
|
||||
* shown against one rider and assigned to another.
|
||||
*
|
||||
* Ported from the Express Console's `dispatch/Preview.js`, where these were
|
||||
* module-local; the logic is unchanged.
|
||||
*/
|
||||
|
||||
// Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for
|
||||
// the Reconcile tab UI and the reconcile-API payload.
|
||||
export const extractRiders = (previewData) => {
|
||||
if (!previewData) return [];
|
||||
const map = new Map();
|
||||
// De-dupe by orderid across the whole tree. A rider can legitimately appear
|
||||
// in multiple zones (one per delivery suburb), so the same rider_id is
|
||||
// visited more than once. Without this guard, any stale copy left behind
|
||||
// by applyReconcileResponse gets concatenated into the rider's orders and
|
||||
// the same orderid is sent twice to /deliveries/createdeliveries.
|
||||
const seenOrderIds = new Set();
|
||||
const push = (riderId, riderName, orders) => {
|
||||
if (riderId == null) return;
|
||||
const key = String(riderId);
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
||||
}
|
||||
const entry = map.get(key);
|
||||
(orders || []).forEach((o) => {
|
||||
const oid = o?.orderid != null ? String(o.orderid) : null;
|
||||
if (oid) {
|
||||
if (seenOrderIds.has(oid)) return;
|
||||
seenOrderIds.add(oid);
|
||||
}
|
||||
entry.orders.push(o);
|
||||
});
|
||||
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
||||
};
|
||||
|
||||
if (Array.isArray(previewData.zones) && previewData.zones.length) {
|
||||
previewData.zones.forEach((z) => {
|
||||
(z.riders || []).forEach((r) => {
|
||||
const id = r.rider_id ?? r.userid;
|
||||
const name = r.rider_name || r.username || `Rider ${id}`;
|
||||
push(id, name, r.orders);
|
||||
});
|
||||
});
|
||||
} else if (Array.isArray(previewData.details)) {
|
||||
previewData.details.forEach((o) => {
|
||||
const id = o.rider_id ?? o.userid;
|
||||
const name = o.rider_name || o.ridername || `Rider ${id}`;
|
||||
push(id, name, [o]);
|
||||
});
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
// Reverse of extractRiders — flatten rider-grouped list into a details-style
|
||||
// array (used as the Assign Orders payload).
|
||||
export const flattenRiders = (riders) => {
|
||||
const out = [];
|
||||
riders.forEach((r) => {
|
||||
// Go backend types Deliveries.userid as int — coerce here so any
|
||||
// upstream string (AI response, riders API, change-rider edit) gets
|
||||
// normalised before the JSON body is built.
|
||||
const ridNum = Number(r.rider_id);
|
||||
const rid = Number.isFinite(ridNum) ? ridNum : r.rider_id;
|
||||
(r.orders || []).forEach((o) => {
|
||||
out.push({
|
||||
...o,
|
||||
rider_id: rid,
|
||||
userid: rid,
|
||||
rider_name: r.rider_name,
|
||||
rider: r.rider_name
|
||||
});
|
||||
});
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
// Move one order from oldRiderId -> newRiderId inside dispatchPreviewData.
|
||||
// Mutates both the zones[].riders[].orders[] tree (so the Dispatch tab
|
||||
// renders the change) AND the flat details[] list (so Assign Orders picks
|
||||
// it up). Returns a NEW preview object (immutable update).
|
||||
export const moveOrderInPreviewData = (preview, { orderId, newRiderId, newRiderName }) => {
|
||||
if (!preview) return preview;
|
||||
const next = JSON.parse(JSON.stringify(preview));
|
||||
|
||||
// 1) Update flat details list
|
||||
if (Array.isArray(next.details)) {
|
||||
next.details = next.details.map((o) =>
|
||||
String(o.orderid) === String(orderId)
|
||||
? { ...o, rider_id: newRiderId, userid: newRiderId, rider_name: newRiderName, rider: newRiderName }
|
||||
: o
|
||||
);
|
||||
}
|
||||
|
||||
// 2) Move within zones[].riders[].orders[]
|
||||
if (Array.isArray(next.zones)) {
|
||||
let movedOrder = null;
|
||||
let homeZoneIdx = -1;
|
||||
|
||||
for (let zi = 0; zi < next.zones.length && !movedOrder; zi++) {
|
||||
const zone = next.zones[zi];
|
||||
if (!Array.isArray(zone.riders)) continue;
|
||||
for (let ri = 0; ri < zone.riders.length && !movedOrder; ri++) {
|
||||
const r = zone.riders[ri];
|
||||
if (!Array.isArray(r.orders)) continue;
|
||||
const oi = r.orders.findIndex((o) => String(o.orderid) === String(orderId));
|
||||
if (oi !== -1) {
|
||||
movedOrder = r.orders[oi];
|
||||
r.orders.splice(oi, 1);
|
||||
homeZoneIdx = zi;
|
||||
// A rider left with zero orders after this move is a ghost entry —
|
||||
// Dispatch's rider list renders every zone.riders[] entry
|
||||
// unconditionally, so it would keep showing as a clickable
|
||||
// 0-trips/0km/₹0 card with nothing inside once its last order is
|
||||
// reassigned elsewhere. Drop it from the zone entirely.
|
||||
if (r.orders.length === 0) {
|
||||
zone.riders.splice(ri, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movedOrder) {
|
||||
const updated = {
|
||||
...movedOrder,
|
||||
rider_id: newRiderId,
|
||||
userid: newRiderId,
|
||||
rider_name: newRiderName,
|
||||
rider: newRiderName
|
||||
};
|
||||
let placed = false;
|
||||
for (const zone of next.zones) {
|
||||
if (!Array.isArray(zone.riders)) continue;
|
||||
const target = zone.riders.find(
|
||||
(r) => String(r.rider_id ?? r.userid) === String(newRiderId)
|
||||
);
|
||||
if (target) {
|
||||
target.orders = target.orders || [];
|
||||
target.orders.push(updated);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed && homeZoneIdx >= 0) {
|
||||
next.zones[homeZoneIdx].riders.push({
|
||||
rider_id: newRiderId,
|
||||
userid: newRiderId,
|
||||
rider_name: newRiderName,
|
||||
orders: [updated]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
|
||||
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
|
||||
// zone containment), then rebuilds the flat details list from the new tree.
|
||||
export const applyReconcileResponse = (preview, response) => {
|
||||
if (!preview || !Array.isArray(response?.riders)) return preview;
|
||||
const next = JSON.parse(JSON.stringify(preview));
|
||||
|
||||
const newOrdersByRider = new Map(
|
||||
response.riders.map((r) => [String(r.rider_id), r.orders || []])
|
||||
);
|
||||
|
||||
if (Array.isArray(next.zones) && next.zones.length) {
|
||||
// Pass 1: wipe every existing copy of a responding rider's orders across
|
||||
// ALL zones. The server's reconciled list is the single source of truth,
|
||||
// and a rider can be present in multiple zones (one per delivery suburb).
|
||||
// The previous "update first match, delete from map" loop left stale
|
||||
// copies in the other zones, which extractRiders then concatenated into
|
||||
// duplicate orderids — surfacing as duplicate deliveries on Assign.
|
||||
next.zones.forEach((zone) => {
|
||||
if (!Array.isArray(zone.riders)) return;
|
||||
zone.riders.forEach((r) => {
|
||||
const key = String(r.rider_id ?? r.userid);
|
||||
if (newOrdersByRider.has(key)) r.orders = [];
|
||||
});
|
||||
});
|
||||
|
||||
// Pass 2: drop the reconciled orders onto the first zone that already
|
||||
// lists the rider. If the rider isn't anywhere in the tree, append a
|
||||
// fresh rider entry to zone[0].
|
||||
newOrdersByRider.forEach((orders, riderKey) => {
|
||||
let placed = false;
|
||||
for (const zone of next.zones) {
|
||||
if (!Array.isArray(zone.riders)) continue;
|
||||
const target = zone.riders.find(
|
||||
(r) => String(r.rider_id ?? r.userid) === riderKey
|
||||
);
|
||||
if (target) {
|
||||
target.orders = orders;
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
const target = next.zones[0];
|
||||
target.riders = target.riders || [];
|
||||
target.riders.push({
|
||||
rider_id: Number(riderKey) || riderKey,
|
||||
rider_name: orders[0]?.rider_name || `Rider ${riderKey}`,
|
||||
orders
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Same ghost-rider cleanup as moveOrderInPreviewData: if the reconcile
|
||||
// response came back with an empty orders[] for a rider (every stop it
|
||||
// had got reassigned elsewhere during reconciliation), don't leave that
|
||||
// rider sitting in the tree as a 0-trips/0km/₹0 card with nothing inside.
|
||||
next.zones.forEach((zone) => {
|
||||
if (!Array.isArray(zone.riders)) return;
|
||||
zone.riders = zone.riders.filter((r) => Array.isArray(r.orders) && r.orders.length > 0);
|
||||
});
|
||||
} else {
|
||||
next.zones = [
|
||||
{
|
||||
zone_name: 'Reconciled',
|
||||
riders: response.riders.map((r) => ({
|
||||
rider_id: r.rider_id,
|
||||
rider_name: r.rider_name || `Rider ${r.rider_id}`,
|
||||
orders: r.orders || []
|
||||
}))
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Rebuild flat details from the updated zones->riders->orders tree.
|
||||
const flatDetails = [];
|
||||
next.zones.forEach((zone) => {
|
||||
(zone.riders || []).forEach((r) => {
|
||||
(r.orders || []).forEach((o) => {
|
||||
flatDetails.push({
|
||||
...o,
|
||||
rider_id: r.rider_id,
|
||||
userid: r.rider_id,
|
||||
rider_name: r.rider_name,
|
||||
rider: r.rider_name
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
next.details = flatDetails;
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
// The delivery amount, recomputed from the rate card the solver echoed back.
|
||||
// Applied at render time so the commit payload always reflects the current
|
||||
// state of the tree without a synchronising effect.
|
||||
export function computeDeliveryAmounts(list) {
|
||||
return list.map((item) => {
|
||||
const cumulativeKms = Number(item.cumulativekms || 0);
|
||||
const minKm = Number(item.minkm || 0);
|
||||
const basePrice = Number(item.baseprice || 0);
|
||||
const pricePerKm = Number(item.priceperkm || 0);
|
||||
if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
|
||||
return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user