- 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
8.4 KiB
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-engagement-overview-new | 03 | mobile-pages |
|
|
|
|
|
|
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):
<h1 className="text-sm font-semibold">Engagement</h1>— scrolls away under sticky chips<EngagementPeriodChips period={period} onPeriodChange={setPeriod} />— sticky strip (D-05)- 4×
<EngagementSummaryCard>stacked (space-y-3): Active users, Total Graph hours, Total Autotask hours, Hours / active user <EngagementHoursSparkline points={trendPoints} period={period} />— compact sparkline card- Sort + search controls (
space-y-2):<EngagementSortChips>then<EngagementSearchInput> - User list (
divide-y border rounded-xl overflow-hidden) - 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.tsxroutes to/mobile/engagement— verified, not modified. - ENG-09:
components/mobile/BottomNav.tsxhas 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_APImap keyed on lowercase values- Default
sortKeystate set to'hours'(not'Hours') EngagementSortChipscalled withvalue={sortKey}andonChange={setSortKey}(matching actual prop names, not the plan template'sactiveSort/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:
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.tsxexists (378 lines, above 200 minimum)- Commit
5daf7f3confirmed npx tsc --noEmit --prettyexits 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(nottext-base) - No
font-mediumin file BottomNav.tsxhas no Engagement entry (ENG-09 — verified, unmodified)MoreDrawer.tsxroutes to/mobile/engagement(DRAWER-03 — verified, unmodified)- No Zod, SWR, or react-query imports
- No
useSearchParamsoruseRouter - All 4 toast.error messages present verbatim
- All copy strings from UI-SPEC Copywriting Contract present