mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
add lashing surcharge for cargo types with hasLashing flag
add lashing surcharge for cargo types with hasLashing flag
This commit is contained in:
@@ -584,9 +584,16 @@ export default function TrainScheduleV2DetailPage() {
|
||||
}
|
||||
if (key === "wagon" && displayWagonPlan.length) {
|
||||
return (
|
||||
<Badge variant="light" color="edr-green" radius="sm">
|
||||
{displayWagonPlan.length} wagons
|
||||
</Badge>
|
||||
<Group gap="xs">
|
||||
{schedule.reverseWagonOrder ? (
|
||||
<Badge variant="light" color="orange" radius="sm">
|
||||
Reversed order
|
||||
</Badge>
|
||||
) : null}
|
||||
<Badge variant="light" color="edr-green" radius="sm">
|
||||
{displayWagonPlan.length} wagons
|
||||
</Badge>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
if (key === "container" && containerUnits.length) {
|
||||
@@ -1056,9 +1063,19 @@ export default function TrainScheduleV2DetailPage() {
|
||||
},
|
||||
{
|
||||
label: "Wagons / load",
|
||||
// Gross: cargo load + the tare of every wagon in the consist — the
|
||||
// weight the locomotive actually hauls.
|
||||
value: `${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
|
||||
schedule.trainSet?.totalWeightTons ?? 0
|
||||
Math.round(
|
||||
((schedule.trainSet?.totalWeightTons ?? 0) +
|
||||
(schedule.trainSet?.wagons ?? []).reduce(
|
||||
(sum, w) => sum + (Number(w.tareWeightTons) || 0),
|
||||
0,
|
||||
)) *
|
||||
100,
|
||||
) / 100
|
||||
}T`,
|
||||
hint: "gross · wagon tare + cargo",
|
||||
icon: Weight,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Group,
|
||||
Menu,
|
||||
Modal,
|
||||
@@ -121,6 +122,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
const [routeId, setRouteId] = useState("");
|
||||
const [scheduleDate, setScheduleDate] = useState("");
|
||||
const [trainId, setTrainId] = useState("");
|
||||
const [reverseWagonOrder, setReverseWagonOrder] = useState(false);
|
||||
// Recomputed each time the create modal opens so a long-lived tab can't keep
|
||||
// offering a stale "now" as the earliest selectable departure.
|
||||
const minScheduleDate = useMemo(
|
||||
@@ -513,10 +515,12 @@ export default function TrainScheduleV2ListPage() {
|
||||
routeId,
|
||||
scheduleDate: new Date(scheduleDate).toISOString(),
|
||||
trainId,
|
||||
reverseWagonOrder,
|
||||
},
|
||||
});
|
||||
toast({ title: "Train schedule created" });
|
||||
showScheduleWarnings(created.warnings);
|
||||
setReverseWagonOrder(false);
|
||||
setCreateOpen(false);
|
||||
navigate(`/dashboard/operations/train-scheduling-v2/${created.id}`);
|
||||
} catch (err) {
|
||||
@@ -805,6 +809,12 @@ export default function TrainScheduleV2ListPage() {
|
||||
: "Select a route first"
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Reverse wagon order"
|
||||
description="Place wagons on the train in reverse — the physically-last wagon becomes position 1. Composition and allocations are unchanged; only the order flips. Applies every time this schedule's wagon plan is built."
|
||||
checked={reverseWagonOrder}
|
||||
onChange={(e) => setReverseWagonOrder(e.currentTarget.checked)}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setCreateOpen(false)}>
|
||||
Cancel
|
||||
|
||||
@@ -7,6 +7,25 @@ import { trainSchedulingService } from "@/services/trainScheduling.service";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { TrainSchedulingGlobalRules } from "@/types/trainScheduling";
|
||||
|
||||
/**
|
||||
* Coerce an API rules object into editable form state: `numeric` columns arrive
|
||||
* as strings ("250.00") and nullable offsets arrive as null — map both to a real
|
||||
* number, or "" for blank/null so a NumberInput edits cleanly and a cleared
|
||||
* offset stays blank.
|
||||
*/
|
||||
function toFormState(
|
||||
rules: TrainSchedulingGlobalRules,
|
||||
): Partial<Record<keyof TrainSchedulingGlobalRules, number | string>> {
|
||||
const numeric: Partial<Record<keyof TrainSchedulingGlobalRules, number | string>> = {};
|
||||
for (const [key, value] of Object.entries(rules)) {
|
||||
if (key === "id") continue;
|
||||
const num = value === "" || value == null ? "" : Number(value);
|
||||
numeric[key as keyof TrainSchedulingGlobalRules] =
|
||||
typeof num === "number" && Number.isNaN(num) ? "" : num;
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
export default function TrainSchedulingGlobalRulesPage() {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -20,18 +39,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
void (async () => {
|
||||
try {
|
||||
const rules = await trainSchedulingService.getGlobalRules();
|
||||
// `numeric` columns come back from the API as strings (e.g. "250.00").
|
||||
// Coerce every field to a real number so Mantine's controlled
|
||||
// NumberInput edits cleanly (a string value fights the caret) and the
|
||||
// default can be cleared and replaced.
|
||||
const numeric: Partial<Record<keyof TrainSchedulingGlobalRules, number | string>> = {};
|
||||
for (const [key, value] of Object.entries(rules)) {
|
||||
if (key === "id") continue;
|
||||
const num = value === "" || value == null ? "" : Number(value);
|
||||
numeric[key as keyof TrainSchedulingGlobalRules] =
|
||||
typeof num === "number" && Number.isNaN(num) ? "" : num;
|
||||
}
|
||||
setForm(numeric);
|
||||
setForm(toFormState(rules));
|
||||
} catch {
|
||||
toast({ title: "Failed to load train scheduling rules", variant: "destructive" });
|
||||
} finally {
|
||||
@@ -73,10 +81,23 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
payload[key] = num;
|
||||
}
|
||||
|
||||
// Close offsets are optional: a blank box means "no offset" (window closes at
|
||||
// departure) and is sent as 0, which the API stores as null. A filled box is
|
||||
// sent as its minute value.
|
||||
const offsetFields: (keyof TrainSchedulingGlobalRules)[] = [
|
||||
"importCloseOffsetMinutes",
|
||||
"exportCloseOffsetMinutes",
|
||||
];
|
||||
for (const key of offsetFields) {
|
||||
const raw = form[key];
|
||||
const num = raw === "" || raw == null ? 0 : Number(raw);
|
||||
payload[key] = Number.isFinite(num) ? num : 0;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const updated = await trainSchedulingService.updateGlobalRules(payload);
|
||||
setForm(updated);
|
||||
setForm(toFormState(updated));
|
||||
toast({ title: "Train scheduling rules saved" });
|
||||
} catch {
|
||||
toast({ title: "Failed to save rules", variant: "destructive" });
|
||||
@@ -198,6 +219,37 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
min={1}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card maw={720} mt="md">
|
||||
<Stack gap="md">
|
||||
<PageHeader
|
||||
title="Booking close offset"
|
||||
subtitle="How long before departure a schedule stops accepting bookings. e.g. a 3-hour import offset closes a 17:00 departure's window at 14:00; a 1-day export offset closes a Jul-10 16:00 departure at Jul-9 16:00. Leave blank to close exactly at departure. Set separately for import and export."
|
||||
/>
|
||||
<DurationField
|
||||
label="Import close offset"
|
||||
description="Import/domestic booking windows close this long before departure (blank = at departure)"
|
||||
value={form.importCloseOffsetMinutes ?? ""}
|
||||
nativeUnit="minutes"
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, importCloseOffsetMinutes: value }))
|
||||
}
|
||||
min={0}
|
||||
disabled={loading}
|
||||
/>
|
||||
<DurationField
|
||||
label="Export close offset"
|
||||
description="Export booking windows close this long before departure (blank = at departure)"
|
||||
value={form.exportCloseOffsetMinutes ?? ""}
|
||||
nativeUnit="minutes"
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, exportCloseOffsetMinutes: value }))
|
||||
}
|
||||
min={0}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button loading={saving} disabled={loading} onClick={() => void handleSave()}>
|
||||
Save rules
|
||||
|
||||
Reference in New Issue
Block a user