Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/EditScheduleDateModal.tsx

154 lines
4.3 KiB
TypeScript

import { useEffect, useState } from "react";
import {
Alert,
Box,
Button,
Group,
Modal,
Stack,
Text,
TextInput,
ThemeIcon,
} from "@mantine/core";
import { isAxiosError } from "axios";
import { CalendarClock, Info } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
function parseError(error: unknown, fallback: string): string {
if (isAxiosError(error)) {
const message = error.response?.data?.message;
if (Array.isArray(message)) return message.join(", ");
if (typeof message === "string") return message;
}
return fallback;
}
/** ISO → the `YYYY-MM-DDTHH:mm` value a datetime-local input expects (local time). */
function toLocalInputValue(iso: string | null | undefined): string {
if (!iso) return "";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "";
const pad = (n: number) => String(n).padStart(2, "0");
return (
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
`T${pad(date.getHours())}:${pad(date.getMinutes())}`
);
}
export interface EditScheduleDateModalProps {
scheduleId: string | null;
currentDate: string | null;
routeName?: string | null;
opened: boolean;
onClose: () => void;
/** Called after a successful save (e.g. to refetch a list). */
onSaved?: () => void;
}
/**
* Reschedule a train's departure date. Only shown for schedules whose booking
* window has not opened yet; the API rejects a date inside the booking lead
* window (import/intercity lead in days, export in hours).
*/
export default function EditScheduleDateModal({
scheduleId,
currentDate,
routeName,
opened,
onClose,
onSaved,
}: EditScheduleDateModalProps) {
const { toast } = useToast();
const save = useMutation(
api.trainScheduling.updateScheduleDate.mutationOptions(),
);
const [value, setValue] = useState("");
// Earliest selectable departure, refreshed each time the modal opens.
const [minValue, setMinValue] = useState("");
useEffect(() => {
if (!opened) return;
setValue(toLocalInputValue(currentDate));
setMinValue(toLocalInputValue(new Date().toISOString()));
}, [opened, currentDate]);
const handleSave = async () => {
if (!scheduleId || !value) {
toast({ title: "Pick a departure date", variant: "destructive" });
return;
}
if (new Date(value).getTime() < Date.now()) {
toast({
title: "Departure date must be in the future",
variant: "destructive",
});
return;
}
try {
await save.mutateAsync({
id: scheduleId,
scheduleDate: new Date(value).toISOString(),
});
toast({ title: "Departure date updated" });
onSaved?.();
onClose();
} catch (err) {
toast({
title: "Update failed",
description: parseError(err, "Could not update departure date"),
variant: "destructive",
});
}
};
return (
<Modal
opened={opened}
onClose={onClose}
centered
radius="lg"
title={
<Group gap="sm">
<ThemeIcon variant="light" color="edr-green" radius="md" size={34}>
<CalendarClock size={18} />
</ThemeIcon>
<Box>
<Text fw={600} lh={1.2}>
Edit departure date
</Text>
<Text size="xs" c="dimmed" lh={1.2}>
{routeName ?? "This schedule only"}
</Text>
</Box>
</Group>
}
>
<Stack gap="md">
<Alert variant="light" color="blue" icon={<Info size={16} />}>
The date can only be changed before the booking window opens, and must
still leave room for the booking lead window before departure.
</Alert>
<TextInput
label="Departure date"
type="datetime-local"
min={minValue}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
<Group justify="flex-end" mt="xs">
<Button variant="default" onClick={onClose} disabled={save.isPending}>
Cancel
</Button>
<Button onClick={handleSave} loading={save.isPending}>
Save
</Button>
</Group>
</Stack>
</Modal>
);
}