mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
462 lines
18 KiB
TypeScript
462 lines
18 KiB
TypeScript
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, unknown>) => 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<string, unknown>;
|
|
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<FleetResourceSlug, string> = {
|
|
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 },
|
|
}));
|