Merge branch 'freight/develop' of github.com:Tria-plc/edr-platform into freight_feature/profile

This commit is contained in:
marshal
2026-06-24 01:22:31 +03:00
53 changed files with 1797 additions and 2638 deletions

View File

@@ -21,20 +21,6 @@ import type {
BookingListSummaryTabs,
} from "@/services/bookings.service";
/** Lifecycle stages for the pipeline distribution bar (in flow order). */
const PIPELINE_STAGES: Array<{
key: keyof BookingListSummaryTabs;
label: string;
color: string;
}> = [
{ key: "intake", label: "Intake", color: "#38bdf8" },
{ key: "in_approval", label: "Approval", color: "#f59e0b" },
{ key: "approved_contract", label: "Contract", color: "#8b5cf6" },
{ key: "payment", label: "Payment", color: "#fb923c" },
{ key: "operations", label: "Operations", color: "#14b8a6" },
{ key: "completed", label: "Completed", color: "#22c55e" },
];
const CARD_STYLE = {
background: "var(--mantine-color-gray-0)",
border: "1px solid var(--mantine-color-gray-2)",
@@ -62,7 +48,12 @@ export function BookingRequestsHeader({
return (
<Stack gap="lg">
<Group justify="flex-end" gap="sm">
<Button color="edr-green" radius="lg" leftSection={<Plus size={18} />} onClick={onCreate}>
<Button
color="edr-green"
radius="lg"
leftSection={<Plus size={18} />}
onClick={onCreate}
>
Create booking
</Button>
<Button
@@ -90,7 +81,9 @@ export function BookingRequestsHeader({
label="Needs action"
value={val(metrics?.needsAction)}
hint="Submitted or pending"
ratio={metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0}
ratio={
metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0
}
accent="orange"
/>
<HeroStat
@@ -137,12 +130,18 @@ function HeroStat({
accent?: OverviewAccent;
variant?: "area" | "line";
}) {
const [, accentDeep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
const [, accentDeep] =
overviewAccentGradients[accent] ?? overviewAccentGradients.default;
const chipBg = ACCENT_CHIP_BG[accent] ?? ACCENT_CHIP_BG.default;
const pct = ratio != null ? Math.round(Math.min(1, Math.max(0, ratio)) * 100) : null;
const pct =
ratio != null ? Math.round(Math.min(1, Math.max(0, ratio)) * 100) : null;
return (
<Paper p="md" radius="lg" style={{ flex: "1 1 180px", minWidth: 160, ...CARD_STYLE }}>
<Paper
p="md"
radius="lg"
style={{ flex: "1 1 180px", minWidth: 160, ...CARD_STYLE }}
>
<Stack gap={8}>
<Group gap="sm" wrap="nowrap" align="center">
<Box
@@ -161,7 +160,13 @@ function HeroStat({
<Icon size={20} strokeWidth={2} />
</Box>
<Stack gap={1} style={{ minWidth: 0, flex: 1 }}>
<Text fw={800} size="24px" lh={1.05} style={{ color: "#0f172a" }} truncate>
<Text
fw={800}
size="24px"
lh={1.05}
style={{ color: "#0f172a" }}
truncate
>
{value}
</Text>
<Text size="xs" fw={600} c="dimmed" truncate>
@@ -179,66 +184,15 @@ function HeroStat({
</Group>
{pct == null ? (
<MiniSparkline variant={variant} accent={accent} baseline={0.5} seed={label} height={22} />
<MiniSparkline
variant={variant}
accent={accent}
baseline={0.5}
seed={label}
height={22}
/>
) : null}
</Stack>
</Paper>
);
}
function PipelineBar({ tabs }: { tabs: BookingListSummaryTabs }) {
const segments = PIPELINE_STAGES.map((s) => ({ ...s, count: tabs[s.key] ?? 0 }));
const total = segments.reduce((sum, s) => sum + s.count, 0);
return (
<Paper p="md" radius="lg" style={CARD_STYLE}>
<Group justify="space-between" mb={10}>
<Text size="sm" fw={700} style={{ color: "#0f172a" }}>
Booking pipeline
</Text>
<Text size="xs" c="dimmed">
{total} active
</Text>
</Group>
<Box
style={{
display: "flex",
height: 14,
borderRadius: 999,
overflow: "hidden",
background: "var(--mantine-color-gray-2)",
gap: 2,
}}
>
{total > 0 ? (
segments.map((s) =>
s.count > 0 ? (
<Box
key={s.key}
title={`${s.label}: ${s.count}`}
style={{ width: `${(s.count / total) * 100}%`, background: s.color, transition: "width 200ms ease" }}
/>
) : null,
)
) : (
<Box style={{ width: "100%" }} />
)}
</Box>
<Group gap="md" mt={10} wrap="wrap">
{segments.map((s) => (
<Group key={s.key} gap={6} wrap="nowrap">
<Box style={{ width: 9, height: 9, borderRadius: 3, background: s.color }} />
<Text size="xs" c="dimmed">
{s.label}
</Text>
<Text size="xs" fw={700} style={{ color: "#0f172a" }}>
{s.count}
</Text>
</Group>
))}
</Group>
</Paper>
);
}

View File

@@ -10,7 +10,15 @@ import {
Wallet,
Weight,
} from "lucide-react";
import { Box, Button, Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core";
import {
Button,
Group,
Paper,
Stack,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import type { LucideIcon } from "lucide-react";
import type { BookingDetail } from "@/types/booking";
@@ -77,7 +85,12 @@ export function BookingRequestHero({
<Group justify="space-between" align="flex-start" wrap="wrap" gap="lg">
<Stack gap="sm" style={{ flex: 1, minWidth: 0 }}>
<Text size="xs" fw={700} tt="uppercase" style={{ letterSpacing: 1, color: "#B26C09" }}>
<Text
size="xs"
fw={700}
tt="uppercase"
style={{ letterSpacing: 1, color: "#B26C09" }}
>
Booking reference
</Text>
<Group gap="sm" align="center" wrap="wrap">
@@ -99,8 +112,14 @@ export function BookingRequestHero({
<Group gap="lg" mt={4}>
<MetaItem icon={Building2} text={customerLabel} strong />
<MetaItem icon={Calendar} text={`Scheduled ${booking.scheduledDate}`} />
<MetaItem icon={Clock} text={`Created ${formatDate(booking.createdAt)}`} />
<MetaItem
icon={Calendar}
text={`Scheduled ${booking.scheduledDate}`}
/>
<MetaItem
icon={Clock}
text={`Created ${formatDate(booking.createdAt)}`}
/>
</Group>
</Stack>
</Group>
@@ -110,7 +129,10 @@ export function BookingRequestHero({
radius="lg"
p={4}
maw={640}
style={{ background: "var(--mantine-color-gray-0)", border: "1px solid var(--mantine-color-gray-2)" }}
style={{
background: "var(--mantine-color-gray-0)",
border: "1px solid var(--mantine-color-gray-2)",
}}
>
<NextStepBanner nextStep={booking.nextStep} />
</Paper>
@@ -120,13 +142,22 @@ export function BookingRequestHero({
<HeroTile
icon={Wallet}
label="Total value"
value={`${booking.paymentCurrency} ${amount.toLocaleString(undefined, {
minimumFractionDigits: 2,
})}`}
value={`${booking.paymentCurrency} ${amount.toLocaleString(
undefined,
{
minimumFractionDigits: 2,
},
)}`}
hint={booking.paymentStatus}
accent="edr-green"
/>
<HeroTile icon={Weight} label="Cargo weight" value={`${weight} T`} hint="VGM total" accent="blue" />
<HeroTile
icon={Weight}
label="Cargo weight"
value={`${weight} T`}
hint="VGM total"
accent="blue"
/>
<HeroTile
icon={ContainerIcon}
label="Containers"
@@ -195,7 +226,13 @@ function HeroTile({
<Icon size={18} />
</ThemeIcon>
<Stack gap={2} style={{ minWidth: 0 }}>
<Text size="xs" fw={600} tt="uppercase" c="dimmed" style={{ letterSpacing: 0.4 }}>
<Text
size="xs"
fw={600}
tt="uppercase"
c="dimmed"
style={{ letterSpacing: 0.4 }}
>
{label}
</Text>
<Text fw={700} size="lg" lh={1.1} style={{ whiteSpace: "nowrap" }}>

View File

@@ -138,6 +138,8 @@ export interface BookingDetailView {
priorityScore: number;
cargoTotalWeightVgm: number;
pnrCode?: string | null;
consolidationPartnerId?: string | null;
consolidationPartner?: (BookingNamedRefView & { reference?: string }) | null;
/** End of the pay window once the booking is SELECTED_FOR_BATCH. */
paymentDeadline?: string | null;
createdAt: string;