feat(train-scheduling): implement day-level booking pool

- Added `unplaced` method in `BookingNotifierService` to log warnings for bookings that cannot be placed on any train.
- Introduced `getAvailableDays` method in `TrainSchedulingService` to retrieve distinct days with open departures for a given route.
- Created `AvailableDaysQueryDto` for querying available days based on origin and destination yards.
- Updated `TrainSchedulingController` to expose an endpoint for available days.
- Modified frontend components to support day-level booking, allowing customers to select only a day without pinning to a specific train.
- Removed references to train schedules in booking forms and review steps, emphasizing day selection.
- Added a database migration to create an index for efficient querying of bookings by route and day.
This commit is contained in:
Marshal
2026-06-18 14:12:46 +00:00
parent 578012dffd
commit 421e0266bc
22 changed files with 626 additions and 332 deletions

View File

@@ -302,8 +302,9 @@ export default function NewBookingPage() {
const allLinesValid = lines.length > 0 && lines.every(lineValid);
const sameYard = Boolean(originYardId && originYardId === destinationYardId);
const scheduleSatisfied =
hasBookableSchedules ? Boolean(trainScheduleId) : Boolean(scheduledDate);
// 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);
const canSubmit =
@@ -311,7 +312,6 @@ export default function NewBookingPage() {
Boolean(destinationYardId) &&
!sameYard &&
Boolean(tradeDirection) &&
scheduleSatisfied &&
Boolean(serviceTypeId) &&
departureSatisfied &&
(isGovernment ? governmentInstitution.trim().length >= 2 : Boolean(companyId)) &&
@@ -473,25 +473,25 @@ export default function NewBookingPage() {
</Group>
{hasBookableSchedules ? (
<Select
label="Train schedule"
label="Train schedule (optional)"
placeholder={
originYardId && destinationYardId
? "Select an open schedule on this route"
? "Leave blank to let the batch engine assign a train"
: "Pick origin & destination first"
}
data={scheduleOptions}
value={trainScheduleId}
onChange={setTrainScheduleId}
searchable
required
clearable
disabled={!originYardId || !destinationYardId || schedulesLoading}
nothingFoundMessage="No open schedules on this route"
description="The booking will be batched against this schedule once its contract is signed."
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. Staff can
link a schedule later.
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}
<Group grow align="flex-end">