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: