87 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|