mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
- Implemented utility to calculate wagon usage metrics for train schedules. - Created for sending wagons to maintenance with optional notes. - Added unit tests for train builder maintenance functionalities, including formatting train run labels and building maintenance notes. - Developed component for merging train schedules with detailed previews and reasons for merging. - Introduced component for selecting wagons with search functionality and selection limits. - Created for displaying and filtering audit logs, including detailed views of individual log entries. - Added for handling API interactions related to audit logs, including fetching logs and entity types.
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
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";
|
|
import toast from "react-hot-toast";
|
|
|
|
import {
|
|
DoCollectionDateFields,
|
|
doDatesComplete,
|
|
toIsoDate,
|
|
useDoCollectionDates,
|
|
} from "@/components/contracts/DoCollectionDateFields";
|
|
import { PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
|
import { contractsService } from "@/services/contracts.service";
|
|
import { bookingsService } from "@/services/bookings.service";
|
|
import { isDeliveryOrderFileCode, isReleaseOrderFileCode, type Freight } from "@edr/types";
|
|
|
|
export type GlClearanceUploadKind = "do" | "ro";
|
|
|
|
export interface GlClearanceUploadModalProps {
|
|
opened: boolean;
|
|
kind: GlClearanceUploadKind | null;
|
|
onClose: () => void;
|
|
entityId: string;
|
|
isBooking: boolean;
|
|
workflowFiles?: Freight.ClearanceWorkflowFile[];
|
|
vesselDepartureDate?: string | null;
|
|
/** Previously recorded DO dates, so a replace opens pre-filled. */
|
|
vesselArrivalDate?: string | null;
|
|
doCollectedDate?: string | null;
|
|
onSuccess?: () => void;
|
|
onPreview?: (file: { name: string; url: string }) => void;
|
|
}
|
|
|
|
export function GlClearanceUploadModal({
|
|
opened,
|
|
kind,
|
|
onClose,
|
|
entityId,
|
|
isBooking,
|
|
workflowFiles = [],
|
|
vesselDepartureDate,
|
|
vesselArrivalDate,
|
|
doCollectedDate,
|
|
onSuccess,
|
|
}: GlClearanceUploadModalProps) {
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
const [vesselDate, setVesselDate] = useState<Date | null>(
|
|
vesselDepartureDate ? new Date(vesselDepartureDate) : null,
|
|
);
|
|
const [doDates, setDoDates] = useDoCollectionDates({
|
|
vesselArrivalDate,
|
|
doCollectedDate,
|
|
});
|
|
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";
|
|
const replaceMode = isDo
|
|
? workflowFiles.some((f) => isDeliveryOrderFileCode(f.code) && f.file)
|
|
: workflowFiles.some((f) => isReleaseOrderFileCode(f.code) && f.file);
|
|
|
|
const close = () => {
|
|
setFiles([]);
|
|
onClose();
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (files.length === 0 || !kind) return;
|
|
if (isRo && !vesselDate) {
|
|
toast.error("Vessel departure date is required.");
|
|
return;
|
|
}
|
|
if (isDo && !doDatesComplete(doDates)) {
|
|
toast.error("Vessel arrival date and DO collected date are both required.");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
if (isDo) {
|
|
const dates = {
|
|
vesselArrivalDate: toIsoDate(doDates.vesselArrival)!,
|
|
doCollectedDate: toIsoDate(doDates.doCollected)!,
|
|
};
|
|
if (isBooking) {
|
|
await bookingsService.uploadDeliveryOrder(entityId, files, dates);
|
|
} else {
|
|
await contractsService.uploadDeliveryOrder(entityId, files, dates);
|
|
}
|
|
toast.success(replaceMode ? "Delivery Order updated" : "Delivery Order uploaded");
|
|
} else {
|
|
const iso = vesselDate!.toISOString().slice(0, 10);
|
|
const result = isBooking
|
|
? await bookingsService.uploadReleaseOrder(entityId, files, iso)
|
|
: await contractsService.uploadReleaseOrder(entityId, files, iso);
|
|
if (result.hold) {
|
|
toast.error(result.holdReason ?? "Vessel date too soon");
|
|
} else {
|
|
toast.success(replaceMode ? "Release Order updated" : "Release Order uploaded");
|
|
}
|
|
}
|
|
setFiles([]);
|
|
onSuccess?.();
|
|
close();
|
|
} catch (e) {
|
|
toast.error(e instanceof Error ? e.message : "Upload failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened && kind != null}
|
|
onClose={close}
|
|
title={
|
|
<Group gap={8}>
|
|
<Ship size={18} />
|
|
<Text fw={700}>{isDo ? "Upload Delivery Order" : "Upload Release Order"}</Text>
|
|
</Group>
|
|
}
|
|
radius="md"
|
|
size="md"
|
|
>
|
|
<Stack gap="md">
|
|
<Text size="sm" c="dimmed">
|
|
{isDo
|
|
? "Upload the Djibouti Delivery Order (DO) and record when the vessel arrived and when the DO was collected. Both dates are required."
|
|
: "Upload the Release Order and confirm the vessel departure date."}
|
|
</Text>
|
|
|
|
{isRo ? (
|
|
<DateInput
|
|
label="Vessel departure date"
|
|
value={vesselDate}
|
|
onChange={(v) => setVesselDate(v ? new Date(v) : null)}
|
|
minDate={todayISODate}
|
|
size="sm"
|
|
required
|
|
/>
|
|
) : (
|
|
<DoCollectionDateFields value={doDates} onChange={setDoDates} />
|
|
)}
|
|
|
|
<PhasedMultiFileDropzone
|
|
label={isDo ? "Delivery Order files" : "Release Order files"}
|
|
description={
|
|
isDo ? "Any file type. Add as many files as needed." : "PDF or image. Add as many files as needed."
|
|
}
|
|
accept={isDo ? "*/*" : undefined}
|
|
value={files}
|
|
onChange={setFiles}
|
|
replaceMode={replaceMode}
|
|
/>
|
|
|
|
<Group justify="flex-end" gap="sm">
|
|
<Button variant="default" onClick={close} disabled={loading}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
color="edr-green"
|
|
loading={loading}
|
|
disabled={
|
|
files.length === 0 ||
|
|
(isRo && !vesselDate) ||
|
|
(isDo && !doDatesComplete(doDates))
|
|
}
|
|
leftSection={<Upload size={16} />}
|
|
onClick={() => void submit()}
|
|
>
|
|
{replaceMode
|
|
? isDo
|
|
? "Replace DO"
|
|
: "Replace RO"
|
|
: isDo
|
|
? "Upload DO"
|
|
: "Upload RO"}
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|