diff --git a/.planning/phases/04-tickets-restyle/04-CONTEXT.md b/.planning/phases/04-tickets-restyle/04-CONTEXT.md new file mode 100644 index 0000000..c2256be --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-CONTEXT.md @@ -0,0 +1,142 @@ +# Phase 4: Tickets Restyle - Context + +**Gathered:** 2026-05-03 (auto mode) +**Status:** Ready for planning + + +## Phase Boundary + +Reskin the mobile Tickets surfaces (`/mobile/tickets` list + `/mobile/tickets/[id]` detail header) to match the new shell. Replace the current sticky search/filter bar with a Collapsible URL-synced filter strip. Replace page-based pagination (`?page=N`, 30/page) with cursor-based infinite scroll (~25/page, IntersectionObserver) plus a "Load more" fallback. Add priority left-edge stripes to list rows. Detail page body stays largely as-is — only its header is reskinned. + +In scope: list page UI + filter strip + URL deep-linking + cursor pagination API change + detail header reskin. + +Out of scope: filter set expansion (only the four spec'd filters), detail body refactor, comment/attach UI changes, search UX overhaul beyond what existing search input provides. + + + +## Implementation Decisions + +### Filter strip +- **D-01:** Use shadcn `Collapsible` component for the filter strip; default state is **collapsed** (only the search input + "Filters" toggle button visible). Reason: TICK-01 spec says default collapsed; matches phone-first density goal. +- **D-02:** Expanded panel exposes exactly four controls: status (multi-select chips: Open/In Progress/Waiting), priority (chips: Critical/High/Medium/Low), queue (Select dropdown sourced from existing queues), and an "Assigned to me" toggle. No additional filter capabilities this phase. +- **D-03:** Keep the existing search input visible at all times (above the Collapsible toggle), debounced 400ms — match current behavior. +- **D-04:** "Clear all" button appears in the expanded panel when ≥1 filter is active. + +### URL sync +- **D-05:** Use `useSearchParams()` + `router.replace()` (NOT `router.push()`) to update query string on filter changes. Reason: replace prevents back-button pollution from filter tweaks; matches Next.js App Router convention. +- **D-06:** URL param keys: `q` (search), `status` (comma-separated ints), `priority` (comma-separated ints), `queue` (int), `mine` (`1`/absent). On reload, page hydrates filter state from these params — deep link works. +- **D-07:** Cursor (`cursor`) is intentionally **not** persisted to URL — fresh visits always start at the top of the list. + +### Pagination — cursor model +- **D-08:** Replace `?page=N&limit=30` with `?cursor=&limit=25`. Reason: TICK-05 mandates cursor-based ~25/page. +- **D-09:** Cursor is a base64-encoded JSON of `{ last_activity_date: ISO string, id: number }`. Reason: `last_activity_date DESC, id DESC` matches the manager's triage mental model (recently-touched bubbles up); tie-breaker on id makes it stable. +- **D-10:** API returns `{ tickets: [...], nextCursor: string | null, hasMore: boolean }`. When `nextCursor` is null, list is exhausted. +- **D-11:** Page size is 25 (per TICK-05). The `limit` query param is accepted but capped server-side at 25 to prevent abuse. + +### Infinite scroll trigger +- **D-12:** Use the browser-native `IntersectionObserver` API (no library). A sentinel `
` lives at the end of the list; when it intersects the viewport with `rootMargin: '200px'`, fetch next page. +- **D-13:** Guard against duplicate fetches: the sentinel callback is a no-op if `loadingMore || !hasMore`. +- **D-14:** Always render a focusable "Load more" button below the sentinel as the accessibility fallback (TICK-06). Clicking it triggers the same fetch path. Hide only when `!hasMore`. + +### List row presentation +- **D-15:** Each row has a 4px-wide left-edge color stripe via `border-l-4` + a priority color class: + - 1 (Critical) → `border-red-500` + - 2 (High) → `border-orange-400` + - 3 (Medium) → `border-amber-400` + - 4 (Low) → `border-slate-300` + Reason: TICK-03 spec. +- **D-16:** Row body shows: ticket number (mono small), title (1-line truncate), company (muted small), age (relative time, muted), assignee (initials avatar or text). Single-tap navigates to `/mobile/tickets/[id]` (TICK-04). +- **D-17:** Remove the priority dot from the existing row (replaced by the stripe). Keep `relTime()` helper as-is. + +### Detail page header +- **D-18:** Detail page (`/mobile/tickets/[id]`) keeps its current body. Only the in-page header bar at the top is reskinned: replace the current title bar with a row containing a back chevron (`ArrowLeft` icon → `router.back()`), the breadcrumb "Tickets / #{ticket_number}", and an external-link icon that opens the desktop ticket URL. The shell's HeaderBar (Wulf + Bell + avatar) already renders above it from `app/mobile/layout.tsx` — no change there. +- **D-19:** No structural changes to the detail body, comments, or attachments — out of scope for this phase. + +### Empty state +- **D-20:** When the active filter set returns zero results, render a centered message: "No tickets match your filters" with a "Clear filters" button. When there are no tickets at all (no filters, empty result), render "No tickets to triage right now" with a refresh affordance. + +### Loading & error states +- **D-21:** Initial load → skeleton rows (5 placeholder cards). Subsequent infinite scroll → small inline spinner above the Load more button. Error → toast + Load more button shows "Retry". + +### Claude's Discretion +- Exact spacing/typography within rows (match existing density) +- Whether to memoize row components (only if perf measurement warrants) +- Cursor encoding helper location (lib/services or inline in route) +- Exact skeleton visual + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Phase spec +- `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §6.2 (Tickets) — spec for filter strip, priority stripes, cursor pagination, detail header reskin +- `.planning/REQUIREMENTS.md` (TICK-01 through TICK-07) — locked acceptance criteria + +### Project conventions +- `CLAUDE.md` — Pulse stack rules (no SWR/react-query, no ORM, fetch-from-clients pattern), `/mobile/*` route boundary, `port 3100` +- `DESIGN.md` — token usage, navigation IA, current cleanup backlog +- `ARCHITECTURE.md` — runtime, data flow, workers (no impact this phase) + +### Prior phase context +- `.planning/phases/02-mobile-shell/02-CONTEXT.md` — MobileShell + HeaderBar decisions (the detail header docks under this) +- `.planning/phases/03-dashboard-restyle/03-01-SUMMARY.md` — pattern for new `/api/mobile/*` shape with TypeScript interface exports (mirror this for tickets endpoint) + +### Existing code (entry points) +- `app/mobile/tickets/page.tsx` — current list page (164 lines, page-based) +- `app/mobile/tickets/[id]/page.tsx` — current detail page (357 lines, body kept as-is) +- `app/api/mobile/tickets/route.ts` — current API route (90 lines, page-based, with `kiosk_settings` company scoping) + + + + +## Existing Code Insights + +### Reusable Assets +- `Collapsible` from shadcn (`components/ui/collapsible.tsx`) — exists in shadcn primitives +- `relTime()` helper inline in `app/mobile/tickets/page.tsx:29-37` — keep +- `kiosk_settings` company scoping helper `getMobileCompanyFilter()` in `app/api/mobile/tickets/route.ts:3-26` — keep, do not regress +- `lucide-react` icons (Search, X, RefreshCw, ChevronRight, Clock, ArrowLeft) — already in deps +- shadcn primitives: `Button`, `Input`, `Select`, `Toggle` (or `Switch`), `Skeleton` — all available in `components/ui/` +- `IntersectionObserver` — browser-native, no dep needed + +### Established Patterns +- Mobile pages are `'use client'` with `useState` + `useEffect` + `fetch('/api/mobile/...')` — NO SWR, NO react-query +- API routes call `postgresClient.query()` with parameterized SQL; manual snake_case → camelCase transform +- `requireAuth()` from `lib/auth-utils.ts` is the auth gate for all `/api/*` routes +- TypeScript interfaces for API response shapes are exported from the route file and imported via `import type` in the page (pattern from Phase 3) + +### Integration Points +- `app/mobile/layout.tsx` (Phase 2) renders the new shell — Tickets pages dock inside it; no layout changes needed +- BottomNav active-tab detection uses `pathname.startsWith('/mobile/tickets')` — already correct +- `kiosk_settings` table for company scoping — existing, already wired into the current ticket route + + + + +## Specific Ideas + +- Mirror Phase 3's pattern: API route exports `MobileTicketListResponse` and the row interface; page imports the response type via `import type`. +- Cursor encoder/decoder should be small (~10 lines) and live inline in the route file unless a second consumer appears. +- Status filter should default to "Open + In Progress + Waiting" (i.e. `status != 5`) matching the existing route's hardcoded filter, so the URL with no status param yields the same default the manager expects. + + + + +## Deferred Ideas + +- Bulk actions (mass close/assign) — out of scope; manager use case here is read-and-tap-into-detail +- Saved filter presets — not in TICK-* scope; consider for a future phase +- Detail page body refactor (comments, time entries) — explicitly out of scope (TICK-07 says "body kept largely as-is") +- Server-sent push of new tickets — no streaming this iteration +- Search highlight in row — nice-to-have, not in TICK-* scope +- Tablet/desktop responsive breakpoints — `/mobile` is phone-only by milestone constraint + + + +--- + +*Phase: 04-tickets-restyle* +*Context gathered: 2026-05-03* diff --git a/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md b/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md new file mode 100644 index 0000000..e788dcf --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md @@ -0,0 +1,127 @@ +# Phase 4: Tickets Restyle - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-05-03 +**Phase:** 04-tickets-restyle +**Mode:** auto (--auto --chain) +**Areas discussed:** Filter strip, URL sync, Pagination model, Infinite scroll trigger, List row presentation, Detail page header, Empty/loading states + +--- + +## Filter strip + +| Option | Description | Selected | +|--------|-------------|----------| +| Always-expanded inline filters | All controls visible by default — more taps reachable | | +| `Collapsible` from shadcn, default collapsed | TICK-01 spec; phone-first density | ✓ | +| Drawer-based filter sheet | Filters open in a Sheet — feels heavier, more taps | | + +**Auto choice rationale:** TICK-01 explicitly says "default collapsed". Spec-locked. + +--- + +## Filter set + +| Option | Description | Selected | +|--------|-------------|----------| +| Status + priority + queue + assigned-to-me | Spec set | ✓ | +| Add company filter | More flexibility; out of TICK-* scope | | +| Add date range | Could be useful; out of TICK-* scope | | + +**Auto choice rationale:** TICK-01 enumerates the four filters. Anything more is scope creep. + +--- + +## URL sync mechanism + +| Option | Description | Selected | +|--------|-------------|----------| +| `router.push()` on every filter change | Each filter tweak adds a back-stack entry | | +| `router.replace()` on every filter change | Filter tweaks don't pollute back stack | ✓ | +| Manual `history.replaceState()` | Lower-level; loses Next.js rerouting integration | | + +**Auto choice rationale:** `router.replace()` is the App Router idiom for filter UIs. Back button should return to whatever surface the user came from, not 8 filter mutations ago. + +--- + +## Pagination model + +| Option | Description | Selected | +|--------|-------------|----------| +| Keep page-based `?page=N&limit=30` | Simpler, current behavior | | +| Cursor `?cursor=&limit=25` with last_activity_date+id | Stable under writes; matches triage workflow | ✓ | +| Cursor on created_date+id | Stable but doesn't reflect "recent activity" priority | | + +**Auto choice rationale:** TICK-05 mandates cursor-based ~25/page. Choosing `last_activity_date DESC, id DESC` because managers triage by "what just changed" not "what was created". + +--- + +## Infinite scroll trigger + +| Option | Description | Selected | +|--------|-------------|----------| +| `react-intersection-observer` library | Hook abstraction, extra dep | | +| Browser-native `IntersectionObserver` | No dep, ~20 lines | ✓ | +| Scroll-event listener with throttle | Less precise, more re-renders | | + +**Auto choice rationale:** CLAUDE.md prohibits adding new state libraries; native IntersectionObserver is sufficient. + +--- + +## "Load more" fallback + +| Option | Description | Selected | +|--------|-------------|----------| +| Skip — sentinel handles it | Fails accessibility | | +| Always-rendered focusable button below sentinel | TICK-06 mandates accessibility | ✓ | +| Show only when keyboard navigation detected | Brittle, not robust | | + +**Auto choice rationale:** TICK-06 requires the fallback button. Always-rendered keeps it focusable and visible to screen readers. + +--- + +## List row priority indicator + +| Option | Description | Selected | +|--------|-------------|----------| +| Existing dot (bg-color-N) | Smaller, less prominent | | +| Left-edge stripe via `border-l-4` | TICK-03 spec | ✓ | +| Background tint of whole row | Too heavy on phone | | + +**Auto choice rationale:** TICK-03 explicitly specifies left-edge stripe. + +--- + +## Detail page header + +| Option | Description | Selected | +|--------|-------------|----------| +| Refactor full detail page | Out of scope per TICK-07 | | +| Reskin in-page header only (back chevron + breadcrumb + ext link) | TICK-07 spec | ✓ | + +**Auto choice rationale:** TICK-07 explicitly says "header reskinned... body kept largely as-is". + +--- + +## Empty/loading states + +| Option | Description | Selected | +|--------|-------------|----------| +| Spinner only on load | Janky, no skeleton | | +| Skeleton rows initial + inline spinner subsequent + empty-state messages | Polished, fits design tokens | ✓ | + +**Auto choice rationale:** Standard mobile pattern; existing `Skeleton` shadcn primitive available. + +--- + +## Auto-resolved scope creep checks + +- **Bulk actions** — flagged and deferred (read-tap-detail is the manager workflow) +- **Saved presets** — deferred to a future phase +- **Detail body refactor** — explicitly excluded by TICK-07 + +## External research + +None. Decisions were derivable from spec + existing code patterns.