Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx
2026-08-27 07:45:32 +00:00

167 lines
5.4 KiB
TypeScript

import { Button, Group, Modal, Stack, Text, Textarea } from "@mantine/core";
import { DateTimePicker } from "@mantine/dates";
import { useMediaQuery } from "@mantine/hooks";
import { useEffect, useState } from "react";
import type { CheckpointHandlingTimes } from "@/types/trainScheduling";
/** The four station-work stamps, in the order they happen. */
const HANDLING_FIELDS = [
["unloadingStartedAt", "Unloading started"],
["unloadingCompletedAt", "Unloading finished"],
["loadingStartedAt", "Loading started"],
["loadingCompletedAt", "Loading finished"],
] as const;
type HandlingField = (typeof HANDLING_FIELDS)[number][0];
type HandlingState = Record<HandlingField, Date | null>;
const EMPTY_HANDLING: HandlingState = {
unloadingStartedAt: null,
unloadingCompletedAt: null,
loadingStartedAt: null,
loadingCompletedAt: null,
};
/**
* Time, station work and note for one leg of a train's journey — used both to
* log a pass (defaults to now) and to correct an already-logged leg
* (prefilled). Past times are allowed (staff record after the fact); the future
* is not, and the server additionally keeps legs in corridor order.
*
* The four handling stamps are what the loading-and-unloading reports measure:
* total handling is unloading start to loading end, and whatever is left of the
* stay is other activity. All four are optional — a stop logged without them
* still reports its staying time.
*/
export function CheckpointTimeModal({
opened,
onClose,
title,
icon,
description,
initialOccurredAt,
initialNote,
initialHandling,
submitLabel,
submitColor = "edr-green",
loading,
onSubmit,
}: {
opened: boolean;
onClose: () => void;
title: string;
icon?: React.ReactNode;
description?: string;
/** ISO; omit to default to now. */
initialOccurredAt?: string | null;
initialNote?: string | null;
initialHandling?: CheckpointHandlingTimes | null;
submitLabel: string;
submitColor?: string;
loading: boolean;
onSubmit: (
values: { occurredAt: string; note: string } & Record<HandlingField, string | null>,
) => void;
}) {
const isSmallScreen = useMediaQuery("(max-width: 48em)");
const [at, setAt] = useState<Date | null>(null);
const [note, setNote] = useState("");
// The four station-work stamps are no longer edited HERE — the track page's
// "Loading & unloading windows" section owns start/end with its own
// permissions. The modal still carries any existing stamps through
// unchanged on submit, so editing a checkpoint never wipes them.
const [handling, setHandling] = useState<HandlingState>(EMPTY_HANDLING);
useEffect(() => {
if (!opened) return;
setAt(initialOccurredAt ? new Date(initialOccurredAt) : new Date());
setNote(initialNote ?? "");
setHandling({
unloadingStartedAt: initialHandling?.unloadingStartedAt
? new Date(initialHandling.unloadingStartedAt)
: null,
unloadingCompletedAt: initialHandling?.unloadingCompletedAt
? new Date(initialHandling.unloadingCompletedAt)
: null,
loadingStartedAt: initialHandling?.loadingStartedAt
? new Date(initialHandling.loadingStartedAt)
: null,
loadingCompletedAt: initialHandling?.loadingCompletedAt
? new Date(initialHandling.loadingCompletedAt)
: null,
});
}, [opened, initialOccurredAt, initialNote, initialHandling]);
return (
<Modal
opened={opened}
onClose={onClose}
centered
fullScreen={isSmallScreen}
radius={18}
title={
<Group gap={8}>
{icon}
<Text fw={700}>{title}</Text>
</Group>
}
>
<Stack gap="md">
{description ? (
<Text size="sm" c="dimmed">
{description}
</Text>
) : null}
<DateTimePicker
label="Time"
description="Defaults to now — pick an earlier time if you are recording after the fact."
value={at}
onChange={(v) => setAt(v ? new Date(v) : null)}
maxDate={new Date()}
dropdownType={isSmallScreen ? "modal" : "popover"}
popoverProps={{ withinPortal: true }}
valueFormat="DD MMM YYYY HH:mm"
clearable={false}
radius="md"
/>
<Textarea
label="Note"
placeholder="Optional"
value={note}
onChange={(e) => setNote(e.currentTarget.value)}
autosize
minRows={2}
maxRows={4}
maxLength={500}
radius="md"
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" radius={9} onClick={onClose} disabled={loading}>
Cancel
</Button>
<Button
radius={9}
color={submitColor}
loading={loading}
disabled={!at}
onClick={() =>
at &&
onSubmit({
occurredAt: at.toISOString(),
note: note.trim(),
unloadingStartedAt: handling.unloadingStartedAt?.toISOString() ?? null,
unloadingCompletedAt: handling.unloadingCompletedAt?.toISOString() ?? null,
loadingStartedAt: handling.loadingStartedAt?.toISOString() ?? null,
loadingCompletedAt: handling.loadingCompletedAt?.toISOString() ?? null,
})
}
>
{submitLabel}
</Button>
</Group>
</Stack>
</Modal>
);
}