diff --git a/apps/edr-freight-api/src/modules/trains/train-builder.service.ts b/apps/edr-freight-api/src/modules/trains/train-builder.service.ts index fbb5301fb..9da3cecf0 100644 --- a/apps/edr-freight-api/src/modules/trains/train-builder.service.ts +++ b/apps/edr-freight-api/src/modules/trains/train-builder.service.ts @@ -510,6 +510,8 @@ export class TrainBuilderService { trainId: null, sequenceNumber: null, status: WagonStatus.Available, + importTrainNumber: null, + exportTrainNumber: null, }); await this.resequenceWagons(manager, train.id); await this.syncLiveScheduleAfterConsistChange( @@ -544,6 +546,8 @@ export class TrainBuilderService { trainId: null, sequenceNumber: null, status: WagonStatus.Maintenance, + importTrainNumber: null, + exportTrainNumber: null, }); // Audit row: which train it came off and when. The wagon does not change // yard here, so from/to are the same — the ledger is the wagon's history @@ -761,7 +765,13 @@ export class TrainBuilderService { .getRepository(Wagon) .update( { trainId: train.id }, - { trainId: null, sequenceNumber: null, status: WagonStatus.Available }, + { + trainId: null, + sequenceNumber: null, + status: WagonStatus.Available, + importTrainNumber: null, + exportTrainNumber: null, + }, ); await manager.getRepository(TrainLocomotive).delete({ trainId: train.id }); await manager.getRepository(Train).remove(train); @@ -1021,6 +1031,10 @@ export class TrainBuilderService { trainId: train.id, sequenceNumber: sequence, status: WagonStatus.Assigned, + // Wagon inherits the train's run numbers on coupling — no per-wagon + // number entry, they ride whatever numbers the train was built with. + importTrainNumber: train.importTrainNumber, + exportTrainNumber: train.exportTrainNumber, }); } return toAttach; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx index 6bea10a8c..06d9a20ab 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx @@ -102,6 +102,7 @@ const FleetResourcePage = () => { const currentYardId = listFilterValues.currentYardId; const availability = listFilterValues.availability; const trainNumber = listFilterValues.trainNumber; + const trainId = listFilterValues.trainId; if (status && status !== "ALL") { (filters as { status?: string }).status = status; } @@ -114,6 +115,9 @@ const FleetResourcePage = () => { if (trainNumber && trainNumber !== "ALL") { (filters as { trainNumber?: string }).trainNumber = trainNumber; } + if (trainId && trainId !== "ALL") { + filters.trainId = trainId; + } // Wagons only: narrow the fleet to one wagon type (the API filters on it). const wagonTypeId = listFilterValues.wagonTypeId; if (wagonTypeId && wagonTypeId !== "ALL") { @@ -191,6 +195,11 @@ const FleetResourcePage = () => { const { data: drivers = [] } = useQuery( api.fleet.list.queryOptions({ input: { slug: "drivers" } }), ); + // Wagons-only: "Train" list filter needs every train's code to pick from. + const { data: trains = [], isLoading: trainsLoading } = useQuery({ + ...api.trains.list.queryOptions(), + enabled: slug === "wagons", + }); useEffect(() => { setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize })); @@ -247,6 +256,9 @@ const FleetResourcePage = () => { const yardOpts = (yards as Array<{ id: string; label?: string; code?: string }>).map( (y) => ({ value: y.id, label: y.label ?? y.code ?? y.id }), ); + const trainOpts = (trains as Array<{ id: string; code: string; trainName?: string | null }>).map( + (t) => ({ value: t.id, label: t.trainName ? `${t.code} - ${t.trainName}` : t.code }), + ); // Carries capacity + trailer configuration so picking a truck type can // pre-fill the vehicle's capacity and drop the trailer plate on a rigid type. @@ -274,8 +286,9 @@ const FleetResourcePage = () => { wagons: [{ label: "Unassigned", value: FLEET_SELECT_NONE }, ...wagonOpts], containers: containerOpts, yards: yardOpts, + trains: trainOpts, }; - }, [wagonTypes, containerTypes, cargoTypes, truckTypes, wagons, containers, yards]); + }, [wagonTypes, containerTypes, cargoTypes, truckTypes, wagons, containers, yards, trains]); const listFilterSelects = useMemo(() => { if (!config?.listFilters?.length) return null; @@ -327,7 +340,8 @@ const FleetResourcePage = () => { truckTypesLoading || wagonsLoading || containersLoading || - yardsLoading; + yardsLoading || + trainsLoading; const filteredRows = useMemo(() => { if (!config) return allRows; 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 9ba826e6f..11ca236be 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 @@ -33,7 +33,8 @@ export type FleetDynamicOptions = | "truckTypes" | "wagons" | "containers" - | "yards"; + | "yards" + | "trains"; /** * A dynamic select option that can carry the record it came from. Picking a @@ -324,6 +325,12 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [ allLabel: "All trains", options: TRAIN_RUN_FILTER_OPTIONS, }, + { + key: "trainId", + label: "Train", + allLabel: "All trains", + dynamicOptions: "trains", + }, ], cardTitleKey: "wagonNumber", cardSubtitleKey: "currentYard", diff --git a/apps/edr-freight-web/backoffice/src/pages/wagons/TransferHistoryPanel.tsx b/apps/edr-freight-web/backoffice/src/pages/wagons/TransferHistoryPanel.tsx index dff9cdaa3..807745b73 100644 --- a/apps/edr-freight-web/backoffice/src/pages/wagons/TransferHistoryPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/wagons/TransferHistoryPanel.tsx @@ -35,6 +35,7 @@ import { STATUS_META, TransferProgress, TransferStatusBadge, + stripHtmlToText, wagonTypeLabel, yardLabel, } from "./wagon-transfer-ui"; @@ -119,9 +120,9 @@ function RequestItem({ request }: { request: WagonTransferRequest }) { {wagonTypeLabel(request.wagonType)} - {request.reason ? ( + {stripHtmlToText(request.reason) ? ( - {request.reason} + {stripHtmlToText(request.reason)} ) : null} diff --git a/apps/edr-freight-web/backoffice/src/pages/wagons/WagonTransfersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/wagons/WagonTransfersPage.tsx index 95705e964..1a08b5c54 100644 --- a/apps/edr-freight-web/backoffice/src/pages/wagons/WagonTransfersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/wagons/WagonTransfersPage.tsx @@ -10,6 +10,7 @@ import { Tabs, Text, TextInput, + UnstyledButton, } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; import { useQuery } from "@tanstack/react-query"; @@ -31,6 +32,7 @@ import { useMutation } from "@tanstack/react-query"; import { useAuth } from "@/auth/useAuth"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; +import { sanitizeHtml } from "@/shared/lib/sanitize"; import { api } from "@/services/api"; import type { TransferRequestListFilter, @@ -55,6 +57,7 @@ import { fmtDateTime, isOpenRequest, outstandingOn, + stripHtmlToText, wagonTypeLabel, yardLabel, } from "./wagon-transfer-ui"; @@ -108,6 +111,9 @@ export default function WagonTransfersPage() { const [closingShort, setClosingShort] = useState( null, ); + const [viewingReason, setViewingReason] = useState( + null, + ); const filter: TransferRequestListFilter = useMemo( () => ({ @@ -197,11 +203,29 @@ export default function WagonTransfersPage() { { id: "reason", header: () => Reason, - cell: ({ row }) => ( - - {row.original.reason || "—"} - - ), + cell: ({ row }) => { + const text = stripHtmlToText(row.original.reason); + return text ? ( + setViewingReason(row.original)} + data-stop-row-click + > + + {text} + + + ) : ( + + — + + ); + }, }, { id: "filed", @@ -522,6 +546,33 @@ export default function WagonTransfersPage() { )} + setViewingReason(null)} + radius="md" + title="Reason" + > + {!viewingReason ? null : ( + + + {yardLabel(viewingReason.fromYard)}{" "} + {" "} + {yardLabel(viewingReason.toYard)} ·{" "} + {wagonTypeLabel(viewingReason.wagonType)} ·{" "} + {viewingReason.quantity} wagon(s) + + + + )} + ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/wagons/wagon-transfer-ui.tsx b/apps/edr-freight-web/backoffice/src/pages/wagons/wagon-transfer-ui.tsx index e60480590..ea73a95d2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/wagons/wagon-transfer-ui.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/wagons/wagon-transfer-ui.tsx @@ -3,6 +3,16 @@ import { Badge, Box, Group, Progress, Text, Tooltip } from "@mantine/core"; import type { WagonTransferRequest } from "@/services/wagon.service"; +/** Reason/note fields come from a rich-text editor and store HTML — this + * gives a plain-text preview for list/table contexts (full formatting is + * shown via `sanitizeHtml` + `dangerouslySetInnerHTML` where there's room). */ +export const stripHtmlToText = (html?: string | null): string => + (html ?? "") + .replace(/<[^>]*>/g, " ") + .replace(/ /g, " ") + .replace(/\s+/g, " ") + .trim(); + export const yardLabel = (y?: { label?: string; code?: string } | null) => y?.label || y?.code || "—";