- New EngagementProfileBreakdown: Time/Communication/Meetings subsections, after-hours and Zoom conditional rows, py-2 per UI-SPEC override - New EngagementRecentEntries: collapsible list up to 10, Billable badge, Set<string> expand state, empty-state copy - New EngagementRecentMeetings: collapsible list up to 10, matched entries + attendees in expanded view, (no subject) fallback, Set<string> expand state - Page updated: 3 new component imports + breakdown/entries/meetings mounted in order - No dangerouslySetInnerHTML; D-01/D-22 guard rails untouched
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
/* EngagementProfileBreakdown — phase 08 (D-14..D-17).
|
|
* Purpose: Single Card with three labeled subsections — Time / Communication / Meetings.
|
|
* Rows use py-2 (UI-SPEC override of D-16's py-1.5). After-hours and Zoom rows are
|
|
* conditional based on value presence (D-15, D-17). */
|
|
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
export interface EngagementProfileBreakdownProps {
|
|
// Time subsection
|
|
hoursWorked: number;
|
|
billableHours: number;
|
|
daysWorked: number;
|
|
// Communication subsection
|
|
teamsMessages: number; // chat + private summed by caller
|
|
emailsSent: number;
|
|
afterHoursMessagesPct: number;
|
|
afterHoursMeetingsPct: number;
|
|
// Meetings subsection
|
|
meetingsAttended: number;
|
|
meetingsOrganized: number;
|
|
meetingDurationSeconds: number;
|
|
// Optional: only render Zoom row if non-null (D-17 presence rule)
|
|
zoomCalls: number | null;
|
|
}
|
|
|
|
const subsectionLabel = "text-sm font-semibold text-muted-foreground mb-2";
|
|
const metricRow = "flex justify-between text-sm py-2";
|
|
|
|
function MetricRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className={metricRow}>
|
|
<dt className="text-muted-foreground">{label}</dt>
|
|
<dd className="text-foreground tabular-nums">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function EngagementProfileBreakdown(props: EngagementProfileBreakdownProps) {
|
|
const utilizationPct = props.hoursWorked > 0
|
|
? Math.round((props.billableHours / props.hoursWorked) * 100)
|
|
: null;
|
|
const meetingHours = props.meetingDurationSeconds / 3600;
|
|
const showAfterHours = props.afterHoursMessagesPct > 0 || props.afterHoursMeetingsPct > 0;
|
|
const showZoom = props.zoomCalls !== null && props.zoomCalls !== undefined;
|
|
|
|
return (
|
|
<Card className="py-0 shadow-none">
|
|
<CardContent className="px-4 py-4 space-y-4">
|
|
{/* Time */}
|
|
<section>
|
|
<h3 className={subsectionLabel}>Time</h3>
|
|
<dl>
|
|
<MetricRow label="Hours worked" value={`${props.hoursWorked.toFixed(1)}h`} />
|
|
<MetricRow label="Billable hours" value={`${props.billableHours.toFixed(1)}h`} />
|
|
<MetricRow label="Days worked" value={String(props.daysWorked)} />
|
|
{utilizationPct !== null && (
|
|
<MetricRow label={`Utilization · ${utilizationPct}%`} value={`${utilizationPct}%`} />
|
|
)}
|
|
</dl>
|
|
</section>
|
|
|
|
{/* Communication */}
|
|
<section className="border-t border-border pt-3">
|
|
<h3 className={subsectionLabel}>Communication</h3>
|
|
<dl>
|
|
<MetricRow label="Teams messages" value={String(props.teamsMessages)} />
|
|
<MetricRow label="Emails sent" value={String(props.emailsSent)} />
|
|
{showAfterHours && (
|
|
<div className={metricRow}>
|
|
<dt className="text-muted-foreground">
|
|
After-hours · {props.afterHoursMessagesPct}% messages, {props.afterHoursMeetingsPct}% meetings
|
|
</dt>
|
|
<dd className="text-foreground tabular-nums" />
|
|
</div>
|
|
)}
|
|
</dl>
|
|
</section>
|
|
|
|
{/* Meetings */}
|
|
<section className="border-t border-border pt-3">
|
|
<h3 className={subsectionLabel}>Meetings</h3>
|
|
<dl>
|
|
<MetricRow label="Meetings attended" value={String(props.meetingsAttended)} />
|
|
<MetricRow label="Meetings organized" value={String(props.meetingsOrganized)} />
|
|
<MetricRow label="Meeting duration" value={`${meetingHours.toFixed(1)}h`} />
|
|
{showZoom && (
|
|
<MetricRow label="Zoom calls" value={String(props.zoomCalls)} />
|
|
)}
|
|
</dl>
|
|
</section>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|