From 17ae928331287103bb58fd5c50eb795e62a62e8c Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Tue, 7 Jul 2026 18:01:43 +0200 Subject: [PATCH] refactor: update pool UI, fix tiles and config overhaul the collection pool page's ready status UI: split the clickable chip into a static status indicator and dedicated toggle button with loading state, improving user clarity between status and actionable elements update all related copy (toast messages, filter labels, button text) to use consistent "awaiting supplier confirmation" terminology across the interface remove invalid `transparent` prop from traffic tile layers in both the collection map and traffic tiles components, update comments to reflect Leaflet does not support this option add wildcard to allowedDevOrigins in next.config.ts to simplify local development testing across different network interfaces add a comprehensive database backfill script to fix assigned order items missing the `ready_for_collection` flag, ensuring drivers see their correct assigned stops in the driver app --- next.config.ts | 2 +- .../2026-07-07-backfill-assigned-rfc.ts | 126 ++++++++++++++++++ src/app/(app)/collection-map/page.tsx | 1 - src/app/(app)/collection-pool/page.tsx | 81 +++++++---- src/components/traffic-tiles.tsx | 5 +- 5 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 scripts/migrations/2026-07-07-backfill-assigned-rfc.ts diff --git a/next.config.ts b/next.config.ts index 47250cd..6194e6f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,7 +6,7 @@ const nextConfig: NextConfig = { // app is normally loaded on `localhost`, but we also load it on // `127.0.0.1` (and on the LAN IP for phone testing). Listing both keeps // the HMR client from hanging on the first load. - allowedDevOrigins: ["127.0.0.1", "localhost", "192.168.1.122", "192.168.1.128"], + allowedDevOrigins: ["127.0.0.1", "localhost", "192.168.1.122", "192.168.1.128", "*"], }; export default nextConfig; diff --git a/scripts/migrations/2026-07-07-backfill-assigned-rfc.ts b/scripts/migrations/2026-07-07-backfill-assigned-rfc.ts new file mode 100644 index 0000000..760740c --- /dev/null +++ b/scripts/migrations/2026-07-07-backfill-assigned-rfc.ts @@ -0,0 +1,126 @@ +/** + * Backfill — flip `ready_for_collection` to `true` on every row that is + * already `item_status = "assigned"` but never had the flag set. + * + * Why this exists as a sibling to the original migration: + * + * `2026-07-07-order-items-ready-for-collection.ts` introduced the + * `ready_for_collection` bool AND backfilled every existing + * `collection_pool` row to `true`. It deliberately did NOT touch + * rows that were already `assigned` — at the time the migration + * ran, the theory was "assigned items have already been hand-vetted + * by the operator, so the flag should already be on them, so we + * don't need to backfill". + * + * That theory was wrong. In practice, several `assigned` rows had + * been created via paths that did not set the flag (direct PB admin + * edits, an older assignment workflow that pre-dated the column). + * The driver app's `getMyAssignedItems` filter requires + * `ready_for_collection = true`, so the driver sees an empty + * Assigned tab even though they have real stops in the DB. + * + * This script mirrors the original pool-backfill shape so the gap + * closes without forcing the operator to re-toggle every row in the + * dashboard. + * + * Idempotent — only flips rows where `ready_for_collection != true`. + * + * Run with (from WFDASH_NEXTJS so .env.local is found): + * npx tsx scripts/migrations/2026-07-07-backfill-assigned-rfc.ts + */ +/* eslint-disable */ +import PocketBase from "pocketbase"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +function loadEnvFile(name: string) { + try { + const raw = readFileSync(resolve(process.cwd(), name), "utf8"); + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i); + if (m && !process.env[m[1]]) { + process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim(); + } + } + } catch { + /* fine — rely on shell env */ + } +} +loadEnvFile(".env"); +loadEnvFile(".env.local"); + +async function main() { + const url = process.env.NEXT_PUBLIC_POCKETBASE_URL; + const email = process.env.PB_ADMIN_EMAIL; + const password = process.env.PB_ADMIN_PASSWORD; + if (!url || !email || !password) { + throw new Error( + "Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD (e.g. sourced from .env.local).", + ); + } + + const pb = new PocketBase(url); + await pb.collection("_superusers").authWithPassword(email, password); + console.log(`[backfill-assigned-rfc] Authenticated against ${url}`); + + // Sanity check — make sure the column exists before we query on it. + const col = await pb.collections.getOne("order_items"); + const hasField = (col.fields as Array<{ name: string }>).some( + (f) => f.name === "ready_for_collection", + ); + if (!hasField) { + throw new Error( + "ready_for_collection field missing on order_items — run 2026-07-07-order-items-ready-for-collection.ts first.", + ); + } + + // Find every assigned row that isn't already ready. + // `!= true` matches false AND null — null is the default for rows + // created before the column existed, plus any rows where the + // boolean was never written. + const rows = await pb.collection("order_items").getFullList({ + filter: 'item_status = "assigned" && ready_for_collection != true', + fields: "id,item_status,ready_for_collection,drv_id", + }); + + console.log( + `[backfill-assigned-rfc] candidates (assigned, rfc!=true): ${rows.length}`, + ); + + let flipped = 0; + let failed = 0; + for (const row of rows) { + try { + await pb.collection("order_items").update(row.id, { + ready_for_collection: true, + }); + flipped++; + } catch (err) { + failed++; + console.error( + `[backfill-assigned-rfc] update failed for ${row.id}:`, + err instanceof Error ? err.message : err, + ); + } + } + + // Post-state snapshot — confirm the filter now returns 0. + const remaining = await pb.collection("order_items").getFullList({ + filter: 'item_status = "assigned" && ready_for_collection != true', + fields: "id", + }); + + console.log(`[backfill-assigned-rfc] flipped: ${flipped}`); + console.log(`[backfill-assigned-rfc] failed: ${failed}`); + console.log( + `[backfill-assigned-rfc] post-state: ${remaining.length} assigned rows still have rfc!=true`, + ); + console.log("[backfill-assigned-rfc] Done."); +} + +main().catch((err) => { + console.error("[backfill-assigned-rfc] failed:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/src/app/(app)/collection-map/page.tsx b/src/app/(app)/collection-map/page.tsx index 17107e1..76f245d 100644 --- a/src/app/(app)/collection-map/page.tsx +++ b/src/app/(app)/collection-map/page.tsx @@ -1196,7 +1196,6 @@ function TrafficOverlay({ enabled }: { enabled: boolean }) { maxZoom: 19, tileSize: 256, opacity: 0.55, - transparent: true, // Silent error — broken tiles fade to the OSM base. If we // ever start 429-ing en masse, this is where we'd surface it. errorTileUrl: diff --git a/src/app/(app)/collection-pool/page.tsx b/src/app/(app)/collection-pool/page.tsx index fe1124b..143d70d 100644 --- a/src/app/(app)/collection-pool/page.tsx +++ b/src/app/(app)/collection-pool/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Truck, FileText, AlertTriangle, ExternalLink, Search, Users, Calendar, Building2, UserRound, Check } from "lucide-react"; +import { Truck, FileText, AlertTriangle, ExternalLink, Search, Users, Calendar, Building2, UserRound, Check, Loader2 } from "lucide-react"; import Link from "next/link"; import { toast } from "sonner"; @@ -464,7 +464,7 @@ export default function CollectionPoolPage() { const text = await res.text().catch(() => ""); throw new Error(text || `HTTP ${res.status}`); } - toast.success(ready ? "Marked ready for collection." : "Marked unready."); + toast.success(ready ? "Marked ready for collection." : "Marked awaiting supplier confirmation."); await load(); } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -506,7 +506,7 @@ export default function CollectionPoolPage() { setBusyId(null); } } - if (ok > 0) toast.success(`${ok} item${ok === 1 ? "" : "s"} marked ${ready ? "ready" : "unready"}.`); + if (ok > 0) toast.success(`${ok} item${ok === 1 ? "" : "s"} marked ${ready ? "ready" : "awaiting supplier confirmation"}.`); if (failed > 0) toast.error(`${failed} failed to update.`); setSelected(new Set()); await load(); @@ -576,7 +576,7 @@ export default function CollectionPoolPage() { : "text-muted-foreground hover:bg-muted" )} > - {opt === "all" ? "All" : opt === "ready" ? "Ready only" : "Unready only"} + {opt === "all" ? "All" : opt === "ready" ? "Ready only" : "Awaiting only"} ))} @@ -853,25 +853,30 @@ function PoolRow({ {ageChip.label} - {/* Ready-for-collection chip — supplier-confirmed gate. - Click toggles the row's ready state. Green = the supplier - has confirmed the goods are ready to pick up (drivers see - this row); amber = still waiting on the supplier quote. */} - + {row.ready_for_collection ? "Ready" : "Awaiting"} + {isAssigned && (
@@ -920,7 +925,7 @@ function PoolRow({ )}
-
+
+
@@ -967,8 +1001,9 @@ function BulkAssignDriverButton({ /** * Supplier-header bulk action that flips `ready_for_collection` for every * selected row at once. Two buttons side-by-side: "Mark ready" (default) - * and "Mark unready". Same visual treatment as the other bulk action so - * the supplier header reads as a small toolbar. + * and "Mark awaiting". Same visual treatment as the other bulk action so + * the supplier header reads as a small toolbar. Vocabulary mirrors the + * row-level status indicator so operators learn one set of labels. */ function BulkToggleReadyButton({ rows, @@ -997,7 +1032,7 @@ function BulkToggleReadyButton({ variant="ghost" onClick={() => onBulkToggleReady(rows.map((r) => r.id), false)} > - Unready ({rows.length}) + Mark awaiting ({rows.length}) ); diff --git a/src/components/traffic-tiles.tsx b/src/components/traffic-tiles.tsx index bf4336f..a3a7895 100644 --- a/src/components/traffic-tiles.tsx +++ b/src/components/traffic-tiles.tsx @@ -73,7 +73,8 @@ export function TrafficTiles({ opacity, // Clear pixel where TomTom has no traffic data, so the OSM base // map shows through instead of rendering a grey block. - transparent: true, + // (Leaflet has no `transparent` option — empty PNG tiles are + // already transparent on render, so this is handled implicitly.) // Fade broken tiles into the OSM base so a single 429 doesn't // black out a quarter of the map. errorTileUrl: @@ -131,7 +132,7 @@ export function createTrafficLayer({ maxZoom: 19, tileSize: 256, opacity, - transparent: true, + // No `transparent` option — Leaflet tiles are whatever the source PNG is. errorTileUrl: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", attribution: