From bbcb04fe3ba48a3aaff43b005555d6eddc1edeb9 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 18:03:14 -0400 Subject: [PATCH] docs(04-01): complete cursor-pagination API and filter components plan summary --- .../04-tickets-restyle/04-01-SUMMARY.md | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 .planning/phases/04-tickets-restyle/04-01-SUMMARY.md diff --git a/.planning/phases/04-tickets-restyle/04-01-SUMMARY.md b/.planning/phases/04-tickets-restyle/04-01-SUMMARY.md new file mode 100644 index 0000000..5d2fb41 --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-01-SUMMARY.md @@ -0,0 +1,188 @@ +--- +phase: 04-tickets-restyle +plan: 01 +subsystem: api, ui +tags: [mobile, tickets, cursor-pagination, typescript, shadcn, collapsible, skeleton] + +# Dependency graph +requires: + - phase: 03-dashboard-restyle + provides: "Pattern for exporting TypeScript interfaces from /api/mobile/* route files" + - phase: 02-mobile-shell-more-drawer + provides: "Mobile layout shell (HeaderBar, BottomNav, MoreDrawer) that ticket pages dock inside" +provides: + - "GET /api/mobile/tickets: cursor-paginated endpoint returning { tickets, nextCursor, hasMore } with exported MobileTicket and MobileTicketListResponse interfaces" + - "TicketRowSkeleton component: 5-row placeholder matching priority-stripe layout" + - "TicketFilterStrip component: Collapsible filter strip with TicketFilterValue and QueueOption interfaces" +affects: [04-02, 04-03] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Cursor-based keyset pagination: base64(JSON({ last_activity_date, id })) with limit+1 fetch to detect hasMore" + - "Controlled filter strip component: parent owns URL sync, component owns Collapsible open state only" + - "TypeScript interface exports from API route file for import type in page (Phase 3 pattern continued)" + +key-files: + created: + - components/mobile/TicketFilterStrip.tsx + - components/mobile/TicketRowSkeleton.tsx + modified: + - app/api/mobile/tickets/route.ts + +key-decisions: + - "Cursor encodes { last_activity_date: ISO string, id: number } as base64 JSON — snake_case preserved for round-trip stability (D-09)" + - "Default status filter [1, 8, 7] (Open + In Progress + Waiting) applied server-side when no status param — matches legacy t.status != 5 behaviour" + - "Server-side limit cap: Math.min(25, ...) regardless of caller input (D-11, T-04-04)" + - "requireAuth() added as security hardening — legacy route lacked auth gate (T-04-03)" + - "getMobileCompanyFilter() preserved byte-identical — kiosk_settings company scoping unmodified" + - "TicketFilterStrip is purely controlled — no internal filter state beyond Collapsible open/close" + +patterns-established: + - "Cursor seek predicate: (t.last_activity_date, t.id) < ($N::timestamp, $M::int) for stable DESC keyset pagination" + - "limit+1 fetch pattern: avoids COUNT query for hasMore detection" + - "Phase comment block on mobile components: /* ComponentName — phase 04 (TICK-NN). */" + +requirements-completed: [TICK-01, TICK-02, TICK-05] + +# Metrics +duration: 4min +completed: 2026-05-03 +--- + +# Phase 4 Plan 01: Tickets API + Filter Strip + Skeleton Summary + +**Cursor-paginated /api/mobile/tickets route with exported TypeScript interfaces, TicketFilterStrip Collapsible component, and TicketRowSkeleton — foundation for Plan 02 page wiring** + +## Performance + +- **Duration:** 4 min +- **Started:** 2026-05-03T21:58:17Z +- **Completed:** 2026-05-03T21:59:04Z +- **Tasks:** 2 +- **Files modified:** 3 + +## Accomplishments + +- Rewrote `app/api/mobile/tickets/route.ts` from page/offset pagination to cursor-based keyset pagination returning `{ tickets, nextCursor, hasMore }` with exported `MobileTicket` and `MobileTicketListResponse` interfaces +- Added `requireAuth()` security gate (legacy route was unauthenticated — T-04-03) +- Created `TicketFilterStrip` with Collapsible panel, four filter controls (status chips, priority chips, queue Select, mine Switch), and `TicketFilterValue`/`QueueOption` exports for Plan 02 +- Created `TicketRowSkeleton` with exact UI-SPEC shape (`border-l-4 border-muted` priority stripe + 3 Skeleton lines) + +## Exported Interface Signatures + +```typescript +// app/api/mobile/tickets/route.ts +export interface MobileTicket { + id: number; + ticket_number: string; + title: string; + status: number; + priority: number; + create_date: string; + last_activity_date: string; + due_date_time: string | null; + queue_id: number; + queue_label: string; + company_name: string; + assigned_to: string; +} + +export interface MobileTicketListResponse { + tickets: MobileTicket[]; + nextCursor: string | null; + hasMore: boolean; +} +``` + +```typescript +// components/mobile/TicketFilterStrip.tsx +export interface QueueOption { id: number; label: string; } +export interface TicketFilterValue { + q: string; + status: number[]; + priority: number[]; + queue: number | null; + mine: boolean; +} +``` + +## API Contract Details + +- **Cursor encoding:** `base64(JSON({ last_activity_date: ISO string, id: number }))` +- **Default status filter:** `[1, 8, 7]` (Open + In Progress + Waiting) — applied when `status` param absent +- **Server-side limit cap:** 25 rows maximum +- **getMobileCompanyFilter():** Preserved byte-identical — kiosk_settings company scoping unmodified +- **Sort order:** `ORDER BY t.last_activity_date DESC NULLS LAST, t.id DESC` + +## Component File Paths + +| File | Exports | +|------|---------| +| `components/mobile/TicketFilterStrip.tsx` | `TicketFilterStrip`, `TicketFilterValue`, `QueueOption`, `TicketFilterStripProps` | +| `components/mobile/TicketRowSkeleton.tsx` | `TicketRowSkeleton` | + +Plan 02 import pattern: +```typescript +import { TicketFilterStrip, type TicketFilterValue, type QueueOption } from '@/components/mobile/TicketFilterStrip'; +import { TicketRowSkeleton } from '@/components/mobile/TicketRowSkeleton'; +import type { MobileTicket, MobileTicketListResponse } from '@/app/api/mobile/tickets/route'; +``` + +## Task Commits + +1. **Task 1: Rewrite /api/mobile/tickets to cursor-paginated shape** - `6268d1f` (feat) +2. **fix: Restore phase 2/3 work lost by worktree soft-reset** - `9658640` (fix — deviation, see below) +3. **Task 2: TicketRowSkeleton and TicketFilterStrip components** - `9e10d65` (feat) + +## Files Created/Modified + +- `app/api/mobile/tickets/route.ts` — Rewritten: cursor pagination, exported interfaces, requireAuth(), 25-row cap, default status filter +- `components/mobile/TicketFilterStrip.tsx` — New: Collapsible filter strip with controlled prop contract +- `components/mobile/TicketRowSkeleton.tsx` — New: 5-row skeleton placeholder matching priority-stripe row layout + +## Decisions Made + +- Cursor encodes snake_case keys (`last_activity_date`, `id`) to match PostgreSQL column names in the MobileTicket interface — preserves round-trip stability without rename +- `TicketFilterStrip` manages only Collapsible open/close state; all filter values are controlled props — URL sync stays in Plan 02 page +- `activeCount` excludes the default `[1, 7, 8]` status set so the badge only counts user-initiated changes above the default + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Restored phase 2/3 files deleted by worktree soft-reset** +- **Found during:** Task 1 commit +- **Issue:** The `git reset --soft 77073ba` staged deletions of all phase 2 and 3 artifacts (mobile shell components, dashboard route, layout, planning files) which were then swept into the Task 1 commit +- **Fix:** Restored all missing files from their source commits (`28b5845` for phase 2/3 work, `77073ba` for phase 4 plan files) and committed the restoration +- **Files modified:** 50 files (see `9658640` commit) +- **Verification:** `ls components/mobile/` shows all 6 prior-phase components present +- **Committed in:** `9658640` (separate fix commit) + +--- + +**Total deviations:** 1 auto-fixed (Rule 3 - blocking) +**Impact on plan:** Required to preserve prior phase work. No scope creep. + +## Issues Encountered + +The worktree base check protocol (`git reset --soft 77073ba`) caused all files added since that commit to appear as staged deletions, which were accidentally swept into the first task commit. The restoration commit (`9658640`) recovers all prior-phase artifacts. Subsequent commits on this worktree are clean. + +## Known Stubs + +None — no data is hardcoded or stubbed. The API route queries live PostgreSQL. Components are presentational with controlled props; data wiring happens in Plan 02. + +## Next Phase Readiness + +Plan 02 (`04-02`) can now: +- `import type { MobileTicket, MobileTicketListResponse }` from the route file +- `import { TicketFilterStrip, type TicketFilterValue, type QueueOption }` for the filter strip +- `import { TicketRowSkeleton }` for the loading state +- Call `GET /api/mobile/tickets?cursor=&limit=25&status=1,8,7&priority=&queue=&mine=` for cursor pagination + +No blockers for Plan 02. + +--- +*Phase: 04-tickets-restyle* +*Completed: 2026-05-03*