dispatch map, plan vs actual, and a fleet page
Three of the features the old console had and ours did not, built on what the data can actually support rather than on what the column names imply. Two findings changed the shape of the work: `riderlogs` is not a GPS trail. Every ping a rider sends carries the SAME coordinate — one rider's 2,404 pings on 14 August all read 11.052998, 76.929958, and the same holds on every day and region checked. Distance, speed and "time moving" cannot come from it. The Fleet page therefore reports presence only: who was online and for how long, inferred from the gaps between check-ins, because `login`, `logout` and `workhours` are empty on all 320,132 August rows. It says out loud that it cannot tell a rider parked all day from one who crossed the city. The delivery ladder is not the order its columns are in. `starttime` is later than `arrivaltime` on 316 of 316 rows, which looks corrupt and is not: `starttime` is per-DROP, stamped when the rider sets off for that address having finished the last one. Read as assign -> arrive -> pickup -> start -> deliver, every duration is positive. On tenant 916 that shows the bottleneck is not the riding: a median 69.5 minutes passes between handing an order to a rider and that rider reaching the shop, against 0.6 minutes at the counter. - Map tab on dispatch, fed by `deliveries.riderslat/lon` — the only rider positions that move (353 distinct across 461 rows). Tenant-scoped, so a shop sees its own rounds. The line joins stops in worked order and says it is not a route. - Plan vs actual tab: promised against delivered, and a step breakdown of where the hours go. `actualkms` is excluded — it equals the planned `kms` to the decimal on every delivered row, so it is a copy, not a measurement. - Fleet page in the platform console: a presence gantt and a map of where each rider is registered. - `ridername` holds a delivery status on more rows than it holds a name for two riders in five, so names are resolved by excluding the status vocabulary first. - leaflet, wrapped directly rather than via react-leaflet, lazy-loaded so only the pages with a map pay for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
This commit is contained in:
@@ -388,4 +388,71 @@ export const partnersApi = {
|
||||
/** The regions one partner covers. */
|
||||
locations: (partnerid: number) =>
|
||||
api.list<PartnerLocation>(`${WEB}/partners/getpartnerlocations`, { partnerid }),
|
||||
|
||||
/**
|
||||
* Every GPS ping a partner's riders sent over a window.
|
||||
*
|
||||
* ── Scope, and why this is a platform endpoint ────────────────────────────
|
||||
*
|
||||
* It filters on `partnerid` or on the rider's `applocationid` — never on a
|
||||
* tenant. A partner's riders serve every merchant that partner supplies, so
|
||||
* there is no tenant this could be scoped to, and asking by region would hand
|
||||
* one merchant every rider in the city. That is why the fleet view lives in
|
||||
* the platform console and not in a shop's.
|
||||
*
|
||||
* ── What comes back, and what it is not ───────────────────────────────────
|
||||
*
|
||||
* One row per ping: rider, timestamp, latitude, longitude. Dense — 320,132
|
||||
* rows across eight riders for August 2026 — so a month-wide window is a
|
||||
* large response and callers ask for a day or two at a time.
|
||||
*
|
||||
* The coordinates are NOT a trail. Every row for a given rider carries the
|
||||
* same pair: one rider's 2,404 pings on 14 August 2026 all read 11.052998,
|
||||
* 76.929958, and the same holds on every day and region checked. The app
|
||||
* stamps a location once and repeats it on each heartbeat, so distance, speed
|
||||
* and "time moving" cannot be derived from this and anything of that shape
|
||||
* would be invented. Rider positions that actually move are written on the
|
||||
* `deliveries` rows; see `deliveryTrack`.
|
||||
*
|
||||
* The row also carries `login`, `logout`, `workhours`, `shorthours` and
|
||||
* `breakhours`, and every one of them is empty or zero on every row measured.
|
||||
* Nothing closes a shift. So the timestamps are what this endpoint is good
|
||||
* for — who was online and for how long — and shifts are inferred from the
|
||||
* gaps between pings; see `riderShifts`.
|
||||
*/
|
||||
riderLogs: (query: { partnerid?: number; applocationid?: number; fromdate: string; todate: string }) =>
|
||||
api.list<RiderPingRow>(`${WEB}/partners/getriderlogs`, {
|
||||
...(query.partnerid ? { partnerid: query.partnerid } : {}),
|
||||
...(query.applocationid ? { applocationid: query.applocationid } : {}),
|
||||
fromdate: query.fromdate,
|
||||
todate: query.todate,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* One row of `getriderlogs`.
|
||||
*
|
||||
* The shift columns are typed because they are sent, and documented as empty
|
||||
* because they are: nothing on the platform writes them. Reading `workhours`
|
||||
* and believing it is the mistake this comment exists to prevent.
|
||||
*/
|
||||
export interface RiderPingRow {
|
||||
logid?: number;
|
||||
logdate: string;
|
||||
userid: number;
|
||||
username?: string;
|
||||
partnerid?: number;
|
||||
latitude?: string;
|
||||
longitude?: string;
|
||||
shiftid?: number;
|
||||
shifthours?: number;
|
||||
/** Always empty on production data. See `riderLogs`. */
|
||||
login?: string;
|
||||
/** Always empty on production data. See `riderLogs`. */
|
||||
logout?: string;
|
||||
/** Always 0 on production data. See `riderLogs`. */
|
||||
workhours?: number;
|
||||
shorthours?: number;
|
||||
breakhours?: number;
|
||||
logstatus?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user