mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(train-scheduling): add day-level booking pool and available days query
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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 ?? ""),
|
||||
|
||||
@@ -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<FreightType>("CONTAINER");
|
||||
const [originYardId, setOriginYardId] = useState<string | null>(null);
|
||||
const [destinationYardId, setDestinationYardId] = useState<string | null>(null);
|
||||
const [trainScheduleId, setTrainScheduleId] = useState<string | null>(null);
|
||||
const [serviceTypeId, setServiceTypeId] = useState<string | null>(null);
|
||||
const [scheduledDate, setScheduledDate] = useState("");
|
||||
// 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
|
||||
@@ -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}
|
||||
/>
|
||||
</Group>
|
||||
{hasBookableSchedules ? (
|
||||
<Select
|
||||
label="Train schedule (optional)"
|
||||
placeholder={
|
||||
originYardId && destinationYardId
|
||||
? "Leave blank to let the batch engine assign a train"
|
||||
: "Pick origin & destination first"
|
||||
}
|
||||
data={scheduleOptions}
|
||||
value={trainScheduleId}
|
||||
onChange={setTrainScheduleId}
|
||||
searchable
|
||||
clearable
|
||||
disabled={!originYardId || !destinationYardId || schedulesLoading}
|
||||
nothingFoundMessage="No open schedules on this route"
|
||||
description="Optional: pin to a specific train. Otherwise the booking joins the day pool and the engine assigns a train by priority."
|
||||
/>
|
||||
) : originYardId && destinationYardId ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
No open train schedule on this route — set a preferred departure below. The batch
|
||||
engine assigns a train on that day, or staff can pin one later.
|
||||
</Text>
|
||||
) : null}
|
||||
<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"
|
||||
@@ -528,21 +524,22 @@ export default function NewBookingPage() {
|
||||
|
||||
<FormSection icon={CalendarClock} title="Schedule & payment" accent="grape">
|
||||
<Group grow align="flex-start">
|
||||
{selectedSchedule ? (
|
||||
<TextInput
|
||||
label="Departure"
|
||||
value={new Date(selectedSchedule.scheduleDate).toLocaleString()}
|
||||
readOnly
|
||||
description="Taken from the selected train schedule"
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label="Preferred departure"
|
||||
type="datetime-local"
|
||||
value={scheduledDate}
|
||||
onChange={(e) => setScheduledDate(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<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={[
|
||||
|
||||
@@ -109,6 +109,21 @@ export const trainSchedulingService = {
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Day-level pool: the days that have an OPEN departure on the route. Staff pick
|
||||
* a day; the batch engine assigns the train. No capacity is returned.
|
||||
*/
|
||||
getAvailableDays: async (
|
||||
originYardId?: string,
|
||||
destinationYardId?: string,
|
||||
): Promise<string[]> => {
|
||||
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<BatchBoardScheduleDetail> => {
|
||||
const response = await client.post<BatchBoardScheduleDetail>(
|
||||
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(scheduleId),
|
||||
|
||||
Reference in New Issue
Block a user