mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
contrat,booking,global logestic
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user