Merge pull request #560 from Tria-plc/freight_feature/usermanagement

Add minDate handling for vessel and departure dates across components
This commit is contained in:
marshal
2026-07-09 07:08:32 +03:00
committed by GitHub
5 changed files with 74 additions and 3 deletions

View File

@@ -50,6 +50,13 @@ import {
import { contractsService } from "@/services/contracts.service"; import { contractsService } from "@/services/contracts.service";
import { bookingsService } from "@/services/bookings.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: * Export customs flow, ordered per the stakeholder process:
* customer docs → RO (DJ) → declaration (ET, auto-releases) → create booking (ET) * customer docs → RO (DJ) → declaration (ET, auto-releases) → create booking (ET)
@@ -937,6 +944,7 @@ export function ReleaseOrderCard({
); );
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [amendLoading, setAmendLoading] = useState(false); const [amendLoading, setAmendLoading] = useState(false);
const minVesselDate = useMemo(todayISODate, []);
return ( return (
<Paper withBorder radius="md" p="md"> <Paper withBorder radius="md" p="md">
@@ -949,6 +957,7 @@ export function ReleaseOrderCard({
label="Vessel departure date" label="Vessel departure date"
value={vesselDate} value={vesselDate}
onChange={(v) => setVesselDate(v ? new Date(v) : null)} onChange={(v) => setVesselDate(v ? new Date(v) : null)}
minDate={minVesselDate}
size="sm" size="sm"
/> />
<Group> <Group>

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import { Button, Group, Modal, Stack, Text } from "@mantine/core"; import { Button, Group, Modal, Stack, Text } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DateInput } from "@mantine/dates";
import { Ship, Upload } from "lucide-react"; import { Ship, Upload } from "lucide-react";
@@ -40,6 +40,13 @@ export function GlClearanceUploadModal({
vesselDepartureDate ? new Date(vesselDepartureDate) : null, vesselDepartureDate ? new Date(vesselDepartureDate) : null,
); );
const [loading, setLoading] = useState(false); 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 isDo = kind === "do";
const isRo = kind === "ro"; const isRo = kind === "ro";
@@ -115,6 +122,7 @@ export function GlClearanceUploadModal({
label="Vessel departure date" label="Vessel departure date"
value={vesselDate} value={vesselDate}
onChange={(v) => setVesselDate(v ? new Date(v) : null)} onChange={(v) => setVesselDate(v ? new Date(v) : null)}
minDate={todayISODate}
size="sm" size="sm"
required required
/> />
@@ -123,6 +131,7 @@ export function GlClearanceUploadModal({
label="Vessel arrival date (optional)" label="Vessel arrival date (optional)"
value={vesselDate} value={vesselDate}
onChange={(v) => setVesselDate(v ? new Date(v) : null)} onChange={(v) => setVesselDate(v ? new Date(v) : null)}
minDate={todayISODate}
size="sm" size="sm"
clearable clearable
/> />

View File

@@ -67,9 +67,13 @@ export default function EditScheduleDateModal({
); );
const [value, setValue] = useState(""); const [value, setValue] = useState("");
// Earliest selectable departure, refreshed each time the modal opens.
const [minValue, setMinValue] = useState("");
useEffect(() => { useEffect(() => {
if (opened) setValue(toLocalInputValue(currentDate)); if (!opened) return;
setValue(toLocalInputValue(currentDate));
setMinValue(toLocalInputValue(new Date().toISOString()));
}, [opened, currentDate]); }, [opened, currentDate]);
const handleSave = async () => { const handleSave = async () => {
@@ -77,6 +81,13 @@ export default function EditScheduleDateModal({
toast({ title: "Pick a departure date", variant: "destructive" }); toast({ title: "Pick a departure date", variant: "destructive" });
return; return;
} }
if (new Date(value).getTime() < Date.now()) {
toast({
title: "Departure date must be in the future",
variant: "destructive",
});
return;
}
try { try {
await save.mutateAsync({ await save.mutateAsync({
id: scheduleId, id: scheduleId,
@@ -124,6 +135,7 @@ export default function EditScheduleDateModal({
<TextInput <TextInput
label="Departure date" label="Departure date"
type="datetime-local" type="datetime-local"
min={minValue}
value={value} value={value}
onChange={(e) => setValue(e.currentTarget.value)} onChange={(e) => setValue(e.currentTarget.value)}
/> />

View File

@@ -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 { Button, Group, Modal, Stack, Text, TextInput, Textarea } from "@mantine/core";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { trainSchedulingService } from "@/services/trainScheduling.service"; 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({ export function RescheduleTrainDialog({
scheduleId, scheduleId,
currentBookingIds, currentBookingIds,
@@ -20,12 +30,21 @@ export function RescheduleTrainDialog({
const [newDepartureDate, setNewDepartureDate] = useState(""); const [newDepartureDate, setNewDepartureDate] = useState("");
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Earliest selectable departure, refreshed each time the dialog opens.
const minDepartureDate = useMemo(
() => (opened ? nowLocalDateTime() : ""),
[opened],
);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!newDepartureDate) { if (!newDepartureDate) {
toast.error("Select a new departure date"); toast.error("Select a new departure date");
return; return;
} }
if (new Date(newDepartureDate).getTime() < Date.now()) {
toast.error("New departure must be in the future");
return;
}
setLoading(true); setLoading(true);
try { try {
await trainSchedulingService.maintenanceReschedule(scheduleId, { await trainSchedulingService.maintenanceReschedule(scheduleId, {
@@ -53,6 +72,7 @@ export function RescheduleTrainDialog({
<TextInput <TextInput
label="New departure" label="New departure"
type="datetime-local" type="datetime-local"
min={minDepartureDate}
value={newDepartureDate} value={newDepartureDate}
onChange={(e) => setNewDepartureDate(e.target.value)} onChange={(e) => setNewDepartureDate(e.target.value)}
/> />

View File

@@ -55,6 +55,13 @@ import { useToast } from "@/hooks/use-toast";
import type { TrainScheduleListItem } from "@/types/trainScheduling"; import type { TrainScheduleListItem } from "@/types/trainScheduling";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; 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) => { const splitDate = (value?: string | null) => {
if (!value) return { day: "—", time: "" }; if (!value) return { day: "—", time: "" };
const date = new Date(value); const date = new Date(value);
@@ -103,6 +110,12 @@ export default function TrainScheduleV2ListPage() {
const [routeId, setRouteId] = useState(""); const [routeId, setRouteId] = useState("");
const [scheduleDate, setScheduleDate] = useState(""); const [scheduleDate, setScheduleDate] = useState("");
const [locomotiveIds, setLocomotiveIds] = useState<string[]>([]); const [locomotiveIds, setLocomotiveIds] = useState<string[]>([]);
// 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( const schedulesQuery = useQuery(
api.trainScheduling.scheduleList.queryOptions({ input: {} }), api.trainScheduling.scheduleList.queryOptions({ input: {} }),
@@ -443,6 +456,13 @@ export default function TrainScheduleV2ListPage() {
}); });
return; return;
} }
if (new Date(scheduleDate).getTime() < Date.now()) {
toast({
title: "Departure date must be in the future",
variant: "destructive",
});
return;
}
try { try {
const created = await create.mutateAsync({ const created = await create.mutateAsync({
payload: { payload: {
@@ -691,6 +711,7 @@ export default function TrainScheduleV2ListPage() {
<TextInput <TextInput
label="Departure date" label="Departure date"
type="datetime-local" type="datetime-local"
min={minScheduleDate}
value={scheduleDate} value={scheduleDate}
onChange={(e) => setScheduleDate(e.currentTarget.value)} onChange={(e) => setScheduleDate(e.currentTarget.value)}
/> />