diff --git a/docs/superpowers/specs/2026-07-09-collection-map-date-filter-and-unassign-design.md b/docs/superpowers/specs/2026-07-09-collection-map-date-filter-and-unassign-design.md new file mode 100644 index 0000000..3f467d9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-collection-map-date-filter-and-unassign-design.md @@ -0,0 +1,90 @@ +# Collection Map — date filter bug + unassign-clears-date fix + +**Goal:** Two related bugs make the Collection Map show almost no stops: + +1. **Date-format bug in the PB filter.** `assigned_at` is stored as `2026-07-09T00:00:00.000Z` (ISO 8601 UTC, with `T` separator), but the filter bounds are `2026-07-09 00:00:00` (with a space, no `Z`). String comparison puts `T` (0x54) before the space (0x20), so the `>=` bound excludes every real row. The map then shows only items with empty `assigned_at` (the un-dated pool fallback) — which is why the user sees "one stop regardless of filter". + +2. **Unassign doesn't clear `assigned_at`.** When the operator returns an item to the pool (no driver), the route passes `assigned_at: undefined` to `normalizeOrderItemStatusInput`, which silently skips the field. The item keeps its old date and gets re-filtered into the wrong day (or hidden by the date-format bug). The user expects: unassign resets the date so the item returns to the un-dated pool. + +## Scope + +### In Scope + +- Fix the day-bound date format in `fetchRows` in the Collection Map page (`page.tsx`). Two filters use it; both fixed. +- When the operator is unassigning (no `driverId`), the route clears `assigned_at: ""` so the item returns to the un-dated pool. +- One new test covering the unassign-clears-date path. + +### Out of Scope + +- Changing how the assign route stores `assigned_at` (it already stores `T00:00:00.000Z` correctly). +- The collection-pool unmark flow (`Mark Awaiting`) — it only flips `ready_for_collection`, doesn't change driver/date, so it's not affected. +- Timezone changes. `assigned_at` is UTC; bounds stay UTC. + +## Current State + +`fetchRows` in `src/app/(app)/collection-map/page.tsx` (around line 340) builds: + +```ts +const dayStart = `${date} 00:00:00`; +const dayEnd = `${date} 23:59:59`; +const filter = `(item_status = "assigned" || item_status = "collection_pool") && ((assigned_at >= "${dayStart}" && assigned_at <= "${dayEnd}") || assigned_at = "" || assigned_at = null)`; +``` + +The assign route stores `assigned_at: ${date}T00:00:00.000Z`. Filter bound is `${date} 00:00:00`. `localeCompare` / string sort puts `T` (0x54) before ` ` (0x20), so the bound `assigned_at >= "${date} 00:00:00"` fails for every real row. + +`fetchRows` second call (the unmapped-rows query) duplicates the bad bounds. + +`/api/v1/collection-map/items/[id]/assign/route.ts` passes `assigned_at: assignedAt` to `normalizeOrderItemStatusInput`. When the operator is unassigning (no `date` provided), `assignedAt` is `undefined`, the normaliser skips the field, and PB keeps the old `assigned_at`. + +## User Confirmed Rule + +- Unassign clears the date. Item returns to the un-dated pool. (Same behavior as a brand-new pool row that has never been assigned.) + +## Behavior + +### Server — unassign clears `assigned_at` + +In the assign route, when `!isAssigning` (operator is returning to the pool), set `patch.assigned_at = ""` after the normaliser call. The normaliser already accepts `""` as a valid string and writes it; we just need to pass it explicitly. + +```ts +if (!isAssigning) { + patch.assigned_at = ""; +} +``` + +The `ready_for_collection` flag is left alone (the supplier-side gate is independent of driver assignment). `drv_id` is already cleared by the normaliser. + +### Client — fixed date bounds + +```ts +const dayStart = `${date}T00:00:00.000Z`; +const dayEnd = `${date}T23:59:59.999Z`; +``` + +Used in both `getFullList` filters in `fetchRows`. `999` (rather than `000` of the next day) is fine for string comparison because the stored value is always at `T00:00:00.000Z` of the assigned day. + +## Testing + +### Route tests + +- New test: `clears assigned_at when returning an item to the pool (no driverId)`. POST with `{ date: "" }` and an existing `assigned_at` on the row. The PATCH payload must contain `assigned_at: ""`. + +Existing tests must still pass: + +- `accepts the unmark path without a note (ready_for_collection: false)` — uses `date` unset, must still 200. +- `writes trimmed notes to the item when ready_notes is provided` — uses a new assignment with a date, must still write `assigned_at`. + +### Client tests + +No automated test for the client filter. The fix is one string constant; manual smoke covers it. + +## Acceptance Criteria + +- Picking any date in the Collection Map shows the assigned items for that date (and the un-dated pool items, as today). +- Unassigning an item from the Collection Map or Collection Pool clears its `assigned_at`, so it shows up under the un-dated pool. +- All targeted tests pass; full suite non-regressive. + +## Risk / Rollback + +- Low risk. Two one-line changes. +- Rollback: revert the route change and the page constant. No schema change. \ No newline at end of file