feat(train-sets): implement multi-locomotive support for train sets

- Added TrainSetLocomotive entity to link multiple locomotives to a train set.
- Updated TrainSet entity to include a OneToMany relationship with TrainSetLocomotive.
- Modified the train scheduling logic to require at least two locomotives for a train set.
- Enhanced the UI components to support selecting multiple locomotives.
- Introduced new permissions for viewing customs clearance.
- Updated migrations to create the train_set_locomotives table and backfill existing data.
- Implemented utility functions for managing train numbers based on cargo type and direction.
- Added tests for train number utilities to ensure correct functionality.
This commit is contained in:
Marshal
2026-06-24 23:54:45 +00:00
parent 15ab9f906e
commit e0c3044933
28 changed files with 817 additions and 118 deletions

View File

@@ -8,6 +8,7 @@ import {
Checkbox,
Group,
Modal,
MultiSelect,
Paper,
Radio,
RingProgress,
@@ -107,7 +108,7 @@ export function AllocateBookingWizard({
const [selectedScheduleId, setSelectedScheduleId] = useState<string | null>(null);
const [routeId, setRouteId] = useState("");
const scheduleDate = booking.scheduledDate;
const [locomotiveId, setLocomotiveId] = useState("");
const [locomotiveIds, setLocomotiveIds] = useState<string[]>([]);
const [extraBookingIds, setExtraBookingIds] = useState<string[]>([]);
const [forceAssign, setForceAssign] = useState(false);
const [previewResult, setPreviewResult] = useState<TrainSchedulePreviewResponse | null>(null);
@@ -152,7 +153,7 @@ export function AllocateBookingWizard({
useEffect(() => {
if (scheduleMode === "new") {
setLocomotiveId("");
setLocomotiveIds([]);
}
}, [routeId, scheduleMode]);
@@ -264,11 +265,11 @@ export function AllocateBookingWizard({
const ensureSchedule = async (): Promise<string> => {
if (scheduleMode === "existing" && selectedScheduleId) return selectedScheduleId;
if (!routeId || !scheduleDate || !locomotiveId) {
throw new Error("Select route, date, and locomotive");
if (!routeId || !scheduleDate || locomotiveIds.length < 2) {
throw new Error("Select route, date, and at least two locomotives");
}
const created = await create.mutateAsync({
payload: { routeId, scheduleDate, locomotiveId },
payload: { routeId, scheduleDate, locomotiveIds },
});
setSelectedScheduleId(created.id);
return created.id;
@@ -526,17 +527,25 @@ export function AllocateBookingWizard({
onChange={(v) => setRouteId(v ?? "")}
searchable
/>
<Select
label="Locomotive"
placeholder={routeId ? "Select locomotive" : "Select a route first"}
<MultiSelect
label="Locomotives"
description="At least two (front and back)"
placeholder={
routeId ? "Select at least two locomotives" : "Select a route first"
}
data={(locomotivesQuery.data ?? []).map((l) => ({
value: l.id,
label: `${l.code}${l.name ? ` · ${l.name}` : ""}`,
}))}
value={locomotiveId || null}
onChange={(v) => setLocomotiveId(v ?? "")}
value={locomotiveIds}
onChange={setLocomotiveIds}
searchable
disabled={!routeId}
error={
locomotiveIds.length > 0 && locomotiveIds.length < 2
? "Select at least two locomotives"
: undefined
}
nothingFoundMessage={
routeId ? "No available locomotives for this corridor" : "Select a route first"
}