Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx
2026-07-01 20:55:17 +03:00

889 lines
32 KiB
TypeScript

import {
ActionIcon,
Badge,
Box,
Button,
Container,
Divider,
Grid,
Group,
NumberInput,
Paper,
SegmentedControl,
Select,
Stack,
Switch,
Text,
Textarea,
TextInput,
ThemeIcon,
Tooltip,
} from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isAxiosError } from "axios";
import {
AlertTriangle,
ArrowLeft,
Boxes,
CalendarClock,
Container as ContainerIcon,
Flame,
Info,
Layers,
MapPin,
Package,
Plus,
Settings2,
Ship,
Trash2,
Weight,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { api as appApi } from "@/services/api";
import { bookingsService } from "@/services/bookings.service";
import { customersService } from "@/services/customers.service";
type FreightType = "CONTAINER" | "BULK";
interface RefNamed {
id: string;
name: string;
code: string;
country?: string;
}
interface RefContainerType {
id: string;
name: string;
code: string;
is_reefer?: boolean;
wagons_per_unit?: number;
}
interface RefContainerGroup {
size: string;
types: RefContainerType[];
}
interface RefCargoChild {
id: string;
name: string;
code: string;
}
interface RefCargoGroup {
id: string;
name: string;
code: string;
children?: RefCargoChild[];
}
interface ReferenceData {
yard?: RefNamed[];
service?: RefNamed[];
shipping_line?: RefNamed[];
containers?: RefContainerGroup[];
cargo_type?: RefCargoGroup[];
}
interface ContainerLine {
key: string;
containerTypeId: string | null;
quantity: number;
vgmPerUnitTons: number;
}
let lineCounter = 0;
const newLine = (): ContainerLine => ({
key: `line-${lineCounter++}`,
containerTypeId: null,
quantity: 1,
vgmPerUnitTons: 20,
});
const fmtTons = (n: number) =>
`${n.toLocaleString(undefined, { maximumFractionDigits: 2 })} t`;
type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC";
function deriveTradeDirectionFromYards(
origin?: RefNamed | null,
destination?: RefNamed | null,
): TradeDirection | null {
const originCountry = origin?.country?.trim();
const destinationCountry = destination?.country?.trim();
if (!originCountry || !destinationCountry) return null;
if (originCountry === "Djibouti") return "IMPORT";
if (destinationCountry === "Djibouti" && originCountry !== "Djibouti") return "EXPORT";
return "DOMESTIC";
}
const tradeDirectionLabel: Record<TradeDirection, string> = {
IMPORT: "Import",
EXPORT: "Export",
DOMESTIC: "Domestic",
};
const parseBookingError = (error: unknown, fallback: string) => {
if (isAxiosError(error)) {
const message = error.response?.data?.message;
if (Array.isArray(message)) return message.join(", ");
if (typeof message === "string") return message;
}
return fallback;
};
/** Section card with a colored icon chip header. */
function FormSection({
icon: Icon,
title,
subtitle,
accent = "edr-green",
right,
children,
}: {
icon: typeof Package;
title: string;
subtitle?: string;
accent?: string;
right?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<Paper radius="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)", overflow: "hidden" }}>
<Box style={{ height: 3, background: `linear-gradient(90deg, var(--mantine-color-${accent}-5), var(--mantine-color-${accent}-7))` }} />
<Group justify="space-between" px="lg" py="md" wrap="nowrap" style={{ borderBottom: "1px solid var(--mantine-color-gray-1)" }}>
<Group gap="sm" wrap="nowrap">
<ThemeIcon size={34} radius="md" variant="light" color={accent}>
<Icon size={17} />
</ThemeIcon>
<Box>
<Text fw={600} size="sm">
{title}
</Text>
{subtitle ? (
<Text size="xs" c="dimmed">
{subtitle}
</Text>
) : null}
</Box>
</Group>
{right}
</Group>
<Box p="lg">{children}</Box>
</Paper>
);
}
export default function NewBookingPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [isGovernment, setIsGovernment] = useState(false);
const [companyId, setCompanyId] = useState<string | null>(null);
// Government bookings bill to a real government company + an explicit profile.
const [govCompanyId, setGovCompanyId] = useState<string | null>(null);
const [govProfileId, setGovProfileId] = useState<string | null>(null);
const [freightType, setFreightType] = useState<FreightType>("CONTAINER");
const [originYardId, setOriginYardId] = useState<string | null>(null);
const [destinationYardId, setDestinationYardId] = useState<string | null>(null);
const [serviceTypeId, setServiceTypeId] = useState<string | null>(null);
// Day-level pool: staff pick a DAY (yyyy-MM-dd); the engine assigns the train.
const [scheduledDay, setScheduledDay] = useState<string | null>(null);
const [paymentCurrency, setPaymentCurrency] = useState("ETB");
// container freight
const [lines, setLines] = useState<ContainerLine[]>([newLine()]);
// bulk freight
const [cargoTypeId, setCargoTypeId] = useState<string | null>(null);
const [cargoFreeText, setCargoFreeText] = useState("");
const [bulkWeight, setBulkWeight] = useState<number>(100);
// extra options
const [equipmentReturn, setEquipmentReturn] = useState("NA");
const [isHazardous, setIsHazardous] = useState(false);
const [shippingLineId, setShippingLineId] = useState<string | null>(null);
const [firstMilePickupAddress, setFirstMile] = useState("");
const [lastMileDeliveryAddress, setLastMile] = useState("");
const { data: refData, isLoading } = useQuery({
queryKey: ["bookings", "reference-data"],
queryFn: () => bookingsService.getReferenceData() as Promise<ReferenceData>,
});
const { data: companiesPage, isLoading: companiesLoading } = useQuery({
queryKey: ["companies", "list"],
queryFn: () => customersService.list({ page: 1, pageSize: 1000 }),
});
const companyOptions = (companiesPage?.items ?? []).map((c) => ({
value: c.id,
label: c.name || c.email || c.tin || c.id,
}));
// Active government companies (kind=government) the booking can bill to.
const { data: govCompaniesPage, isLoading: govCompaniesLoading } = useQuery({
queryKey: ["companies", "government", "active"],
queryFn: () =>
customersService.list({
page: 1,
pageSize: 1000,
kind: "government",
status: "active",
}),
enabled: isGovernment,
});
const govCompanies = govCompaniesPage?.items ?? [];
const govCompanyOptions = govCompanies.map((c) => ({
value: c.id,
label: c.name || c.tin || c.id,
}));
// Profiles (importer/exporter) of the chosen government company — the booking
// must link to one explicitly.
const selectedGovCompany = govCompanies.find((c) => c.id === govCompanyId);
const govProfileOptions = (selectedGovCompany?.companyProfiles ?? [])
.filter((p) => p.status === "active")
.map((p) => ({
value: p.id,
label: `${p.type === "importer" ? "Import" : p.type === "exporter" ? "Export" : p.type}${
p.reference ? `${p.reference}` : ""
}`,
}));
// Reset the chosen profile when the government company changes.
useEffect(() => {
setGovProfileId(null);
}, [govCompanyId]);
// Day-level pool: fetch only the days that have a departure on the route (no
// train, no capacity). The batch engine assigns the train after booking.
const { data: availableDays, isLoading: daysLoading } = useQuery(
appApi.trainScheduling.availableDays.queryOptions({
input: { originYardId, destinationYardId },
enabled: Boolean(originYardId && destinationYardId),
}),
);
const dayOptions = (availableDays ?? []).map((day) => ({
value: day,
label: new Date(`${day}T00:00:00`).toLocaleDateString(undefined, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
}),
}));
const hasAvailableDays = (availableDays ?? []).length > 0;
// The chosen day becomes the booking's scheduledDate (start of day, ISO).
const effectiveDepartureIso = scheduledDay
? new Date(`${scheduledDay}T00:00:00`).toISOString()
: "";
const yardRecords = refData?.yard ?? [];
const yards = yardRecords.map((y) => ({ value: y.id, label: y.name ?? y.code }));
const originYard = yardRecords.find((y) => y.id === originYardId) ?? null;
const destinationYard = yardRecords.find((y) => y.id === destinationYardId) ?? null;
const tradeDirection = deriveTradeDirectionFromYards(originYard, destinationYard);
// Reset the day when the route changes — available days depend on the route.
useEffect(() => {
setScheduledDay(null);
}, [originYardId, destinationYardId]);
const services = (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name ?? s.code }));
const shippingLines = (refData?.shipping_line ?? []).map((s) => ({ value: s.id, label: s.name ?? s.code }));
const containerGroupData = useMemo(
() =>
(refData?.containers ?? []).map((g) => ({
group: g.size,
items: g.types.map((t) => ({
value: t.id,
label: `${t.code}${t.name && t.name !== t.code ? `${t.name}` : ""}`,
})),
})),
[refData?.containers],
);
const { cargoData } = useMemo(() => {
const groups = refData?.cargo_type ?? [];
const data = groups.map((g) => {
if (g.children?.length) {
return { group: g.name, items: g.children.map((c) => ({ value: c.id, label: c.name })) };
}
return { value: g.id, label: g.name };
});
return { cargoData: data };
}, [refData?.cargo_type]);
const showFreeText = freightType === "BULK" && Boolean(cargoTypeId);
// ---- derived totals ----
const totalContainers = lines.reduce((s, l) => s + (l.quantity || 0), 0);
const containerWeight = lines.reduce((s, l) => s + (l.quantity || 0) * (l.vgmPerUnitTons || 0), 0);
const cargoTotalWeightVgm = freightType === "CONTAINER" ? containerWeight : bulkWeight;
// ---- validation ----
const lineValid = (l: ContainerLine) =>
Boolean(l.containerTypeId) && l.quantity >= 1 && l.vgmPerUnitTons > 0;
const allLinesValid = lines.length > 0 && lines.every(lineValid);
const sameYard = Boolean(originYardId && originYardId === destinationYardId);
// Day-level pool: a shipment DAY is all staff pick. The batch engine assigns
// the train afterwards (same flow as the customer portal).
const departureSatisfied = Boolean(scheduledDay);
const canSubmit =
Boolean(originYardId) &&
Boolean(destinationYardId) &&
!sameYard &&
Boolean(tradeDirection) &&
Boolean(serviceTypeId) &&
departureSatisfied &&
(isGovernment ? Boolean(govCompanyId && govProfileId) : Boolean(companyId)) &&
(freightType === "BULK"
? Boolean(cargoTypeId) && bulkWeight > 0
: allLinesValid);
const updateLine = (key: string, patch: Partial<ContainerLine>) =>
setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l)));
const removeLine = (key: string) =>
setLines((prev) => (prev.length === 1 ? prev : prev.filter((l) => l.key !== key)));
const createMutation = useMutation({
mutationFn: () =>
bookingsService.create({
isGovernment,
companyId: isGovernment ? govCompanyId || undefined : companyId || undefined,
companyProfileId: isGovernment ? govProfileId || undefined : undefined,
freightType,
contractType: "NEW",
equipmentReturn,
tradeDirection: tradeDirection!,
paymentCurrency,
isHazardous,
scheduledDate: effectiveDepartureIso || new Date().toISOString(),
originYardId,
destinationYardId,
// Day-level pool: no trainScheduleId — the engine assigns the train.
serviceTypeId,
shippingLineId: shippingLineId || undefined,
firstMilePickupAddress: firstMilePickupAddress.trim() || undefined,
lastMileDeliveryAddress: lastMileDeliveryAddress.trim() || undefined,
cargoTotalWeightVgm,
cargoTypeId: freightType === "BULK" ? cargoTypeId : undefined,
cargoFreeText: freightType === "BULK" ? cargoFreeText.trim() || undefined : undefined,
containers:
freightType === "CONTAINER"
? lines.map((l) => ({
containerTypeId: l.containerTypeId,
quantity: l.quantity,
vgmPerUnitTons: l.vgmPerUnitTons,
}))
: undefined,
}),
onSuccess: async (booking) => {
if (isGovernment) {
await bookingsService.governmentExpedite(booking.id);
toast.success("Government booking created and expedited to scheduling");
} else {
toast.success("Booking created as draft");
}
void queryClient.invalidateQueries({ queryKey: ["bookings"] });
navigate(`/dashboard/booking-requests/${booking.id}`);
},
onError: (error) => toast.error(parseBookingError(error, "Failed to create booking")),
});
return (
<Container size="xl" py="lg">
<Breadcrumbs
items={[
{ label: "Operations" },
{ label: "Booking requests", href: "/dashboard/booking-requests" },
{ label: "Create" },
]}
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
radius="lg"
leftSection={<ArrowLeft size={16} />}
onClick={() => navigate("/dashboard/booking-requests")}
>
Back to list
</Button>
</Group>
<Grid gap="lg" mt="lg">
{/* LEFT — form */}
<Grid.Col span={{ base: 12, lg: 8 }}>
<Stack gap="lg">
<FormSection icon={Layers} title="Booking type" subtitle="Who is booking and what kind of freight" accent="edr-green">
<Stack gap="md">
<Switch
label="Government booking"
description="Bills to a government entity + profile. Expedited to the scheduling queue."
checked={isGovernment}
onChange={(e) => setIsGovernment(e.currentTarget.checked)}
/>
{isGovernment ? (
<Group grow align="flex-start">
<Select
label="Government entity"
placeholder="Select government company"
data={govCompanyOptions}
value={govCompanyId}
onChange={setGovCompanyId}
searchable
required
disabled={govCompaniesLoading}
nothingFoundMessage="No active government companies"
/>
<Select
label="Profile"
placeholder={
govCompanyId ? "Select import/export profile" : "Pick an entity first"
}
data={govProfileOptions}
value={govProfileId}
onChange={setGovProfileId}
searchable
required
disabled={!govCompanyId}
nothingFoundMessage="No active profiles for this entity"
/>
</Group>
) : (
<Select
label="Customer"
placeholder="Select company"
data={companyOptions}
value={companyId}
onChange={setCompanyId}
searchable
required
disabled={companiesLoading}
nothingFoundMessage="No companies found"
/>
)}
<Box>
<Text size="sm" fw={500} mb={6}>
Freight type
</Text>
<SegmentedControl
fullWidth
value={freightType}
onChange={(v) => setFreightType(v as FreightType)}
data={[
{ value: "CONTAINER", label: "Container" },
{ value: "BULK", label: "Bulk" },
]}
/>
</Box>
</Stack>
</FormSection>
<FormSection icon={MapPin} title="Route & service" subtitle="Origin, destination and service type" accent="blue">
<Stack gap="md">
<Group grow align="flex-start">
<Select
label="Origin yard"
placeholder="Select origin"
data={yards}
value={originYardId}
onChange={(v) => {
setOriginYardId(v);
}}
searchable
disabled={isLoading}
error={sameYard ? "Same as destination" : undefined}
/>
<Select
label="Destination yard"
placeholder="Select destination"
data={yards}
value={destinationYardId}
onChange={(v) => {
setDestinationYardId(v);
setScheduledDay(null);
}}
searchable
disabled={isLoading}
error={sameYard ? "Same as origin" : undefined}
/>
</Group>
<Select
label="Shipment day"
placeholder={
originYardId && destinationYardId
? "Select a day with a departure"
: "Pick origin & destination first"
}
data={dayOptions}
value={scheduledDay}
onChange={setScheduledDay}
searchable
disabled={!originYardId || !destinationYardId || daysLoading}
nothingFoundMessage={
hasAvailableDays ? "No match" : "No departures on this route"
}
description="Pick a day with a departure. The batch engine assigns the train by priority."
/>
<Group grow align="flex-end">
<Select
label="Service type"
placeholder="Select service"
data={services}
value={serviceTypeId}
onChange={setServiceTypeId}
searchable
disabled={isLoading}
/>
<Box>
<Text size="sm" fw={500} mb={4}>
Trade direction
</Text>
<Badge
size="lg"
variant="light"
color={
tradeDirection === "DOMESTIC"
? "grape"
: tradeDirection === "EXPORT"
? "orange"
: "blue"
}
>
{tradeDirection ? tradeDirectionLabel[tradeDirection] : "Select yards"}
</Badge>
</Box>
</Group>
</Stack>
</FormSection>
<FormSection icon={CalendarClock} title="Schedule & payment" accent="grape">
<Group grow align="flex-start">
<TextInput
label="Shipment day"
value={
scheduledDay
? new Date(`${scheduledDay}T00:00:00`).toLocaleDateString(undefined, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})
: ""
}
placeholder="Pick a day in the Route section"
readOnly
description="The engine assigns the train on this day"
/>
<Select
label="Payment currency"
data={[
{ value: "ETB", label: "ETB — Birr" },
{ value: "USD", label: "USD — Dollar" },
]}
value={paymentCurrency}
onChange={(v) => setPaymentCurrency(v ?? "ETB")}
/>
</Group>
</FormSection>
{/* Cargo */}
{freightType === "CONTAINER" ? (
<FormSection
icon={ContainerIcon}
title="Container lines"
subtitle="Add one row per container type"
accent="teal"
right={
<Badge variant="light" color="teal" radius="sm">
{totalContainers} container{totalContainers === 1 ? "" : "s"}
</Badge>
}
>
<Stack gap="sm">
{lines.map((line, idx) => {
const invalid = !lineValid(line);
return (
<Paper
key={line.key}
p="sm"
radius="md"
withBorder
style={{
borderColor: invalid ? "var(--mantine-color-gray-3)" : "var(--mantine-color-teal-2)",
background: "var(--mantine-color-gray-0)",
}}
>
<Group align="flex-end" gap="sm" wrap="nowrap">
<Text fw={700} c="dimmed" size="sm" w={22} ta="center" style={{ flexShrink: 0 }}>
{idx + 1}
</Text>
<Select
label="Container type"
placeholder="Select type"
data={containerGroupData}
value={line.containerTypeId}
onChange={(v) => updateLine(line.key, { containerTypeId: v })}
searchable
disabled={isLoading}
style={{ flex: 2, minWidth: 160 }}
/>
<NumberInput
label="Qty"
value={line.quantity}
onChange={(v) => updateLine(line.key, { quantity: Number(v) || 0 })}
min={1}
step={1}
style={{ width: 90, flexShrink: 0 }}
/>
<NumberInput
label="VGM / unit"
value={line.vgmPerUnitTons}
onChange={(v) => updateLine(line.key, { vgmPerUnitTons: Number(v) || 0 })}
min={0}
suffix=" t"
decimalScale={2}
style={{ width: 130, flexShrink: 0 }}
/>
<Stack gap={2} style={{ width: 92, flexShrink: 0 }}>
<Text size="xs" c="dimmed">
Line total
</Text>
<Text fw={700} size="sm">
{fmtTons((line.quantity || 0) * (line.vgmPerUnitTons || 0))}
</Text>
</Stack>
<Tooltip label={lines.length === 1 ? "At least one line" : "Remove line"}>
<ActionIcon
variant="subtle"
color="red"
disabled={lines.length === 1}
onClick={() => removeLine(line.key)}
style={{ flexShrink: 0 }}
>
<Trash2 size={16} />
</ActionIcon>
</Tooltip>
</Group>
</Paper>
);
})}
<Group justify="space-between">
<Button
variant="light"
color="teal"
size="compact-sm"
leftSection={<Plus size={15} />}
onClick={() => setLines((prev) => [...prev, newLine()])}
>
Add container line
</Button>
<Text size="sm" c="dimmed">
Total VGM:{" "}
<Text span fw={700} c="dark">
{fmtTons(containerWeight)}
</Text>
</Text>
</Group>
</Stack>
</FormSection>
) : (
<FormSection icon={Boxes} title="Bulk cargo" subtitle="Select the bulk cargo type and total weight" accent="orange">
<Stack gap="md">
<Select
label="Cargo type"
placeholder="Select bulk cargo type"
data={cargoData}
value={cargoTypeId}
onChange={(value) => setCargoTypeId(value as string | null)}
searchable
disabled={isLoading}
/>
{showFreeText ? (
<Textarea
label="Cargo description"
placeholder="Describe the cargo"
autosize
minRows={2}
value={cargoFreeText}
onChange={(e) => setCargoFreeText(e.currentTarget.value)}
/>
) : null}
<NumberInput
label="Total weight (VGM)"
value={bulkWeight}
onChange={(v) => setBulkWeight(Number(v) || 0)}
min={0}
suffix=" t"
decimalScale={2}
/>
</Stack>
</FormSection>
)}
<FormSection icon={Settings2} title="Additional details" subtitle="Optional — equipment, handling and references" accent="indigo">
<Stack gap="md">
<Group grow>
<Select
label="Equipment return"
data={[
{ value: "NA", label: "Not applicable" },
{ value: "WITH_RETURN", label: "With return" },
{ value: "WITHOUT_RETURN", label: "Without return" },
]}
value={equipmentReturn}
onChange={(v) => setEquipmentReturn(v ?? "NA")}
/>
<Select
label="Shipping line (optional)"
placeholder="Select shipping line"
data={shippingLines}
value={shippingLineId}
onChange={setShippingLineId}
searchable
clearable
disabled={isLoading}
/>
</Group>
<Group grow>
<TextInput
label="First-mile pickup (optional)"
placeholder="Pickup address"
value={firstMilePickupAddress}
onChange={(e) => setFirstMile(e.currentTarget.value)}
/>
<TextInput
label="Last-mile delivery (optional)"
placeholder="Delivery address"
value={lastMileDeliveryAddress}
onChange={(e) => setLastMile(e.currentTarget.value)}
/>
</Group>
<Switch
label="Hazardous cargo"
checked={isHazardous}
onChange={(e) => setIsHazardous(e.currentTarget.checked)}
thumbIcon={isHazardous ? <Flame size={12} /> : undefined}
color="red"
/>
</Stack>
</FormSection>
</Stack>
</Grid.Col>
{/* RIGHT — sticky summary */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Box style={{ position: "sticky", top: 16 }}>
<Paper radius="lg" withBorder p="lg" style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Group gap="sm" mb="md">
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
<Weight size={17} />
</ThemeIcon>
<Text fw={700}>Summary</Text>
</Group>
<Group gap="xs" mb="md">
<Badge variant="light" color={freightType === "BULK" ? "orange" : "teal"} radius="sm">
{freightType === "BULK" ? "Bulk" : "Container"}
</Badge>
<Badge variant="light" color="blue" radius="sm">
{tradeDirection}
</Badge>
{isGovernment ? (
<Badge variant="light" color="grape" radius="sm">
Government
</Badge>
) : null}
{isHazardous ? (
<Badge variant="light" color="red" radius="sm" leftSection={<Flame size={10} />}>
Hazardous
</Badge>
) : null}
</Group>
<Stack gap={8}>
{freightType === "CONTAINER" ? (
<>
<SummaryRow label="Container lines" value={String(lines.length)} />
<SummaryRow label="Total containers" value={String(totalContainers)} />
</>
) : null}
<SummaryRow label="Total VGM weight" value={fmtTons(cargoTotalWeightVgm)} strong />
<Divider my={4} />
<SummaryRow
label="Route"
value={
originYardId && destinationYardId
? `${yards.find((y) => y.value === originYardId)?.label ?? "—"}${
yards.find((y) => y.value === destinationYardId)?.label ?? "—"
}`
: "Not set"
}
/>
<SummaryRow
label="Departure"
value={effectiveDepartureIso ? new Date(effectiveDepartureIso).toLocaleString() : "Not set"}
/>
</Stack>
{sameYard ? (
<Group gap={6} mt="md" c="red.7">
<AlertTriangle size={14} />
<Text size="xs">Origin and destination must differ.</Text>
</Group>
) : null}
<Stack gap="sm" mt="lg">
<Button
size="md"
fullWidth
loading={createMutation.isPending}
disabled={!canSubmit}
onClick={() => createMutation.mutate()}
leftSection={<Ship size={18} />}
>
{isGovernment ? "Create & expedite" : "Create draft"}
</Button>
<Button variant="default" fullWidth onClick={() => navigate("/dashboard/booking-requests")}>
Cancel
</Button>
</Stack>
<Group gap={6} mt="md" align="flex-start" wrap="nowrap">
<Info size={14} color="var(--mantine-color-gray-5)" style={{ marginTop: 2, flexShrink: 0 }} />
<Text size="xs" c="dimmed">
{isGovernment
? "Government bookings skip the commercial 3-hour hold and enter the priority lane."
: "Overweight container lines are allowed here and flagged later at scheduling."}
</Text>
</Group>
</Paper>
</Box>
</Grid.Col>
</Grid>
</Container>
);
}
function SummaryRow({ label, value, strong }: { label: string; value: string; strong?: boolean }) {
return (
<Group justify="space-between" wrap="nowrap" gap="md">
<Text size="sm" c="dimmed" style={{ flexShrink: 0 }}>
{label}
</Text>
<Text size="sm" fw={strong ? 700 : 500} ta="right" style={{ minWidth: 0 }} truncate>
{value}
</Text>
</Group>
);
}