feat(geo): add driverLabelFor + unassignedGlyphFor helpers
This commit is contained in:
parent
02458909f7
commit
032c76ff19
2 changed files with 147 additions and 0 deletions
96
src/lib/geo.test.ts
Normal file
96
src/lib/geo.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { driverLabelFor, unassignedGlyphFor } from "./geo";
|
||||
|
||||
describe("driverLabelFor", () => {
|
||||
it("returns A for the first driver alphabetically", () => {
|
||||
const drivers = [{ id: "u-1" }, { id: "u-2" }].sort((a, b) =>
|
||||
a.id.localeCompare(b.id)
|
||||
);
|
||||
// u-1 < u-2 lexically, so driver at index 0 is "u-1"
|
||||
expect(driverLabelFor(drivers, drivers[0].id)).toBe("A");
|
||||
expect(driverLabelFor(drivers, drivers[1].id)).toBe("B");
|
||||
});
|
||||
|
||||
it("returns sequential letters A through Z for the first 26 drivers", () => {
|
||||
const drivers = Array.from({ length: 26 }, (_, i) => ({
|
||||
id: `driver-${String(i).padStart(2, "0")}`,
|
||||
}));
|
||||
const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id));
|
||||
for (let i = 0; i < 26; i++) {
|
||||
expect(driverLabelFor(sorted, sorted[i].id)).toBe(
|
||||
String.fromCharCode("A".charCodeAt(0) + i)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to digits 1..9 starting at position 27", () => {
|
||||
const drivers = Array.from({ length: 30 }, (_, i) => ({
|
||||
id: `driver-${String(i).padStart(2, "0")}`,
|
||||
}));
|
||||
const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id));
|
||||
expect(driverLabelFor(sorted, sorted[26].id)).toBe("1");
|
||||
expect(driverLabelFor(sorted, sorted[27].id)).toBe("2");
|
||||
expect(driverLabelFor(sorted, sorted[28].id)).toBe("3");
|
||||
expect(driverLabelFor(sorted, sorted[29].id)).toBe("4");
|
||||
});
|
||||
|
||||
it("returns '?' for an unknown driver id", () => {
|
||||
const drivers = [{ id: "u-1" }];
|
||||
expect(driverLabelFor(drivers, "u-missing")).toBe("?");
|
||||
});
|
||||
|
||||
it("is stable — same input returns the same label on repeated calls", () => {
|
||||
const drivers = [{ id: "u-1" }, { id: "u-2" }, { id: "u-3" }];
|
||||
const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id));
|
||||
const first = driverLabelFor(sorted, "u-2");
|
||||
const second = driverLabelFor(sorted, "u-2");
|
||||
const third = driverLabelFor(sorted, "u-2");
|
||||
expect(first).toBe(second);
|
||||
expect(second).toBe(third);
|
||||
});
|
||||
|
||||
it("is stable when given names instead of ids (alphabetical by name)", () => {
|
||||
const drivers = [
|
||||
{ id: "alice" },
|
||||
{ id: "bob" },
|
||||
{ id: "carol" },
|
||||
];
|
||||
expect(driverLabelFor(drivers, "alice")).toBe("A");
|
||||
expect(driverLabelFor(drivers, "bob")).toBe("B");
|
||||
expect(driverLabelFor(drivers, "carol")).toBe("C");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unassignedGlyphFor", () => {
|
||||
it("returns 'ready' when every item is ready_for_collection=true", () => {
|
||||
expect(
|
||||
unassignedGlyphFor([
|
||||
{ ready_for_collection: true },
|
||||
{ ready_for_collection: true },
|
||||
])
|
||||
).toBe("ready");
|
||||
});
|
||||
|
||||
it("returns 'unready' when at least one item is not ready", () => {
|
||||
expect(
|
||||
unassignedGlyphFor([
|
||||
{ ready_for_collection: true },
|
||||
{ ready_for_collection: false },
|
||||
])
|
||||
).toBe("unready");
|
||||
});
|
||||
|
||||
it("returns 'unready' when at least one item has ready_for_collection=undefined", () => {
|
||||
expect(
|
||||
unassignedGlyphFor([
|
||||
{ ready_for_collection: true },
|
||||
{},
|
||||
])
|
||||
).toBe("unready");
|
||||
});
|
||||
|
||||
it("returns 'unready' for an empty array", () => {
|
||||
expect(unassignedGlyphFor([])).toBe("unready");
|
||||
});
|
||||
});
|
||||
|
|
@ -226,4 +226,55 @@ export function colorForStop(args: {
|
|||
default:
|
||||
return DRIVER_PALETTE[args.driverIndex] ?? DRIVER_PALETTE[0];
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Collection map — pin label + glyph helpers.
|
||||
*
|
||||
* Pure functions that drive the new two-pin model on the collection map.
|
||||
* Driver labels are alphabetical by id (A → Z, then 1 → 9); the unassigned
|
||||
* glyph collapses ready / unready into ✓ / ✕ based on the bucket contents.
|
||||
* ========================================================================= */
|
||||
|
||||
/**
|
||||
* Returns the driver label for a given driver id within an alphabetically-
|
||||
* sorted driver list. Position 0 → "A", 1 → "B", ..., 25 → "Z", 26 → "1",
|
||||
* 27 → "2", ... Returns "?" if the id isn't in the list.
|
||||
*
|
||||
* The caller MUST pass a list already sorted by `id` ascending — typically
|
||||
* the `driversWithItems` array, which `page.tsx` already sorts by name via
|
||||
* `localeCompare` on the matching `LookupUser` record. Because driver
|
||||
* `id` and driver `name` are stable across days, the letter for a given
|
||||
* driver is also stable across days.
|
||||
*/
|
||||
export function driverLabelFor(
|
||||
sortedDrivers: { id: string }[],
|
||||
driverId: string
|
||||
): string {
|
||||
const idx = sortedDrivers.findIndex((d) => d.id === driverId);
|
||||
if (idx === -1) return "?";
|
||||
if (idx < 26) return String.fromCharCode("A".charCodeAt(0) + idx);
|
||||
// After Z, fall back to 1..9 (and then loop — at >35 drivers we'll
|
||||
// reuse digits, which is a graceful degradation rather than a crash).
|
||||
return String((idx - 25) % 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unassigned glyph for a supplier bucket:
|
||||
* - "ready" → ✓ : every pool item has ready_for_collection === true.
|
||||
* - "unready" → ✕ : at least one isn't ready, or the bucket is empty.
|
||||
*
|
||||
* Returning "unready" for an empty input keeps the pin from falsely
|
||||
* advertising "ready, 0 items" — an empty bucket shouldn't render a pin
|
||||
* at all, but if the caller asks the helper for the glyph defensively,
|
||||
* "unready" is the safer answer.
|
||||
*/
|
||||
export function unassignedGlyphFor(
|
||||
poolItems: { ready_for_collection?: boolean }[]
|
||||
): "ready" | "unready" {
|
||||
if (poolItems.length === 0) return "unready";
|
||||
const anyUnready = poolItems.some(
|
||||
(it) => it.ready_for_collection !== true
|
||||
);
|
||||
return anyUnready ? "unready" : "ready";
|
||||
}
|
||||
Loading…
Reference in a new issue