mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 10:10:57 +00:00
fix batch page ui
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Group, Progress, Text, Tooltip } from "@mantine/core";
|
||||
|
||||
import "./batchVisuals.css";
|
||||
|
||||
/**
|
||||
* Shared visual building blocks for the batch board surfaces (list + detail).
|
||||
* Everything keys off the same booking-pipeline color language so the two
|
||||
* pages read as one clean, professional product on a white surface.
|
||||
*/
|
||||
|
||||
export type BatchCounts = {
|
||||
allocated: number;
|
||||
selectedForBatch: number;
|
||||
ready: number;
|
||||
waiting: number;
|
||||
pendingContract: number;
|
||||
expired: number;
|
||||
};
|
||||
|
||||
export const PIPELINE_STAGES: ReadonlyArray<{
|
||||
key: keyof BatchCounts;
|
||||
label: string;
|
||||
color: string;
|
||||
hint: string;
|
||||
}> = [
|
||||
{
|
||||
key: "allocated",
|
||||
label: "Allocated",
|
||||
color: "green",
|
||||
hint: "Assigned to wagons on the train",
|
||||
},
|
||||
{
|
||||
key: "selectedForBatch",
|
||||
label: "Selected",
|
||||
color: "orange",
|
||||
hint: "Picked by batch — customer notified to pay",
|
||||
},
|
||||
{
|
||||
key: "ready",
|
||||
label: "Ready",
|
||||
color: "teal",
|
||||
hint: "Contract signed — waiting for batch pick",
|
||||
},
|
||||
{
|
||||
key: "waiting",
|
||||
label: "Waiting",
|
||||
color: "blue",
|
||||
hint: "Paid — waiting for a slot",
|
||||
},
|
||||
{
|
||||
key: "pendingContract",
|
||||
label: "Pending contract",
|
||||
color: "gray",
|
||||
hint: "Contract not signed yet",
|
||||
},
|
||||
{
|
||||
key: "expired",
|
||||
label: "Expired",
|
||||
color: "red",
|
||||
hint: "Payment deadline missed",
|
||||
},
|
||||
];
|
||||
|
||||
export function totalBookingCount(counts: BatchCounts): number {
|
||||
return PIPELINE_STAGES.reduce((sum, stage) => sum + counts[stage.key], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stacked booking-pipeline bar: one colored segment per batch state, with an
|
||||
* optional dot legend underneath. Reads as a single glanceable funnel.
|
||||
*/
|
||||
export function BookingPipeline({
|
||||
counts,
|
||||
size = 10,
|
||||
showLegend = true,
|
||||
}: {
|
||||
counts: BatchCounts;
|
||||
size?: number;
|
||||
showLegend?: boolean;
|
||||
}) {
|
||||
const total = totalBookingCount(counts);
|
||||
const stages = PIPELINE_STAGES.filter((s) => counts[s.key] > 0);
|
||||
|
||||
if (!total) {
|
||||
return (
|
||||
<Box>
|
||||
<Progress value={0} size={size} radius="xl" />
|
||||
<Text size="xs" c="dimmed" mt={6}>
|
||||
No bookings yet
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Progress.Root size={size} radius="xl">
|
||||
{stages.map((stage) => (
|
||||
<Tooltip
|
||||
key={stage.key}
|
||||
label={`${counts[stage.key]} ${stage.label.toLowerCase()} — ${stage.hint}`}
|
||||
withArrow
|
||||
>
|
||||
<Progress.Section
|
||||
value={(counts[stage.key] / total) * 100}
|
||||
color={stage.color}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Progress.Root>
|
||||
{showLegend ? (
|
||||
<Group gap={12} mt={9} wrap="wrap">
|
||||
{stages.map((stage) => (
|
||||
<Group key={stage.key} gap={5} wrap="nowrap">
|
||||
<Box
|
||||
w={8}
|
||||
h={8}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: `var(--mantine-color-${stage.color}-6)`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text component="span" size="xs" fw={700} c="dark.4">
|
||||
{counts[stage.key]}
|
||||
</Text>{" "}
|
||||
{stage.label.toLowerCase()}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const WINDOW_META: Record<string, { color: string; label: string; pulse: boolean }> = {
|
||||
OPEN: { color: "green", label: "Window open", pulse: true },
|
||||
FULL: { color: "orange", label: "Full", pulse: false },
|
||||
CLOSED: { color: "gray", label: "Closed", pulse: false },
|
||||
};
|
||||
|
||||
/**
|
||||
* Booking-window status pill with a status dot (pulsing while OPEN), styled for
|
||||
* a clean white surface.
|
||||
*/
|
||||
export function WindowStatusPill({ status }: { status: string }) {
|
||||
const meta = WINDOW_META[status] ?? { color: "gray", label: status, pulse: false };
|
||||
return (
|
||||
<Group
|
||||
gap={6}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
padding: "4px 11px",
|
||||
borderRadius: 999,
|
||||
background: `var(--mantine-color-${meta.color}-0)`,
|
||||
border: `1px solid var(--mantine-color-${meta.color}-2)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={7}
|
||||
h={7}
|
||||
className={meta.pulse ? "bb-pulse-dot" : undefined}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
background: `var(--mantine-color-${meta.color}-6)`,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
c={`${meta.color}.8`}
|
||||
style={{ letterSpacing: 0.3, lineHeight: 1, whiteSpace: "nowrap" }}
|
||||
>
|
||||
{meta.label}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Small neutral info chip used in the page headers (date, loco, status, …).
|
||||
* Clean light surface that reads well on white.
|
||||
*/
|
||||
export function HeroChip({
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Group
|
||||
gap={6}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
padding: "4px 10px",
|
||||
borderRadius: 8,
|
||||
background: "var(--mantine-color-gray-0)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
color: "var(--mantine-color-gray-7)",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<Text size="xs" fw={600} c="gray.7" style={{ whiteSpace: "nowrap" }}>
|
||||
{children}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user