wulf-pulse/components/mobile/EngagementUserRow.tsx

87 lines
3.1 KiB
TypeScript

'use client';
/* EngagementUserRow — phase 07 (ENG-04).
* Purpose: Per-employee row card — avatar (initials) + name + role + hours + hours bar.
* Entire row is a Link to /mobile/engagement/[graphUserId] (D-19; Phase 8 owns destination).
* Hours bar width = (billableHours / maxHours) * 100% — bounded at 100%.
* Props: user (EngagementUserRowData shape), maxHours (largest billableHours in current page set; computed by parent). */
import Link from 'next/link';
export interface EngagementUserRowData {
graphUserId: string;
displayName: string;
userEmail: string;
jobTitle: string | null;
billableHours: number;
hoursWorked: number;
}
export interface EngagementUserRowProps {
user: EngagementUserRowData;
maxHours: number; // largest billableHours in the loaded set (parent computes)
}
/**
* getInitials — first letter of first word + first letter of last word of displayName,
* uppercased. E.g. "Jordan Walsh" → "JW", "Alex" → "A". Exported for Phase 8 reuse
* (the user profile header may share the avatar identity block per UI-SPEC).
*/
export function getInitials(displayName: string): string {
const parts = displayName.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return '??';
if (parts.length === 1) return (parts[0]![0] ?? '?').toUpperCase();
const first = parts[0]![0] ?? '';
const last = parts[parts.length - 1]![0] ?? '';
return (first + last).toUpperCase();
}
export function EngagementUserRow({ user, maxHours }: EngagementUserRowProps) {
const initials = getInitials(user.displayName);
const hoursLabel = `${user.billableHours.toFixed(1)}h`;
const barWidthPct = maxHours > 0
? Math.min(100, (user.billableHours / maxHours) * 100)
: 0;
return (
<Link
href={`/mobile/engagement/${user.graphUserId}`}
className="block px-4 py-3 hover:bg-muted/50 transition-colors active:bg-muted/50"
>
{/* Top line: avatar + identity + hours value */}
<div className="flex items-center gap-3">
<span
aria-hidden="true"
className="h-8 w-8 rounded-full bg-muted flex items-center justify-center shrink-0 text-[10px] font-semibold text-foreground"
>
{initials}
</span>
<div className="flex-1 min-w-0 flex items-baseline gap-2">
<span className="text-sm font-semibold truncate flex-1">
{user.displayName}
</span>
<span className="text-sm font-semibold shrink-0 text-right">
{hoursLabel}
</span>
</div>
</div>
{/* Role line — render only if jobTitle present (UI-SPEC: "render nothing (no empty line) if absent") */}
{user.jobTitle && (
<p className="text-xs text-muted-foreground truncate pl-11 mt-0.5">
{user.jobTitle}
</p>
)}
{/* Hours bar */}
<div className="mt-2">
<div className="h-1.5 rounded-full bg-muted overflow-hidden" role="presentation" aria-hidden="true">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{ width: `${barWidthPct}%` }}
/>
</div>
</div>
</Link>
);
}