mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
154 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|