Files
edr-platform/apps/edr-freight-web/backoffice/src/components/contracts/ContractRevisionTimeline.tsx
2026-07-20 12:24:58 +00:00

135 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}