docs(07-03): complete mobile engagement page plan summary
- Documents orchestration model (3 fetches on period, 1 on sort, 0 on search) - Documents deviation: lowercase EngagementSortKey values vs plan template (Wave 2 actual) - Documents field mapping: existing endpoint 'email' -> component 'userEmail' - Confirms DRAWER-03 reachability and ENG-09 BottomNav unchanged - Flags inherited risk T-07-12 (IDOR on /api/engagement/users) for follow-up
This commit is contained in:
parent
5daf7f31e5
commit
0af6136864
1 changed files with 165 additions and 0 deletions
165
.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md
Normal file
165
.planning/phases/07-engagement-overview-new/07-03-SUMMARY.md
Normal file
|
|
@ -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. `<h1 className="text-sm font-semibold">Engagement</h1>` — scrolls away under sticky chips
|
||||
2. `<EngagementPeriodChips period={period} onPeriodChange={setPeriod} />` — sticky strip (D-05)
|
||||
3. 4× `<EngagementSummaryCard>` stacked (`space-y-3`): Active users, Total Graph hours, Total Autotask hours, Hours / active user
|
||||
4. `<EngagementHoursSparkline points={trendPoints} period={period} />` — compact sparkline card
|
||||
5. Sort + search controls (`space-y-2`): `<EngagementSortChips>` then `<EngagementSearchInput>`
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue