docs(04-01): complete cursor-pagination API and filter components plan summary
This commit is contained in:
parent
9e10d654ad
commit
bbcb04fe3b
1 changed files with 188 additions and 0 deletions
188
.planning/phases/04-tickets-restyle/04-01-SUMMARY.md
Normal file
188
.planning/phases/04-tickets-restyle/04-01-SUMMARY.md
Normal file
|
|
@ -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=<b64>&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*
|
||||
Loading…
Add table
Add a link
Reference in a new issue