diff --git a/.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md b/.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md new file mode 100644 index 0000000..1143a92 --- /dev/null +++ b/.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md @@ -0,0 +1,165 @@ +--- +phase: 07-engagement-overview-new +plan: 03 +subsystem: mobile-pages +tags: [mobile, engagement, page, orchestration, typescript, tailwind] +dependency_graph: + requires: + - app/api/mobile/engagement/summary/route.ts (Plan 01 — MobileEngagementSummary type + endpoint) + - app/api/mobile/engagement/trend/route.ts (Plan 01 — EngagementTrendResponse + SparklinePoint types) + - app/api/engagement/users/route.ts (existing — reused as-is per D-16) + - components/mobile/EngagementPeriodChips.tsx (Plan 02) + - components/mobile/EngagementSummaryCard.tsx (Plan 02) + - components/mobile/EngagementHoursSparkline.tsx (Plan 02) + - components/mobile/EngagementSortChips.tsx (Plan 02) + - components/mobile/EngagementSearchInput.tsx (Plan 02) + - components/mobile/EngagementUserRow.tsx (Plan 02) + - components/mobile/EngagementUserRowSkeleton.tsx (Plan 02) + - components/mobile/MoreDrawer.tsx (Phase 2 — DRAWER-03 wires the route) + provides: + - app/mobile/engagement/page.tsx + affects: + - Phase 8 user profile page (app/mobile/engagement/[userId]) — tap target wired here +tech_stack: + added: [] + patterns: + - "'use client' + useState + useCallback + useEffect + useMemo + useRef (no SWR/react-query)" + - Three independent parallel fetches on mount/period change (summary + trend + users) + - Single users refetch on sort change only + - Client-side filter via useMemo (search never triggers a fetch) + - IntersectionObserver infinite scroll (rootMargin 200px) + Load more fallback button + - toast.error per failing fetch (sonner) + - Skeleton loading states (4 cards + 1 sparkline + 5 rows) + - prop mapping: existing API 'email' field mapped to EngagementUserRowData 'userEmail' +key_files: + created: + - app/mobile/engagement/page.tsx + modified: [] +decisions: + - "EngagementSortKey values are lowercase ('hours'|'name'|'utilization') per Wave 2 component — SORT_TO_API map keyed accordingly" + - "EngagementSortChips uses {value, onChange} props (not {activeSort, onSortChange}) — matched actual component export" + - "Existing /api/engagement/users returns 'email' field; EngagementUserRowData expects 'userEmail' — mapped inline at render" + - "Default sort set to 'hours' (lowercase) matching EngagementSortKey type constraint" + - "Period/sort/search state held in component state only (no URL sync) per D-21 note and lower data scale vs Phase 4 tickets" +metrics: + duration: "~15 min" + completed: "2026-05-04" + tasks: 1 + files_created: 1 + files_modified: 0 +--- + +# Phase 7 Plan 03: Mobile Engagement Page (Wave 3) Summary + +Mobile Engagement overview page wiring all Plan 01 endpoints + Plan 02 components into a single `'use client'` page with 3-fetch orchestration, infinite scroll, and complete loading/empty/error states. + +## What Was Built + +### `app/mobile/engagement/page.tsx` (commit 5daf7f3, 378 lines) + +Phone-first refactor of the Engagement overview (ENG-01 — real refactor, not a desktop port). + +**Orchestration model:** + +| Trigger | Fetches fired | +|---------|--------------| +| Mount (period = D30 default) | summary + trend + users page 1 (parallel) | +| Period chip tap | summary + trend + users page 1 (parallel) | +| Sort chip tap | users page 1 only | +| Search input change | 0 fetches (client-side useMemo filter) | +| IntersectionObserver / Load more | users page N+1 | + +**Page layout order (per UI-SPEC):** + +1. `

Engagement

