mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
Merge pull request #1034 from Tria-plc/freight_feature/usermanagement
add goverment booking
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -25,16 +24,13 @@ import { isAxiosError } from "axios";
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Boxes,
|
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
Container as ContainerIcon,
|
Container as ContainerIcon,
|
||||||
Flame,
|
|
||||||
Info,
|
Info,
|
||||||
Layers,
|
Layers,
|
||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
Package,
|
||||||
Plus,
|
Plus,
|
||||||
Settings2,
|
|
||||||
Ship,
|
Ship,
|
||||||
Trash2,
|
Trash2,
|
||||||
Weight,
|
Weight,
|
||||||
@@ -48,8 +44,6 @@ import { api as appApi } from "@/services/api";
|
|||||||
import { bookingsService } from "@/services/bookings.service";
|
import { bookingsService } from "@/services/bookings.service";
|
||||||
import { customersService } from "@/services/customers.service";
|
import { customersService } from "@/services/customers.service";
|
||||||
|
|
||||||
type FreightType = "CONTAINER" | "BULK";
|
|
||||||
|
|
||||||
interface RefNamed {
|
interface RefNamed {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -103,24 +97,17 @@ const newLine = (): ContainerLine => ({
|
|||||||
const fmtTons = (n: number) =>
|
const fmtTons = (n: number) =>
|
||||||
`${n.toLocaleString(undefined, { maximumFractionDigits: 2 })} t`;
|
`${n.toLocaleString(undefined, { maximumFractionDigits: 2 })} t`;
|
||||||
|
|
||||||
type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC";
|
type TradeDirection = "IMPORT" | "EXPORT";
|
||||||
|
|
||||||
function deriveTradeDirectionFromYards(
|
// The direction fixes which end of the route is inside Ethiopia — Djibouti
|
||||||
origin?: RefNamed | null,
|
// yards stand in for "outside" (the port), mirroring the customer portal and
|
||||||
destination?: RefNamed | null,
|
// the backend's deriveTradeDirection.
|
||||||
): TradeDirection | null {
|
const countriesFor: Record<
|
||||||
const originCountry = origin?.country?.trim();
|
TradeDirection,
|
||||||
const destinationCountry = destination?.country?.trim();
|
{ origin: string; destination: string }
|
||||||
if (!originCountry || !destinationCountry) return null;
|
> = {
|
||||||
if (originCountry === "Djibouti") return "IMPORT";
|
IMPORT: { origin: "Djibouti", destination: "Ethiopia" },
|
||||||
if (destinationCountry === "Djibouti" && originCountry !== "Djibouti") return "EXPORT";
|
EXPORT: { origin: "Ethiopia", destination: "Djibouti" },
|
||||||
return "DOMESTIC";
|
|
||||||
}
|
|
||||||
|
|
||||||
const tradeDirectionLabel: Record<TradeDirection, string> = {
|
|
||||||
IMPORT: "Import",
|
|
||||||
EXPORT: "Export",
|
|
||||||
DOMESTIC: "Domestic",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseBookingError = (error: unknown, fallback: string) => {
|
const parseBookingError = (error: unknown, fallback: string) => {
|
||||||
@@ -183,7 +170,8 @@ export default function NewBookingPage() {
|
|||||||
// Government bookings bill to a real government company + an explicit profile.
|
// Government bookings bill to a real government company + an explicit profile.
|
||||||
const [govCompanyId, setGovCompanyId] = useState<string | null>(null);
|
const [govCompanyId, setGovCompanyId] = useState<string | null>(null);
|
||||||
const [govProfileId, setGovProfileId] = useState<string | null>(null);
|
const [govProfileId, setGovProfileId] = useState<string | null>(null);
|
||||||
const [freightType, setFreightType] = useState<FreightType>("CONTAINER");
|
// Container freight only for now — bulk is disabled on this page.
|
||||||
|
const [tradeDirection, setTradeDirection] = useState<TradeDirection>("IMPORT");
|
||||||
const [originYardId, setOriginYardId] = useState<string | null>(null);
|
const [originYardId, setOriginYardId] = useState<string | null>(null);
|
||||||
const [destinationYardId, setDestinationYardId] = useState<string | null>(null);
|
const [destinationYardId, setDestinationYardId] = useState<string | null>(null);
|
||||||
const [serviceTypeId, setServiceTypeId] = useState<string | null>(null);
|
const [serviceTypeId, setServiceTypeId] = useState<string | null>(null);
|
||||||
@@ -191,21 +179,8 @@ export default function NewBookingPage() {
|
|||||||
const [scheduledDay, setScheduledDay] = useState<string | null>(null);
|
const [scheduledDay, setScheduledDay] = useState<string | null>(null);
|
||||||
const [paymentCurrency, setPaymentCurrency] = useState("ETB");
|
const [paymentCurrency, setPaymentCurrency] = useState("ETB");
|
||||||
|
|
||||||
// container freight
|
|
||||||
const [lines, setLines] = useState<ContainerLine[]>([newLine()]);
|
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({
|
const { data: refData, isLoading } = useQuery({
|
||||||
queryKey: ["bookings", "reference-data"],
|
queryKey: ["bookings", "reference-data"],
|
||||||
queryFn: () => bookingsService.getReferenceData() as Promise<ReferenceData>,
|
queryFn: () => bookingsService.getReferenceData() as Promise<ReferenceData>,
|
||||||
@@ -283,16 +258,35 @@ export default function NewBookingPage() {
|
|||||||
|
|
||||||
const yardRecords = refData?.yard ?? [];
|
const yardRecords = refData?.yard ?? [];
|
||||||
const yards = yardRecords.map((y) => ({ value: y.id, label: y.name ?? y.code }));
|
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;
|
// Yards filtered to the side of the route the direction dictates (import:
|
||||||
const tradeDirection = deriveTradeDirectionFromYards(originYard, destinationYard);
|
// Djibouti → Ethiopia; export: the reverse), excluding the yard already
|
||||||
|
// picked on the other end so origin and destination can never match.
|
||||||
|
const yardsForCountry = (country: string, excludeYardId: string | null) =>
|
||||||
|
yardRecords
|
||||||
|
.filter((y) => y.id !== excludeYardId && y.country?.trim() === country)
|
||||||
|
.map((y) => ({ value: y.id, label: y.name ?? y.code }));
|
||||||
|
const originYardOptions = yardsForCountry(
|
||||||
|
countriesFor[tradeDirection].origin,
|
||||||
|
destinationYardId,
|
||||||
|
);
|
||||||
|
const destinationYardOptions = yardsForCountry(
|
||||||
|
countriesFor[tradeDirection].destination,
|
||||||
|
originYardId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switching direction flips which country each end must be in — clear both
|
||||||
|
// ends so a stale yard can't contradict the chosen direction.
|
||||||
|
useEffect(() => {
|
||||||
|
setOriginYardId(null);
|
||||||
|
setDestinationYardId(null);
|
||||||
|
}, [tradeDirection]);
|
||||||
|
|
||||||
// Reset the day when the route changes — available days depend on the route.
|
// Reset the day when the route changes — available days depend on the route.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setScheduledDay(null);
|
setScheduledDay(null);
|
||||||
}, [originYardId, destinationYardId]);
|
}, [originYardId, destinationYardId]);
|
||||||
const services = (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name ?? s.code }));
|
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(
|
const containerGroupData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -306,23 +300,12 @@ export default function NewBookingPage() {
|
|||||||
[refData?.containers],
|
[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 ----
|
// ---- derived totals ----
|
||||||
const totalContainers = lines.reduce((s, l) => s + (l.quantity || 0), 0);
|
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 = lines.reduce(
|
||||||
const cargoTotalWeightVgm = freightType === "CONTAINER" ? containerWeight : bulkWeight;
|
(s, l) => s + (l.quantity || 0) * (l.vgmPerUnitTons || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// 20ft containers ride two per wagon, so a booking must hold an even number
|
// 20ft containers ride two per wagon, so a booking must hold an even number
|
||||||
// of them — odd counts would leave half a wagon waiting on a co-loader
|
// of them — odd counts would leave half a wagon waiting on a co-loader
|
||||||
@@ -337,7 +320,7 @@ export default function NewBookingPage() {
|
|||||||
return String(size).includes("20") ? sum + (l.quantity || 0) : sum;
|
return String(size).includes("20") ? sum + (l.quantity || 0) : sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [refData?.containers, lines]);
|
}, [refData?.containers, lines]);
|
||||||
const hasOdd20ft = freightType === "CONTAINER" && twentyFtCount % 2 === 1;
|
const hasOdd20ft = twentyFtCount % 2 === 1;
|
||||||
|
|
||||||
// ---- validation ----
|
// ---- validation ----
|
||||||
const lineValid = (l: ContainerLine) =>
|
const lineValid = (l: ContainerLine) =>
|
||||||
@@ -353,13 +336,11 @@ export default function NewBookingPage() {
|
|||||||
Boolean(originYardId) &&
|
Boolean(originYardId) &&
|
||||||
Boolean(destinationYardId) &&
|
Boolean(destinationYardId) &&
|
||||||
!sameYard &&
|
!sameYard &&
|
||||||
Boolean(tradeDirection) &&
|
|
||||||
Boolean(serviceTypeId) &&
|
Boolean(serviceTypeId) &&
|
||||||
departureSatisfied &&
|
departureSatisfied &&
|
||||||
(isGovernment ? Boolean(govCompanyId && govProfileId) : Boolean(companyId)) &&
|
(isGovernment ? Boolean(govCompanyId && govProfileId) : Boolean(companyId)) &&
|
||||||
(freightType === "BULK"
|
allLinesValid &&
|
||||||
? Boolean(cargoTypeId) && bulkWeight > 0
|
!hasOdd20ft;
|
||||||
: allLinesValid && !hasOdd20ft);
|
|
||||||
|
|
||||||
const updateLine = (key: string, patch: Partial<ContainerLine>) =>
|
const updateLine = (key: string, patch: Partial<ContainerLine>) =>
|
||||||
setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l)));
|
setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l)));
|
||||||
@@ -372,31 +353,23 @@ export default function NewBookingPage() {
|
|||||||
isGovernment,
|
isGovernment,
|
||||||
companyId: isGovernment ? govCompanyId || undefined : companyId || undefined,
|
companyId: isGovernment ? govCompanyId || undefined : companyId || undefined,
|
||||||
companyProfileId: isGovernment ? govProfileId || undefined : undefined,
|
companyProfileId: isGovernment ? govProfileId || undefined : undefined,
|
||||||
freightType,
|
freightType: "CONTAINER",
|
||||||
contractType: "NEW",
|
contractType: "NEW",
|
||||||
equipmentReturn,
|
equipmentReturn: "NA",
|
||||||
tradeDirection: tradeDirection!,
|
tradeDirection,
|
||||||
paymentCurrency,
|
paymentCurrency,
|
||||||
isHazardous,
|
isHazardous: false,
|
||||||
scheduledDate: effectiveDepartureIso || new Date().toISOString(),
|
scheduledDate: effectiveDepartureIso || new Date().toISOString(),
|
||||||
originYardId,
|
originYardId,
|
||||||
destinationYardId,
|
destinationYardId,
|
||||||
// Day-level pool: no trainScheduleId — the engine assigns the train.
|
// Day-level pool: no trainScheduleId — the engine assigns the train.
|
||||||
serviceTypeId,
|
serviceTypeId,
|
||||||
shippingLineId: shippingLineId || undefined,
|
|
||||||
firstMilePickupAddress: firstMilePickupAddress.trim() || undefined,
|
|
||||||
lastMileDeliveryAddress: lastMileDeliveryAddress.trim() || undefined,
|
|
||||||
cargoTotalWeightVgm,
|
cargoTotalWeightVgm,
|
||||||
cargoTypeId: freightType === "BULK" ? cargoTypeId : undefined,
|
containers: lines.map((l) => ({
|
||||||
cargoFreeText: freightType === "BULK" ? cargoFreeText.trim() || undefined : undefined,
|
containerTypeId: l.containerTypeId,
|
||||||
containers:
|
quantity: l.quantity,
|
||||||
freightType === "CONTAINER"
|
vgmPerUnitTons: l.vgmPerUnitTons,
|
||||||
? lines.map((l) => ({
|
})),
|
||||||
containerTypeId: l.containerTypeId,
|
|
||||||
quantity: l.quantity,
|
|
||||||
vgmPerUnitTons: l.vgmPerUnitTons,
|
|
||||||
}))
|
|
||||||
: undefined,
|
|
||||||
}),
|
}),
|
||||||
onSuccess: async (booking) => {
|
onSuccess: async (booking) => {
|
||||||
if (isGovernment) {
|
if (isGovernment) {
|
||||||
@@ -489,51 +462,50 @@ export default function NewBookingPage() {
|
|||||||
nothingFoundMessage="No companies found"
|
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>
|
</Stack>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<FormSection icon={MapPin} title="Route & service" subtitle="Origin, destination and service type" accent="blue">
|
<FormSection icon={MapPin} title="Route & service" subtitle="Direction, origin, destination and service type" accent="blue">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" fw={500} mb={6}>
|
||||||
|
Trade direction
|
||||||
|
</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
fullWidth
|
||||||
|
value={tradeDirection}
|
||||||
|
onChange={(v) => setTradeDirection(v as TradeDirection)}
|
||||||
|
data={[
|
||||||
|
{ value: "IMPORT", label: "Import" },
|
||||||
|
{ value: "EXPORT", label: "Export" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed" mt={4}>
|
||||||
|
{tradeDirection === "IMPORT"
|
||||||
|
? "Origin in Djibouti, destination in Ethiopia."
|
||||||
|
: "Origin in Ethiopia, destination in Djibouti."}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
<Group grow align="flex-start">
|
<Group grow align="flex-start">
|
||||||
<Select
|
<Select
|
||||||
label="Origin yard"
|
label="Origin yard"
|
||||||
placeholder="Select origin"
|
placeholder={`Select origin (${countriesFor[tradeDirection].origin})`}
|
||||||
data={yards}
|
data={originYardOptions}
|
||||||
value={originYardId}
|
value={originYardId}
|
||||||
onChange={(v) => {
|
onChange={setOriginYardId}
|
||||||
setOriginYardId(v);
|
|
||||||
}}
|
|
||||||
searchable
|
searchable
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
error={sameYard ? "Same as destination" : undefined}
|
nothingFoundMessage={`No ${countriesFor[tradeDirection].origin} yards`}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Destination yard"
|
label="Destination yard"
|
||||||
placeholder="Select destination"
|
placeholder={`Select destination (${countriesFor[tradeDirection].destination})`}
|
||||||
data={yards}
|
data={destinationYardOptions}
|
||||||
value={destinationYardId}
|
value={destinationYardId}
|
||||||
onChange={(v) => {
|
onChange={setDestinationYardId}
|
||||||
setDestinationYardId(v);
|
|
||||||
setScheduledDay(null);
|
|
||||||
}}
|
|
||||||
searchable
|
searchable
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
error={sameYard ? "Same as origin" : undefined}
|
nothingFoundMessage={`No ${countriesFor[tradeDirection].destination} yards`}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
@@ -553,35 +525,15 @@ export default function NewBookingPage() {
|
|||||||
}
|
}
|
||||||
description="Pick a day with a departure. The batch engine assigns the train by priority."
|
description="Pick a day with a departure. The batch engine assigns the train by priority."
|
||||||
/>
|
/>
|
||||||
<Group grow align="flex-end">
|
<Select
|
||||||
<Select
|
label="Service type"
|
||||||
label="Service type"
|
placeholder="Select service"
|
||||||
placeholder="Select service"
|
data={services}
|
||||||
data={services}
|
value={serviceTypeId}
|
||||||
value={serviceTypeId}
|
onChange={setServiceTypeId}
|
||||||
onChange={setServiceTypeId}
|
searchable
|
||||||
searchable
|
disabled={isLoading}
|
||||||
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>
|
</Stack>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
@@ -616,8 +568,7 @@ export default function NewBookingPage() {
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
{/* Cargo */}
|
{/* Cargo */}
|
||||||
{freightType === "CONTAINER" ? (
|
<FormSection
|
||||||
<FormSection
|
|
||||||
icon={ContainerIcon}
|
icon={ContainerIcon}
|
||||||
title="Container lines"
|
title="Container lines"
|
||||||
subtitle="Add one row per container type"
|
subtitle="Add one row per container type"
|
||||||
@@ -646,6 +597,14 @@ export default function NewBookingPage() {
|
|||||||
<Text fw={700} c="dimmed" size="sm" w={22} ta="center" style={{ flexShrink: 0 }}>
|
<Text fw={700} c="dimmed" size="sm" w={22} ta="center" style={{ flexShrink: 0 }}>
|
||||||
{idx + 1}
|
{idx + 1}
|
||||||
</Text>
|
</Text>
|
||||||
|
<NumberInput
|
||||||
|
label="Qty"
|
||||||
|
value={line.quantity}
|
||||||
|
onChange={(v) => updateLine(line.key, { quantity: Number(v) || 0 })}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
style={{ width: 90, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Container type"
|
label="Container type"
|
||||||
placeholder="Select type"
|
placeholder="Select type"
|
||||||
@@ -657,15 +616,7 @@ export default function NewBookingPage() {
|
|||||||
style={{ flex: 2, minWidth: 160 }}
|
style={{ flex: 2, minWidth: 160 }}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Qty"
|
label="Weight / unit"
|
||||||
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}
|
value={line.vgmPerUnitTons}
|
||||||
onChange={(v) => updateLine(line.key, { vgmPerUnitTons: Number(v) || 0 })}
|
onChange={(v) => updateLine(line.key, { vgmPerUnitTons: Number(v) || 0 })}
|
||||||
min={0}
|
min={0}
|
||||||
@@ -708,95 +659,14 @@ export default function NewBookingPage() {
|
|||||||
Add container line
|
Add container line
|
||||||
</Button>
|
</Button>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Total VGM:{" "}
|
Total weight:{" "}
|
||||||
<Text span fw={700} c="dark">
|
<Text span fw={700} c="dark">
|
||||||
{fmtTons(containerWeight)}
|
{fmtTons(cargoTotalWeightVgm)}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FormSection>
|
</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>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
@@ -812,32 +682,23 @@ export default function NewBookingPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="xs" mb="md">
|
<Group gap="xs" mb="md">
|
||||||
<Badge variant="light" color={freightType === "BULK" ? "orange" : "teal"} radius="sm">
|
<Badge variant="light" color="teal" radius="sm">
|
||||||
{freightType === "BULK" ? "Bulk" : "Container"}
|
Container
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="light" color="blue" radius="sm">
|
<Badge variant="light" color={tradeDirection === "EXPORT" ? "orange" : "blue"} radius="sm">
|
||||||
{tradeDirection}
|
{tradeDirection === "EXPORT" ? "Export" : "Import"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isGovernment ? (
|
{isGovernment ? (
|
||||||
<Badge variant="light" color="grape" radius="sm">
|
<Badge variant="light" color="grape" radius="sm">
|
||||||
Government
|
Government
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{isHazardous ? (
|
|
||||||
<Badge variant="light" color="red" radius="sm" leftSection={<Flame size={10} />}>
|
|
||||||
Hazardous
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap={8}>
|
<Stack gap={8}>
|
||||||
{freightType === "CONTAINER" ? (
|
<SummaryRow label="Container lines" value={String(lines.length)} />
|
||||||
<>
|
<SummaryRow label="Total containers" value={String(totalContainers)} />
|
||||||
<SummaryRow label="Container lines" value={String(lines.length)} />
|
<SummaryRow label="Total weight" value={fmtTons(cargoTotalWeightVgm)} strong />
|
||||||
<SummaryRow label="Total containers" value={String(totalContainers)} />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<SummaryRow label="Total VGM weight" value={fmtTons(cargoTotalWeightVgm)} strong />
|
|
||||||
<Divider my={4} />
|
<Divider my={4} />
|
||||||
<SummaryRow
|
<SummaryRow
|
||||||
label="Route"
|
label="Route"
|
||||||
|
|||||||
Reference in New Issue
Block a user