From 8c21347ecc8bb7f655af72084d9905427993329d Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 12 Jul 2026 00:46:44 +0200 Subject: [PATCH] feat(collection-map): drop date filter; thread ready-gate into assign dialog --- src/app/(app)/collection-map/page.test.ts | 38 ++++---- src/app/(app)/collection-map/page.tsx | 111 +++++----------------- 2 files changed, 46 insertions(+), 103 deletions(-) diff --git a/src/app/(app)/collection-map/page.test.ts b/src/app/(app)/collection-map/page.test.ts index 496be13..66703b1 100644 --- a/src/app/(app)/collection-map/page.test.ts +++ b/src/app/(app)/collection-map/page.test.ts @@ -68,31 +68,37 @@ describe("splitSupplierBucket", () => { describe("collection-map toolbar shape contract", () => { // The toolbar lives in page.tsx. We snapshot the literal labels that must - // remain visible (date + quick chips + Refresh + overlays) and the labels - // that must NOT come back (the deleted filter chips + Drivers card). - // These are static source-text assertions: a future PR that re-adds the - // filter chips won't pass the pipeline, even if the chips "look right". + // remain visible (Refresh) and the labels / state / JSX that must NOT + // come back (the deleted date picker + quick chips + filter chips + + // Drivers card). These are static source-text assertions: a future PR + // that re-adds removed controls won't pass the pipeline, even if the + // controls "look right". const src = fs.readFileSync(path.resolve(__dirname, "./page.tsx"), "utf8"); - it("renders the locked toolbar labels", () => { - // Quick-chip labels. - expect(src).toMatch(/Today/); - expect(src).toMatch(/Tomorrow/); - expect(src).toMatch(/\+3 days/); - expect(src).toMatch(/Next week/); - // Overlay buttons + manual refresh. + it("renders only the Refresh control in the page toolbar", () => { + expect(src).toMatch(/Refresh/); + // Quick-chip labels deleted in 2026-07-12 (date-filter removal). + expect(src).not.toMatch(/Tomorrow/); + expect(src).not.toMatch(/\+3 days/); + expect(src).not.toMatch(/Next week/); + // Map chrome (not page toolbar) — keep these locked in too so the + // overlay buttons don't regress. expect(src).toMatch(/Traffic/); expect(src).toMatch(/Fullscreen|Exit fullscreen|Minimize2/); - expect(src).toMatch(/Refresh/); }); - it("does not re-introduce the deleted filter chips", () => { - // State + JSX for the deleted chips — must not silently come back. + it("does not re-introduce the deleted filter chips, search, or Drivers card", () => { expect(src).not.toMatch(/setStatusFilter|statusFilter|StatusFilter/); expect(src).not.toMatch(/setReadyFilter|readyFilter/); - // Search input + placeholder. expect(src).not.toMatch(/Search PO, supplier/); - // Drivers sidebar card heading. expect(src).not.toMatch(/>\s*Drivers\s* { + // State setter must be gone. + expect(src).not.toMatch(/useState\(ymdToday\(\)\)/); + // The day-window filter string must be gone from both queries. + expect(src).not.toMatch(/assigned_at >= "\$\{date\}T00:00:00\.000Z"/); + expect(src).not.toMatch(/assigned_at <= "\$\{date\}T23:59:59\.999Z"/); + }); }); diff --git a/src/app/(app)/collection-map/page.tsx b/src/app/(app)/collection-map/page.tsx index 4903bf7..ce4b1d9 100644 --- a/src/app/(app)/collection-map/page.tsx +++ b/src/app/(app)/collection-map/page.tsx @@ -36,13 +36,12 @@ import { useMap, useMapEvents, } from "react-leaflet"; -import { Calendar, ExternalLink, MapPin, RefreshCw, Truck, User as UserIcon, Users, X, Layers } from "lucide-react"; +import { ExternalLink, MapPin, RefreshCw, Truck, User as UserIcon, Users, X, Layers } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; import { Maximize2, Minimize2 } from "lucide-react"; import { AssignDriverDialog, type AssignDriverSubmitPayload } from "@/components/ui/assign-driver-dialog"; import { getPocketBase } from "@/lib/pocketbase"; @@ -146,22 +145,6 @@ type LookupUser = Pick; * Helpers * ========================================================================= */ -function ymdToday(): string { - const now = new Date(); - return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; -} - -function ymdForDate(d: Date): string { - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; -} - -function addDays(ymd: string, days: number): string { - const [y, m, d] = ymd.split("-").map(Number); - const date = new Date(Date.UTC(y, m - 1, d)); - date.setUTCDate(date.getUTCDate() + days); - return ymdForDate(date); -} - function formatDateLabel(ymd: string): string { const [y, m, d] = ymd.split("-").map(Number); const date = new Date(y, m - 1, d); @@ -283,7 +266,10 @@ export default function CollectionMapPage() { const [items, setItems] = useState([]); const [suppliersWithoutCoords, setSuppliersWithoutCoords] = useState([]); - const [date, setDate] = useState(ymdToday()); + // The page no longer exposes a date filter; the map shows every + // assigned + collection_pool item regardless of `assigned_at`. The + // assign dialog still picks a date internally ("when is the pickup") + // — that's a per-row concern, not a page-level filter. // Traffic overlay toggle — default OFF to conserve the TomTom // freemium 2,500-tile/day budget. Operators can flip it on for a // single planning session; Leaflet caches tiles client-side so @@ -307,23 +293,10 @@ export default function CollectionMapPage() { setLoading(true); try { const pb = getPocketBase(); - // Build a UTC ISO 8601 day window. `assigned_at` is stored at - // "YYYY-MM-DDT00:00:00.000Z" (see /api/v1/collection-map/items/[id]/assign), - // so the bounds need the same "T" and trailing "Z" or string - // comparison puts `T` (0x54) before the space (0x20) and the bound - // excludes every real row. - const dayStart = `${date}T00:00:00.000Z`; - const dayEnd = `${date}T23:59:59.999Z`; - // Union of: - // (a) assigned items whose assigned_at lands on the picked date, - // (b) un-dated pool items (collection_pool + empty assigned_at) so - // the operator can plan them visually on the map. - // Items scheduled for a *future* date stay hidden until that date - // — the map's date picker still means "the day I'm planning for". - const filter = [ - `(item_status = "assigned" || item_status = "collection_pool")`, - `((assigned_at >= "${dayStart}" && assigned_at <= "${dayEnd}") || (assigned_at = "" || assigned_at = null))`, - ].join(" && "); + // The map shows every assigned + collection_pool row regardless of + // `assigned_at` — operators plan across all open pickups, not just + // today's. The page no longer exposes a date filter. + const filter = `(item_status = "assigned" || item_status = "collection_pool")`; const rows = (await pb.collection("order_items").getFullList({ filter, @@ -367,7 +340,7 @@ export default function CollectionMapPage() { // Also collect items whose suppliers have NO coordinates — show in // the sidebar so the operator knows they exist but can't be mapped. const unmappedRows = (await pb.collection("order_items").getFullList({ - filter: `(item_status = "assigned" || item_status = "collection_pool") && ((assigned_at >= "${date}T00:00:00.000Z" && assigned_at <= "${date}T23:59:59.999Z") || assigned_at = "" || assigned_at = null)`, + filter: `(item_status = "assigned" || item_status = "collection_pool")`, expand: "purchase_order.supplier", fields: "id,purchase_order", $autoCancel: false, @@ -388,7 +361,8 @@ export default function CollectionMapPage() { } finally { setLoading(false); } - }, [date]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const fetchUsers = useCallback(async () => { try { @@ -521,9 +495,7 @@ export default function CollectionMapPage() { /* -- assign dialog -------------------------------------------------------- */ const openAssignDialog = useCallback( (row: StopItem) => { - const initialDate = row.assigned_at - ? row.assigned_at.slice(0, 10) - : date; + const initialDate = row.assigned_at ? row.assigned_at.slice(0, 10) : ""; setAssignDialog({ open: true, row, @@ -531,7 +503,8 @@ export default function CollectionMapPage() { initialDate, }); }, - [date] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const submitAssign = useCallback( @@ -599,7 +572,7 @@ export default function CollectionMapPage() {

{stopCount}

- For {formatDateLabel(date)} across {stopsWithCoords.length} supplier{stopsWithCoords.length === 1 ? "" : "s"} + Across {stopsWithCoords.length} supplier{stopsWithCoords.length === 1 ? "" : "s"}

@@ -624,50 +597,11 @@ export default function CollectionMapPage() { {/* Toolbar */} -
-
- {/* Date picker */} -
- - -
-
- {[ - { label: "Today", offset: 0 }, - { label: "Tomorrow", offset: 1 }, - { label: "+3 days", offset: 3 }, - { label: "Next week", offset: 7 }, - ].map((q) => { - const target = addDays(ymdToday(), q.offset); - const active = target === date; - return ( - - ); - })} -
-
- -
- -
+
+
@@ -907,6 +841,9 @@ export default function CollectionMapPage() { onSubmit={submitAssign} initialDriverId={assignDialog.initialDriverId} initialDate={assignDialog.initialDate} + requireReadyForCollection + readyForCollection={assignDialog.row?.ready_for_collection ?? false} + busy={loading} eyebrow={ assignDialog.row ? (