From 8671b982c5807a59156f8f321598d68748edc9b1 Mon Sep 17 00:00:00 2001 From: natib21 Date: Fri, 17 Jul 2026 10:18:41 +0000 Subject: [PATCH 1/2] fix null value --- .../src/components/fleet/FleetFormDialog.tsx | 13 +++++++++++-- .../backoffice/src/pages/fleet/config/resources.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx index dfb27ed36..8686f9cdf 100644 --- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx @@ -263,6 +263,14 @@ const FleetFormDialog = ({ return map; }, [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 = {}; + fields.forEach((f) => (map[f.name] = Boolean(f.clearable))); + return map; + }, [fields]); + const handleSubmit = () => { // Hard gate: a driver record cannot be saved until its identity is verified // with Fayda. Mirrored server-side in DriversService. @@ -280,8 +288,8 @@ const FleetFormDialog = ({ const payload = Object.fromEntries( Object.entries(submitted) .map(([key, value]) => { - if (value === FLEET_SELECT_NONE || value === "") - return [key, undefined]; + if (value === FLEET_SELECT_NONE || value === "" || value == null) + return [key, clearableByName[key] ? null : undefined]; if (fieldTypeByName[key] === "number") { const num = Number(value); return [key, Number.isNaN(num) ? undefined : num]; @@ -357,6 +365,7 @@ const FleetFormDialog = ({ } error={error} searchable + clearable={field.clearable} disabled={selectOptionsLoading || isDisabled} rightSection={ selectOptionsLoading ? ( diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts index 7a473fdb1..83928e146 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts @@ -47,6 +47,13 @@ export interface FleetFormFieldDef extends FormFieldDef { * 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. @@ -327,6 +334,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [ description: "Odd — Ethiopia → Djibouti runs", placeholder: "e.g. 8001", options: EXPORT_TRAIN_OPTIONS, + clearable: true, }, { name: "importTrainNumber", @@ -336,6 +344,8 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [ placeholder: "e.g. 8002", derivedValue: (values) => 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: "wagonTypeId", label: "Wagon type", type: "select", required: true, dynamicOptions: "wagonTypes" }, From 3bb4c8fb4262202d6d557228466ba2ba56942df2 Mon Sep 17 00:00:00 2001 From: natib21 Date: Fri, 17 Jul 2026 10:29:27 +0000 Subject: [PATCH 2/2] fix build num --- .../trainBuilder/BuildTrainModal.tsx | 42 +++++++++---------- .../backoffice/src/constants/trainRuns.ts | 35 ++++++++++++++++ .../src/pages/fleet/config/resources.ts | 31 +------------- 3 files changed, 57 insertions(+), 51 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/constants/trainRuns.ts diff --git a/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx index 7f60cabe9..49dd19bcc 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from "react"; import { api } from "@/services/api"; import type { TrainComposition } from "@/services/trainBuilder.service"; import { useToast } from "@/hooks/use-toast"; +import { EXPORT_TRAIN_OPTIONS, importRunFor } from "@/constants/trainRuns"; const parseError = (error: unknown, fallback: string) => { if (isAxiosError(error)) { @@ -26,10 +27,6 @@ const parseError = (error: unknown, fallback: string) => { 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 * 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([]); }, [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(() => { if (!opened) { setExportTrainNumber(""); @@ -78,9 +81,11 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain }); 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({ - 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", }); return; @@ -133,31 +138,24 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain maxLength={100} /> - setExportTrainNumber(e.currentTarget.value)} - maxLength={20} - error={ - exportTrainNumber && !isOddNumber(exportTrainNumber) - ? "Must be numeric and odd" - : undefined - } + data={EXPORT_TRAIN_OPTIONS} + value={exportTrainNumber || null} + onChange={(value) => setExportTrainNumber(value ?? "")} + searchable + clearable /> + {/* Fixed by the export run — derived, never typed. */} setImportTrainNumber(e.currentTarget.value)} - maxLength={20} - error={ - importTrainNumber && !isEvenNumber(importTrainNumber) - ? "Must be numeric and even" - : undefined - } + readOnly + variant="filled" />