diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 54bc405..c2ba87c 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -165,8 +165,8 @@ Decimal phases appear between their surrounding integers in numeric order. 2. The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position 3. The profile renders single-column: identity header → period selector → key metrics (compact) → activity breakdown list → recent items, sourced from the existing engagement profile data endpoints (no new data) **Plans**: 2 plans -- [ ] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26) -- [ ] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08) +- [x] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26) +- [x] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08) **UI hint**: yes ### Phase 9: User Profile & Preferences (NEW) diff --git a/.planning/STATE.md b/.planning/STATE.md index 2f8a6b4..12ff3d0 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,14 +4,14 @@ milestone: v1.0 milestone_name: milestone status: executing stopped_at: Phase 8 UI-SPEC approved -last_updated: "2026-05-07T23:45:22.929Z" -last_activity: 2026-05-07 -- Phase 08 planning complete +last_updated: "2026-05-08T13:58:28.740Z" +last_activity: 2026-05-08 progress: total_phases: 10 - completed_phases: 8 + completed_phases: 9 total_plans: 24 - completed_plans: 22 - percent: 92 + completed_plans: 24 + percent: 100 --- # Project State @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-05-03) **Core value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. -**Current focus:** Phase 07 — Engagement Overview (NEW) +**Current focus:** Phase 08 — engagement-user-profile-new ## Current Position -Phase: 8 +Phase: 9 Plan: Not started -Status: Ready to execute -Last activity: 2026-05-07 -- Phase 08 planning complete +Status: Executing Phase 08 +Last activity: 2026-05-08 Progress: [░░░░░░░░░░] 0% @@ -36,7 +36,7 @@ Progress: [░░░░░░░░░░] 0% **Velocity:** -- Total plans completed: 22 +- Total plans completed: 24 - Average duration: — - Total execution time: 0.0 hours @@ -52,6 +52,7 @@ Progress: [░░░░░░░░░░] 0% | 06 | 3 | - | - | | 07 | 3 | - | - | | 07.1 | 5 | - | - | +| 08 | 2 | - | - | **Recent Trend:** diff --git a/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md b/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md new file mode 100644 index 0000000..bc69290 --- /dev/null +++ b/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md @@ -0,0 +1,113 @@ +--- +phase: 08-engagement-user-profile-new +verified: 2026-05-08T00:00:00Z +status: human_needed +score: 12/13 must-haves verified +re_verification: false +human_verification: + - test: "Navigate to /mobile/engagement, tap an employee row, press the device back button" + expected: "Returns to /mobile/engagement at the same scroll position as before tapping" + why_human: "Scroll restoration (SC#2) is programmatic — the sessionStorage shim was added to app/mobile/engagement/page.tsx but the SUMMARY documents it as partially working. Browser-back scroll position cannot be verified by grep or static analysis. User has accepted the partial behavior as a known limitation." +--- + +# Phase 8: Engagement User Profile (NEW) — Verification Report + +**Phase Goal:** From the Engagement overview, a manager taps an employee row and arrives at a real, shareable profile page — single-column phone-first — and the device back gesture returns them to the overview. +**Verified:** 2026-05-08 +**Status:** human_needed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | Tapping a row in /mobile/engagement navigates to /mobile/engagement/{graphUserId} | ✓ VERIFIED | `EngagementUserRow.tsx:48` has `href={'/mobile/engagement/${user.graphUserId}'}` pointing to the real route at `app/mobile/engagement/[userId]/page.tsx` | +| 2 | The browser back gesture returns to the overview at the prior scroll position (SC#2) | ? HUMAN NEEDED | sessionStorage shim exists at `app/mobile/engagement/page.tsx:175-225` (rAF save + retry restore), but SUMMARY and context notes both document this as partial — does not reliably restore scroll. User accepted as known limitation. | +| 3 | The profile renders single-column in the specified order: H1 → period chips → identity header → 2×2 metric grid → activity breakdown → recent entries → recent meetings | ✓ VERIFIED | `page.tsx:183-224` confirms exact render order: `

`, ``, ``, ``, ``, ``, `` | +| 4 | Identity header shows photo (or initials fallback), display name, jobTitle, department (omitted if null), email as mailto, last-active row when signal exists | ✓ VERIFIED | `EngagementProfileHeader.tsx:35-88` — `photoFailed` useState toggle, `onError={() => setPhotoFailed(true)}`, department conditional at line 78, `href={mailto:${email}}` at line 82, `lastActiveLabel &&` at line 87 | +| 5 | 2×2 hero metric cards (Hours worked / Billable hours / Days worked / Meetings attended) render in grid-cols-2 from /api/engagement/user/[userId]?period= | ✓ VERIFIED | `EngagementProfileMetricGrid.tsx:24` has `className="grid grid-cols-2 gap-3"` with all four metric labels at lines 28, 34, 40, 46; data flows from page.tsx via props | +| 6 | Period chip selection refetches data; recent-items remain bound to 10 regardless of period | ✓ VERIFIED | `page.tsx:117` — useEffect deps `[userId, period, retryNonce]`; fetch at line 93 uses `?period=${period}`; EngagementRecentEntries/Meetings components receive raw `recentEntries` / `recentTeamsMeetings` arrays not filtered by period | +| 7 | Activity breakdown renders Time / Communication / Meetings subsections with after-hours and Zoom conditional rows | ✓ VERIFIED | `EngagementProfileBreakdown.tsx:51-91` — three labeled `

