diff --git a/.planning/phases/08-engagement-user-profile-new/08-CONTEXT.md b/.planning/phases/08-engagement-user-profile-new/08-CONTEXT.md new file mode 100644 index 0000000..4f57bc0 --- /dev/null +++ b/.planning/phases/08-engagement-user-profile-new/08-CONTEXT.md @@ -0,0 +1,167 @@ +# Phase 8: Engagement User Profile (NEW) - Context + +**Gathered:** 2026-05-07 +**Status:** Ready for planning + + +## Phase Boundary + +A real, shareable per-employee profile page at `/mobile/engagement/[userId]` rendering identity → period selector → 4 hero metrics (2×2 grid) → categorized activity breakdown → two recent-items sections (time entries + meetings). Sourced from existing engagement endpoints (no new data). Replaces the desktop user-detail modal pattern on mobile only — desktop stays as-is. + +Scope anchor (from ROADMAP.md): tapping an Engagement overview row navigates to `/mobile/engagement/[userId]`; the profile is a real page so the device back gesture returns to the overview at the same scroll position; layout is single-column per ENG-06; reuses existing endpoints per ENG-08. + + + + +## Implementation Decisions + +### Route, segment, and shell integration +- **D-01:** New page at `app/mobile/engagement/[userId]/page.tsx`. The `EngagementUserRow` component (Phase 7, `components/mobile/EngagementUserRow.tsx`) already renders an entire-row `Link` to `/mobile/engagement/[graphUserId]` per Phase 7 D-19 — Phase 8 owns the destination. **Do NOT modify `EngagementUserRow.tsx`.** +- **D-02:** `'use client'` + `useState` + `useEffect` + `fetch` (CLAUDE.md: no SWR/react-query, match Phase 7 pattern). +- **D-03:** `[userId]` in the route segment is the `graph_users.id` (UUID-like string from Microsoft Graph), matching the existing `/api/engagement/user/[userId]` endpoint contract. Not the Better Auth `user.id`. Same convention used by Phase 7's `EngagementUserRow.graphUserId`. +- **D-04:** Scroll restoration to overview: rely on Next.js App Router's default `scrollRestoration: true` — `Link` prefetch + browser back/forward restores scroll position automatically. No `sessionStorage` workaround needed unless the planner discovers the default doesn't hold. Treat as "verify in execution; if broken, then mitigate." + +### Identity header +- **D-05:** Avatar source — Microsoft Graph photo with **initials fallback** when no photo is available. Use the existing `getMsgraphClient()` factory and a server-side photo fetch through a new thin endpoint (e.g. `/api/mobile/engagement/user/[userId]/photo` returning a small JPEG or 404). Initials computed via Phase 7's exported `getInitials(displayName)` helper from `EngagementUserRow.tsx`. +- **D-06:** Fields under name (in this stacked order): + 1. **Job title** — `graph_users.job_title` + 2. **Department** — `graph_users.department` (omit row if NULL) + 3. **Email** — `graph_users.email`, rendered as `mailto:` link + 4. **Last active** — most recent of (`time_entries.entry_date`, last `engagement_snapshots` activity timestamp), formatted via `useUserTimezone()`. Display as relative if ≤7 days ("2 hours ago"), absolute if older ("2026-04-15"). Omit row if no signal. +- **D-07:** Header card uses `Card` + `CardContent` with horizontal layout: avatar (left, ~56px) + identity stack (right). No subtitle, no role badge — the four rows above carry sufficient identity weight on a phone. + +### Period selector +- **D-08:** Reuse Phase 7's `EngagementPeriodChips` component as-is. 3 chips: `7d` / `30d` / `90d` mapping to `D7`/`D30`/`D90`. +- **D-09:** **Default period = `D30`** (matches Phase 7 overview default, preserves user's mental model carried from the previous screen). +- **D-10:** Sticky behavior: same as Phase 7 — `sticky top-0 z-10 bg-background pt-2 pb-3 -mx-4 px-4` so chips run edge-to-edge while metrics scroll above. Header card scrolls under the chips. + +### Key metrics — 2×2 grid (4 hero metrics) +- **D-11:** **Layout:** 2×2 grid of `Card` + `CardContent`, mirroring Phase 3 dashboard KPI grid. Each card: big number (`text-2xl font-semibold`), small label (`text-xs text-muted-foreground`), optional inline qualifier. `gap-3` between cards, no shadows (matches Phase 7 D-10). +- **D-12:** **Hero metrics** (in reading order: top-left, top-right, bottom-left, bottom-right): + 1. **Hours worked** — sum of `time_entries.hours_worked` over the selected period + 2. **Billable hours** — sum where `billable = true` + 3. **Days worked** — distinct `entry_date` count + 4. **Meetings attended** — `teams_meetings_attended` summed over the period from `engagement_snapshots` +- **D-13:** When the value is `0` or `NULL`, render `0` (not `—`). When the user has no activity at all, render the cards with zeros — empty-state messaging belongs at the recent-items section, not the metric grid. + +### Activity breakdown — categorized rows +- **D-14:** Below the metric grid, a **single `Card`** with three labeled subsections in this order: + 1. **Time** — Hours worked / Billable hours / Days worked / utilization% (billable ÷ hours, if applicable) + 2. **Communication** — Teams messages (chat + private summed) / Emails sent / **After-hours: X% of messages, Y% of meetings** (the after-hours signal lives here, per D-15) + 3. **Meetings** — Meetings attended / Meetings organized / Total meeting duration (hours, from `meeting_duration_seconds`) / Zoom calls (only if `zoom` block in the response is non-null) +- **D-15:** **After-hours** signal — single row inside Communication: `After-hours · {messagesPct}% messages, {meetingsPct}% meetings`. Tucked in, not callout-styled. Hide row when both are 0%. +- **D-16:** Each subsection is a label (`text-sm font-medium text-muted-foreground`) followed by metric rows (label left, value right, `flex justify-between text-sm py-1.5`). No charts, no sparklines — keeps render light and matches DASH-04 / Phase 7 §6.5 ("no multi-series chart on mobile"). +- **D-17:** Hide a row entirely when its underlying field is `null` or `0` AND it's a "presence" signal (e.g. zoom calls when zoom not configured). For first-class metrics (hours, meetings) always render with `0`. + +### Recent items — two separate sections, tap-to-expand +- **D-18:** Two separate sections rendered in this order (after activity breakdown): + 1. **Recent time entries** — last 10 from `recentEntries` (existing endpoint already returns these), sorted by `entry_date` descending. Each row collapsed shows: `entry_date` (formatted via `useUserTimezone()`), `hours_worked`, billable badge if applicable, ticket/project ref (if present), one-line notes preview. **Tap expands inline** to reveal: full notes, ticket title (if available), full project ref, exact timestamp. + 2. **Recent meetings** — last 10 from `recentTeamsMeetings`, sorted by `date` descending. Each row collapsed: subject, date (TZ-formatted), duration (from `meetingMins`), attendee count if available. **Tap expands inline** to reveal: matched time entries (the existing `matchedEntries` array), Zoom call linkage (if present), organizer. +- **D-19:** **Bound to 10 each** (count-bounded, not date-bounded). Period selector does NOT affect recent-items count — it remains 10/10 regardless of D7/D30/D90. (Period changes the metrics + breakdown only.) +- **D-20:** **Tap-to-expand mechanism:** local component state (`Set` of expanded entry IDs / meeting IDs). No URL state, no router push. Expanded rows animate via Tailwind `transition-all` + height; collapsed by default. Reuse shadcn `Collapsible` if it fits cleanly, else hand-roll. +- **D-21:** Empty states — when `recentEntries` is empty: show "No time entries in the last 30 days" inline (one row). Same for meetings. Section header still renders. + +### Data fetching +- **D-22:** Reuse `/api/engagement/user/[userId]?period={D7|D30|D90}` as-is per ENG-08. The endpoint already returns `user`, `hours`, `recentEntries`, `recentTeamsMeetings`, `dailyActivity`, `zoom`, `afterHours`, `peerMax` — Phase 8 ignores `peerMax` (radar/peer comparison is a desktop-only flourish) and `dailyActivity` (we render via metrics, no chart). +- **D-23:** Single fetch on mount + on period change. Loading skeleton matches Phase 7 pattern: header skeleton + 4 metric-card skeletons + breakdown card skeleton + 2 recent-list skeletons. +- **D-24:** Error handling: if 404 → "User not found" empty page with back link to `/mobile/engagement`. If 500 → toast (sonner) + retry button on the page body. + +### Avatar/photo endpoint +- **D-25:** New thin route `/api/mobile/engagement/user/[userId]/photo` (server-side) — calls Microsoft Graph `/users/{id}/photo/$value` via `getMsgraphClient()`, returns the binary or 404. `requireAuth()` first. Cache headers: `Cache-Control: private, max-age=3600`. Browser caches the photo per-tab. Fallback to initials happens client-side when the `` errors out. +- **D-26:** Photo fetch is best-effort. If `MSGRAPH_*` env not configured, the endpoint returns 503 — the client treats any non-200 as "use initials." Phase 8 doesn't gate on Graph being configured. + +### Claude's Discretion +- Exact card/row spacing, typography weights within Phase 7's established tokens (`text-2xl`, `text-xs`, `text-sm`, `space-y-3`, `gap-3`) +- Whether to use `Collapsible` from shadcn or a hand-rolled disclosure for D-20 +- Skeleton component composition (use Phase 7 shapes as reference) +- Toast wording for the 500 error case (D-24) +- Whether to memoize the expand-state `Set` or use a plain object — implementation detail +- The exact threshold for "Last active" relative-vs-absolute (D-06): treat ≤7d as relative as a starting heuristic; planner can refine + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Phase scope and requirements +- `.planning/ROADMAP.md` §"Phase 8" — goal, depends-on, success criteria +- `.planning/REQUIREMENTS.md` ENG-06, ENG-07, ENG-08 — single-column layout, real-page-not-modal, reuse existing endpoints + +### Prior-phase context this builds on +- `.planning/phases/07-engagement-overview-new/07-CONTEXT.md` — period chip mapping (D-04..07), summary card visual tokens (D-10), sparkline pattern, list patterns; **D-19 establishes the row→`/mobile/engagement/[graphUserId]` link** +- `.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-04-SUMMARY.md` — `useUserTimezone()` hook signature, formatting pattern (`{ ...options, timeZone: tz }`) +- `.planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md` — `/mobile` shell layout, sticky header pattern + +### Existing endpoints to reuse (no modification) +- `app/api/engagement/user/[userId]/route.ts` — main data source (581 lines): returns `user`, `hours`, `recentEntries`, `recentTeamsMeetings`, `dailyActivity`, `zoom`, `afterHours`, `peerMax`, `snapshots` +- `app/api/engagement/user/[userId]/history/route.ts` — monthly history (per-month metrics), **not used in Phase 8 v1** + +### Components to reuse from Phase 7 +- `components/mobile/EngagementPeriodChips.tsx` — chip component (D-08) +- `components/mobile/EngagementUserRow.tsx` exports `getInitials()` (D-05) +- `components/mobile/EngagementSummaryCard.tsx` — referenced as visual token source for metric cards +- `components/ui/card.tsx`, `components/ui/skeleton.tsx`, `components/ui/collapsible.tsx`, `components/ui/badge.tsx` — shadcn primitives + +### Existing services and helpers +- `lib/services/msgraph-factory.ts` — `getMsgraphClient()` for D-25 photo endpoint +- `lib/hooks/use-user-timezone.ts` — TZ formatting (D-06, D-18) +- `lib/auth-utils.ts` — `requireAuth()` for the photo endpoint + +### Desktop reference (do NOT replicate visuals) +- `app/engagement/profile/page.tsx` — desktop modal pattern using recharts (`BarChart`, `RadarChart`); **kept as-is**, not deleted, not migrated. Phase 8 only adds the mobile real-page; desktop modal continues to serve desktop users. + + + + +## Existing Code Insights + +### Reusable Assets +- `components/mobile/EngagementPeriodChips.tsx` — drop-in for period selector (D-08) +- `components/mobile/EngagementUserRow.tsx` exports `getInitials(displayName)` (D-05) +- `lib/hooks/use-user-timezone.ts` — `useUserTimezone()` returns the user's IANA tz string (D-06, D-18) +- `lib/services/msgraph-factory.ts` — `getMsgraphClient()` + `isMsgraphConfigured()` for the photo endpoint (D-25) +- shadcn `Collapsible` — likely fit for the tap-to-expand recent rows (D-20) + +### Established Patterns +- Mobile pages are `'use client'` + `useState` + `useEffect` + `fetch` (CLAUDE.md, Phase 7 D-03) +- Sticky chips below H1 use `sticky top-0 z-10 bg-background pt-2 pb-3 -mx-4 px-4` (Phase 7 D-05) +- Metric cards: big number `text-2xl font-semibold`, label `text-xs text-muted-foreground`, no shadow, just border (Phase 7 D-10) +- TZ-formatted dates use `{ ...options, timeZone: tz }` from `useUserTimezone()` (Phase 7.1) +- API routes return JSON; auth via `requireAuth()` from `lib/auth-utils.ts`; error response shape `{ error, message }` + +### Integration Points +- Entry: `EngagementUserRow.tsx`'s existing `Link href="/mobile/engagement/{graphUserId}"` (Phase 7 wired this; Phase 8 only creates the destination page) +- Data: `/api/engagement/user/[userId]` (existing) + new thin `/api/mobile/engagement/user/[userId]/photo` (D-25) +- Auth/middleware: `/api/mobile/*` is whitelisted in `middleware.ts`; route handlers gate via `requireAuth()` +- Navigation: Browser back gesture handled by Next.js App Router default scroll restoration (D-04) — no custom code unless verification reveals it's broken + + + + +## Specific Ideas + +- "I want this to feel like the row card I just tapped — same avatar treatment, same identity weight" — header avatar reuses `getInitials()` and the visual rhythm of Phase 7 row cards +- "When a manager opens this, they want to see the time numbers first" — 2×2 hero grid leads with Hours/Billable, then Days/Meetings; communication and meeting detail go below in the breakdown card +- "Don't bury after-hours" — after-hours% gets a visible row inside Communication subsection rather than being hidden in a tooltip or collapsed section +- Tap-to-expand is **inline** (no new page, no modal) — preserves the back-gesture-restores-scroll guarantee from SC#2 + + + + +## Deferred Ideas + +- **Peer comparison / radar chart** — desktop has `peerMax` data + a radar visualization; not on mobile v1. Could be a future "compare to team" toggle. +- **Monthly history view** — `/api/engagement/user/[userId]/history` exists with 16 metrics × N months. Could power a "history" tab on the profile in a future phase. Not v1. +- **`dailyActivity` chart** — the endpoint returns daily breakdown points; could render a single-series sparkline like Phase 7's `EngagementHoursSparkline`. Skipped for v1 to keep the page screen-bounded; reconsider if managers ask for it. +- **Tap-to-open ticket/meeting** — D-20 picked tap-to-expand-inline. Future enhancement: an explicit "View ticket" button inside the expanded entry that deep-links to `/mobile/tickets/[id]`. +- **D1 ("today") period chip** — Phase 7 D-07 deferred this; same applies here. Aggregate granularity is D7+ until a D1 sync lands. +- **Zoom-only client-meeting filter** — desktop differentiates "client meetings" from total meetings using calendar metadata. Mobile v1 shows the totals; client-only breakdown is a future enhancement. + + + +--- + +*Phase: 08-engagement-user-profile-new* +*Context gathered: 2026-05-07* diff --git a/.planning/phases/08-engagement-user-profile-new/08-DISCUSSION-LOG.md b/.planning/phases/08-engagement-user-profile-new/08-DISCUSSION-LOG.md new file mode 100644 index 0000000..e4fbd18 --- /dev/null +++ b/.planning/phases/08-engagement-user-profile-new/08-DISCUSSION-LOG.md @@ -0,0 +1,152 @@ +# Phase 8: Engagement User Profile (NEW) - 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-07 +**Phase:** 08-engagement-user-profile-new +**Areas discussed:** Identity header, Key metrics layout, Period default, Activity breakdown, Recent items + +--- + +## Identity header + +### Avatar source + +| Option | Description | Selected | +|--------|-------------|----------| +| Initials only (Recommended) | Reuse Phase 7's `getInitials()`. Zero external calls, consistent with overview row. | | +| Graph photo with initials fallback | Fetch `user.photo` from Microsoft Graph; nicer visually if photos exist. | ✓ | +| Graph photo only (no fallback) | Skip avatar entirely if no photo. | | + +**User's choice:** Graph photo with initials fallback +**Notes:** Per D-25/D-26 — new thin endpoint at `/api/mobile/engagement/user/[userId]/photo` calls Graph; client falls back to initials on `` error. + +### Fields under name (multi-select) + +| Option | Description | Selected | +|--------|-------------|----------| +| Job title | From `graph_users.job_title` — already on the row card | ✓ | +| Email | Tap-to-email on phone (`mailto:`) | ✓ | +| Department | From `graph_users.department` if populated | ✓ | +| Last active timestamp | Most-recent activity (last time entry / last Teams message) | ✓ | + +**User's choice:** All four selected +**Notes:** Renders in stacked order: title → department → email → last active. Department/last-active rows hide if NULL. + +--- + +## Key metrics layout + +### Layout pattern + +| Option | Description | Selected | +|--------|-------------|----------| +| 2×2 grid — 4 hero metrics (Recommended) | Mirrors Phase 3 dashboard KPI grid | ✓ | +| Stacked cards — 6 metrics | Mirrors Phase 7 overview summary cards | | +| Compact strip — 6 in horizontal scroll | Saves vertical, breaks Pulse pattern | | + +**User's choice:** 2×2 grid + +### Default period + +| Option | Description | Selected | +|--------|-------------|----------| +| D30 — match overview default (Recommended) | Consistent with Phase 7 | ✓ | +| D90 — detail-view convention | Heavier data load | | +| D7 — most recent context | Spot-check oriented | | + +**User's choice:** D30 + +### Top metrics for the 2×2 grid + +| Option | Description | Selected | +|--------|-------------|----------| +| Hours worked + Billable hours + Days + Meetings | Time-focused set | ✓ | +| Hours + Billable + Meetings + After-hours% | Time + workload signal | | +| Hours + Meetings + Teams msgs + Emails | Activity-focused | | +| Hours + Billable + Days + Meetings + Teams msgs + Emails (6 stacked) | Full picture, stacked | | + +**User's choice:** Hours worked + Billable hours + Days worked + Meetings attended + +--- + +## Activity breakdown + +### Breakdown structure + +| Option | Description | Selected | +|--------|-------------|----------| +| Categorized rows — Time / Communication / Meetings (Recommended) | Three labeled subsections, scannable, no charts | ✓ | +| Single mixed list — all metrics flat | Simpler, loses grouping | | +| Per-metric mini-sparklines | Visually rich but heavy | | +| Daily activity timeline | Combined chart, less per-metric detail | | + +**User's choice:** Categorized rows + +### After-hours% placement + +| Option | Description | Selected | +|--------|-------------|----------| +| Inside Communication section as a row (Recommended) | Tucked but visible | ✓ | +| Standalone row above breakdown | Highlighted callout | | +| Drop — not on mobile v1 | Skip for v1 | | + +**User's choice:** Inside Communication section + +--- + +## Recent items + +### What gets shown + +| Option | Description | Selected | +|--------|-------------|----------| +| Recent time entries only (Recommended) | Last ~10 from `recentEntries` | | +| Recent meetings only | Last ~10 from `recentTeamsMeetings` | | +| Mixed feed — interleaved by date | Single timeline | | +| Two separate sections — Time entries + Meetings | Both, kept separate | ✓ | + +**User's choice:** Two separate sections + +### Scope + +| Option | Description | Selected | +|--------|-------------|----------| +| Last 10 items (Recommended) | Bounded, fits one screen | ✓ | +| Last 20 items | Fuller history | | +| Last 7 days bounded by date | Time-bounded | | +| Match selected period (D7/D30/D90) | Grows with period | | + +**User's choice:** Last 10 items (each section) + +### Tap behavior + +| Option | Description | Selected | +|--------|-------------|----------| +| Read-only display, no tap action (Recommended) | Match Phase 7's read-only-on-mobile principle | | +| Tap to open ticket / meeting detail | Adds nav surfaces | | +| Tap to expand inline — show notes, attendees, full details | Stays on profile, reveals more rows | ✓ | + +**User's choice:** Tap to expand inline +**Notes:** Local component state (`Set` of expanded IDs); no URL state. + +--- + +## Claude's Discretion + +- Exact spacing/typography within Phase 7's established tokens +- Whether to use shadcn `Collapsible` or hand-rolled disclosure +- Skeleton component composition +- Toast wording for 500 errors +- Memoization detail of expand-state +- "Last active" relative-vs-absolute threshold (started at ≤7d) + +## Deferred Ideas + +- Peer comparison / radar chart (desktop-only flourish) +- Monthly history view (`/history` endpoint) +- `dailyActivity` chart +- Tap-to-open ticket/meeting deep links (future enhancement to inline expand) +- D1 "today" period chip (Phase 7 deferral carries forward) +- Zoom client-meeting filter (mobile v1 shows totals)