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"; /** * 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> { 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; } return numeric; } 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(); setForm(toFormState(rules)); } 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)[] = [ "maxWagonsPerTrain", "importWindowLeadDays", "exportBookingLeadHours", "windowOpenHour", "windowCloseHour", "windowDurationHours", "docReviewMinutes", "paymentWindowMinutes", ]; 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; } // 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(toFormState(updated)); toast({ title: "Train scheduling rules saved" }); } catch { toast({ title: "Failed to save rules", variant: "destructive" }); } finally { setSaving(false); } }; return ( {/* setForm((current) => ({ ...current, maxWagonsPerTrain: value })) } clampBehavior="none" allowNegative={false} allowDecimal min={1} 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" allowNegative={false} allowDecimal min={0} max={23} disabled={loading} /> setForm((current) => ({ ...current, windowCloseHour: value })) } clampBehavior="none" allowNegative={false} 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, importCloseOffsetMinutes: value })) } min={0} disabled={loading} /> setForm((current) => ({ ...current, exportCloseOffsetMinutes: value })) } min={0} disabled={loading} /> ); }