import { Freight } from "@edr/types"; import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources"; import { IMPORT_TRAIN_OPTIONS, TRAIN_RUN_FILTER_OPTIONS, exportRunFor, } from "@/constants/trainRuns"; import { vehiclesConfig, VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS } from "./vehicles"; import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers"; // Re-export options for backward compatibility export { VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS, DRIVER_STATUS_OPTIONS }; export type FleetResourceSlug = | "locomotives" | "trains" | "wagons" | "containers" | "cargoes" | "vehicles" | "drivers"; export const FLEET_SELECT_NONE = "__none__"; import type { WagonListFilters } from "@/services/wagon.service"; export type FleetListFilters = WagonListFilters; export type FleetDynamicOptions = | "wagonTypes" | "containerTypes" | "cargoTypes" | "wagons" | "containers" | "yards"; export interface FleetResourceColumn { id: string; header: string; accessorKey: string; format?: ColumnFormat | "statusBadge" | "verifiedBadge"; size?: number; } export interface FleetFormFieldDef extends FormFieldDef { dynamicOptions?: FleetDynamicOptions; noneOption?: boolean; /** * Read-only field whose value is computed from the other fields rather than * typed. Rendered non-editable and recomputed on every change, so the stored * form value for this field is never trusted — the function is the source of * truth at both render and submit. */ derivedValue?: (values: Record) => string; /** * Field can be emptied back to NULL. Empty values are normally dropped from * the payload (so a PATCH leaves them untouched); a clearable field instead * submits an explicit `null`, which is what actually unsets the column. Also * renders a clear button on a `select`. */ clearable?: boolean; /** * Field is owned by the Fayda identity — populated only by verification and * never hand-edited. Rendered disabled in the form. */ faydaLocked?: boolean; /** * Direction a `date` field is constrained to. "future" = must be after today * (e.g. a license expiry); "past" (default) = cannot be in the future. */ dateBound?: "past" | "future"; } export interface FleetListFilterDef { key: "status" | "availability" | "currentYardId" | "wagonTypeId" | "trainId" | "trainNumber"; label: string; options?: Array<{ value: string; label: string }>; allLabel?: string; dynamicOptions?: FleetDynamicOptions; } export interface FleetResourceConfig { slug: FleetResourceSlug; label: string; subtitle: string; basePath: string; addLabel: string; entityLabel: string; searchPlaceholder: string; supportsSearch: boolean; /** Server-side list filters (e.g. wagon status / readiness). */ listFilters?: FleetListFilterDef[]; columns: FleetResourceColumn[]; formFields: FleetFormFieldDef[]; emptyValues: Record; removeAction: "delete" | "decommission"; removeActionLabel?: string; removeConfirmMessage?: string; removeSuccessMessage?: string; detailPath?: string; cardTitleKey?: string; cardCodeKey?: string; cardSubtitleKey?: string; searchKeys: string[]; /** Offer Fayda identity verification in the add/edit form (drivers). */ faydaVerification?: boolean; } export const FLEET_BASE_PATH_BY_SLUG: Record = { locomotives: "/dashboard/locomotives", trains: "/dashboard/trains", wagons: "/dashboard/wagons", containers: "/dashboard/containers", cargoes: "/dashboard/cargoes", vehicles: "/dashboard/vehicles", drivers: "/dashboard/drivers", }; const LOCOMOTIVE_TYPE_OPTIONS = [ { label: "Diesel", value: "DIESEL" }, { label: "Electric", value: "ELECTRIC" }, ]; const LOCOMOTIVE_STATUS_OPTIONS = [ { label: "Available", value: "AVAILABLE" }, { label: "Maintenance", value: "MAINTENANCE" }, { label: "Assigned", value: "ASSIGNED" }, { label: "Out of service", value: "OUT_OF_SERVICE" }, ]; // Every status a wagon can hold — for FILTERING the list. ASSIGNED belongs here: // staff still need to search for assigned wagons. const WAGON_STATUS_OPTIONS = [ { label: "Available", value: Freight.WagonStatus.Available }, { label: "Assigned", value: Freight.WagonStatus.Assigned }, { label: "Maintenance", value: Freight.WagonStatus.Maintenance }, { label: "Detained", value: Freight.WagonStatus.Detained }, ]; // Statuses staff may set BY HAND on the create/edit form. ASSIGNED is omitted // on purpose: a wagon becomes ASSIGNED as a side effect of being built into a // train, never by editing it directly. Setting it by hand produced wagons that // claim to be assigned while coupled to nothing, which the yard workspace then // counts as in-yard stock. const WAGON_EDITABLE_STATUS_OPTIONS = WAGON_STATUS_OPTIONS.filter( (o) => o.value !== Freight.WagonStatus.Assigned, ); export const FLEET_RESOURCES: FleetResourceConfig[] = [ { slug: "locomotives", label: "Locomotives", subtitle: "Manage locomotive master data used by train scheduling and fleet operations", basePath: "/dashboard/locomotives", addLabel: "Add Locomotive", entityLabel: "Locomotive", searchPlaceholder: "Search locomotives…", supportsSearch: true, removeAction: "decommission", removeActionLabel: "Decommission", removeConfirmMessage: "Decommission this locomotive?", removeSuccessMessage: "Locomotive decommissioned", cardTitleKey: "name", cardCodeKey: "code", listFilters: [ { key: "status", label: "Status", allLabel: "All statuses", options: LOCOMOTIVE_STATUS_OPTIONS, }, { key: "currentYardId", label: "Current Yard", allLabel: "All yards", dynamicOptions: "yards", }, ], cardSubtitleKey: "currentYard", searchKeys: ["code", "name", "locomotiveType", "status", "currentYardId"], columns: [ { id: "code", header: "Code", accessorKey: "code", format: "code" }, { id: "name", header: "Name", accessorKey: "name" }, { id: "locomotiveType", header: "Type", accessorKey: "locomotiveType" }, { id: "currentYard", header: "Current Yard", accessorKey: "currentYard", format: "entityLabel" }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge" }, { id: "maxPullWeightTons", header: "Max pull (tons)", accessorKey: "maxPullWeightTons", format: "number" }, { id: "maxTrainLengthMeters", header: "Max length (m)", accessorKey: "maxTrainLengthMeters", format: "number" }, { id: "overageToleranceTons", header: "Weight tolerance (t)", accessorKey: "overageToleranceTons", format: "number" }, { id: "overageToleranceMeters", header: "Length tolerance (m)", accessorKey: "overageToleranceMeters", format: "number" }, ], // Code is auto-generated server-side (LOCO-NNN) — omitted from the form. formFields: [ { name: "name", label: "Name", type: "text" }, { name: "locomotiveType", label: "Locomotive type", type: "select", required: true, options: LOCOMOTIVE_TYPE_OPTIONS }, { name: "status", label: "Status", type: "select", required: true, options: LOCOMOTIVE_STATUS_OPTIONS }, { name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" }, { name: "maxPullWeightTons", label: "Max pulling weight (tons)", type: "number", required: true }, { name: "maxTrainLengthMeters", label: "Max train length (meters)", type: "number", required: true }, // Scheduling accepts a consist up to (max + tolerance) on each axis: 37 PW2 // wagons gross 3,522.4T against a 3,500T pull limit and only board because // of the weight tolerance. { name: "overageToleranceTons", label: "Weight tolerance (tons over max pull)", type: "number" }, { name: "overageToleranceMeters", label: "Length tolerance (meters over max length)", type: "number" }, { name: "powerKw", label: "Power (kW)", type: "number" }, { name: "tractionForceKn", label: "Traction force (kN)", type: "number" }, { name: "maxSpeedKmh", label: "Max speed (km/h)", type: "number" }, ], emptyValues: { name: "", locomotiveType: "DIESEL", status: "AVAILABLE", currentYardId: "", maxPullWeightTons: 2500, maxTrainLengthMeters: 760, overageToleranceTons: "", overageToleranceMeters: "", powerKw: "", tractionForceKn: "", maxSpeedKmh: "", }, }, { slug: "trains", label: "Trains", subtitle: "Manage train master data independently from train scheduling", basePath: "/dashboard/trains", addLabel: "Add Train", entityLabel: "Train", searchPlaceholder: "Search trains…", supportsSearch: true, removeAction: "delete", detailPath: "/dashboard/trains/:id", cardTitleKey: "trainName", cardCodeKey: "code", cardSubtitleKey: "trainNumber", searchKeys: ["code", "trainNumber", "trainName", "status"], columns: [ { id: "code", header: "Code", accessorKey: "code", format: "code" }, { id: "trainNumber", header: "Number", accessorKey: "trainNumber" }, { id: "trainName", header: "Name", accessorKey: "trainName" }, { id: "capacityTons", header: "Capacity (tons)", accessorKey: "capacityTons", format: "number" }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge" }, ], formFields: [ { name: "code", label: "Code", type: "text", required: true }, { name: "capacityTons", label: "Capacity (tons)", type: "number", required: true }, { name: "trainNumber", label: "Train number", type: "text" }, { name: "trainName", label: "Train name", type: "text" }, { name: "locomotiveNumber", label: "Locomotive number", type: "text" }, { name: "status", label: "Status", type: "text" }, { name: "notes", label: "Notes", type: "textarea" }, { name: "remarks", label: "Remarks", type: "textarea" }, ], emptyValues: { code: "", capacityTons: 0, trainNumber: "", trainName: "", locomotiveNumber: "", status: "AVAILABLE", notes: "", remarks: "", }, }, { slug: "wagons", label: "Wagons", subtitle: "Manage wagon master data. Operational scheduling uses train schedules separately", basePath: "/dashboard/wagons", addLabel: "Add Wagon", entityLabel: "Wagon", searchPlaceholder: "Search wagons…", supportsSearch: true, removeAction: "delete", listFilters: [ { key: "status", label: "Status", allLabel: "All statuses", options: WAGON_STATUS_OPTIONS, }, { key: "currentYardId", label: "Current Yard", allLabel: "All yards", dynamicOptions: "yards", }, { key: "trainNumber", label: "Train number", allLabel: "All trains", options: TRAIN_RUN_FILTER_OPTIONS, }, ], cardTitleKey: "wagonNumber", cardSubtitleKey: "currentYard", searchKeys: [ "wagonNumber", "wagonTypeId", "trainId", "exportTrainNumber", "importTrainNumber", "status", "currentYardId", ], columns: [ // Tare weight and payload capacity are not wagon columns — they belong to the // wagon type and are shown through it (see WagonsCrudPage in FleetCrudPages). { id: "wagonNumber", header: "Number", accessorKey: "wagonNumber", format: "code" }, { id: "wagonTypeId", header: "Type", accessorKey: "wagonTypeId", format: "entityLabel" }, // Unset on a wagon that is not on a run — renders as a dimmed dash. { id: "exportTrainNumber", header: "Export train no.", accessorKey: "exportTrainNumber", format: "code" }, { id: "importTrainNumber", header: "Import train no.", accessorKey: "importTrainNumber", format: "code" }, { id: "currentYard", header: "Current Yard", accessorKey: "currentYard", format: "entityLabel" }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge" }, ], formFields: [ // Run numbers are optional — a wagon sits in the fleet unassigned to any // run until an operator picks an import run. The export run is fixed by // that choice, so it is derived rather than typed. { name: "exportTrainNumber", label: "Export train number", type: "text", description: "Odd — Ethiopia → Djibouti runs", placeholder: "e.g. 8001", derivedValue: (values) => exportRunFor(values.importTrainNumber), // Follows the import run to NULL when that is cleared. clearable: true, }, { name: "importTrainNumber", label: "Import train number", type: "select", description: "Even — Djibouti → Ethiopia runs", placeholder: "e.g. 8002", options: IMPORT_TRAIN_OPTIONS, clearable: true, }, { name: "wagonNumber", label: "Wagon number", type: "text", required: true }, { name: "wagonTypeId", label: "Wagon type", type: "select", required: true, dynamicOptions: "wagonTypes" }, { name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" }, { name: "status", label: "Status", type: "select", required: true, options: WAGON_EDITABLE_STATUS_OPTIONS }, { name: "notes", label: "Notes", type: "textarea" }, ], emptyValues: { exportTrainNumber: "", importTrainNumber: "", wagonNumber: "", wagonTypeId: "", currentYardId: "", status: Freight.WagonStatus.Available, notes: "", }, }, { slug: "containers", label: "Containers", subtitle: "Manage container master data and wagon assignments", basePath: "/dashboard/containers", addLabel: "Add Container", entityLabel: "Container", searchPlaceholder: "Search containers…", supportsSearch: true, removeAction: "delete", cardTitleKey: "containerNumber", cardSubtitleKey: "status", searchKeys: ["containerNumber", "containerTypeId", "wagonId", "status"], columns: [ { id: "containerNumber", header: "Number", accessorKey: "containerNumber", format: "code" }, { id: "containerTypeId", header: "Type", accessorKey: "containerTypeId", format: "entityLabel" }, { id: "wagonId", header: "Wagon", accessorKey: "wagonId", format: "entityLabel" }, { id: "maxGrossWeight", header: "Max gross", accessorKey: "maxGrossWeight", format: "number" }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge" }, ], formFields: [ { name: "containerNumber", label: "Container number", type: "text", required: true }, { name: "containerTypeId", label: "Container type", type: "select", required: true, dynamicOptions: "containerTypes" }, { name: "wagonId", label: "Wagon", type: "select", dynamicOptions: "wagons", noneOption: true }, { name: "position", label: "Position", type: "number" }, { name: "tareWeight", label: "Tare weight", type: "number", required: true }, { name: "maxGrossWeight", label: "Max gross weight", type: "number", required: true }, { name: "sealNumber", label: "Seal number", type: "text" }, { name: "status", label: "Status", type: "text" }, ], emptyValues: { containerNumber: "", containerTypeId: "", wagonId: "", position: "", tareWeight: 0, maxGrossWeight: 0, sealNumber: "", status: "AVAILABLE", }, }, { slug: "cargoes", label: "Cargoes", subtitle: "Manage cargo records linked to containers", basePath: "/dashboard/cargoes", addLabel: "Add Cargo", entityLabel: "Cargo", searchPlaceholder: "Search cargoes…", supportsSearch: true, removeAction: "delete", cardTitleKey: "cargoReference", cardSubtitleKey: "status", searchKeys: ["cargoReference", "description", "containerId", "status"], columns: [ { id: "cargoReference", header: "Reference", accessorKey: "cargoReference", format: "code" }, { id: "cargoTypeId", header: "Cargo type", accessorKey: "cargoTypeId", format: "entityLabel" }, { id: "containerId", header: "Container", accessorKey: "containerId", format: "entityLabel" }, { id: "quantity", header: "Quantity", accessorKey: "quantity", format: "number" }, { id: "weight", header: "Weight", accessorKey: "weight", format: "number" }, { id: "status", header: "Status", accessorKey: "status", format: "statusBadge" }, ], formFields: [ { name: "cargoReference", label: "Cargo reference", type: "text", required: true }, { name: "shipmentId", label: "Shipment ID", type: "text", required: true }, { name: "containerId", label: "Container", type: "select", required: true, dynamicOptions: "containers" }, { name: "cargoTypeId", label: "Cargo type", type: "select", dynamicOptions: "cargoTypes", noneOption: true }, { name: "description", label: "Description", type: "textarea" }, { name: "quantity", label: "Quantity", type: "number", required: true }, { name: "weight", label: "Weight", type: "number", required: true }, { name: "volume", label: "Volume", type: "number" }, { name: "status", label: "Status", type: "text" }, ], emptyValues: { cargoReference: "", shipmentId: "", containerId: "", cargoTypeId: "", description: "", quantity: 0, weight: 0, volume: "", status: "PENDING", }, }, vehiclesConfig, driversConfig, ]; export const getFleetResource = (slug: string): FleetResourceConfig | undefined => FLEET_RESOURCES.find((resource) => resource.slug === slug); export const getFleetSlugFromPath = (pathname: string): FleetResourceSlug | undefined => { const normalized = pathname.toLowerCase(); return FLEET_RESOURCES.find((resource) => normalized === resource.basePath.toLowerCase())?.slug; }; export const getFleetRouteMeta = () => FLEET_RESOURCES.map((resource) => ({ prefix: resource.basePath, meta: { title: resource.label, subtitle: resource.subtitle }, }));