mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user