import { useQuery } from "@tanstack/react-query"; import { History, User } from "lucide-react"; import { Avatar, Badge, Group, Loader, Stack, Text, Timeline, Tooltip, } from "@mantine/core"; import type { Freight } from "@edr/types"; import { contractsService } from "@/services/contracts.service"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; interface ContractRevisionTimelineProps { contractId: string; /** Rendered as a plain block instead of a SectionCard (own-tab layout). */ bare?: boolean; } type Change = Freight.IContractDocumentChange; /** Badge colour + verb per change kind, so a revision reads at a glance. */ const CHANGE_STYLES: Record = { ARTICLE_ADDED: { color: "green", label: "Added" }, ARTICLE_REMOVED: { color: "red", label: "Removed" }, ARTICLE_RENAMED: { color: "violet", label: "Renamed" }, ARTICLE_BODY_CHANGED: { color: "blue", label: "Edited" }, ARTICLE_REORDERED: { color: "gray", label: "Reordered" }, DOCUMENT_TITLE_CHANGED: { color: "grape", label: "Title" }, WHEREAS_CHANGED: { color: "teal", label: "Recitals" }, FIELD_CHANGED: { color: "orange", label: "Field" }, }; /** Initials for the actor avatar — "Abenezer Haile" → "AH". */ function initials(name: string): string { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() ?? "") .join(""); } /** Role slugs arrive like "-marketing-director-"; render them readably. */ function prettyRole(role: string): string { const cleaned = role.replace(/^-+|-+$/g, "").replace(/[-_]+/g, " ").trim(); if (!cleaned) return role; return cleaned.charAt(0).toUpperCase() + cleaned.slice(1).toLowerCase(); } /** What the change applies to — an article title, or the document itself. */ function changeSubject(change: Change): string { switch (change.kind) { case "DOCUMENT_TITLE_CHANGED": return change.fromTitle ? `“${change.fromTitle}” → “${change.title}”` : change.title; case "WHEREAS_CHANGED": { const parts: string[] = []; if (change.added) parts.push(`+${change.added}`); if (change.removed) parts.push(`−${change.removed}`); return parts.join(" ") || "changed"; } case "ARTICLE_RENAMED": return `“${change.fromTitle}” → “${change.title}”`; case "ARTICLE_REORDERED": return `${change.title} (${change.fromOrder} → ${change.toOrder})`; case "FIELD_CHANGED": return `${change.label}: ${change.from ?? "—"} → ${change.to ?? "—"}`; default: return change.title; } } function formatWhen(iso: string): string { const date = new Date(iso); return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); } /** "3 hours ago" — the at-a-glance read; the exact stamp sits beside it. */ 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"; } /** * Audit trail of edits to the contract document. The document stays editable * through the approval chain, so this is the record of who changed what. */ export function ContractRevisionTimeline({ contractId, bare = false, }: ContractRevisionTimelineProps) { const { data: revisions, isLoading } = useQuery({ queryKey: ["contracts", contractId, "document-revisions"], queryFn: () => contractsService.getContractDocumentRevisions(contractId), }); const body = isLoading ? ( Loading history… ) : !revisions?.length ? ( No edits recorded yet Every change to this contract — its articles during review and approval, or its details while the customer can still edit it — is logged here with who made it and when. ) : ( {revisions.map((revision) => { const who = revision.actorName?.trim(); const role = revision.actorRole ? prettyRole(revision.actorRole) : null; return ( {who ? initials(who) : } } title={ {who ?? role ?? "Unknown user"} {role && who && ( {role} )} {formatAgo(revision.createdAt)} } > {revision.summary && ( {revision.summary} )} {revision.changes.map((change, index) => { const style = CHANGE_STYLES[change.kind]; return ( {style?.label ?? change.kind} {changeSubject(change)} ); })} ); })} ); if (bare) return body; return ( {body} ); }