diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx index dc59c4dfc..c94e005ae 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx @@ -50,6 +50,13 @@ import { import { contractsService } from "@/services/contracts.service"; import { bookingsService } from "@/services/bookings.service"; +/** Today as `yyyy-MM-dd` in the browser's local zone (a DateInput `minDate`). */ +function todayISODate(): string { + const now = new Date(); + const tz = now.getTimezoneOffset() * 60000; + return new Date(now.getTime() - tz).toISOString().slice(0, 10); +} + /** * Export customs flow, ordered per the stakeholder process: * customer docs → RO (DJ) → declaration (ET, auto-releases) → create booking (ET) @@ -937,6 +944,7 @@ export function ReleaseOrderCard({ ); const [loading, setLoading] = useState(false); const [amendLoading, setAmendLoading] = useState(false); + const minVesselDate = useMemo(todayISODate, []); return ( @@ -949,6 +957,7 @@ export function ReleaseOrderCard({ label="Vessel departure date" value={vesselDate} onChange={(v) => setVesselDate(v ? new Date(v) : null)} + minDate={minVesselDate} size="sm" /> diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx index 87a8993ee..1953796dd 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Button, Group, Modal, Stack, Text } from "@mantine/core"; import { DateInput } from "@mantine/dates"; import { Ship, Upload } from "lucide-react"; @@ -40,6 +40,13 @@ export function GlClearanceUploadModal({ vesselDepartureDate ? new Date(vesselDepartureDate) : null, ); const [loading, setLoading] = useState(false); + // Earliest selectable vessel date (today, local) — refreshed on each open. + const todayISODate = useMemo(() => { + if (!opened) return undefined; + const now = new Date(); + const tz = now.getTimezoneOffset() * 60000; + return new Date(now.getTime() - tz).toISOString().slice(0, 10); + }, [opened]); const isDo = kind === "do"; const isRo = kind === "ro"; @@ -115,6 +122,7 @@ export function GlClearanceUploadModal({ label="Vessel departure date" value={vesselDate} onChange={(v) => setVesselDate(v ? new Date(v) : null)} + minDate={todayISODate} size="sm" required /> @@ -123,6 +131,7 @@ export function GlClearanceUploadModal({ label="Vessel arrival date (optional)" value={vesselDate} onChange={(v) => setVesselDate(v ? new Date(v) : null)} + minDate={todayISODate} size="sm" clearable /> diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/EditScheduleDateModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/EditScheduleDateModal.tsx index 6ea7dfc5b..c173614dc 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/EditScheduleDateModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/EditScheduleDateModal.tsx @@ -67,9 +67,13 @@ export default function EditScheduleDateModal({ ); const [value, setValue] = useState(""); + // Earliest selectable departure, refreshed each time the modal opens. + const [minValue, setMinValue] = useState(""); useEffect(() => { - if (opened) setValue(toLocalInputValue(currentDate)); + if (!opened) return; + setValue(toLocalInputValue(currentDate)); + setMinValue(toLocalInputValue(new Date().toISOString())); }, [opened, currentDate]); const handleSave = async () => { @@ -77,6 +81,13 @@ export default function EditScheduleDateModal({ 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, @@ -124,6 +135,7 @@ export default function EditScheduleDateModal({ setValue(e.currentTarget.value)} /> diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/RescheduleTrainDialog.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/RescheduleTrainDialog.tsx index d0f670e51..271603bca 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/RescheduleTrainDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/RescheduleTrainDialog.tsx @@ -1,9 +1,19 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Button, Group, Modal, Stack, Text, TextInput, Textarea } from "@mantine/core"; import toast from "react-hot-toast"; import { trainSchedulingService } from "@/services/trainScheduling.service"; +/** `min` for a `datetime-local` input: now, in the browser's local zone. */ +function nowLocalDateTime(): string { + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, "0"); + return ( + `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + + `T${pad(now.getHours())}:${pad(now.getMinutes())}` + ); +} + export function RescheduleTrainDialog({ scheduleId, currentBookingIds, @@ -20,12 +30,21 @@ export function RescheduleTrainDialog({ const [newDepartureDate, setNewDepartureDate] = useState(""); const [reason, setReason] = useState(""); const [loading, setLoading] = useState(false); + // Earliest selectable departure, refreshed each time the dialog opens. + const minDepartureDate = useMemo( + () => (opened ? nowLocalDateTime() : ""), + [opened], + ); const handleSubmit = async () => { if (!newDepartureDate) { toast.error("Select a new departure date"); return; } + if (new Date(newDepartureDate).getTime() < Date.now()) { + toast.error("New departure must be in the future"); + return; + } setLoading(true); try { await trainSchedulingService.maintenanceReschedule(scheduleId, { @@ -53,6 +72,7 @@ export function RescheduleTrainDialog({ setNewDepartureDate(e.target.value)} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx index 14432d8ed..859efece1 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx @@ -55,6 +55,13 @@ import { useToast } from "@/hooks/use-toast"; import type { TrainScheduleListItem } from "@/types/trainScheduling"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; +/** `min` for a `datetime-local` input: now, in the browser's local zone. */ +const nowLocalDateTime = () => { + const now = new Date(); + now.setMinutes(now.getMinutes() - now.getTimezoneOffset()); + return now.toISOString().slice(0, 16); +}; + const splitDate = (value?: string | null) => { if (!value) return { day: "—", time: "" }; const date = new Date(value); @@ -103,6 +110,12 @@ export default function TrainScheduleV2ListPage() { const [routeId, setRouteId] = useState(""); const [scheduleDate, setScheduleDate] = useState(""); const [locomotiveIds, setLocomotiveIds] = useState([]); + // 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( + () => (createOpen ? nowLocalDateTime() : ""), + [createOpen], + ); const schedulesQuery = useQuery( api.trainScheduling.scheduleList.queryOptions({ input: {} }), @@ -443,6 +456,13 @@ export default function TrainScheduleV2ListPage() { }); return; } + if (new Date(scheduleDate).getTime() < Date.now()) { + toast({ + title: "Departure date must be in the future", + variant: "destructive", + }); + return; + } try { const created = await create.mutateAsync({ payload: { @@ -691,6 +711,7 @@ export default function TrainScheduleV2ListPage() { setScheduleDate(e.currentTarget.value)} />