Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingRequestHero.tsx
2026-06-22 08:11:11 +00:00

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>
);
}