diff --git a/app/mobile/engagement/[userId]/page.tsx b/app/mobile/engagement/[userId]/page.tsx
index c8b0626..e3452da 100644
--- a/app/mobile/engagement/[userId]/page.tsx
+++ b/app/mobile/engagement/[userId]/page.tsx
@@ -5,6 +5,8 @@ import Link from 'next/link';
import { toast } from 'sonner';
import { EngagementPeriodChips, type EngagementPeriod } from '@/components/mobile/EngagementPeriodChips';
import { EngagementProfileSkeleton } from '@/components/mobile/EngagementProfileSkeleton';
+import { EngagementProfileHeader } from '@/components/mobile/EngagementProfileHeader';
+import { EngagementProfileMetricGrid } from '@/components/mobile/EngagementProfileMetricGrid';
interface ApiResponse {
user: {
@@ -185,11 +187,21 @@ export default function MobileEngagementUserProfilePage({
) : (
<>
- {/* Task 1b will mount EngagementProfileHeader + EngagementProfileMetricGrid here.
- Task 2 will add EngagementProfileBreakdown + EngagementRecentEntries + EngagementRecentMeetings. */}
-
- Loaded: {data.user.displayName}. Header and metric grid wired in Task 1b.
-
+
+
+ {/* Task 2 will mount EngagementProfileBreakdown + EngagementRecentEntries + EngagementRecentMeetings here. */}
>
)}
diff --git a/components/mobile/EngagementProfileHeader.tsx b/components/mobile/EngagementProfileHeader.tsx
new file mode 100644
index 0000000..22918e4
--- /dev/null
+++ b/components/mobile/EngagementProfileHeader.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+/* EngagementProfileHeader — phase 08 (D-05, D-06, D-07).
+ * Purpose: Identity header card — avatar (photo or initials), display name, jobTitle,
+ * department, email mailto: link, and last-active row.
+ * Avatar: photo from /api/mobile/engagement/user/[userId]/photo; on error → initials.
+ * Last-active: relative (≤7d via date-fns) or absolute (>7d via formatInUserTimezone). */
+
+import { useState } from 'react';
+import { formatDistanceToNow } from 'date-fns';
+import { Card, CardContent } from '@/components/ui/card';
+import { getInitials } from '@/components/mobile/EngagementUserRow';
+import { useUserTimezone, formatInUserTimezone } from '@/lib/hooks/use-user-timezone';
+
+export interface EngagementProfileHeaderProps {
+ displayName: string;
+ email: string;
+ jobTitle: string | null;
+ department: string | null;
+ // Last-active source: caller derives from response (most recent of recentEntries[0].entry_date,
+ // or the most-recent snapshots[].period_end). null if no signal at all.
+ lastActiveAt: string | null; // ISO date string or null (D-06)
+ // Used to build the photo URL — userId is the same value as the route segment
+ userId: string;
+}
+
+export function EngagementProfileHeader({
+ displayName,
+ email,
+ jobTitle,
+ department,
+ lastActiveAt,
+ userId,
+}: EngagementProfileHeaderProps) {
+ const [photoFailed, setPhotoFailed] = useState(false);
+ const tz = useUserTimezone();
+
+ // Compute last-active label (D-06)
+ let lastActiveLabel: string | null = null;
+ if (lastActiveAt) {
+ const ms = Date.now() - new Date(lastActiveAt).getTime();
+ if (ms <= 7 * 24 * 60 * 60 * 1000) {
+ // Relative — ≤7d
+ lastActiveLabel = `Active ${formatDistanceToNow(new Date(lastActiveAt), { addSuffix: true })}`;
+ } else {
+ // Absolute — >7d
+ const formatted = formatInUserTimezone(lastActiveAt, tz, { month: 'short', day: 'numeric', year: 'numeric' });
+ lastActiveLabel = `Last active ${formatted}`;
+ }
+ }
+
+ return (
+
+
+ {/* Avatar — photo or initials fallback */}
+ {!photoFailed ? (
+
setPhotoFailed(true)}
+ />
+ ) : (
+
+ {getInitials(displayName)}
+
+ )}
+
+ {/* Identity stack */}
+
+
{displayName}
+ {jobTitle && (
+
{jobTitle}
+ )}
+ {department && (
+
{department}
+ )}
+
+ {email}
+
+ {lastActiveLabel && (
+
{lastActiveLabel}
+ )}
+
+
+
+ );
+}
diff --git a/components/mobile/EngagementProfileMetricGrid.tsx b/components/mobile/EngagementProfileMetricGrid.tsx
new file mode 100644
index 0000000..2871894
--- /dev/null
+++ b/components/mobile/EngagementProfileMetricGrid.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+/* EngagementProfileMetricGrid — phase 08 (D-11, D-12, D-13).
+ * Purpose: 2×2 grid of 4 hero metric cards (Hours worked / Billable hours /
+ * Days worked / Meetings attended). Values always render as numbers —
+ * never '—' (D-13). Layout mirrors Phase 3 dashboard KPI grid. */
+
+import { Card, CardContent } from '@/components/ui/card';
+
+export interface EngagementProfileMetricGridProps {
+ hoursWorked: number; // already period-scoped by caller
+ billableHours: number;
+ daysWorked: number;
+ meetingsAttended: number;
+}
+
+export function EngagementProfileMetricGrid({
+ hoursWorked,
+ billableHours,
+ daysWorked,
+ meetingsAttended,
+}: EngagementProfileMetricGridProps) {
+ return (
+
+
+
+ {hoursWorked.toFixed(1)}h
+ Hours worked
+
+
+
+
+ {billableHours.toFixed(1)}h
+ Billable hours
+
+
+
+
+ {daysWorked.toString()}
+ Days worked
+
+
+
+
+ {meetingsAttended.toString()}
+ Meetings attended
+
+
+
+ );
+}