diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx index 2f947c703..a1de7dc3e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx @@ -15,7 +15,6 @@ import { Stack, Switch, Text, - Textarea, TextInput, ThemeIcon, Tooltip, @@ -25,16 +24,13 @@ import { isAxiosError } from "axios"; import { AlertTriangle, ArrowLeft, - Boxes, CalendarClock, Container as ContainerIcon, - Flame, Info, Layers, MapPin, Package, Plus, - Settings2, Ship, Trash2, Weight, @@ -48,8 +44,6 @@ 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; @@ -103,24 +97,17 @@ const newLine = (): ContainerLine => ({ const fmtTons = (n: number) => `${n.toLocaleString(undefined, { maximumFractionDigits: 2 })} t`; -type TradeDirection = "IMPORT" | "EXPORT" | "DOMESTIC"; +type TradeDirection = "IMPORT" | "EXPORT"; -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 = { - IMPORT: "Import", - EXPORT: "Export", - DOMESTIC: "Domestic", +// The direction fixes which end of the route is inside Ethiopia — Djibouti +// yards stand in for "outside" (the port), mirroring the customer portal and +// the backend's deriveTradeDirection. +const countriesFor: Record< + TradeDirection, + { origin: string; destination: string } +> = { + IMPORT: { origin: "Djibouti", destination: "Ethiopia" }, + EXPORT: { origin: "Ethiopia", destination: "Djibouti" }, }; 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. const [govCompanyId, setGovCompanyId] = useState(null); const [govProfileId, setGovProfileId] = useState(null); - const [freightType, setFreightType] = useState("CONTAINER"); + // Container freight only for now — bulk is disabled on this page. + const [tradeDirection, setTradeDirection] = useState("IMPORT"); const [originYardId, setOriginYardId] = useState(null); const [destinationYardId, setDestinationYardId] = useState(null); const [serviceTypeId, setServiceTypeId] = useState(null); @@ -191,21 +179,8 @@ export default function NewBookingPage() { const [scheduledDay, setScheduledDay] = useState(null); const [paymentCurrency, setPaymentCurrency] = useState("ETB"); - // container freight const [lines, setLines] = useState([newLine()]); - // bulk freight - const [cargoTypeId, setCargoTypeId] = useState(null); - const [cargoFreeText, setCargoFreeText] = useState(""); - const [bulkWeight, setBulkWeight] = useState(100); - - // extra options - const [equipmentReturn, setEquipmentReturn] = useState("NA"); - const [isHazardous, setIsHazardous] = useState(false); - const [shippingLineId, setShippingLineId] = useState(null); - const [firstMilePickupAddress, setFirstMile] = useState(""); - const [lastMileDeliveryAddress, setLastMile] = useState(""); - const { data: refData, isLoading } = useQuery({ queryKey: ["bookings", "reference-data"], queryFn: () => bookingsService.getReferenceData() as Promise, @@ -283,16 +258,35 @@ export default function NewBookingPage() { 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); + + // Yards filtered to the side of the route the direction dictates (import: + // 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. 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( () => @@ -306,23 +300,12 @@ export default function NewBookingPage() { [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; + const cargoTotalWeightVgm = lines.reduce( + (s, l) => s + (l.quantity || 0) * (l.vgmPerUnitTons || 0), + 0, + ); // 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 @@ -337,7 +320,7 @@ export default function NewBookingPage() { return String(size).includes("20") ? sum + (l.quantity || 0) : sum; }, 0); }, [refData?.containers, lines]); - const hasOdd20ft = freightType === "CONTAINER" && twentyFtCount % 2 === 1; + const hasOdd20ft = twentyFtCount % 2 === 1; // ---- validation ---- const lineValid = (l: ContainerLine) => @@ -353,13 +336,11 @@ export default function NewBookingPage() { Boolean(originYardId) && Boolean(destinationYardId) && !sameYard && - Boolean(tradeDirection) && Boolean(serviceTypeId) && departureSatisfied && (isGovernment ? Boolean(govCompanyId && govProfileId) : Boolean(companyId)) && - (freightType === "BULK" - ? Boolean(cargoTypeId) && bulkWeight > 0 - : allLinesValid && !hasOdd20ft); + allLinesValid && + !hasOdd20ft; const updateLine = (key: string, patch: Partial) => setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l))); @@ -372,31 +353,23 @@ export default function NewBookingPage() { isGovernment, companyId: isGovernment ? govCompanyId || undefined : companyId || undefined, companyProfileId: isGovernment ? govProfileId || undefined : undefined, - freightType, + freightType: "CONTAINER", contractType: "NEW", - equipmentReturn, - tradeDirection: tradeDirection!, + equipmentReturn: "NA", + tradeDirection, paymentCurrency, - isHazardous, + isHazardous: false, 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, + containers: lines.map((l) => ({ + containerTypeId: l.containerTypeId, + quantity: l.quantity, + vgmPerUnitTons: l.vgmPerUnitTons, + })), }), onSuccess: async (booking) => { if (isGovernment) { @@ -489,51 +462,50 @@ export default function NewBookingPage() { nothingFoundMessage="No companies found" /> )} - - - - Freight type - - setFreightType(v as FreightType)} - data={[ - { value: "CONTAINER", label: "Container" }, - { value: "BULK", label: "Bulk" }, - ]} - /> - - + + + + Trade direction + + setTradeDirection(v as TradeDirection)} + data={[ + { value: "IMPORT", label: "Import" }, + { value: "EXPORT", label: "Export" }, + ]} + /> + + {tradeDirection === "IMPORT" + ? "Origin in Djibouti, destination in Ethiopia." + : "Origin in Ethiopia, destination in Djibouti."} + + { - setDestinationYardId(v); - setScheduledDay(null); - }} + onChange={setDestinationYardId} searchable disabled={isLoading} - error={sameYard ? "Same as origin" : undefined} + nothingFoundMessage={`No ${countriesFor[tradeDirection].destination} yards`} /> - - - Trade direction - - - {tradeDirection ? tradeDirectionLabel[tradeDirection] : "Select yards"} - - - + updateLine(line.key, { quantity: Number(v) || 0 })} - min={1} - step={1} - style={{ width: 90, flexShrink: 0 }} - /> - updateLine(line.key, { vgmPerUnitTons: Number(v) || 0 })} min={0} @@ -708,95 +659,14 @@ export default function NewBookingPage() { Add container line - Total VGM:{" "} + Total weight:{" "} - {fmtTons(containerWeight)} + {fmtTons(cargoTotalWeightVgm)} - ) : ( - - -