mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
Merge pull request #757 from Tria-plc/freight/feature/user_management_UI
Freight/feature/user management UI
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-wagon EXPORT/IMPORT run numbers, editable from the wagon form.
|
||||||
|
*
|
||||||
|
* Nullable with no default: a wagon is not on a run until an operator says so.
|
||||||
|
* Mirrors the width of trains.export_train_number / trains.import_train_number
|
||||||
|
* (varchar 20) so the two stay comparable.
|
||||||
|
*/
|
||||||
|
export class AddWagonTrainNumbers2270000000000 implements MigrationInterface {
|
||||||
|
name = 'AddWagonTrainNumbers2270000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
ADD COLUMN IF NOT EXISTS export_train_number varchar(20),
|
||||||
|
ADD COLUMN IF NOT EXISTS import_train_number varchar(20);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.wagons
|
||||||
|
DROP COLUMN IF EXISTS export_train_number,
|
||||||
|
DROP COLUMN IF EXISTS import_train_number;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,16 @@ export class CreateWagonDto {
|
|||||||
// Tare weight and payload capacity are not accepted here: they belong to the
|
// Tare weight and payload capacity are not accepted here: they belong to the
|
||||||
// wagon type and are resolved through wagonTypeId.
|
// wagon type and are resolved through wagonTypeId.
|
||||||
|
|
||||||
|
/** EXPORT run number — odd, Ethiopia → Djibouti (e.g. 8001). */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
exportTrainNumber?: string;
|
||||||
|
|
||||||
|
/** IMPORT run number — even, Djibouti → Ethiopia (e.g. 8002). */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
importTrainNumber?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(WagonStatus)
|
@IsEnum(WagonStatus)
|
||||||
status?: WagonStatus;
|
status?: WagonStatus;
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export class Wagon extends BaseEntity {
|
|||||||
// Tare weight and payload capacity are properties of the wagon TYPE — read them
|
// Tare weight and payload capacity are properties of the wagon TYPE — read them
|
||||||
// through `wagonType`, never off the individual wagon.
|
// through `wagonType`, never off the individual wagon.
|
||||||
|
|
||||||
|
/** EXPORT run number — odd, Ethiopia → Djibouti (e.g. 8001). Null until set. */
|
||||||
|
@Column({ name: 'export_train_number', type: 'varchar', length: 20, nullable: true })
|
||||||
|
exportTrainNumber!: string | null;
|
||||||
|
|
||||||
|
/** IMPORT run number — even, Djibouti → Ethiopia (e.g. 8002). Null until set. */
|
||||||
|
@Column({ name: 'import_train_number', type: 'varchar', length: 20, nullable: true })
|
||||||
|
importTrainNumber!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
|
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
|
||||||
status!: WagonStatusType;
|
status!: WagonStatusType;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export class WagonsService {
|
|||||||
if (dto.trainId === undefined) wagon.trainId = null;
|
if (dto.trainId === undefined) wagon.trainId = null;
|
||||||
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
|
if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null;
|
||||||
if (dto.currentYardId === undefined) wagon.currentYardId = null;
|
if (dto.currentYardId === undefined) wagon.currentYardId = null;
|
||||||
|
if (dto.exportTrainNumber === undefined) wagon.exportTrainNumber = null;
|
||||||
|
if (dto.importTrainNumber === undefined) wagon.importTrainNumber = null;
|
||||||
return this.wagonRepo.save(wagon);
|
return this.wagonRepo.save(wagon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,8 +271,14 @@ const FleetFormDialog = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!validate()) return;
|
if (!validate()) return;
|
||||||
|
// Derived fields are never edited, so form state for them can be stale (or
|
||||||
|
// seeded from the record) — recompute before building the payload.
|
||||||
|
const submitted: Record<string, unknown> = { ...values };
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (field.derivedValue) submitted[field.name] = field.derivedValue(values);
|
||||||
|
});
|
||||||
const payload = Object.fromEntries(
|
const payload = Object.fromEntries(
|
||||||
Object.entries(values)
|
Object.entries(submitted)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (value === FLEET_SELECT_NONE || value === "")
|
if (value === FLEET_SELECT_NONE || value === "")
|
||||||
return [key, undefined];
|
return [key, undefined];
|
||||||
@@ -294,6 +300,23 @@ const FleetFormDialog = ({
|
|||||||
// only by verification and never hand-edited.
|
// only by verification and never hand-edited.
|
||||||
const isDisabled = Boolean(field.disabled || field.faydaLocked);
|
const isDisabled = Boolean(field.disabled || field.faydaLocked);
|
||||||
|
|
||||||
|
// Computed from other fields (e.g. the import run implied by the export
|
||||||
|
// run) — read-only, and recomputed here rather than read from form state.
|
||||||
|
if (field.derivedValue) {
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
key={field.name}
|
||||||
|
label={field.label}
|
||||||
|
description={field.description}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
value={field.derivedValue(values)}
|
||||||
|
readOnly
|
||||||
|
variant="filled"
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === "radio") {
|
if (field.type === "radio") {
|
||||||
return (
|
return (
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
@@ -319,6 +342,8 @@ const FleetFormDialog = ({
|
|||||||
<Select
|
<Select
|
||||||
key={field.name}
|
key={field.name}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
|
description={field.description}
|
||||||
|
placeholder={field.placeholder}
|
||||||
data={field.options ?? []}
|
data={field.options ?? []}
|
||||||
value={
|
value={
|
||||||
value == null || value === ""
|
value == null || value === ""
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export interface FleetResourceColumn {
|
|||||||
export interface FleetFormFieldDef extends FormFieldDef {
|
export interface FleetFormFieldDef extends FormFieldDef {
|
||||||
dynamicOptions?: FleetDynamicOptions;
|
dynamicOptions?: FleetDynamicOptions;
|
||||||
noneOption?: boolean;
|
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 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.
|
||||||
@@ -116,6 +123,32 @@ 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. Listed explicitly rather
|
||||||
|
* than computed as export+1, so a pair that ever breaks that convention stays
|
||||||
|
* correct here.
|
||||||
|
*/
|
||||||
|
const TRAIN_RUN_PAIRS: Record<string, string> = {
|
||||||
|
"8001": "8002",
|
||||||
|
"81001": "81002",
|
||||||
|
"82001": "82002",
|
||||||
|
"83001": "83002",
|
||||||
|
"84001": "84002",
|
||||||
|
"85001": "85002",
|
||||||
|
"86001": "86002",
|
||||||
|
"87001": "87002",
|
||||||
|
"88001": "88002",
|
||||||
|
"8901": "8902",
|
||||||
|
"9001": "9002",
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXPORT_TRAIN_OPTIONS = Object.keys(TRAIN_RUN_PAIRS).map((run) => ({
|
||||||
|
label: run,
|
||||||
|
value: run,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
||||||
@@ -271,6 +304,26 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
|||||||
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge" },
|
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge" },
|
||||||
],
|
],
|
||||||
formFields: [
|
formFields: [
|
||||||
|
// Run numbers are optional — a wagon sits in the fleet unassigned to any
|
||||||
|
// run until an operator picks an export run. The import run is fixed by
|
||||||
|
// that choice, so it is derived rather than typed.
|
||||||
|
{
|
||||||
|
name: "exportTrainNumber",
|
||||||
|
label: "Export train number",
|
||||||
|
type: "select",
|
||||||
|
description: "Odd — Ethiopia → Djibouti runs",
|
||||||
|
placeholder: "e.g. 8001",
|
||||||
|
options: EXPORT_TRAIN_OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "importTrainNumber",
|
||||||
|
label: "Import train number",
|
||||||
|
type: "text",
|
||||||
|
description: "Even — Djibouti → Ethiopia runs",
|
||||||
|
placeholder: "e.g. 8002",
|
||||||
|
derivedValue: (values) =>
|
||||||
|
TRAIN_RUN_PAIRS[String(values.exportTrainNumber ?? "")] ?? "",
|
||||||
|
},
|
||||||
{ 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" },
|
||||||
{ name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" },
|
{ name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" },
|
||||||
@@ -278,6 +331,8 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
|||||||
{ name: "notes", label: "Notes", type: "textarea" },
|
{ name: "notes", label: "Notes", type: "textarea" },
|
||||||
],
|
],
|
||||||
emptyValues: {
|
emptyValues: {
|
||||||
|
exportTrainNumber: "",
|
||||||
|
importTrainNumber: "",
|
||||||
wagonNumber: "",
|
wagonNumber: "",
|
||||||
wagonTypeId: "",
|
wagonTypeId: "",
|
||||||
currentYardId: "",
|
currentYardId: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user