Files
edr-platform/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx
marshalyordanos 5da36eb128 feat: add wagon usage computation and maintenance logging features
- 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.
2026-08-12 09:36:50 +03:00

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>
);
}