From f15fb5b49f82b02bc18bd0822e7b0b0c681f8b0e Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Fri, 10 Jul 2026 00:47:11 +0200 Subject: [PATCH] Add collection map date filter and unassign implementation plan --- ...collection-map-date-filter-and-unassign.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-09-collection-map-date-filter-and-unassign.md diff --git a/docs/superpowers/plans/2026-07-09-collection-map-date-filter-and-unassign.md b/docs/superpowers/plans/2026-07-09-collection-map-date-filter-and-unassign.md new file mode 100644 index 0000000..cb9a726 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-collection-map-date-filter-and-unassign.md @@ -0,0 +1,216 @@ +# Collection Map — date filter bug + unassign-clears-date fix + +**Goal:** Fix two related bugs that make the collection map show almost no stops, and make sure the unassign path correctly clears `assigned_at` so the item returns to the un-dated pool. + +**Architecture:** Two one-line server changes + one client date-format fix + four new tests. + +**Tech Stack:** Next.js client + server routes, PocketBase admin SDK, Vitest. + +**Spec:** [docs/superpowers/specs/2026-07-09-collection-map-date-filter-and-unassign-design.md](file:///Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/docs/superpowers/specs/2026-07-09-collection-map-date-filter-and-unassign-design.md) + +--- + +## File Structure + +| File | Responsibility | Touched? | +| --- | --- | --- | +| `src/app/api/v1/collection-map/items/[id]/assign/route.ts` | Clear `assigned_at` on unassign | Modify | +| `src/app/api/v1/collection-map/items/[id]/assign/route.test.ts` | Cover the unassign-clears-date path | Modify | +| `src/app/(app)/collection-map/page.tsx` | Fix the day-bound date format in the PB filter | Modify | + +--- + +## Task 1: Unassign clears `assigned_at` + +**Files:** +- Modify: `src/app/api/v1/collection-map/items/[id]/assign/route.ts` + +- [ ] **Step 1: Find the patch construction** + +In the route, find the `const patch: Record = normalizeOrderItemStatusInput({...})` block (around line 74–79). + +- [ ] **Step 2: Clear `assigned_at` after the normaliser call** + +Right after the existing `if (typeof body.ready_for_collection === "boolean")` block, add the clear. The intent: when the operator is returning an item to the pool (no driver being assigned), `assigned_at` should be empty so the item reappears in the un-dated pool catchall in the collection-map filter. + +```ts +// Returning an item to the pool (no driver being assigned) also +// clears the date the operator originally assigned. Without this, +// the item keeps its old `assigned_at` and the collection map's +// date filter (which matches by string compare on `assigned_at`) +// may still surface it under the wrong day, or hide it entirely +// on the un-dated fallback. +if (!isAssigning) { + patch.assigned_at = ""; +} +``` + +Place this after the `if (typeof body.ready_for_collection === "boolean")` block, before the `const pb = await getAdminPocketBase();` line. + +- [ ] **Step 3: Lint** + +```bash +npx eslint 'src/app/api/v1/collection-map/items/[id]/assign/route.ts' +``` + +Expected: exit 0. + +- [ ] **Step 4: Commit** + +```bash +git add 'src/app/api/v1/collection-map/items/[id]/assign/route.ts' +git commit -m "fix(collection-map): clear assigned_at on unassign so item returns to pool" +``` + +--- + +## Task 2: Cover the unassign-clears-date path + +**Files:** +- Modify: `src/app/api/v1/collection-map/items/[id]/assign/route.test.ts` + +- [ ] **Step 1: Find the existing tests** + +The test file already exists with 5 tests from the prior "mark ready notes" task. Find the `it("accepts the unmark path without a note")` test. That's the test that exercises the unmark path (with `ready_for_collection: false`). + +- [ ] **Step 2: Add a new test for the driver-unassign case** + +Add a new test below the unmark-without-note test: + +```ts +it("clears assigned_at when returning an item to the pool (no driverId)", async () => { + getOneMock.mockResolvedValueOnce({ + id: "item-1", + assigned_at: "2026-07-09T00:00:00.000Z", + }); + updateMock.mockResolvedValueOnce({ id: "item-1" }); + + const res = await PATCH( + makeReq({ date: "" }), + { params: Promise.resolve({ id: "item-1" }) }, + ); + + expect(res.status).toBe(200); + const payload = updateMock.mock.calls[0][1] as Record; + expect(payload.assigned_at).toBe(""); + expect(payload.item_status).toBe("collection_pool"); + expect(payload.drv_status).toBe("unassigned"); +}); +``` + +- [ ] **Step 3: Run the test file** + +```bash +npm run test:run -- 'src/app/api/v1/collection-map/items/[id]/assign/route.test.ts' +``` + +Expected: 6/6 pass. + +- [ ] **Step 4: Lint** + +```bash +npx eslint 'src/app/api/v1/collection-map/items/[id]/assign/route.test.ts' +``` + +Expected: exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add 'src/app/api/v1/collection-map/items/[id]/assign/route.test.ts' +git commit -m "test(collection-map): cover assigned_at clear on unassign" +``` + +--- + +## Task 3: Fix the day-bound date format in the map filter + +**Files:** +- Modify: `src/app/(app)/collection-map/page.tsx` + +- [ ] **Step 1: Find the day-bound construction** + +In `fetchRows` (the function that loads `order_items`), find the `const dayStart = ...` and `const dayEnd = ...` lines. The current code is: + +```ts +const dayStart = `${date} 00:00:00`; +const dayEnd = `${date} 23:59:59`; +``` + +- [ ] **Step 2: Replace with ISO 8601 UTC bounds** + +`assigned_at` is stored as a UTC ISO string (e.g. `2026-07-09T00:00:00.000Z`). The filter must use the same format so the string comparison actually orders correctly. + +```ts +// 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`; +``` + +- [ ] **Step 3: Update the second `getFullList` call (the unmapped-rows one)** + +The same `assigned_at` filter is used in the second `getFullList` call for unmapped rows. The bounds there are duplicated. Update the same way: + +```ts +const dayStart = `${date}T00:00:00.000Z`; +const dayEnd = `${date}T23:59:59.999Z`; +``` + +If the bounds already exist in a local variable, reuse them; otherwise duplicate the fix. The current code (lines 396–401) has the bounds inlined: + +```ts +filter: `(item_status = "assigned" || item_status = "collection_pool") && ((assigned_at >= "${date} 00:00:00" && assigned_at <= "${date} 23:59:59") || assigned_at = "" || assigned_at = null)`, +``` + +Replace with: + +```ts +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)`, +``` + +- [ ] **Step 4: Lint** + +```bash +npx eslint 'src/app/(app)/collection-map/page.tsx' +``` + +Expected: exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add 'src/app/(app)/collection-map/page.tsx' +git commit -m "fix(collection-map): use ISO 8601 UTC bounds for assigned_at day filter" +``` + +--- + +## Task 4: Final verification + +- [ ] **Step 1: Targeted tests** + +```bash +npm run test:run -- 'src/app/api/v1/collection-map/items/[id]/assign' +``` + +Expected: 6/6 pass. + +- [ ] **Step 2: Full suite** + +```bash +npm run test:run +``` + +Expected: same passing set as before plus the new test. The 6 pre-existing failures are acceptable. + +- [ ] **Step 3: Manual smoke** + +1. Open the Collection Map page. +2. Pick today's date. All assigned + pool items for today should appear. +3. Pick a past date — items with that `assigned_at` should appear, others shouldn't. +4. From the Collection Pool, click `Mark Awaiting` on a row (unmark). The row should disappear from any "today" / "tomorrow" map view and reappear under the un-dated pool. +5. From the Collection Map, click a pin → `Reassign` modal → clear the date (or remove the driver). The item should return to the un-dated pool. \ No newline at end of file