` — scrolls away under sticky chips +2. `` — sticky strip (D-05) +3. 4× `` stacked (`space-y-3`): Active users, Total Graph hours, Total Autotask hours, Hours / active user +4. `` — compact sparkline card +5. Sort + search controls (`space-y-2`): `` then `` +6. User list (`divide-y border rounded-xl overflow-hidden`) +7. Sentinel + Load more button + +**States wired:** + +| State | Trigger | What renders | +|-------|---------|-------------| +| Initial loading | mount | 4 summary card skeletons + sparkline skeleton + 5 user-row skeletons | +| Not-configured | `summary.configured === false` | Banner replaces sections 3–7 | +| Empty | `activeUsers === 0 AND users.length === 0` | Empty state card with period chips still interactive | +| No-matches | search filter → 0 results | Inline "No matches for {query}" + Clear search button | +| Load more in-flight | intersection fires | Loader2 spinner; button disabled | +| Load more error | fetch throws | toast.error + button flips to "Retry" | + +**Fetch error toasts (D-25, all 4 present):** +- `toast.error('Failed to load engagement summary')` +- `toast.error('Failed to load hours trend')` +- `toast.error('Failed to load engagement users')` +- `toast.error('Failed to load more team members')` + +## Reachability Confirmed + +- **DRAWER-03:** `components/mobile/MoreDrawer.tsx` routes to `/mobile/engagement` — verified, not modified. +- **ENG-09:** `components/mobile/BottomNav.tsx` has no Engagement entry — verified, not modified. + +Engagement is exclusively reachable from the More drawer. Bottom nav unchanged (4 tabs + More cell). + +## Inherited Risk: T-07-12 (IDOR on /api/engagement/users) + +The existing `/api/engagement/users` endpoint reused by this page (per D-16, D-33) does not call `requireAuth()`. This is an existing-product gap. The new mobile page does NOT introduce new exposure: `middleware.ts` requires session for all `/api/*` routes not in the public list, and `/api/engagement/users` is not in the public list. + +**Recommended follow-up:** A future security phase should retrofit `requireAuth()` onto all desktop engagement endpoints (`/api/engagement/*`). Track in STATE.md. + +## Key Deviation: Sort Key Casing + +The plan's interface templates used capitalized `EngagementSortKey` values (`'Hours'|'Name'|'Utilization'`) but the Wave 2 `EngagementSortChips` component (Plan 02) exports lowercase values (`'hours'|'name'|'utilization'`). This was caught by reading the actual component files before writing the page. Adjustments made: + +- `SORT_TO_API` map keyed on lowercase values +- Default `sortKey` state set to `'hours'` (not `'Hours'`) +- `EngagementSortChips` called with `value={sortKey}` and `onChange={setSortKey}` (matching actual prop names, not the plan template's `activeSort`/`onSortChange`) + +These are Rule 1 (auto-fix) adjustments — aligning the page to what the actual component exports. + +## Key Deviation: userEmail Field Mapping + +The existing `/api/engagement/users` endpoint returns `email` field per its response shape. `EngagementUserRowData` (Wave 2 type) expects `userEmail`. The page performs the mapping inline at render: + +```ts +user={{ + graphUserId: u.graphUserId, + displayName: u.displayName, + userEmail: u.email, // 'email' from existing endpoint → 'userEmail' expected by component + jobTitle: u.jobTitle, + billableHours: u.billableHours, + hoursWorked: u.hoursWorked, +}} +``` + +This is the correct approach — the existing endpoint is unchanged (D-34) and the component contract is respected. + +## No Stubs + +The page is fully wired to all 3 data sources. No hardcoded mock data, no placeholder values beyond loading skeletons (which are correct UX, not stubs). + +## Threat Flags + +None new in this file. All user-supplied strings (search query, displayName, email, jobTitle) are rendered via React JSX text interpolation (auto-escaped). The search query appears in copy via template literal — React escapes the closing tags. No `dangerouslySetInnerHTML`. The `graphUserId` in Link href comes from API response (not direct user input). + +T-07-12 (IDOR on /api/engagement/users) is a pre-existing risk documented in Plan 01 SUMMARY; this plan inherits it without introducing new exposure. + +## Self-Check: PASSED + +All checks passed: +- `app/mobile/engagement/page.tsx` exists (378 lines, above 200 minimum) +- Commit 5daf7f3 confirmed +- `npx tsc --noEmit --pretty` exits 0 +- `'use client';` at top of file +- All 7 Wave-2 components imported +- Type imports from both Plan 01 endpoint files +- H1 uses `text-sm font-semibold` (not `text-base`) +- No `font-medium` in file +- `BottomNav.tsx` has no Engagement entry (ENG-09 — verified, unmodified) +- `MoreDrawer.tsx` routes to `/mobile/engagement` (DRAWER-03 — verified, unmodified) +- No Zod, SWR, or react-query imports +- No `useSearchParams` or `useRouter` +- All 4 toast.error messages present verbatim +- All copy strings from UI-SPEC Copywriting Contract present