Add train deactivation feature and import train number management

- Introduced DEACTIVATED status for trains, allowing staff to park trains indefinitely.
- Implemented methods to deactivate and reactivate trains in the TrainBuilderService.
- Added UI components for train deactivation and reactivation in TrainBuilderDetailPage.
- Created a dropdown setting for admin-managed import train numbers, with corresponding migrations.
- Updated yard code length to accommodate soft-delete suffix.
- Enhanced train status handling to include DEACTIVATED state.
This commit is contained in:
Marshal
2026-07-20 07:04:23 +00:00
parent ab355d0e16
commit 771aa2a605
20 changed files with 467 additions and 36 deletions

View File

@@ -0,0 +1,67 @@
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { IMPORT_TRAIN_OPTIONS } from "@/constants/trainRuns";
import { api } from "@/services/api";
/** Dropdown-settings code holding the admin-managed IMPORT run numbers. */
export const IMPORT_TRAIN_NUMBERS_CODE = "import_train_numbers";
export interface ImportTrainNumberOption {
value: string;
label: string;
disabled?: boolean;
}
/**
* Selectable IMPORT run numbers for the Train Builder, sourced from the
* admin-managed `import_train_numbers` dropdown setting (admins add new runs
* from the Dropdown Settings editor). Falls back to the legacy hardcoded run
* list while the setting is missing or has no options.
*
* Numbers already claimed by an existing train are kept in the list but
* disabled and tagged "in use". Pass `currentNumber` when editing a train so
* its own number stays pickable, and so a legacy number that was removed from
* the setting still renders.
*/
export function useImportTrainNumberOptions(currentNumber?: string | null) {
const settingQuery = useQuery(
api.dropdownSettings.getByCode.queryOptions({
input: { code: IMPORT_TRAIN_NUMBERS_CODE },
staleTime: 5 * 60_000,
retry: false,
}),
);
const usedQuery = useQuery(
api.trainBuilder.usedTrainNumbers.queryOptions({ staleTime: 30_000 }),
);
const options = useMemo<ImportTrainNumberOption[]>(() => {
const configured = [...(settingQuery.data?.children ?? [])]
.filter((option) => !option.disabled)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
.map((option) => ({
value: option.value,
label: option.label || option.value,
}));
const base = configured.length ? configured : IMPORT_TRAIN_OPTIONS;
const used = new Set(usedQuery.data?.importTrainNumbers ?? []);
if (currentNumber) used.delete(currentNumber);
const items: ImportTrainNumberOption[] = base.map((option) =>
used.has(option.value)
? { ...option, label: `${option.label} — in use`, disabled: true }
: option,
);
if (currentNumber && !items.some((option) => option.value === currentNumber)) {
items.unshift({ value: currentNumber, label: currentNumber });
}
return items;
}, [settingQuery.data, usedQuery.data, currentNumber]);
return {
options,
isLoading: settingQuery.isLoading || usedQuery.isLoading,
};
}