mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
||
import { History } from "lucide-react";
|
||
import { Badge, Group, Loader, Stack, Text, Timeline } 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;
|
||
}
|
||
|
||
type Change = Freight.IContractDocumentChange;
|
||
|
||
/** Badge colour + verb per change kind, so a revision reads at a glance. */
|
||
const CHANGE_STYLES: Record<Change["kind"], { color: string; label: string }> = {
|
||
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" },
|
||
};
|
||
|
||
/** 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})`;
|
||
default:
|
||
return change.title;
|
||
}
|
||
}
|
||
|
||
function formatWhen(iso: string): string {
|
||
const date = new Date(iso);
|
||
return date.toLocaleString(undefined, {
|
||
dateStyle: "medium",
|
||
timeStyle: "short",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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,
|
||
}: ContractRevisionTimelineProps) {
|
||
const { data: revisions, isLoading } = useQuery({
|
||
queryKey: ["contracts", contractId, "document-revisions"],
|
||
queryFn: () => contractsService.getContractDocumentRevisions(contractId),
|
||
});
|
||
|
||
return (
|
||
<SectionCard icon={History} title="Document history">
|
||
{isLoading ? (
|
||
<Group gap="xs">
|
||
<Loader size="xs" />
|
||
<Text size="sm" c="dimmed">
|
||
Loading history…
|
||
</Text>
|
||
</Group>
|
||
) : !revisions?.length ? (
|
||
<Text size="sm" c="dimmed">
|
||
No edits recorded yet. Changes made to the contract articles during
|
||
approval will appear here.
|
||
</Text>
|
||
) : (
|
||
<Timeline
|
||
active={revisions.length}
|
||
bulletSize={18}
|
||
lineWidth={2}
|
||
color="edr-green"
|
||
>
|
||
{revisions.map((revision) => (
|
||
<Timeline.Item
|
||
key={revision.id}
|
||
title={
|
||
<Group gap="xs" wrap="nowrap">
|
||
<Text size="sm" fw={600}>
|
||
{revision.actorRole ?? "Staff"}
|
||
</Text>
|
||
<Text size="xs" c="dimmed">
|
||
{formatWhen(revision.createdAt)}
|
||
</Text>
|
||
</Group>
|
||
}
|
||
>
|
||
<Stack gap={6} mt={4}>
|
||
{revision.summary && (
|
||
<Text size="xs" c="dimmed">
|
||
{revision.summary}
|
||
</Text>
|
||
)}
|
||
{revision.changes.map((change, index) => {
|
||
const style = CHANGE_STYLES[change.kind];
|
||
return (
|
||
<Group key={index} gap="xs" wrap="nowrap" align="flex-start">
|
||
<Badge
|
||
size="xs"
|
||
variant="light"
|
||
color={style?.color ?? "gray"}
|
||
style={{ flexShrink: 0 }}
|
||
>
|
||
{style?.label ?? change.kind}
|
||
</Badge>
|
||
<Text size="xs" style={{ lineHeight: 1.5 }}>
|
||
{changeSubject(change)}
|
||
</Text>
|
||
</Group>
|
||
);
|
||
})}
|
||
</Stack>
|
||
</Timeline.Item>
|
||
))}
|
||
</Timeline>
|
||
)}
|
||
</SectionCard>
|
||
);
|
||
}
|