Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/WorkflowStep.tsx
2026-06-09 23:25:52 +00:00

221 lines
6.1 KiB
TypeScript

import type { ReactNode } from "react";
import { Badge, Box, Collapse, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import type { LucideIcon } from "lucide-react";
import { Check, ChevronDown, Lock } from "lucide-react";
import { freightBrand } from "@/theme/freight-brand";
export type WorkflowStepState = "complete" | "active" | "upcoming";
/**
* A single collapsible step in the scheduling workflow. Steps are stacked
* inside <WorkflowRail> which paints the continuous connector behind the
* status circles.
*/
export function WorkflowStep({
index,
icon: Icon,
title,
subtitle,
state,
open,
onToggle,
rightSlot,
locked = false,
children,
}: {
index: number;
icon: LucideIcon;
title: string;
subtitle?: string;
state: WorkflowStepState;
open: boolean;
onToggle: () => void;
rightSlot?: ReactNode;
locked?: boolean;
children: ReactNode;
}) {
const isComplete = state === "complete";
const isActive = state === "active";
const circle = (() => {
if (isComplete) {
return {
bg: freightBrand.primary,
color: "white",
border: freightBrand.primary,
shadow: `0 4px 10px ${freightBrand.ring}`,
};
}
if (isActive) {
return {
bg: "white",
color: freightBrand.primary,
border: freightBrand.primary,
shadow: `0 0 0 4px ${freightBrand.ring}`,
};
}
return {
bg: "var(--mantine-color-gray-1)",
color: "var(--mantine-color-gray-5)",
border: "var(--mantine-color-gray-3)",
shadow: "none",
};
})();
return (
<Group gap="md" align="stretch" wrap="nowrap">
{/* Status circle (sits above the rail) */}
<Box
style={{
width: 40,
flexShrink: 0,
display: "flex",
justifyContent: "center",
}}
>
<Box
style={{
width: 40,
height: 40,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: circle.bg,
color: circle.color,
border: `2px solid ${circle.border}`,
boxShadow: circle.shadow,
fontWeight: 700,
fontSize: 15,
transition: "all 150ms ease",
position: "relative",
zIndex: 1,
}}
>
{isComplete ? <Check size={20} /> : <Icon size={18} />}
</Box>
</Box>
{/* Step card */}
<Box
style={{
flex: 1,
minWidth: 0,
marginBottom: 4,
borderRadius: 16,
border: `1px solid ${
isActive ? freightBrand.mutedBorder : "var(--mantine-color-gray-2)"
}`,
background: isActive ? freightBrand.mutedBg : "white",
boxShadow: isActive ? `0 6px 20px ${freightBrand.ring}` : "none",
overflow: "hidden",
transition: "all 150ms ease",
}}
>
<UnstyledButton
onClick={onToggle}
style={{ display: "block", width: "100%", padding: "14px 18px" }}
>
<Group justify="space-between" wrap="nowrap" gap="sm">
<Stack gap={2} style={{ minWidth: 0 }}>
<Group gap={8} wrap="nowrap">
<Text
size="xs"
fw={700}
c={isComplete || isActive ? "green.7" : "dimmed"}
style={{ letterSpacing: 0.6 }}
>
STEP {index + 1}
</Text>
{isComplete ? (
<Badge size="xs" variant="light" color="green" radius="sm">
Done
</Badge>
) : null}
</Group>
<Text fw={700} size="md" lh={1.2} truncate>
{title}
</Text>
{subtitle ? (
<Text size="xs" c="dimmed" truncate>
{subtitle}
</Text>
) : null}
</Stack>
<Group gap="sm" wrap="nowrap" style={{ flexShrink: 0 }}>
{rightSlot}
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
borderRadius: 8,
background: open
? "var(--mantine-color-green-0)"
: "var(--mantine-color-gray-1)",
color: open
? "var(--mantine-color-green-7)"
: "var(--mantine-color-gray-6)",
}}
>
{locked ? (
<Lock size={14} />
) : (
<ChevronDown
size={16}
style={{
transform: open ? "rotate(180deg)" : "none",
transition: "transform 150ms ease",
}}
/>
)}
</Box>
</Group>
</Group>
</UnstyledButton>
<Collapse expanded={open}>
<Box
px="lg"
pb="lg"
pt={4}
style={{ borderTop: "1px solid var(--mantine-color-gray-1)" }}
>
<Box pt="md">{children}</Box>
</Box>
</Collapse>
</Box>
</Group>
);
}
/**
* Wraps a list of <WorkflowStep> and paints the continuous vertical rail that
* connects the status circles.
*/
export function WorkflowRail({ children }: { children: ReactNode }) {
return (
<Box style={{ position: "relative" }}>
{/* connector rail behind the circles (circle is 40px → center at 20) */}
<Box
style={{
position: "absolute",
left: 19,
top: 24,
bottom: 24,
width: 2,
background:
"linear-gradient(180deg, var(--mantine-color-green-3) 0%, var(--mantine-color-gray-3) 100%)",
borderRadius: 2,
pointerEvents: "none",
}}
/>
<Stack gap="md">{children}</Stack>
</Box>
);
}