map the selected rider and shop, drop the map tab
The separate Map tab is gone. "By rider" and "By store" now draw the map where the stops table was, so picking a rider from the rail shows their round on a map and picking a branch shows its day. A round and a shop's day are both shapes — where the work is, in what order, how far apart — and a table of addresses shows neither. You can read twenty rows and still not see that a rider crossed the city twice. Two views keep the table, on purpose: the waiting queue, whose rows are ticked to assign them (a checkbox cannot live on a map pin, and nothing there has been worked yet), and "By customer", where one customer's drops are all at one address. The map draws all three places a delivery has, not just the rider: shop a square, one per branch — pickuplat/pickuplon, 500/500 rows drop a circle per stop, by status — droplat/droplon, 500/500 rows rider a hollow ring — riderslat/riderslon, 461/500 and 324/500 Shop and drop are on every row, so the map is never empty on a day with work in it. The rider fix is real: on delivered rows it sits a median 134 m from the drop, a reading taken at the customer's door. The endpoints spell the columns differently — `pickuplon` on getdeliveries against `pickuplong` on getorders, one letter, and reading the wrong one puts every shop off the coast of Ghana. `droplat` is filled on deliveries and empty on orders. `coordOf` takes every spelling so a waiting order maps the same as a delivered one. Also fixes the rail itself: it named each group from its first stop's `ridername`, and that column holds a delivery status on more rows than a name for two riders in five. The board was listing a phantom rider called "delivered" carrying 100 of the day's stops, next to the real riders. Grouping was never wrong — that is on userid — only the label. `isRealName` now excludes the status vocabulary and both the rail and the map take the most common name that survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { CustomerInfo } from '@/api/customers';
|
||||
import type { DeliveryRow, OrderRow, TenantLocation } from '@/api/types';
|
||||
import { awaitingRider, assignedFrom } from './assignDelivery';
|
||||
import { isRealName } from './orderStatus';
|
||||
|
||||
/**
|
||||
* A day's dispatch, grouped three ways.
|
||||
@@ -180,6 +181,31 @@ function fold(group: Group, stop: Stop): void {
|
||||
if (rider) group.riders.add(String(rider));
|
||||
}
|
||||
|
||||
/**
|
||||
* The most common value in `ridername` that is not a delivery status.
|
||||
*
|
||||
* Returns undefined when the column held nothing but statuses, so the caller
|
||||
* keeps whatever fallback it had rather than being handed a wrong name.
|
||||
*/
|
||||
function commonRiderName(stops: readonly Stop[]): string | undefined {
|
||||
const counts = new Map<string, number>();
|
||||
for (const stop of stops) {
|
||||
if (stop.kind !== 'delivery') continue;
|
||||
const name = stop.row.ridername?.trim();
|
||||
if (!name || !isRealName(name)) continue;
|
||||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
}
|
||||
let best: string | undefined;
|
||||
let most = 0;
|
||||
for (const [name, count] of counts) {
|
||||
if (count > most) {
|
||||
best = name;
|
||||
most = count;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Busiest first, then alphabetical, so the board does not reshuffle randomly. */
|
||||
function order(groups: Group[]): Group[] {
|
||||
return groups.sort((a, b) => b.stops.length - a.stops.length || a.name.localeCompare(b.name));
|
||||
@@ -298,19 +324,39 @@ export function groupByRider(stops: readonly Stop[]): Group[] {
|
||||
const id = rider ? String(rider) : UNASSIGNED;
|
||||
let group = map.get(id);
|
||||
if (!group) {
|
||||
const name =
|
||||
id === UNASSIGNED
|
||||
? 'Waiting for a rider'
|
||||
: (stop.kind === 'delivery' ? stop.row.ridername : '') || `Rider ${id}`;
|
||||
const detail = stop.kind === 'delivery' ? stop.row.ridercontact : undefined;
|
||||
group = blank(id, name, detail);
|
||||
// Named once the group is complete — see below.
|
||||
group = blank(id, id === UNASSIGNED ? 'Waiting for a rider' : `Rider ${id}`, detail);
|
||||
map.set(id, group);
|
||||
}
|
||||
fold(group, stop);
|
||||
}
|
||||
|
||||
/*
|
||||
Named from the whole group, not from whichever stop arrived first.
|
||||
|
||||
`ridername` is not reliably a name: every rider on tenant 916 has BOTH their
|
||||
name and a delivery status in that column, and for two of the five the status
|
||||
is the MORE common value —
|
||||
|
||||
883 { "Rajan": 21, "delivered": 42 }
|
||||
897 { "Varun": 69, "delivered": 75 }
|
||||
1111 { "Murali": 33, "delivered": 74, "cancelled": 1 }
|
||||
1114 { "Tamilazhagan": 37, "delivered": 63 }
|
||||
|
||||
Taking the first stop's value put a rider called "delivered" in the rail, next
|
||||
to the real riders, carrying 100 of the day's stops. Grouping was never
|
||||
affected — that is on `userid` — but the label was, and the rail is the list
|
||||
an operator picks from. So statuses are excluded by vocabulary and the most
|
||||
common of what survives is the name; a rider whose every row carried a status
|
||||
keeps their id, which is at least honest about being an id.
|
||||
*/
|
||||
for (const group of map.values()) {
|
||||
group.stops.sort((a, b) => stopTime(a).localeCompare(stopTime(b)));
|
||||
if (group.id !== UNASSIGNED) {
|
||||
const name = commonRiderName(group.stops);
|
||||
if (name) group.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
const groups = order([...map.values()]);
|
||||
|
||||
Reference in New Issue
Block a user