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:
@@ -253,3 +253,221 @@
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Plan vs actual ──────────────────────────────────────────────────────── */
|
||||
|
||||
/* One colour per step, reused by the stacked bar and the legend swatches so the
|
||||
two read as the same object. Ordered as the journey runs — cool at the shop,
|
||||
warm on the road — rather than by a palette's own sequence. */
|
||||
.pva-bar {
|
||||
display: flex;
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
border-radius: 7px;
|
||||
background: var(--color-surface-sunken);
|
||||
}
|
||||
|
||||
.pva-bar i,
|
||||
.pva-swatch {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pva-bar i[data-step='toShop'],
|
||||
.pva-swatch[data-step='toShop'] {
|
||||
background: #662582;
|
||||
}
|
||||
|
||||
.pva-bar i[data-step='counter'],
|
||||
.pva-swatch[data-step='counter'] {
|
||||
background: #8b6bab;
|
||||
}
|
||||
|
||||
.pva-bar i[data-step='inRound'],
|
||||
.pva-swatch[data-step='inRound'] {
|
||||
background: #c2410c;
|
||||
}
|
||||
|
||||
.pva-bar i[data-step='onRoad'],
|
||||
.pva-swatch[data-step='onRoad'] {
|
||||
background: #0f8a5f;
|
||||
}
|
||||
|
||||
.pva-swatch {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pva-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 4px 16px;
|
||||
}
|
||||
|
||||
.pva-step {
|
||||
display: grid;
|
||||
grid-template-columns: 9px minmax(0, 1fr) auto;
|
||||
gap: 4px 8px;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* The step costing the most is the finding. Outlined rather than recoloured, so
|
||||
the swatch keeps naming its own segment of the bar above. */
|
||||
.pva-step[data-bottleneck='true'] {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-surface-subtle);
|
||||
}
|
||||
|
||||
.pva-step-label {
|
||||
font-size: 12.5px;
|
||||
color: var(--color-ink-1);
|
||||
}
|
||||
|
||||
.pva-step-label em {
|
||||
margin-left: 6px;
|
||||
font-size: 10.5px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: #b45309;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pva-step strong {
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-ink-1);
|
||||
}
|
||||
|
||||
.pva-step-note {
|
||||
grid-column: 2 / -1;
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
|
||||
.pva-figure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--color-border);
|
||||
}
|
||||
|
||||
.pva-figure[data-tone='good'] {
|
||||
border-left-color: #0f8a5f;
|
||||
}
|
||||
|
||||
.pva-figure[data-tone='watch'] {
|
||||
border-left-color: #b45309;
|
||||
}
|
||||
|
||||
.pva-figure[data-tone='act'] {
|
||||
border-left-color: #b91c1c;
|
||||
}
|
||||
|
||||
.pva-figure-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--color-ink-3);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pva-figure strong {
|
||||
font-size: 19px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
color: var(--color-ink-1);
|
||||
}
|
||||
|
||||
.pva-figure-note {
|
||||
font-size: 11.5px;
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
|
||||
.pva-note {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
align-items: flex-start;
|
||||
padding: 8px 10px;
|
||||
border-radius: 7px;
|
||||
background: var(--color-surface-subtle);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.pva-note svg {
|
||||
flex: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pva-toggle {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 12.5px;
|
||||
color: var(--color-ink-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Rows here are read, not opened — nothing lies behind one — so the pointer and
|
||||
hover lift the stops table uses would promise a click that does nothing. */
|
||||
.pva-table tbody tr {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pva-late[data-late='yes'] {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.pva-late[data-late='no'] {
|
||||
color: #0f8a5f;
|
||||
}
|
||||
|
||||
.pva-late[data-late='none'] {
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
|
||||
/* ── The map's rider filter ──────────────────────────────────────────────── */
|
||||
|
||||
.rider-chip {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rider-chip:hover {
|
||||
background: var(--color-surface-subtle);
|
||||
}
|
||||
|
||||
.rider-chip[data-active='true'] {
|
||||
border-color: var(--color-brand);
|
||||
background: var(--color-brand-tint);
|
||||
color: var(--color-ink-1);
|
||||
}
|
||||
|
||||
.rider-chip:focus-visible {
|
||||
outline: 2px solid var(--color-brand);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* The swatch matches this rider's line on the map, which is the only thing
|
||||
connecting a chip to the shape it filters to. */
|
||||
.rider-chip i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user