mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
Merge pull request #760 from Tria-plc/freight/feature/user_management_UI
Freight/feature/user management UI
This commit is contained in:
@@ -263,6 +263,14 @@ const FleetFormDialog = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
|
// Emptying one of these means "unset the column", so it submits an explicit
|
||||||
|
// null instead of being dropped from the payload like other empty fields.
|
||||||
|
const clearableByName = useMemo(() => {
|
||||||
|
const map: Record<string, boolean> = {};
|
||||||
|
fields.forEach((f) => (map[f.name] = Boolean(f.clearable)));
|
||||||
|
return map;
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
// Hard gate: a driver record cannot be saved until its identity is verified
|
// Hard gate: a driver record cannot be saved until its identity is verified
|
||||||
// with Fayda. Mirrored server-side in DriversService.
|
// with Fayda. Mirrored server-side in DriversService.
|
||||||
@@ -280,8 +288,8 @@ const FleetFormDialog = ({
|
|||||||
const payload = Object.fromEntries(
|
const payload = Object.fromEntries(
|
||||||
Object.entries(submitted)
|
Object.entries(submitted)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (value === FLEET_SELECT_NONE || value === "")
|
if (value === FLEET_SELECT_NONE || value === "" || value == null)
|
||||||
return [key, undefined];
|
return [key, clearableByName[key] ? null : undefined];
|
||||||
if (fieldTypeByName[key] === "number") {
|
if (fieldTypeByName[key] === "number") {
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
return [key, Number.isNaN(num) ? undefined : num];
|
return [key, Number.isNaN(num) ? undefined : num];
|
||||||
@@ -357,6 +365,7 @@ const FleetFormDialog = ({
|
|||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
searchable
|
searchable
|
||||||
|
clearable={field.clearable}
|
||||||
disabled={selectOptionsLoading || isDisabled}
|
disabled={selectOptionsLoading || isDisabled}
|
||||||
rightSection={
|
rightSection={
|
||||||
selectOptionsLoading ? (
|
selectOptionsLoading ? (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { TrainComposition } from "@/services/trainBuilder.service";
|
import type { TrainComposition } from "@/services/trainBuilder.service";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { EXPORT_TRAIN_OPTIONS, importRunFor } from "@/constants/trainRuns";
|
||||||
|
|
||||||
const parseError = (error: unknown, fallback: string) => {
|
const parseError = (error: unknown, fallback: string) => {
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
@@ -26,10 +27,6 @@ const parseError = (error: unknown, fallback: string) => {
|
|||||||
return fallback;
|
return fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run-number parity carries the trade direction: odd = export, even = import.
|
|
||||||
const isOddNumber = (value: string) => /^\d*[13579]$/.test(value.trim());
|
|
||||||
const isEvenNumber = (value: string) => /^\d*[02468]$/.test(value.trim());
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step one of the Train Builder: pick the yard it is being assembled in and
|
* Step one of the Train Builder: pick the yard it is being assembled in and
|
||||||
* couple at least two locomotives from that yard. The train code is assigned by
|
* couple at least two locomotives from that yard. The train code is assigned by
|
||||||
@@ -59,6 +56,12 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
setLocomotiveIds([]);
|
setLocomotiveIds([]);
|
||||||
}, [yardId]);
|
}, [yardId]);
|
||||||
|
|
||||||
|
// The import run is fixed by the export run, so it tracks it rather than
|
||||||
|
// being entered by hand (and clears back to empty when the export is cleared).
|
||||||
|
useEffect(() => {
|
||||||
|
setImportTrainNumber(importRunFor(exportTrainNumber));
|
||||||
|
}, [exportTrainNumber]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
setExportTrainNumber("");
|
setExportTrainNumber("");
|
||||||
@@ -78,9 +81,11 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isOddNumber(exportTrainNumber) || !isEvenNumber(importTrainNumber)) {
|
// Both numbers come from the fixed run pairs, so parity cannot be wrong —
|
||||||
|
// only "nothing picked" is reachable here.
|
||||||
|
if (!exportTrainNumber || !importTrainNumber) {
|
||||||
toast({
|
toast({
|
||||||
title: "Enter both run numbers — export must be odd (e.g. 8001), import even (e.g. 8002)",
|
title: "Pick an export train number (e.g. 8001) — the import run follows it",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -133,31 +138,24 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<TextInput
|
<Select
|
||||||
label="Export train number"
|
label="Export train number"
|
||||||
description="Odd — Ethiopia → Djibouti runs"
|
description="Odd — Ethiopia → Djibouti runs"
|
||||||
placeholder="e.g. 8001"
|
placeholder="e.g. 8001"
|
||||||
value={exportTrainNumber}
|
data={EXPORT_TRAIN_OPTIONS}
|
||||||
onChange={(e) => setExportTrainNumber(e.currentTarget.value)}
|
value={exportTrainNumber || null}
|
||||||
maxLength={20}
|
onChange={(value) => setExportTrainNumber(value ?? "")}
|
||||||
error={
|
searchable
|
||||||
exportTrainNumber && !isOddNumber(exportTrainNumber)
|
clearable
|
||||||
? "Must be numeric and odd"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
{/* Fixed by the export run — derived, never typed. */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Import train number"
|
label="Import train number"
|
||||||
description="Even — Djibouti → Ethiopia runs"
|
description="Even — Djibouti → Ethiopia runs"
|
||||||
placeholder="e.g. 8002"
|
placeholder="e.g. 8002"
|
||||||
value={importTrainNumber}
|
value={importTrainNumber}
|
||||||
onChange={(e) => setImportTrainNumber(e.currentTarget.value)}
|
readOnly
|
||||||
maxLength={20}
|
variant="filled"
|
||||||
error={
|
|
||||||
importTrainNumber && !isEvenNumber(importTrainNumber)
|
|
||||||
? "Must be numeric and even"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
35
apps/edr-freight-web/backoffice/src/constants/trainRuns.ts
Normal file
35
apps/edr-freight-web/backoffice/src/constants/trainRuns.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* EDR run-number pairs, keyed by the odd EXPORT run (Ethiopia → Djibouti). The
|
||||||
|
* even IMPORT run (Djibouti → Ethiopia) is fixed by the export run, so choosing
|
||||||
|
* an export number fully determines the import one.
|
||||||
|
*
|
||||||
|
* Run numbers are always 4 digits (8401, never 84001). Pairs are listed out
|
||||||
|
* rather than computed from the 8001/+100/+1 pattern, so a run that ever breaks
|
||||||
|
* the convention stays correct here.
|
||||||
|
*
|
||||||
|
* Mirrors RUN_WAGONS/IMPORT_RUN in the API's SeedWagonRunNumbers migration —
|
||||||
|
* keep the two in sync when runs are added or retired.
|
||||||
|
*/
|
||||||
|
export const TRAIN_RUN_PAIRS: Record<string, string> = {
|
||||||
|
"8001": "8002",
|
||||||
|
"8101": "8102",
|
||||||
|
"8201": "8202",
|
||||||
|
"8301": "8302",
|
||||||
|
"8401": "8402",
|
||||||
|
"8501": "8502",
|
||||||
|
"8601": "8602",
|
||||||
|
"8701": "8702",
|
||||||
|
"8801": "8802",
|
||||||
|
"8901": "8902",
|
||||||
|
"9001": "9002",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Selectable export runs, in run order. */
|
||||||
|
export const EXPORT_TRAIN_OPTIONS = Object.keys(TRAIN_RUN_PAIRS).map((run) => ({
|
||||||
|
label: run,
|
||||||
|
value: run,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** The import run implied by an export run; empty string when unset/unknown. */
|
||||||
|
export const importRunFor = (exportRun: unknown): string =>
|
||||||
|
TRAIN_RUN_PAIRS[String(exportRun ?? "")] ?? "";
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Freight } from "@edr/types";
|
import { Freight } from "@edr/types";
|
||||||
import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources";
|
import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources";
|
||||||
|
import { EXPORT_TRAIN_OPTIONS, importRunFor } from "@/constants/trainRuns";
|
||||||
import { vehiclesConfig, VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS } from "./vehicles";
|
import { vehiclesConfig, VEHICLE_TYPE_OPTIONS, FUEL_TYPE_OPTIONS, VEHICLE_STATUS_OPTIONS } from "./vehicles";
|
||||||
import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers";
|
import { driversConfig, DRIVER_STATUS_OPTIONS } from "./drivers";
|
||||||
|
|
||||||
@@ -47,6 +48,13 @@ export interface FleetFormFieldDef extends FormFieldDef {
|
|||||||
* truth at both render and submit.
|
* truth at both render and submit.
|
||||||
*/
|
*/
|
||||||
derivedValue?: (values: Record<string, unknown>) => string;
|
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
|
* Field is owned by the Fayda identity — populated only by verification and
|
||||||
* never hand-edited. Rendered disabled in the form.
|
* never hand-edited. Rendered disabled in the form.
|
||||||
@@ -123,33 +131,6 @@ const WAGON_STATUS_OPTIONS = [
|
|||||||
{ label: "Detained", value: Freight.WagonStatus.Detained },
|
{ label: "Detained", value: Freight.WagonStatus.Detained },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* EDR run-number pairs, keyed by the odd EXPORT run (Ethiopia → Djibouti). The
|
|
||||||
* even IMPORT run (Djibouti → Ethiopia) is fixed by the export run, so choosing
|
|
||||||
* an export number fully determines the import one.
|
|
||||||
*
|
|
||||||
* Run numbers are always 4 digits (8401, never 84001). Pairs are listed out
|
|
||||||
* rather than computed from the 8001/+100/+1 pattern, so a run that ever breaks
|
|
||||||
* the convention stays correct here.
|
|
||||||
*/
|
|
||||||
const TRAIN_RUN_PAIRS: Record<string, string> = {
|
|
||||||
"8001": "8002",
|
|
||||||
"8101": "8102",
|
|
||||||
"8201": "8202",
|
|
||||||
"8301": "8302",
|
|
||||||
"8401": "8402",
|
|
||||||
"8501": "8502",
|
|
||||||
"8601": "8602",
|
|
||||||
"8701": "8702",
|
|
||||||
"8801": "8802",
|
|
||||||
"8901": "8902",
|
|
||||||
"9001": "9002",
|
|
||||||
};
|
|
||||||
|
|
||||||
const EXPORT_TRAIN_OPTIONS = Object.keys(TRAIN_RUN_PAIRS).map((run) => ({
|
|
||||||
label: run,
|
|
||||||
value: run,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -327,6 +308,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
|||||||
description: "Odd — Ethiopia → Djibouti runs",
|
description: "Odd — Ethiopia → Djibouti runs",
|
||||||
placeholder: "e.g. 8001",
|
placeholder: "e.g. 8001",
|
||||||
options: EXPORT_TRAIN_OPTIONS,
|
options: EXPORT_TRAIN_OPTIONS,
|
||||||
|
clearable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "importTrainNumber",
|
name: "importTrainNumber",
|
||||||
@@ -334,8 +316,9 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
|||||||
type: "text",
|
type: "text",
|
||||||
description: "Even — Djibouti → Ethiopia runs",
|
description: "Even — Djibouti → Ethiopia runs",
|
||||||
placeholder: "e.g. 8002",
|
placeholder: "e.g. 8002",
|
||||||
derivedValue: (values) =>
|
derivedValue: (values) => importRunFor(values.exportTrainNumber),
|
||||||
TRAIN_RUN_PAIRS[String(values.exportTrainNumber ?? "")] ?? "",
|
// Follows the export run to NULL when that is cleared.
|
||||||
|
clearable: true,
|
||||||
},
|
},
|
||||||
{ name: "wagonNumber", label: "Wagon number", type: "text", required: true },
|
{ name: "wagonNumber", label: "Wagon number", type: "text", required: true },
|
||||||
{ name: "wagonTypeId", label: "Wagon type", type: "select", required: true, dynamicOptions: "wagonTypes" },
|
{ name: "wagonTypeId", label: "Wagon type", type: "select", required: true, dynamicOptions: "wagonTypes" },
|
||||||
|
|||||||
Reference in New Issue
Block a user