mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
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.
This commit is contained in:
@@ -1006,7 +1006,7 @@ export function ReleaseOrderCard({
|
||||
clearance: ClearanceViewLike & { vesselDepartureDate?: string | null };
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [vesselDate, setVesselDate] = useState<Date | null>(
|
||||
clearance.vesselDepartureDate ? new Date(clearance.vesselDepartureDate) : null,
|
||||
);
|
||||
@@ -1020,7 +1020,14 @@ export function ReleaseOrderCard({
|
||||
Release Order
|
||||
</Text>
|
||||
<Stack gap="sm">
|
||||
<FileInput label="Release Order" value={file} onChange={setFile} size="sm" />
|
||||
<FileInput
|
||||
label="Release Order"
|
||||
placeholder="Select one or more files"
|
||||
multiple
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
size="sm"
|
||||
/>
|
||||
<DateInput
|
||||
label="Vessel departure date"
|
||||
value={vesselDate}
|
||||
@@ -1032,15 +1039,15 @@ export function ReleaseOrderCard({
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={!file || !vesselDate}
|
||||
disabled={files.length === 0 || !vesselDate}
|
||||
onClick={async () => {
|
||||
if (!file || !vesselDate) return;
|
||||
if (files.length === 0 || !vesselDate) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const iso = vesselDate.toISOString().slice(0, 10);
|
||||
const result = isBooking
|
||||
? await bookingsService.uploadReleaseOrder(entityId, file, iso)
|
||||
: await contractsService.uploadReleaseOrder(entityId, file, iso);
|
||||
? await bookingsService.uploadReleaseOrder(entityId, files, iso)
|
||||
: await contractsService.uploadReleaseOrder(entityId, files, iso);
|
||||
if (result.hold) {
|
||||
toast.error(result.holdReason ?? "Vessel date too soon");
|
||||
} else {
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
toIsoDate,
|
||||
useDoCollectionDates,
|
||||
} from "@/components/contracts/DoCollectionDateFields";
|
||||
import { PhasedFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import { findWorkflowFile } from "@/components/contracts/PhasedUploadedFileRow";
|
||||
import { PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import type { Freight } from "@edr/types";
|
||||
import { isDeliveryOrderFileCode, isReleaseOrderFileCode, type Freight } from "@edr/types";
|
||||
|
||||
export type GlClearanceUploadKind = "do" | "ro";
|
||||
|
||||
@@ -44,9 +43,8 @@ export function GlClearanceUploadModal({
|
||||
vesselArrivalDate,
|
||||
doCollectedDate,
|
||||
onSuccess,
|
||||
onPreview,
|
||||
}: GlClearanceUploadModalProps) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [vesselDate, setVesselDate] = useState<Date | null>(
|
||||
vesselDepartureDate ? new Date(vesselDepartureDate) : null,
|
||||
);
|
||||
@@ -66,16 +64,16 @@ export function GlClearanceUploadModal({
|
||||
const isDo = kind === "do";
|
||||
const isRo = kind === "ro";
|
||||
const replaceMode = isDo
|
||||
? Boolean(findWorkflowFile(workflowFiles, "delivery_order"))
|
||||
: Boolean(findWorkflowFile(workflowFiles, "release_order"));
|
||||
? workflowFiles.some((f) => isDeliveryOrderFileCode(f.code) && f.file)
|
||||
: workflowFiles.some((f) => isReleaseOrderFileCode(f.code) && f.file);
|
||||
|
||||
const close = () => {
|
||||
setFile(null);
|
||||
setFiles([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!file || !kind) return;
|
||||
if (files.length === 0 || !kind) return;
|
||||
if (isRo && !vesselDate) {
|
||||
toast.error("Vessel departure date is required.");
|
||||
return;
|
||||
@@ -93,23 +91,23 @@ export function GlClearanceUploadModal({
|
||||
doCollectedDate: toIsoDate(doDates.doCollected)!,
|
||||
};
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadDeliveryOrder(entityId, file, dates);
|
||||
await bookingsService.uploadDeliveryOrder(entityId, files, dates);
|
||||
} else {
|
||||
await contractsService.uploadDeliveryOrder(entityId, file, dates);
|
||||
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, file, iso)
|
||||
: await contractsService.uploadReleaseOrder(entityId, file, iso);
|
||||
? 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");
|
||||
}
|
||||
}
|
||||
setFile(null);
|
||||
setFiles([]);
|
||||
onSuccess?.();
|
||||
close();
|
||||
} catch (e) {
|
||||
@@ -152,14 +150,15 @@ export function GlClearanceUploadModal({
|
||||
<DoCollectionDateFields value={doDates} onChange={setDoDates} />
|
||||
)}
|
||||
|
||||
<PhasedFileDropzone
|
||||
label={isDo ? "Delivery Order file" : "Release Order file"}
|
||||
description={isDo ? "Any file type." : "PDF or image."}
|
||||
<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={file}
|
||||
onChange={setFile}
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
replaceMode={replaceMode}
|
||||
onPreview={onPreview}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
@@ -170,7 +169,7 @@ export function GlClearanceUploadModal({
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={
|
||||
!file ||
|
||||
files.length === 0 ||
|
||||
(isRo && !vesselDate) ||
|
||||
(isDo && !doDatesComplete(doDates))
|
||||
}
|
||||
|
||||
@@ -34,12 +34,15 @@ import {
|
||||
Truck,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
import {
|
||||
deliveryOrderFileLabel,
|
||||
isDeliveryOrderFileCode,
|
||||
type Freight,
|
||||
} from "@edr/types";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
||||
import { ExportClearanceStepper } from "@/components/contracts/ExportClearanceStepper";
|
||||
import { PhasedDocumentUploadField } from "@/components/contracts/PhasedDocumentUploadField";
|
||||
import {
|
||||
DoCollectionDateFields,
|
||||
doDatesComplete,
|
||||
@@ -2133,56 +2136,82 @@ function DeliveryOrderStep({
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [files, setFiles] = useState<Record<string, File | null>>({
|
||||
delivery_order: null,
|
||||
});
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [doDates, setDoDates] = useDoCollectionDates({
|
||||
vesselArrivalDate,
|
||||
doCollectedDate,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const hasFile = Boolean(files.delivery_order);
|
||||
|
||||
const submit = async () => {
|
||||
if (files.length === 0 || !doDatesComplete(doDates)) return;
|
||||
const dates = {
|
||||
vesselArrivalDate: toIsoDate(doDates.vesselArrival)!,
|
||||
doCollectedDate: toIsoDate(doDates.doCollected)!,
|
||||
};
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadDeliveryOrder(entityId, files, dates);
|
||||
} else {
|
||||
await contractsService.uploadDeliveryOrder(entityId, files, dates);
|
||||
}
|
||||
setFiles([]);
|
||||
toast.success(replaceMode ? "Delivery Order updated" : "Delivery Order uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PhasedDocumentUploadField
|
||||
fields={[{ key: "delivery_order", label: "Delivery Order" }]}
|
||||
files={files}
|
||||
onChange={(key, file) => setFiles((prev) => ({ ...prev, [key]: file }))}
|
||||
workflowFiles={workflowFiles}
|
||||
replaceMode={replaceMode}
|
||||
loading={loading}
|
||||
disabled={!hasFile || !doDatesComplete(doDates)}
|
||||
helperText="Upload the Djibouti Delivery Order (DO) and record when the vessel arrived and when the DO was collected."
|
||||
submitLabel={replaceMode ? "Replace DO" : "Upload DO"}
|
||||
extraFields={
|
||||
<DoCollectionDateFields value={doDates} onChange={setDoDates} />
|
||||
}
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSubmit={async () => {
|
||||
const file = files.delivery_order;
|
||||
if (!file || !doDatesComplete(doDates)) return;
|
||||
const dates = {
|
||||
vesselArrivalDate: toIsoDate(doDates.vesselArrival)!,
|
||||
doCollectedDate: toIsoDate(doDates.doCollected)!,
|
||||
};
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadDeliveryOrder(entityId, file, dates);
|
||||
} else {
|
||||
await contractsService.uploadDeliveryOrder(entityId, file, dates);
|
||||
}
|
||||
setFiles({ delivery_order: null });
|
||||
toast.success(replaceMode ? "Delivery Order updated" : "Delivery Order uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
Upload the Djibouti Delivery Order (DO) and record when the vessel arrived and when the
|
||||
DO was collected. Add as many files as needed.
|
||||
</Text>
|
||||
|
||||
{workflowFiles
|
||||
.filter((wf) => isDeliveryOrderFileCode(wf.code) && wf.file)
|
||||
.map((wf, index) => (
|
||||
<PhasedUploadedFileRow
|
||||
key={wf.code}
|
||||
label={deliveryOrderFileLabel(wf.code, index)}
|
||||
file={wf.file!}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
/>
|
||||
))}
|
||||
|
||||
<DoCollectionDateFields value={doDates} onChange={setDoDates} />
|
||||
|
||||
<PhasedMultiFileDropzone
|
||||
label="Delivery Order files"
|
||||
description={
|
||||
replaceMode
|
||||
? "Replace the DO — upload one or more files (any file type)."
|
||||
: "Upload one or more Delivery Order files (any file type)."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
accept="*/*"
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
replaceMode={replaceMode}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={files.length === 0 || !doDatesComplete(doDates)}
|
||||
leftSection={<Upload size={16} />}
|
||||
fullWidth
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
{replaceMode ? "Replace DO" : "Upload DO"}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user