import { useMemo } from "react"; import { BadgeCheck, CalendarClock, Flame, Send, ShieldCheck, FileSignature, } from "lucide-react"; import { Group, Stack, Text, Timeline, Tooltip } from "@mantine/core"; import type { Freight } from "@edr/types"; import { formatDate } from "@/lib/format"; import { CONTRACT_APPROVAL_ROLE_LABELS, HAZARDOUS_APPROVAL_ROLE_PERMISSION, } from "@/lib/permissions"; interface ContractMilestonesTimelineProps { contract: Freight.IContract; } const SIGNATURE_ROLE_LABELS: Record = { CUSTOMER: "Signed by customer", STAFF: "Signed by EDR — line staff", DIRECTOR: "Signed by EDR — director", CEO: "Signed by EDR — CEO", }; /** "27 Jul 2026, 18:18" — the exact stamp, shown in the tooltip. */ function formatWhen(iso: string): string { return new Date(iso).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); } /** "3 hours ago" — the at-a-glance read. */ function formatAgo(iso: string): string { const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000); if (seconds < 60) return "just now"; const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ["year", 31536000], ["month", 2592000], ["day", 86400], ["hour", 3600], ["minute", 60], ]; const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); for (const [unit, secondsPerUnit] of units) { if (seconds >= secondsPerUnit) { return rtf.format(-Math.floor(seconds / secondsPerUnit), unit); } } return "just now"; } type MilestoneIcon = typeof Send; interface Milestone { key: string; at: string; title: string; detail?: string; color: string; icon: MilestoneIcon; } /** * The dated moments of a contract's life — submission, hazardous approval, * final approval, both parties' signatures, full execution — read straight off * the contract and its already-loaded approvalSteps/signatures (no extra * fetch). Sits above the document edit history on the History tab. */ export function ContractMilestonesTimeline({ contract, }: ContractMilestonesTimelineProps) { const milestones = useMemo(() => { const items: Milestone[] = []; // A DRAFT/RENEWAL_DRAFT contract hasn't been (re)submitted yet — nothing // to date. submittedAt is only tracked going forward; a contract that // reached SUBMITTED before that column existed falls back to createdAt. const submittedAt = contract.submittedAt ?? (contract.status !== "DRAFT" && contract.status !== "RENEWAL_DRAFT" ? contract.createdAt : null); if (submittedAt) { items.push({ key: "submitted", at: submittedAt, title: "Submitted for review", color: "blue", icon: Send, }); } // Every acted approval step, not just hazardous ones — this is the one // place the approval-time record shows up in the page's main content // (the sidebar's ContractApprovalStepsCard has the same times, but only // there, and only while the chain is still actionable). for (const step of contract.approvalSteps ?? []) { if (!step.actedAt) continue; const hazard = step.requiredRole in HAZARDOUS_APPROVAL_ROLE_PERMISSION; items.push({ key: `step-${step.id}`, at: step.actedAt, title: `${CONTRACT_APPROVAL_ROLE_LABELS[step.requiredRole] ?? step.requiredRole} ${step.status === "REJECTED" ? "rejected" : "approved"}`, detail: step.note ?? undefined, color: step.status === "REJECTED" ? "red" : hazard ? "orange" : "edr-green", icon: hazard ? Flame : ShieldCheck, }); } if (contract.contractGeneratedAt) { items.push({ key: "approved", at: contract.contractGeneratedAt, title: "Contract approved", detail: "Every approval step cleared and the document was generated", color: "edr-green", icon: ShieldCheck, }); } for (const sig of contract.signatures ?? []) { items.push({ key: `signature-${sig.id}`, at: sig.signedAt, title: SIGNATURE_ROLE_LABELS[sig.role] ?? `Signed by ${sig.role}`, detail: sig.signerDisplayName, color: "grape", icon: FileSignature, }); } if (contract.fullyExecutedAt) { items.push({ key: "executed", at: contract.fullyExecutedAt, title: "Fully executed", detail: "Both parties have signed", color: "edr-green", icon: BadgeCheck, }); } return items.sort( (a, b) => new Date(a.at).getTime() - new Date(b.at).getTime(), ); }, [contract]); const hasValidity = contract.contractValidFrom && contract.contractValidUntil; if (milestones.length === 0 && !hasValidity) { return ( No dated milestones recorded yet. ); } return ( {hasValidity && ( Valid {formatDate(contract.contractValidFrom!)} {" → "} {formatDate(contract.contractValidUntil!)} )} {milestones.length > 0 && ( {milestones.map((m) => { const Icon = m.icon; return ( } color={m.color} title={ {m.title} {formatAgo(m.at)} } > {m.detail && ( {m.detail} )} ); })} )} ); }