mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Group,
|
|
Stack,
|
|
Text,
|
|
Textarea,
|
|
ThemeIcon,
|
|
} from "@mantine/core";
|
|
import { Check, Circle, Clock, MinusCircle } from "lucide-react";
|
|
import type { Freight } from "@edr/types";
|
|
|
|
export interface ClearanceMilestoneTimelineProps {
|
|
milestones: Freight.IClearanceMilestone[];
|
|
/** Complete a milestone by code (omit to render read-only). */
|
|
onComplete?: (code: string, note?: string) => void;
|
|
/** True while a complete mutation is in flight. */
|
|
busy?: boolean;
|
|
}
|
|
|
|
const STATUS_META: Record<
|
|
Freight.MilestoneStatus,
|
|
{ color: string; label: string }
|
|
> = {
|
|
COMPLETED: { color: "edr-green", label: "Completed" },
|
|
PENDING: { color: "gray", label: "Pending" },
|
|
SKIPPED: { color: "gray", label: "Skipped" },
|
|
};
|
|
|
|
/** Vertical timeline of GL clearance milestones with inline complete actions. */
|
|
export function ClearanceMilestoneTimeline({
|
|
milestones,
|
|
onComplete,
|
|
busy,
|
|
}: ClearanceMilestoneTimelineProps) {
|
|
const [openNote, setOpenNote] = useState<Record<string, boolean>>({});
|
|
const [notes, setNotes] = useState<Record<string, string>>({});
|
|
|
|
const sorted = [...milestones].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
const nextPending = sorted.find((m) => m.status === "PENDING");
|
|
|
|
if (sorted.length === 0) {
|
|
return (
|
|
<Text size="sm" c="dimmed">
|
|
No milestones for this shipment yet.
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack gap={0}>
|
|
{sorted.map((m, index) => {
|
|
const isLast = index === sorted.length - 1;
|
|
const meta = STATUS_META[m.status];
|
|
const isNext = nextPending?.id === m.id;
|
|
const Icon =
|
|
m.status === "COMPLETED"
|
|
? Check
|
|
: m.status === "SKIPPED"
|
|
? MinusCircle
|
|
: isNext
|
|
? Clock
|
|
: Circle;
|
|
|
|
return (
|
|
<Group key={m.id} gap="sm" wrap="nowrap" align="flex-start">
|
|
<Stack gap={0} align="center" style={{ flexShrink: 0 }}>
|
|
<ThemeIcon
|
|
variant={m.status === "COMPLETED" ? "filled" : "light"}
|
|
color={isNext ? "edr-green" : meta.color}
|
|
radius="xl"
|
|
size={30}
|
|
>
|
|
<Icon size={15} strokeWidth={2.2} />
|
|
</ThemeIcon>
|
|
{!isLast && (
|
|
<Box
|
|
style={{
|
|
width: 2,
|
|
flex: 1,
|
|
minHeight: 28,
|
|
background:
|
|
m.status === "COMPLETED"
|
|
? "var(--mantine-color-edr-green-4)"
|
|
: "var(--mantine-color-gray-2)",
|
|
}}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
|
|
<Box pb={isLast ? 0 : "md"} style={{ flex: 1, minWidth: 0 }}>
|
|
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
|
<Box style={{ minWidth: 0 }}>
|
|
<Text size="sm" fw={600} truncate>
|
|
{m.milestoneLabel}
|
|
</Text>
|
|
<Group gap={6} mt={2} wrap="nowrap">
|
|
<Badge size="xs" variant="light" color={meta.color}>
|
|
{meta.label}
|
|
</Badge>
|
|
{m.ownerRegion ? (
|
|
<Badge size="xs" variant="default">
|
|
{m.ownerRegion}
|
|
</Badge>
|
|
) : null}
|
|
</Group>
|
|
{m.note ? (
|
|
<Text size="xs" c="dimmed" mt={4}>
|
|
{m.note}
|
|
</Text>
|
|
) : null}
|
|
</Box>
|
|
|
|
{onComplete && m.status === "PENDING" && isNext ? (
|
|
!openNote[m.milestoneCode] ? (
|
|
<Button
|
|
size="compact-xs"
|
|
color="edr-green"
|
|
leftSection={<Check size={13} />}
|
|
disabled={busy}
|
|
onClick={() =>
|
|
setOpenNote((o) => ({
|
|
...o,
|
|
[m.milestoneCode]: true,
|
|
}))
|
|
}
|
|
>
|
|
Complete
|
|
</Button>
|
|
) : null
|
|
) : null}
|
|
</Group>
|
|
|
|
{onComplete && openNote[m.milestoneCode] && (
|
|
<Box mt="xs">
|
|
<Textarea
|
|
placeholder="Optional note for this milestone…"
|
|
value={notes[m.milestoneCode] ?? ""}
|
|
onChange={(e) =>
|
|
setNotes((n) => ({
|
|
...n,
|
|
[m.milestoneCode]: e.currentTarget.value,
|
|
}))
|
|
}
|
|
autosize
|
|
minRows={2}
|
|
size="sm"
|
|
radius="md"
|
|
/>
|
|
<Group justify="flex-end" gap={8} mt={8}>
|
|
<Button
|
|
size="compact-xs"
|
|
variant="subtle"
|
|
color="gray"
|
|
disabled={busy}
|
|
onClick={() =>
|
|
setOpenNote((o) => ({
|
|
...o,
|
|
[m.milestoneCode]: false,
|
|
}))
|
|
}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="compact-xs"
|
|
color="edr-green"
|
|
leftSection={<Check size={13} />}
|
|
loading={busy}
|
|
onClick={() => {
|
|
onComplete(
|
|
m.milestoneCode,
|
|
notes[m.milestoneCode]?.trim() || undefined,
|
|
);
|
|
setOpenNote((o) => ({
|
|
...o,
|
|
[m.milestoneCode]: false,
|
|
}));
|
|
}}
|
|
>
|
|
Mark complete
|
|
</Button>
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Group>
|
|
);
|
|
})}
|
|
</Stack>
|
|
);
|
|
}
|