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 || "—";