Replace wagon/locomotive readiness with yard tracking, assign unassigned bookings from origin-yard fleet, standardize rates on USD with CBE ETB conversion, and update fleet/scheduling UI

This commit is contained in:
marshal
2026-06-14 01:32:39 +03:00
parent b73bf2154e
commit 87b0ce6339
45 changed files with 1249 additions and 520 deletions

View File

@@ -16,6 +16,7 @@ import { useWagonTypes } from "@/hooks/use-wagon-types";
import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet";
import { useContainers } from "@/hooks/useContainers";
import { useToast } from "@/hooks/use-toast";
import { useRouteYards } from "@/hooks/useRoutes";
import { useWagons } from "@/hooks/useWagons";
import type { FleetListFilters } from "@/services/fleet/fleet.service";
import {
@@ -49,12 +50,12 @@ const FleetResourcePage = () => {
if (slug !== "wagons" && slug !== "locomotives") return undefined;
const filters: FleetListFilters = {};
const status = listFilterValues.status;
const readiness = listFilterValues.readiness;
const currentYardId = listFilterValues.currentYardId;
if (status && status !== "ALL") {
filters.status = status as FleetListFilters["status"];
(filters as { status?: string }).status = status;
}
if (readiness && readiness !== "ALL") {
filters.readiness = readiness as FleetListFilters["readiness"];
if (currentYardId && currentYardId !== "ALL") {
filters.currentYardId = currentYardId;
}
if (slug === "wagons" && search.trim()) {
filters.search = search.trim();
@@ -70,6 +71,7 @@ const FleetResourcePage = () => {
const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useCargoTypes();
const { data: wagons = [], isLoading: wagonsLoading } = useWagons();
const { data: containers = [], isLoading: containersLoading } = useContainers();
const { data: yards = [], isLoading: yardsLoading } = useRouteYards();
useEffect(() => {
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
@@ -98,18 +100,6 @@ const FleetResourcePage = () => {
];
}, [allRows, hasStatusColumn, usesServerListFilters]);
const listFilterSelects = useMemo(() => {
if (!config?.listFilters?.length) return null;
return config.listFilters.map((filter) => ({
...filter,
value: listFilterValues[filter.key] ?? "ALL",
data: [
{ value: "ALL", label: filter.allLabel ?? `All ${filter.label.toLowerCase()}` },
...filter.options.map((opt) => ({ value: opt.value, label: opt.label })),
],
}));
}, [config?.listFilters, listFilterValues]);
const dynamicOptions = useMemo(() => {
const wagonTypeOpts = (wagonTypes as Array<{ id: string; code: string; name?: string }>).map(
(t) => ({ value: t.id, label: `${t.code}${t.name ? ` - ${t.name}` : ""}` }),
@@ -128,14 +118,41 @@ const FleetResourcePage = () => {
(c) => ({ value: c.id, label: c.containerNumber }),
);
const yardOpts = (yards as Array<{ id: string; label?: string; code?: string }>).map(
(y) => ({ value: y.id, label: y.label ?? y.code ?? y.id }),
);
registerFleetOptionLabels("currentYardId", yardOpts);
return {
wagonTypes: wagonTypeOpts,
containerTypes: containerTypeOpts,
cargoTypes: [{ label: "None", value: FLEET_SELECT_NONE }, ...cargoTypeOpts],
wagons: [{ label: "Unassigned", value: FLEET_SELECT_NONE }, ...wagonOpts],
containers: containerOpts,
yards: yardOpts,
};
}, [wagonTypes, containerTypes, cargoTypes, wagons, containers]);
}, [wagonTypes, containerTypes, cargoTypes, wagons, containers, yards]);
const listFilterSelects = useMemo(() => {
if (!config?.listFilters?.length) return null;
return config.listFilters.map((filter) => {
const dynamicOpts = filter.dynamicOptions
? (dynamicOptions[filter.dynamicOptions] ?? [])
: [];
const staticOpts =
filter.options?.map((opt) => ({ value: opt.value, label: opt.label })) ?? [];
const opts = filter.dynamicOptions ? dynamicOpts : staticOpts;
return {
...filter,
value: listFilterValues[filter.key] ?? "ALL",
data: [
{ value: "ALL", label: filter.allLabel ?? `All ${filter.label.toLowerCase()}` },
...opts,
],
};
});
}, [config?.listFilters, listFilterValues, dynamicOptions]);
useEffect(() => {
registerFleetOptionLabels("wagonTypeId", dynamicOptions.wagonTypes);
@@ -146,6 +163,7 @@ const FleetResourcePage = () => {
);
registerFleetOptionLabels("wagonId", dynamicOptions.wagons);
registerFleetOptionLabels("containerId", dynamicOptions.containers);
registerFleetOptionLabels("currentYardId", dynamicOptions.yards);
}, [dynamicOptions]);
const formFields = useMemo((): FleetFormFieldDef[] => {
@@ -158,7 +176,12 @@ const FleetResourcePage = () => {
}, [config, dynamicOptions]);
const selectOptionsLoading =
wagonTypesLoading || containerTypesLoading || cargoTypesLoading || wagonsLoading || containersLoading;
wagonTypesLoading ||
containerTypesLoading ||
cargoTypesLoading ||
wagonsLoading ||
containersLoading ||
yardsLoading;
const filteredRows = useMemo(() => {
if (!config) return allRows;
@@ -222,7 +245,7 @@ const FleetResourcePage = () => {
});
return base;
}, [config]);
}, [config, dynamicOptions.yards]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";

View File

@@ -1,5 +1,4 @@
import { Freight } from "@edr/types";
import type { ColumnFormat, FormFieldDef } from "@/pages/ruleEngine/config/resources";
export type FleetResourceSlug =
@@ -20,7 +19,8 @@ export type FleetDynamicOptions =
| "containerTypes"
| "cargoTypes"
| "wagons"
| "containers";
| "containers"
| "yards";
export interface FleetResourceColumn {
id: string;
@@ -35,10 +35,11 @@ export interface FleetFormFieldDef extends FormFieldDef {
}
export interface FleetListFilterDef {
key: "status" | "readiness" | "wagonTypeId" | "trainId";
key: "status" | "currentYardId" | "wagonTypeId" | "trainId";
label: string;
options: Array<{ value: string; label: string }>;
options?: Array<{ value: string; label: string }>;
allLabel?: string;
dynamicOptions?: FleetDynamicOptions;
}
export interface FleetResourceConfig {
@@ -93,10 +94,6 @@ const WAGON_STATUS_OPTIONS = [
{ label: "Retired", value: Freight.WagonStatus.Retired },
];
const WAGON_READINESS_OPTIONS = [
{ label: "Import ready", value: Freight.WagonReadiness.ImportReady },
{ label: "Export ready", value: Freight.WagonReadiness.ExportReady },
];
export const FLEET_RESOURCES: FleetResourceConfig[] = [
{
@@ -122,19 +119,19 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
options: LOCOMOTIVE_STATUS_OPTIONS,
},
{
key: "readiness",
label: "Readiness",
allLabel: "All readiness",
options: WAGON_READINESS_OPTIONS,
key: "currentYardId",
label: "Current Yard",
allLabel: "All yards",
dynamicOptions: "yards",
},
],
cardSubtitleKey: "readiness",
searchKeys: ["code", "name", "locomotiveType", "status", "readiness"],
cardSubtitleKey: "currentYard",
searchKeys: ["code", "name", "locomotiveType", "status", "currentYardId"],
columns: [
{ id: "code", header: "Code", accessorKey: "code", format: "code" },
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "locomotiveType", header: "Type", accessorKey: "locomotiveType" },
{ id: "readiness", header: "Readiness", accessorKey: "readiness", format: "statusBadge" },
{ id: "currentYard", header: "Current Yard", accessorKey: "currentYard", format: "entityLabel" },
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge" },
{ id: "maxPullWeightTons", header: "Max pull (tons)", accessorKey: "maxPullWeightTons", format: "number" },
{ id: "maxTrainLengthMeters", header: "Max length (m)", accessorKey: "maxTrainLengthMeters", format: "number" },
@@ -144,7 +141,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
{ name: "name", label: "Name", type: "text" },
{ name: "locomotiveType", label: "Locomotive type", type: "select", required: true, options: LOCOMOTIVE_TYPE_OPTIONS },
{ name: "status", label: "Status", type: "select", required: true, options: LOCOMOTIVE_STATUS_OPTIONS },
{ name: "readiness", label: "Readiness", type: "select", required: true, options: WAGON_READINESS_OPTIONS },
{ name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" },
{ name: "maxPullWeightTons", label: "Max pulling weight (tons)", type: "number", required: true },
{ name: "maxTrainLengthMeters", label: "Max train length (meters)", type: "number", required: true },
{ name: "powerKw", label: "Power (kW)", type: "number" },
@@ -156,7 +153,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
name: "",
locomotiveType: "DIESEL",
status: "AVAILABLE",
readiness: Freight.WagonReadiness.ImportReady,
currentYardId: "",
maxPullWeightTons: 0,
maxTrainLengthMeters: 760,
powerKw: "",
@@ -225,20 +222,20 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
options: WAGON_STATUS_OPTIONS,
},
{
key: "readiness",
label: "Readiness",
allLabel: "All readiness",
options: WAGON_READINESS_OPTIONS,
key: "currentYardId",
label: "Current Yard",
allLabel: "All yards",
dynamicOptions: "yards",
},
],
cardTitleKey: "wagonNumber",
cardSubtitleKey: "readiness",
searchKeys: ["wagonNumber", "wagonTypeId", "trainId", "status", "readiness"],
cardSubtitleKey: "currentYard",
searchKeys: ["wagonNumber", "wagonTypeId", "trainId", "status", "currentYardId"],
columns: [
{ id: "wagonNumber", header: "Number", accessorKey: "wagonNumber", format: "code" },
{ id: "wagonTypeId", header: "Type", accessorKey: "wagonTypeId", format: "entityLabel" },
{ id: "maxPayloadWeight", header: "Max payload", accessorKey: "maxPayloadWeight", format: "number" },
{ id: "readiness", header: "Readiness", accessorKey: "readiness", format: "statusBadge" },
{ id: "currentYard", header: "Current Yard", accessorKey: "currentYard", format: "entityLabel" },
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge" },
],
formFields: [
@@ -246,7 +243,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
{ name: "wagonTypeId", label: "Wagon type", type: "select", required: true, dynamicOptions: "wagonTypes" },
{ name: "tareWeight", label: "Tare weight", type: "number", required: true },
{ name: "maxPayloadWeight", label: "Max payload weight", type: "number", required: true },
{ name: "readiness", label: "Readiness", type: "select", required: true, options: WAGON_READINESS_OPTIONS },
{ name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" },
{ name: "status", label: "Status", type: "select", required: true, options: WAGON_STATUS_OPTIONS },
{ name: "notes", label: "Notes", type: "textarea" },
],
@@ -255,7 +252,7 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
wagonTypeId: "",
tareWeight: 0,
maxPayloadWeight: 0,
readiness: Freight.WagonReadiness.ImportReady,
currentYardId: "",
status: Freight.WagonStatus.Available,
notes: "",
},

View File

@@ -296,9 +296,12 @@ const RuleEngineResourcePage = () => {
};
const handleFormSubmit = (values: Record<string, unknown>) => {
const payload =
config.slug === "rates" ? { ...values, currency: "USD" } : values;
if (editing?.id) {
update.mutate(
{ id: editing.id, payload: values },
{ id: editing.id, payload },
{
onSuccess: () => {
setFormOpen(false);
@@ -307,7 +310,7 @@ const RuleEngineResourcePage = () => {
},
);
} else {
create.mutate(values, {
create.mutate(payload, {
onSuccess: () => {
setFormOpen(false);
setEditing(null);

View File

@@ -110,10 +110,7 @@ const RATE_UNITS = ["PER_WAGON", "PER_TON", "PER_CONTAINER", "PER_KM", "FLAT"].m
value: v,
}));
const CURRENCIES = [
{ label: "ETB", value: "ETB" },
{ label: "USD", value: "USD" },
];
const CURRENCIES = [{ label: "USD", value: "USD" }];
const codeColumn = (key: string, header = "Code"): ResourceColumn => ({
id: key,
@@ -430,7 +427,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
type: "select",
options: TRADE_DIRECTIONS,
},
{ name: "currency", label: "Currency", type: "select", required: true, options: CURRENCIES },
{ name: "rateValue", label: "Rate value", type: "number", required: true },
{ name: "rateUnit", label: "Rate unit", type: "select", required: true, options: RATE_UNITS },
{ name: "effectiveFrom", label: "Effective from", type: "date", required: true },

View File

@@ -90,7 +90,7 @@ export default function TrainScheduleTrackPage() {
onSuccess: () => {
toast({
title: isFinal
? "Train arrived — assets freed, readiness flipped"
? "Train arrived — assets freed, moved to destination yard"
: "Checkpoint logged",
});
},

View File

@@ -785,11 +785,11 @@ export default function TrainScheduleV2DetailPage() {
label="Locomotive"
value={schedule.trainSet?.locomotive?.code ?? "—"}
hint={
schedule.trainSet?.locomotive?.readiness === "EXPORT_READY"
? "Export-ready"
: schedule.trainSet?.locomotive?.readiness === "IMPORT_READY"
? "Import-ready"
: undefined
schedule.trainSet?.locomotive?.currentYardId
? schedule.trainSet.locomotive.currentYardId === schedule.originStation?.id
? `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? "origin yard"}`
: "Not at schedule origin yard"
: "No current yard set"
}
accent="#F2A516"
graph="area"

View File

@@ -89,15 +89,13 @@ export default function TrainScheduleV2ListPage() {
const selectedRoute = activeRoutes.find((r) => r.id === routeId);
const locomotiveReadinessHint = useMemo(() => {
const locomotiveYardHint = useMemo(() => {
if (!selectedRoute) return "Select a route first";
const origin = selectedRoute.originYard?.country?.trim();
const dest = selectedRoute.destinationYard?.country?.trim();
if (origin === "Djibouti") return "Import corridor — import-ready locomotives only";
if (dest === "Djibouti" && origin !== "Djibouti") {
return "Export corridor — export-ready locomotives only";
}
return "Domestic corridor — any readiness";
const originLabel =
selectedRoute.originYard?.label ??
selectedRoute.originYard?.code ??
"the route origin yard";
return `Only locomotives currently at ${originLabel} are shown`;
}, [selectedRoute]);
useEffect(() => {
@@ -525,7 +523,7 @@ export default function TrainScheduleV2ListPage() {
/>
{routeId ? (
<Text size="xs" c="dimmed">
{locomotiveReadinessHint}
{locomotiveYardHint}
</Text>
) : null}
<TextInput
@@ -542,9 +540,7 @@ export default function TrainScheduleV2ListPage() {
placeholder={routeId ? "Select locomotive" : "Select a route first"}
data={(locomotivesQuery.data ?? []).map((l) => ({
value: l.id,
label: `${l.code}${l.name ? `${l.name}` : ""} · ${
l.readiness === "EXPORT_READY" ? "Export-ready" : "Import-ready"
}`,
label: `${l.code}${l.name ? `${l.name}` : ""}`,
}))}
value={locomotiveId || null}
onChange={(v) => setLocomotiveId(v ?? "")}