mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 01:15:03 +00:00
changes
This commit is contained in:
@@ -2,7 +2,11 @@ import { Check } from "lucide-react";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
const BRAND_GREEN = "var(--freight-brand, #0A6F4D)";
|
||||
const GREEN = "#0A8A5F";
|
||||
const BLUE = "#1D6FD1";
|
||||
const BORDER = "#E4EBF1";
|
||||
const MUTED = "#93A4B5";
|
||||
const INK = "#10202F";
|
||||
|
||||
const IMPORT_PHASES = [
|
||||
"CUSTOMER_INTAKE",
|
||||
@@ -24,6 +28,18 @@ const PHASE_LABELS: Record<string, string> = {
|
||||
POST_TRANSIT: "Transit",
|
||||
};
|
||||
|
||||
/** Which desk owns each phase — shown under the label, as in the design. */
|
||||
const PHASE_ACTOR: Record<string, string> = {
|
||||
CUSTOMER_INTAKE: "CUSTOMER",
|
||||
GL_ET_REVIEW: "GL ET",
|
||||
GL_ET_OUTPUT: "GL ET",
|
||||
CUSTOMER_DUTY: "CUSTOMER",
|
||||
GL_ET_POST_CLEARANCE: "GL ET",
|
||||
GL_DJ_COLLECTION: "GL DJ",
|
||||
GL_DJ_LOADING: "GL DJ",
|
||||
POST_TRANSIT: "OPS",
|
||||
};
|
||||
|
||||
const EXPORT_PHASES = [
|
||||
"CUSTOMER_INTAKE",
|
||||
"GL_ET_REVIEW",
|
||||
@@ -38,6 +54,20 @@ function phaseIndex(phases: readonly string[], current?: string | null): number
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
/** Half-width connector; only the segment behind a completed dot is green. */
|
||||
function Line({ done, hidden }: { done: boolean; hidden: boolean }) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 2,
|
||||
borderRadius: 2,
|
||||
background: hidden ? "transparent" : done ? GREEN : BORDER,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClearancePhaseStepper({
|
||||
clearance,
|
||||
tradeDirection,
|
||||
@@ -50,61 +80,69 @@ export function ClearancePhaseStepper({
|
||||
const phases = tradeDirection === "EXPORT" ? EXPORT_PHASES : IMPORT_PHASES;
|
||||
const current = clearance?.phase ?? phases[0];
|
||||
const activeIdx = phaseIndex(phases, current);
|
||||
const dot = compact ? 26 : 28;
|
||||
|
||||
return (
|
||||
<Group gap={0} wrap="nowrap" align="flex-start" style={{ overflowX: "auto" }}>
|
||||
{phases.map((phase, index) => {
|
||||
const isComplete = index < activeIdx;
|
||||
const isActive = index === activeIdx;
|
||||
const isLast = index === phases.length - 1;
|
||||
const actor = PHASE_ACTOR[phase];
|
||||
|
||||
return (
|
||||
<Box key={phase} style={{ flex: isLast ? "0 0 auto" : 1, minWidth: compact ? 72 : 88 }}>
|
||||
<Group gap={0} wrap="nowrap" align="center">
|
||||
<Stack gap={4} align="center" style={{ flexShrink: 0 }}>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: compact ? 28 : 34,
|
||||
height: compact ? 28 : 34,
|
||||
borderRadius: "50%",
|
||||
background: isComplete ? BRAND_GREEN : isActive ? "white" : "var(--mantine-color-gray-1)",
|
||||
border: isActive
|
||||
? `2px solid ${BRAND_GREEN}`
|
||||
: isComplete
|
||||
? "2px solid transparent"
|
||||
: "2px solid var(--mantine-color-gray-3)",
|
||||
color: isComplete ? "white" : isActive ? BRAND_GREEN : "var(--mantine-color-gray-5)",
|
||||
}}
|
||||
>
|
||||
{isComplete ? <Check size={compact ? 14 : 16} strokeWidth={3} /> : null}
|
||||
</Box>
|
||||
<Text
|
||||
size={compact ? "10px" : "xs"}
|
||||
fw={isActive ? 600 : 500}
|
||||
c={isActive ? "edr-green.7" : isComplete ? "dark" : "dimmed"}
|
||||
ta="center"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{PHASE_LABELS[phase] ?? phase}
|
||||
</Text>
|
||||
</Stack>
|
||||
{!isLast && (
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 2,
|
||||
marginInline: 6,
|
||||
marginBottom: compact ? 16 : 20,
|
||||
borderRadius: 2,
|
||||
background: isComplete ? BRAND_GREEN : "var(--mantine-color-gray-2)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Stack
|
||||
key={phase}
|
||||
gap={7}
|
||||
align="center"
|
||||
style={{ flex: 1, minWidth: compact ? 92 : 112 }}
|
||||
>
|
||||
{/* Dot sits centred on its own row so the connectors meet it edge-to-edge. */}
|
||||
<Group gap={0} wrap="nowrap" align="center" style={{ width: "100%" }}>
|
||||
<Line done={isComplete || isActive} hidden={index === 0} />
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
width: dot,
|
||||
height: dot,
|
||||
borderRadius: 999,
|
||||
background: isComplete ? GREEN : "#FFFFFF",
|
||||
border: `2px solid ${
|
||||
isComplete ? GREEN : isActive ? BLUE : BORDER
|
||||
}`,
|
||||
color: isComplete ? "#FFFFFF" : isActive ? BLUE : MUTED,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{isComplete ? <Check size={14} strokeWidth={3} /> : index + 1}
|
||||
</Box>
|
||||
<Line done={isComplete} hidden={index === phases.length - 1} />
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
fz={10.5}
|
||||
fw={700}
|
||||
lh={1.3}
|
||||
ta="center"
|
||||
style={{ color: isActive || isComplete ? INK : MUTED }}
|
||||
>
|
||||
{PHASE_LABELS[phase] ?? phase}
|
||||
</Text>
|
||||
{actor ? (
|
||||
<Text
|
||||
fz={9}
|
||||
fw={700}
|
||||
lts="0.3px"
|
||||
style={{ color: isActive ? BLUE : MUTED, marginTop: -3 }}
|
||||
>
|
||||
{actor}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
|
||||
@@ -2,10 +2,12 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Progress,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Stack,
|
||||
@@ -14,14 +16,9 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import { TransitAssigneePanel } from "@/components/contracts/TransitAssigneePanel";
|
||||
import {
|
||||
TransitPermitMultiUpload,
|
||||
type TransitPermitUploadedRow,
|
||||
} from "@/components/contracts/TransitPermitMultiUpload";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileText,
|
||||
@@ -30,10 +27,17 @@ import {
|
||||
PackageOpen,
|
||||
Receipt,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Ship,
|
||||
Truck,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import { TransitAssigneePanel } from "@/components/contracts/TransitAssigneePanel";
|
||||
import {
|
||||
TransitPermitMultiUpload,
|
||||
type TransitPermitUploadedRow,
|
||||
} from "@/components/contracts/TransitPermitMultiUpload";
|
||||
import {
|
||||
deliveryOrderFileLabel,
|
||||
isDeliveryOrderFileCode,
|
||||
@@ -113,6 +117,9 @@ export function isBookingMilestoneDone(
|
||||
return m?.status === "COMPLETED" || m?.status === "SKIPPED";
|
||||
}
|
||||
|
||||
/** Number of steps in the import stepper — drives the header progress bar. */
|
||||
const IMPORT_STEP_COUNT = 12;
|
||||
|
||||
function computeImportActiveStep(
|
||||
clearance: ClearanceViewLike,
|
||||
bookingCreated: boolean,
|
||||
@@ -322,19 +329,64 @@ export function PhasedClearanceActionPanel({
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{clearance.nextAction ? (
|
||||
<Alert color="blue" variant="light" title="Next step">
|
||||
<Text size="sm">
|
||||
<strong>{clearance.nextAction.actor.replace("_", " ")}</strong> —{" "}
|
||||
{clearance.nextAction.action}
|
||||
</Text>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Paper withBorder radius={13} p={0} style={{ overflow: "hidden" }}>
|
||||
{/* Header: what this workflow is, and how far along it is. */}
|
||||
<Group
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
px={18}
|
||||
py={15}
|
||||
style={{ borderBottom: "1px solid #EFF3F7" }}
|
||||
>
|
||||
<Group gap={9} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<ShieldCheck size={16} color="#0A8A5F" />
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz={14} fw={700} c="edr-text">
|
||||
Import pre-booking clearance
|
||||
</Text>
|
||||
<Text fz={11.5} c="#93A4B5" truncate>
|
||||
Step {Math.min(activeStep + 1, IMPORT_STEP_COUNT)} of{" "}
|
||||
{IMPORT_STEP_COUNT}
|
||||
{clearance.nextAction
|
||||
? ` · ${clearance.nextAction.action}`
|
||||
: ""}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
<Group gap={8} wrap="nowrap" style={{ flexShrink: 0 }}>
|
||||
<Progress
|
||||
value={Math.round((activeStep / IMPORT_STEP_COUNT) * 100)}
|
||||
color="edr-green"
|
||||
radius="xl"
|
||||
size={6}
|
||||
w={110}
|
||||
/>
|
||||
<Text fz={11.5} c="#67788A" fw={600}>
|
||||
{Math.round((activeStep / IMPORT_STEP_COUNT) * 100)}%
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Text fw={600} size="sm" mb="md">
|
||||
Import pre-booking clearance
|
||||
</Text>
|
||||
{/* Whose desk the flow is sitting on right now. */}
|
||||
{clearance.nextAction ? (
|
||||
<Group
|
||||
gap={10}
|
||||
wrap="nowrap"
|
||||
px={18}
|
||||
py={12}
|
||||
style={{ background: "#E9F1FC", borderBottom: "1px solid #EFF3F7" }}
|
||||
>
|
||||
<ArrowRight size={15} color="#1D6FD1" style={{ flexShrink: 0 }} />
|
||||
<Text fz={10.5} fw={700} lts="0.4px" c="#1D6FD1" style={{ flexShrink: 0 }}>
|
||||
{clearance.nextAction.actor.replace("_", " ").toUpperCase()}
|
||||
</Text>
|
||||
<Text fz={11.5} fw={600} c="edr-text" style={{ minWidth: 0 }}>
|
||||
{clearance.nextAction.action}
|
||||
</Text>
|
||||
</Group>
|
||||
) : null}
|
||||
|
||||
<Box p="md">
|
||||
<Stepper
|
||||
active={activeStep}
|
||||
orientation="vertical"
|
||||
@@ -826,7 +878,8 @@ export function PhasedClearanceActionPanel({
|
||||
onDownloadFile={onDownloadFile}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
</Stepper>
|
||||
</Stepper>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user