mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
214 lines
6.2 KiB
TypeScript
214 lines
6.2 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import {
|
|
ArrowLeft,
|
|
Building2,
|
|
Calendar,
|
|
Clock,
|
|
Container as ContainerIcon,
|
|
Flame,
|
|
RefreshCw,
|
|
Wallet,
|
|
Weight,
|
|
} from "lucide-react";
|
|
import { Box, Button, Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core";
|
|
import type { LucideIcon } from "lucide-react";
|
|
|
|
import type { BookingDetail } from "@/types/booking";
|
|
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
|
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
|
|
import { SchedulingStatusBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
|
import { NextStepBanner } from "@/components/bookings/NextStepBanner";
|
|
|
|
import { formatDate } from "./booking-detail.styles";
|
|
|
|
export interface BookingRequestHeroProps {
|
|
booking: BookingDetail;
|
|
customerLabel: string;
|
|
onBack: () => void;
|
|
onRefresh: () => void;
|
|
isFetching?: boolean;
|
|
}
|
|
|
|
/** Top hero for the request detail page: identity, status, next step, key figures. */
|
|
export function BookingRequestHero({
|
|
booking,
|
|
customerLabel,
|
|
onBack,
|
|
onRefresh,
|
|
isFetching,
|
|
}: BookingRequestHeroProps) {
|
|
const amount = Number(booking.totalAmount);
|
|
const containers = booking.bookingContainers ?? [];
|
|
const containerCount = containers.reduce(
|
|
(sum, c) => sum + Number(c.quantity ?? 0),
|
|
0,
|
|
);
|
|
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
|
|
|
|
return (
|
|
<Paper
|
|
radius="xl"
|
|
p="xl"
|
|
style={{ position: "relative", overflow: "hidden" }}
|
|
>
|
|
<Stack gap="lg" style={{ position: "relative" }}>
|
|
<Group justify="space-between" align="flex-start" wrap="wrap">
|
|
<Button
|
|
variant="default"
|
|
size="compact-sm"
|
|
radius="lg"
|
|
leftSection={<ArrowLeft size={16} />}
|
|
onClick={onBack}
|
|
>
|
|
Back to list
|
|
</Button>
|
|
<Button
|
|
variant="light"
|
|
color="edr-green"
|
|
size="compact-sm"
|
|
radius="lg"
|
|
leftSection={<RefreshCw size={15} />}
|
|
loading={isFetching}
|
|
onClick={onRefresh}
|
|
>
|
|
Refresh
|
|
</Button>
|
|
</Group>
|
|
|
|
<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" }}>
|
|
Booking reference
|
|
</Text>
|
|
<Group gap="sm" align="center" wrap="wrap">
|
|
<Title order={2} fw={700} style={{ letterSpacing: "-0.4px" }}>
|
|
{booking.reference}
|
|
</Title>
|
|
<BookingStatusBadge status={booking.status} />
|
|
<BookingPriorityBadge score={booking.priorityScore} />
|
|
{booking.schedulingStatus ? (
|
|
<SchedulingStatusBadge status={booking.schedulingStatus} />
|
|
) : null}
|
|
</Group>
|
|
|
|
{booking.holdExpiresAt && booking.schedulingStatus === "HOLDING" ? (
|
|
<Text size="xs" c="orange.7">
|
|
Hold expires {new Date(booking.holdExpiresAt).toLocaleString()}
|
|
</Text>
|
|
) : null}
|
|
|
|
<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)}`} />
|
|
</Group>
|
|
</Stack>
|
|
</Group>
|
|
|
|
{booking.nextStep ? (
|
|
<Paper
|
|
radius="lg"
|
|
p={4}
|
|
maw={640}
|
|
style={{ background: "var(--mantine-color-gray-0)", border: "1px solid var(--mantine-color-gray-2)" }}
|
|
>
|
|
<NextStepBanner nextStep={booking.nextStep} />
|
|
</Paper>
|
|
) : null}
|
|
|
|
<Group grow gap="md" align="stretch" wrap="wrap">
|
|
<HeroTile
|
|
icon={Wallet}
|
|
label="Total value"
|
|
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={ContainerIcon}
|
|
label="Containers"
|
|
value={containerCount || "—"}
|
|
hint={`${containers.length} line${containers.length === 1 ? "" : "s"}`}
|
|
accent="teal"
|
|
/>
|
|
<HeroTile
|
|
icon={Flame}
|
|
label="Priority score"
|
|
value={booking.priorityScore ?? 0}
|
|
hint={booking.tradeDirection}
|
|
accent="orange"
|
|
/>
|
|
</Group>
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
function MetaItem({
|
|
icon: Icon,
|
|
text,
|
|
strong,
|
|
}: {
|
|
icon: LucideIcon;
|
|
text: ReactNode;
|
|
strong?: boolean;
|
|
}) {
|
|
return (
|
|
<Group gap={6} wrap="nowrap">
|
|
<Icon size={14} color="var(--mantine-color-gray-5)" />
|
|
<Text size="sm" fw={strong ? 600 : 400} c={strong ? "dark" : "dimmed"}>
|
|
{text}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
function HeroTile({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
hint,
|
|
accent = "edr-green",
|
|
}: {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
value: ReactNode;
|
|
hint?: ReactNode;
|
|
accent?: string;
|
|
}) {
|
|
return (
|
|
<Paper
|
|
p="md"
|
|
radius="lg"
|
|
style={{
|
|
flex: "1 1 160px",
|
|
minWidth: 150,
|
|
background: "var(--mantine-color-gray-0)",
|
|
border: "1px solid var(--mantine-color-gray-2)",
|
|
}}
|
|
>
|
|
<Group gap="sm" wrap="nowrap" align="flex-start">
|
|
<ThemeIcon size={36} radius="md" variant="light" color={accent}>
|
|
<Icon size={18} />
|
|
</ThemeIcon>
|
|
<Stack gap={2} style={{ minWidth: 0 }}>
|
|
<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" }}>
|
|
{value}
|
|
</Text>
|
|
{hint ? (
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{hint}
|
|
</Text>
|
|
) : null}
|
|
</Stack>
|
|
</Group>
|
|
</Paper>
|
|
);
|
|
}
|