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:
2026-09-09 17:47:40 +05:30
parent 34bf7989f7
commit 35250717fc
11 changed files with 915 additions and 585 deletions

View File

@@ -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()]);