diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index ab023d1b3..11100e346 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -143,6 +143,7 @@ export const URL_CONSTANTS = { TRAIN_SCHEDULING: { ELIGIBLE_BOOKINGS: "/train-scheduling/eligible-bookings", BOOKABLE_SCHEDULES: "/train-scheduling/bookable-schedules", + AVAILABLE_DAYS: "/train-scheduling/available-days", AVAILABLE_LOCOMOTIVES: "/train-scheduling/available-locomotives", BATCH_BOARD: "/train-scheduling/batch-board", BATCH_BOARD_DETAIL: (scheduleId: string) => diff --git a/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts b/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts index 8c6536c5c..823d13324 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts @@ -132,6 +132,29 @@ export const useBookableSchedules = ( enabled: Boolean(originYardId && destinationYardId), }); +/** + * Day-level pool: which days have an OPEN departure on the route. Staff pick a + * day (not a train) when creating a booking; the engine assigns the train. + */ +export const useAvailableDays = ( + originYardId?: string | null, + destinationYardId?: string | null, +) => + useQuery({ + queryKey: [ + ...QUERY_KEYS.TRAIN_SCHEDULING.ROOT, + "available-days", + originYardId ?? "", + destinationYardId ?? "", + ], + queryFn: () => + trainSchedulingService.getAvailableDays( + originYardId ?? undefined, + destinationYardId ?? undefined, + ), + enabled: Boolean(originYardId && destinationYardId), + }); + export const useTrainTrack = (id: string | undefined) => useQuery({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.track(id ?? ""), 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 86c188d86..042067edb 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx @@ -44,7 +44,7 @@ import toast from "react-hot-toast"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; import { bookingsService } from "@/services/bookings.service"; -import { useBookableSchedules } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useAvailableDays } from "@/hooks/trainScheduling/useTrainScheduling"; import { api } from "@/auth/http"; import { unwrap } from "@/utils/endpoint"; import { URL_CONSTANTS } from "@/constants/URLS"; @@ -194,9 +194,9 @@ export default function NewBookingPage() { const [freightType, setFreightType] = useState("CONTAINER"); const [originYardId, setOriginYardId] = useState(null); const [destinationYardId, setDestinationYardId] = useState(null); - const [trainScheduleId, setTrainScheduleId] = useState(null); const [serviceTypeId, setServiceTypeId] = useState(null); - const [scheduledDate, setScheduledDate] = useState(""); + // Day-level pool: staff pick a DAY (yyyy-MM-dd); the engine assigns the train. + const [scheduledDay, setScheduledDay] = useState(null); const [paymentCurrency, setPaymentCurrency] = useState("ETB"); // container freight @@ -232,34 +232,37 @@ export default function NewBookingPage() { label: c.name || c.email || c.tin || c.id, })); - const { data: bookableSchedules, isLoading: schedulesLoading } = useBookableSchedules( + // 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 } = useAvailableDays( originYardId, destinationYardId, ); - const scheduleOptions = (bookableSchedules ?? []).map((s) => ({ - value: s.id, - label: `${s.routeName ?? `${s.origin} → ${s.destination}`} · ${new Date( - s.scheduleDate, - ).toLocaleString()} · ${s.remainingWagons}/${s.maxWagons} wagons free`, + 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 selectedSchedule = (bookableSchedules ?? []).find((s) => s.id === trainScheduleId); + const hasAvailableDays = (availableDays ?? []).length > 0; - // When a schedule is chosen its date IS the departure; otherwise fall back to the manual field. - const effectiveDepartureIso = selectedSchedule - ? new Date(selectedSchedule.scheduleDate).toISOString() - : scheduledDate - ? new Date(scheduledDate).toISOString() - : ""; + // 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); - const hasBookableSchedules = (bookableSchedules ?? []).length > 0; + // Reset the day when the route changes — available days depend on the route. useEffect(() => { - setTrainScheduleId(null); + 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 })); @@ -302,10 +305,9 @@ export default function NewBookingPage() { const allLinesValid = lines.length > 0 && lines.every(lineValid); const sameYard = Boolean(originYardId && originYardId === destinationYardId); - // Day-level pool: a shipment DAY is enough to proceed. Pinning a specific train - // (trainScheduleId) is an optional staff override — the batch engine otherwise - // assigns the train. A selected schedule implies its day, so either satisfies. - const departureSatisfied = Boolean(selectedSchedule) || Boolean(scheduledDate); + // 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) && @@ -339,7 +341,7 @@ export default function NewBookingPage() { scheduledDate: effectiveDepartureIso || new Date().toISOString(), originYardId, destinationYardId, - trainScheduleId: trainScheduleId || undefined, + // Day-level pool: no trainScheduleId — the engine assigns the train. serviceTypeId, shippingLineId: shippingLineId || undefined, firstMilePickupAddress: firstMilePickupAddress.trim() || undefined, @@ -464,36 +466,30 @@ export default function NewBookingPage() { value={destinationYardId} onChange={(v) => { setDestinationYardId(v); - setTrainScheduleId(null); + setScheduledDay(null); }} searchable disabled={isLoading} error={sameYard ? "Same as origin" : undefined} /> - {hasBookableSchedules ? ( - => { + const response = await client.get<{ days: string[] }>( + URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS, + { params: { originYardId, destinationYardId } }, + ); + return unwrap(response.data).days; + }, + runBatch: async (scheduleId: string): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(scheduleId),