diff --git a/apps/edr-freight-web/backoffice/src/pages/contract_templates/ContractTemplatesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contract_templates/ContractTemplatesPage.tsx index e13c8a741..4a3654aa3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contract_templates/ContractTemplatesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contract_templates/ContractTemplatesPage.tsx @@ -2,17 +2,25 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Badge, + Box, Button, Card, - Center, Group, - Loader, SimpleGrid, + Skeleton, Stack, Text, ThemeIcon, + Tooltip, } from "@mantine/core"; -import { Boxes, Container, Eye, FileSignature, Pencil } from "lucide-react"; +import { + Boxes, + Clock, + Container, + Eye, + FileText, + Pencil, +} from "lucide-react"; import { PageContainer, PageHeader } from "@/components/page"; import { useContractTemplates } from "@/hooks/contract-templates/useContractTemplates"; @@ -25,10 +33,10 @@ const DIRECTION_LABEL: Record = { INTERCITY: "Intercity", }; -const DIRECTION_COLOR: Record = { - IMPORT: "edr-green", - EXPORT: "teal", - INTERCITY: "lime", +const DIRECTION_DOT: Record = { + IMPORT: "var(--mantine-color-blue-5)", + EXPORT: "var(--mantine-color-violet-5)", + INTERCITY: "var(--mantine-color-orange-5)", }; function templateDirection(code: ContractTemplate["code"]): string { @@ -39,6 +47,14 @@ 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(); @@ -53,94 +69,20 @@ export default function ContractTemplatesPage() { subtitle="The six contract documents generated when a contract is approved — one per trade direction and freight type. Articles are fully editable." /> - {isLoading ? ( -
- -
- ) : ( - - {(templates ?? []).map((template) => { - const direction = templateDirection(template.code); - return ( - - - - - {isBulk(template.code) ? ( - - ) : ( - - )} - - - - {DIRECTION_LABEL[direction] ?? direction} - - - {isBulk(template.code) ? "Bulk" : "Container"} - - {!template.isActive && ( - - Inactive - - )} - - - -
- - {template.name} - - - {template.description || template.documentTitle} - -
- - - - - {template.articles.length} articles · updated{" "} - {new Date(template.updatedAt).toLocaleDateString("en-GB", { - day: "numeric", - month: "short", - year: "numeric", - })} - - - - - - - -
-
- ); - })} -
- )} + + {isLoading + ? Array.from({ length: 6 }, (_, i) => ) + : (templates ?? []).map((template) => ( + setPreviewCode(template.code)} + onEdit={() => + navigate(`/dashboard/contract-templates/${template.code}`) + } + /> + ))} + ); } + +function TemplateCard({ + template, + onPreview, + onEdit, +}: { + template: ContractTemplate; + onPreview: () => void; + onEdit: () => void; +}) { + const direction = templateDirection(template.code); + const bulk = isBulk(template.code); + + return ( + + + {/* Kicker row: muted icon well + category label + state */} + + + + {bulk ? ( + + ) : ( + + )} + + + + + {DIRECTION_LABEL[direction] ?? direction} ·{" "} + {bulk ? "Bulk" : "Container"} + + + + {!template.isActive && ( + + + Inactive + + + )} + + + {/* Name + description */} +
+ + {template.name} + + + {template.description || template.documentTitle} + +
+ + {/* Meta stats */} + + + + + {template.articles.length} article + {template.articles.length !== 1 ? "s" : ""} + + + + + + Updated {formatUpdated(template.updatedAt)} + + + +
+ + {/* Footer actions, separated by a hairline */} + + + + + + +
+ ); +} + +function TemplateCardSkeleton() { + return ( + + + + + + +
+ + + +
+ + + + +
+ + + + + + +
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/services/routes.service.ts b/apps/edr-freight-web/backoffice/src/services/routes.service.ts index 9c59e31ad..2317c693d 100644 --- a/apps/edr-freight-web/backoffice/src/services/routes.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/routes.service.ts @@ -43,8 +43,18 @@ export interface SaveRoutePayload { * Human-readable route label: yard names, not yard codes. Staff read * "Addis Ababa → Dire Dawa", not "ADDIS_ABABA → DIRE_DAWA". Falls back to the * code only when a yard has no label. + * + * When milestones are present they ARE the full ordered corridor (origin first, + * destination last), so the label shows every stop: + * "Addis Ababa → Adama → Dire Dawa". */ export function formatRouteLabel(route: RouteRecord): string { + const stops = [...(route.milestones ?? [])] + .sort((a, b) => a.sequenceNo - b.sequenceNo) + .map((m) => m.yard?.label ?? m.yard?.code) + .filter((name): name is string => Boolean(name)); + if (stops.length >= 2) return stops.join(' → '); + const origin = route.originYard?.label ?? route.originYard?.code ?? 'Origin'; const dest =