mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
243 lines
7.0 KiB
TypeScript
243 lines
7.0 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
Group,
|
|
SimpleGrid,
|
|
Skeleton,
|
|
Stack,
|
|
Text,
|
|
ThemeIcon,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import {
|
|
Boxes,
|
|
Clock,
|
|
Container,
|
|
Eye,
|
|
FileText,
|
|
Pencil,
|
|
} from "lucide-react";
|
|
|
|
import { PageContainer, PageHeader } from "@/components/page";
|
|
import { useContractTemplates } from "@/hooks/contract-templates/useContractTemplates";
|
|
import type { ContractTemplate } from "@/services/contract-templates.service";
|
|
import TemplatePreviewModal from "./TemplatePreviewModal";
|
|
|
|
const DIRECTION_LABEL: Record<string, string> = {
|
|
IMPORT: "Import",
|
|
EXPORT: "Export",
|
|
INTERCITY: "Intercity",
|
|
};
|
|
|
|
const DIRECTION_DOT: Record<string, string> = {
|
|
IMPORT: "var(--mantine-color-blue-5)",
|
|
EXPORT: "var(--mantine-color-violet-5)",
|
|
INTERCITY: "var(--mantine-color-orange-5)",
|
|
};
|
|
|
|
function templateDirection(code: ContractTemplate["code"]): string {
|
|
return code.split("_")[0];
|
|
}
|
|
|
|
function isBulk(code: ContractTemplate["code"]): boolean {
|
|
return code.endsWith("_BULK");
|
|
}
|
|
|
|
function formatUpdated(value: string): string {
|
|
return new Date(value).toLocaleDateString("en-GB", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
export default function ContractTemplatesPage() {
|
|
const navigate = useNavigate();
|
|
const { data: templates, isLoading } = useContractTemplates();
|
|
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
|
|
|
const previewTemplate = templates?.find((t) => t.code === previewCode);
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title="Contract templates"
|
|
subtitle="The six contract documents generated when a contract is approved — one per trade direction and freight type. Articles are fully editable."
|
|
/>
|
|
|
|
<SimpleGrid cols={{ base: 1, md: 2, xl: 3 }} spacing="lg">
|
|
{isLoading
|
|
? Array.from({ length: 6 }, (_, i) => <TemplateCardSkeleton key={i} />)
|
|
: (templates ?? []).map((template) => (
|
|
<TemplateCard
|
|
key={template.code}
|
|
template={template}
|
|
onPreview={() => setPreviewCode(template.code)}
|
|
onEdit={() =>
|
|
navigate(`/dashboard/contract-templates/${template.code}`)
|
|
}
|
|
/>
|
|
))}
|
|
</SimpleGrid>
|
|
|
|
<TemplatePreviewModal
|
|
code={previewCode}
|
|
title={previewTemplate ? `${previewTemplate.name} — preview` : undefined}
|
|
onClose={() => setPreviewCode(null)}
|
|
/>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
function TemplateCard({
|
|
template,
|
|
onPreview,
|
|
onEdit,
|
|
}: {
|
|
template: ContractTemplate;
|
|
onPreview: () => void;
|
|
onEdit: () => void;
|
|
}) {
|
|
const direction = templateDirection(template.code);
|
|
const bulk = isBulk(template.code);
|
|
|
|
return (
|
|
<Card
|
|
withBorder
|
|
radius="lg"
|
|
padding={0}
|
|
className="group flex flex-col overflow-hidden transition-all duration-150 hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<Stack gap="md" p="lg" style={{ flex: 1 }}>
|
|
{/* Kicker row: muted icon well + category label + state */}
|
|
<Group justify="space-between" align="center" wrap="nowrap">
|
|
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<ThemeIcon size={40} radius="md" variant="light" color="gray" c="gray.6">
|
|
{bulk ? (
|
|
<Boxes size={20} strokeWidth={1.75} />
|
|
) : (
|
|
<Container size={20} strokeWidth={1.75} />
|
|
)}
|
|
</ThemeIcon>
|
|
<Group gap={7} wrap="nowrap">
|
|
<Box
|
|
w={7}
|
|
h={7}
|
|
style={{
|
|
borderRadius: 999,
|
|
flexShrink: 0,
|
|
background: DIRECTION_DOT[direction] ?? "var(--mantine-color-gray-5)",
|
|
}}
|
|
/>
|
|
<Text size="xs" fw={600} tt="uppercase" lts="0.06em" c="dimmed">
|
|
{DIRECTION_LABEL[direction] ?? direction} ·{" "}
|
|
{bulk ? "Bulk" : "Container"}
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
{!template.isActive && (
|
|
<Tooltip label="Not used for new contracts" withArrow>
|
|
<Badge size="sm" variant="light" color="red">
|
|
Inactive
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
</Group>
|
|
|
|
{/* Name + description */}
|
|
<div>
|
|
<Text fw={600} size="md" lh={1.35}>
|
|
{template.name}
|
|
</Text>
|
|
<Text size="sm" c="dimmed" lineClamp={2} mt={4} lh={1.5}>
|
|
{template.description || template.documentTitle}
|
|
</Text>
|
|
</div>
|
|
|
|
{/* Meta stats */}
|
|
<Group gap="lg" mt="auto">
|
|
<Group gap={5} wrap="nowrap">
|
|
<FileText size={13} className="text-gray-400" />
|
|
<Text size="xs" c="dimmed">
|
|
{template.articles.length} article
|
|
{template.articles.length !== 1 ? "s" : ""}
|
|
</Text>
|
|
</Group>
|
|
<Group gap={5} wrap="nowrap">
|
|
<Clock size={13} className="text-gray-400" />
|
|
<Text size="xs" c="dimmed">
|
|
Updated {formatUpdated(template.updatedAt)}
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
</Stack>
|
|
|
|
{/* Footer actions, separated by a hairline */}
|
|
<Box
|
|
px="md"
|
|
py="xs"
|
|
style={{ borderTop: "1px solid var(--mantine-color-default-border)" }}
|
|
>
|
|
<Group justify="space-between">
|
|
<Button
|
|
variant="subtle"
|
|
color="gray"
|
|
size="compact-sm"
|
|
radius="md"
|
|
leftSection={<Eye size={14} />}
|
|
onClick={onPreview}
|
|
>
|
|
Preview
|
|
</Button>
|
|
<Button
|
|
variant="light"
|
|
color="edr-green"
|
|
size="compact-sm"
|
|
radius="md"
|
|
leftSection={<Pencil size={14} />}
|
|
onClick={onEdit}
|
|
>
|
|
Edit articles
|
|
</Button>
|
|
</Group>
|
|
</Box>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function TemplateCardSkeleton() {
|
|
return (
|
|
<Card withBorder radius="lg" padding={0} className="overflow-hidden">
|
|
<Stack gap="md" p="lg">
|
|
<Group gap="sm">
|
|
<Skeleton height={40} width={40} radius="md" />
|
|
<Skeleton height={10} width={120} radius="xl" />
|
|
</Group>
|
|
<div>
|
|
<Skeleton height={14} width="70%" radius="xl" />
|
|
<Skeleton height={10} width="95%" radius="xl" mt={10} />
|
|
<Skeleton height={10} width="60%" radius="xl" mt={6} />
|
|
</div>
|
|
<Group gap="lg">
|
|
<Skeleton height={10} width={70} radius="xl" />
|
|
<Skeleton height={10} width={110} radius="xl" />
|
|
</Group>
|
|
</Stack>
|
|
<Box
|
|
px="md"
|
|
py="xs"
|
|
style={{ borderTop: "1px solid var(--mantine-color-default-border)" }}
|
|
>
|
|
<Group justify="space-between">
|
|
<Skeleton height={26} width={90} radius="md" />
|
|
<Skeleton height={26} width={110} radius="md" />
|
|
</Group>
|
|
</Box>
|
|
</Card>
|
|
);
|
|
}
|