import { useEffect, useState } from "react"; import { Button, Card, Group, NumberInput, Stack } from "@mantine/core"; import { PageContainer, PageHeader } from "@/components/page"; import DurationField from "@/components/trainScheduling/DurationField"; import { trainSchedulingService } from "@/services/trainScheduling.service"; import { useToast } from "@/hooks/use-toast"; import type { TrainSchedulingGlobalRules } from "@/types/trainScheduling"; export default function TrainSchedulingGlobalRulesPage() { const { toast } = useToast(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); // Fields hold raw NumberInput values (number | string) while editing; coerced to Number on save. const [form, setForm] = useState< Partial> >({}); useEffect(() => { 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> = {}; 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); } catch { toast({ title: "Failed to load train scheduling rules", variant: "destructive" }); } finally { setLoading(false); } })(); // Run once on mount only. `toast` from useToast is a fresh function every // render — listing it here re-fired the effect on every render, refetching // the rules and overwriting whatever the user was typing (values snapped // back to the saved defaults). // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSave = async () => { // Every field must hold a real number — an empty box (cleared but not // refilled) must not silently save as 0. Collect the numeric payload and // reject if any value is blank or NaN. const fields: (keyof TrainSchedulingGlobalRules)[] = [ "maxTrainLengthMeters", "maxTrainWeightTons", "maxWagonsPerTrain", "max20ftContainerWeightTons", "max20ftPairWeightDiffTons", "importWindowLeadDays", "exportBookingLeadHours", "windowOpenHour", "windowDurationHours", "docReviewMinutes", "paymentWindowMinutes", "reopenDelayMinutes", ]; const payload: Partial> = {}; for (const key of fields) { const raw = form[key]; const num = raw === "" || raw == null ? NaN : Number(raw); if (!Number.isFinite(num)) { toast({ title: "All fields are required — fill every value before saving.", variant: "destructive", }); return; } payload[key] = num; } setSaving(true); try { const updated = await trainSchedulingService.updateGlobalRules(payload); setForm(updated); toast({ title: "Train scheduling rules saved" }); } catch { toast({ title: "Failed to save rules", variant: "destructive" }); } finally { setSaving(false); } }; return ( setForm((current) => ({ ...current, maxTrainLengthMeters: value })) } clampBehavior="none" allowDecimal min={1} disabled={loading} /> setForm((current) => ({ ...current, maxTrainWeightTons: value })) } clampBehavior="none" allowDecimal min={1} disabled={loading} /> setForm((current) => ({ ...current, maxWagonsPerTrain: value })) } clampBehavior="none" allowDecimal min={1} disabled={loading} /> setForm((current) => ({ ...current, max20ftContainerWeightTons: value, })) } clampBehavior="none" allowDecimal min={0.001} disabled={loading} /> setForm((current) => ({ ...current, max20ftPairWeightDiffTons: value, })) } clampBehavior="none" allowDecimal min={0} disabled={loading} /> setForm((current) => ({ ...current, importWindowLeadDays: value })) } min={0} disabled={loading} /> setForm((current) => ({ ...current, exportBookingLeadHours: value })) } min={1} disabled={loading} /> setForm((current) => ({ ...current, windowOpenHour: value })) } clampBehavior="none" allowDecimal min={0} max={23} disabled={loading} /> setForm((current) => ({ ...current, windowDurationHours: value })) } min={1} disabled={loading} /> setForm((current) => ({ ...current, docReviewMinutes: value })) } min={0} disabled={loading} /> setForm((current) => ({ ...current, paymentWindowMinutes: value })) } min={1} disabled={loading} /> setForm((current) => ({ ...current, reopenDelayMinutes: value })) } min={1} disabled={loading} /> ); }