` subsections; `showAfterHours` at line 45 gates after-hours row; `showZoom` at line 46 gates Zoom row | +| 8 | Recent entries and meetings collapse/expand via shadcn Collapsible; Set state; period changes don't reset expansion | ✓ VERIFIED | Both components use `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` from shadcn; `useState>(new Set())` for expansion tracking; period changes propagate to page, not through the component state | +| 9 | 404 from data endpoint renders inline "User not found" with back-to-Engagement link | ✓ VERIFIED | `page.tsx:120-132` — `errorState === 'not-found'` branch renders `

User not found

` and `Back to Engagement` | +| 10 | 500/network failure renders sonner toast + inline Retry button via retryNonce increment | ✓ VERIFIED | `page.tsx:5` imports `toast from 'sonner'`; `page.tsx:111` calls `toast.error()`; Retry button at line 143 calls `setRetryNonce((n) => n + 1)` | +| 11 | Photo endpoint non-200 silently falls back to initials — no toast, no error UI | ✓ VERIFIED | `EngagementProfileHeader.tsx:61` — `onError={() => setPhotoFailed(true)}` toggles the `photoFailed` state, rendering initials only; no toast import in that component | +| 12 | EngagementUserRow.tsx was NOT modified by this plan | ✓ VERIFIED | `git log components/mobile/EngagementUserRow.tsx` — last commit is `d637892` (Phase 7); no Phase 8 commits touch this file | +| 13 | /api/engagement/user/[userId]/route.ts was NOT modified by this plan | ✓ VERIFIED | `git log app/api/engagement/user/[userId]/route.ts` — last commit is `c518eef` (unrelated); no Phase 8 commits touch this file | + +**Score:** 12/13 truths verified (1 requires human testing — SC#2 scroll restoration) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `lib/services/msgraph-client.ts` | getUserPhotoBytes() method | ✓ VERIFIED | Line 419 — `async getUserPhotoBytes(userId)` exists; returns ArrayBuffer on 2xx, null on 404, throws on other non-2xx; uses `this.getToken()` | +| `app/api/mobile/engagement/user/[userId]/photo/route.ts` | Photo proxy GET handler | ✓ VERIFIED | 67 lines; exports `GET`; `requireAuth` is first call (line 10); all status codes present (400, 404, 503) | +| `app/mobile/engagement/[userId]/page.tsx` | Mobile profile page | ✓ VERIFIED | 230 lines (>= 120 minimum); `'use client'` at line 1; all 7 components wired | +| `components/mobile/EngagementProfileSkeleton.tsx` | Full-page skeleton | ✓ VERIFIED | 77 lines; exports `EngagementProfileSkeleton`; contains `Skeleton` | +| `components/mobile/EngagementProfileHeader.tsx` | Identity header card | ✓ VERIFIED | 94 lines; exports `EngagementProfileHeader`; photo + initials fallback wired | +| `components/mobile/EngagementProfileMetricGrid.tsx` | 2×2 metric grid | ✓ VERIFIED | 51 lines; exports `EngagementProfileMetricGrid`; contains `grid-cols-2` | +| `components/mobile/EngagementProfileBreakdown.tsx` | Activity breakdown card | ✓ VERIFIED | 96 lines; exports `EngagementProfileBreakdown`; three subsections present | +| `components/mobile/EngagementRecentEntries.tsx` | Collapsible time entries list | ✓ VERIFIED | 103 lines; exports `EngagementRecentEntries`; Collapsible from shadcn; `Number(entry.hours_worked).toFixed(1)` coercion | +| `components/mobile/EngagementRecentMeetings.tsx` | Collapsible meetings list | ✓ VERIFIED | 142 lines; exports `EngagementRecentMeetings`; Collapsible from shadcn; `Number(te.hours_worked).toFixed(1)` coercion | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `photo/route.ts` | `lib/services/msgraph-factory.ts` | `import { getMsgraphClient, isMsgraphConfigured }` | ✓ WIRED | Lines 3, 14, 39 — both functions imported and called | +| `photo/route.ts` | `lib/auth-utils.ts` | `import { requireAuth }` | ✓ WIRED | Line 2 imports, line 10 calls `requireAuth()` as first handler action | +| `photo/route.ts` | `MsGraphClient.getUserPhotoBytes` | method call | ✓ WIRED | Line 40 — `client.getUserPhotoBytes(userId)` | +| `page.tsx` | `/api/engagement/user/[userId]` | fetch in useEffect | ✓ WIRED | Line 93 — `fetch('/api/engagement/user/${userId}?period=${period}')` in useEffect with deps `[userId, period, retryNonce]` | +| `page.tsx` | `EngagementPeriodChips` | import + JSX | ✓ WIRED | Line 6 imports, lines 138 and 186 render `` | +| `EngagementProfileHeader.tsx` | `/api/mobile/engagement/user/[userId]/photo` | `