mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
fix transi permit file upload
This commit is contained in:
@@ -15,6 +15,10 @@ import {
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import {
|
||||
TransitPermitMultiUpload,
|
||||
type TransitPermitUploadedRow,
|
||||
} from "@/components/contracts/TransitPermitMultiUpload";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -332,7 +336,7 @@ export function PhasedClearanceActionPanel({
|
||||
>
|
||||
{showEt &&
|
||||
canEt &&
|
||||
!clearance.bookingReady &&
|
||||
!bookingCreated &&
|
||||
(activeStep >= 4 ||
|
||||
isMilestoneDone(clearance.milestones, "TRANSIT_PERMIT_UPLOADED")) ? (
|
||||
<TransitPermitStep
|
||||
@@ -839,79 +843,30 @@ function TransitPermitStep({
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploaded = importTransitFilesFromWorkflow(workflowFiles);
|
||||
const uploaded: TransitPermitUploadedRow[] = importTransitFilesFromWorkflow(workflowFiles);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={700}>
|
||||
Transit Permit
|
||||
</Text>
|
||||
|
||||
{uploaded.length > 0 ? (
|
||||
<Stack gap={8}>
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
|
||||
Current file{uploaded.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
{uploaded.map((row) => (
|
||||
<PhasedUploadedFileRow
|
||||
key={row.code}
|
||||
label={row.label}
|
||||
file={row.file}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
<Paper withBorder radius="md" p="md" bg="var(--mantine-color-gray-0)">
|
||||
<PhasedMultiFileDropzone
|
||||
label="Transit permit documents"
|
||||
description={
|
||||
replaceMode
|
||||
? "Replace transit permit files — upload one or more documents."
|
||||
: "Upload one or more transit permit documents (PDF or image)."
|
||||
<TransitPermitMultiUpload
|
||||
uploaded={uploaded}
|
||||
replaceMode={replaceMode}
|
||||
fileFieldPrefix="transit_permit"
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSubmit={async (payload) => {
|
||||
try {
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadTransitPermit(entityId, payload);
|
||||
} else {
|
||||
await contractsService.uploadContractTransitPermit(entityId, payload);
|
||||
}
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
replaceMode={replaceMode}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={files.length === 0}
|
||||
leftSection={<Upload size={16} />}
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = Object.fromEntries(
|
||||
files.map((file, index) => [`transit_permit_${index}`, file]),
|
||||
) as Record<string, File>;
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadTransitPermit(entityId, payload);
|
||||
} else {
|
||||
await contractsService.uploadContractTransitPermit(entityId, payload);
|
||||
}
|
||||
setFiles([]);
|
||||
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{replaceMode ? "Replace transit permit" : "Upload transit permit"}
|
||||
</Button>
|
||||
</Stack>
|
||||
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
throw e;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1320,6 +1275,18 @@ function ExportPostBookingSection({
|
||||
);
|
||||
}
|
||||
|
||||
function exportTransitFilesFromWorkflow(
|
||||
workflowFiles: Freight.ClearanceWorkflowFile[],
|
||||
): TransitPermitUploadedRow[] {
|
||||
return workflowFiles
|
||||
.filter((f) => f.category === "transit" && f.file)
|
||||
.map((f) => ({
|
||||
code: f.code,
|
||||
label: f.label,
|
||||
file: f.file!,
|
||||
}));
|
||||
}
|
||||
|
||||
function ExportTransitPermitStep({
|
||||
bookingId,
|
||||
workflowFiles = [],
|
||||
@@ -1335,50 +1302,43 @@ function ExportTransitPermitStep({
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [files, setFiles] = useState<Record<string, File | null>>({
|
||||
export_transport_document: null,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const hasFile = Boolean(files.export_transport_document);
|
||||
const existing = findWorkflowFile(workflowFiles, "export_transport_document");
|
||||
const uploaded = exportTransitFilesFromWorkflow(workflowFiles);
|
||||
const hasUploaded = uploaded.length > 0;
|
||||
|
||||
if (existing && !replaceMode) {
|
||||
if (hasUploaded && !replaceMode) {
|
||||
return (
|
||||
<PhasedUploadedFileRow
|
||||
label="Transit permit"
|
||||
file={existing}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
/>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={700}>
|
||||
Transit Permit
|
||||
</Text>
|
||||
{uploaded.map((row) => (
|
||||
<PhasedUploadedFileRow
|
||||
key={row.code}
|
||||
label={row.label}
|
||||
file={row.file}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PhasedDocumentUploadField
|
||||
fields={[{ key: "export_transport_document", label: "Transit permit" }]}
|
||||
files={files}
|
||||
onChange={(key, file) => setFiles((prev) => ({ ...prev, [key]: file }))}
|
||||
workflowFiles={workflowFiles}
|
||||
replaceMode={replaceMode}
|
||||
loading={loading}
|
||||
disabled={!hasFile}
|
||||
helperText="Screenshot or PDF of the transit permitted status — upload after wagon allocation."
|
||||
submitLabel={replaceMode ? "Replace transit permit" : "Upload transit permit"}
|
||||
<TransitPermitMultiUpload
|
||||
replaceMode={replaceMode || hasUploaded}
|
||||
uploaded={uploaded}
|
||||
fileFieldPrefix="export_transport_document"
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSubmit={async () => {
|
||||
const file = files.export_transport_document;
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
onSubmit={async (payload) => {
|
||||
try {
|
||||
await contractsService.uploadTransportDocument(bookingId, file);
|
||||
setFiles({ export_transport_document: null });
|
||||
await contractsService.uploadTransportDocument(bookingId, payload);
|
||||
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
throw e;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Paper, Stack, Text } from "@mantine/core";
|
||||
import { Upload } from "lucide-react";
|
||||
|
||||
import { PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import { PhasedUploadedFileRow } from "@/components/contracts/PhasedUploadedFileRow";
|
||||
|
||||
export type TransitPermitUploadedRow = {
|
||||
code: string;
|
||||
label: string;
|
||||
file: { id: string; name: string };
|
||||
};
|
||||
|
||||
export interface TransitPermitMultiUploadProps {
|
||||
title?: string;
|
||||
uploaded?: TransitPermitUploadedRow[];
|
||||
replaceMode?: boolean;
|
||||
submitLabel?: string;
|
||||
fileFieldPrefix: string;
|
||||
disabled?: boolean;
|
||||
onSubmit: (files: Record<string, File>) => Promise<void>;
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}
|
||||
|
||||
/** Multi-file transit permit upload — import pre-booking and export post-booking. */
|
||||
export function TransitPermitMultiUpload({
|
||||
title = "Transit Permit",
|
||||
uploaded = [],
|
||||
replaceMode = false,
|
||||
submitLabel,
|
||||
fileFieldPrefix,
|
||||
disabled = false,
|
||||
onSubmit,
|
||||
onViewFile,
|
||||
onDownloadFile,
|
||||
}: TransitPermitMultiUploadProps) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const label = submitLabel ?? (replaceMode ? "Replace transit permit" : "Upload transit permit");
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={700}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{uploaded.length > 0 ? (
|
||||
<Stack gap={8}>
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
|
||||
Current file{uploaded.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
{uploaded.map((row) => (
|
||||
<PhasedUploadedFileRow
|
||||
key={row.code}
|
||||
label={row.label}
|
||||
file={row.file}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
<Paper withBorder radius="md" p="md" bg="var(--mantine-color-gray-0)">
|
||||
<PhasedMultiFileDropzone
|
||||
label="Transit permit documents"
|
||||
description={
|
||||
replaceMode
|
||||
? "Replace transit permit files — upload one or more documents (PDF or image)."
|
||||
: "Upload one or more transit permit documents (PDF or image)."
|
||||
}
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
replaceMode={replaceMode}
|
||||
disabled={disabled || loading}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={disabled || files.length === 0}
|
||||
leftSection={<Upload size={16} />}
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = Object.fromEntries(
|
||||
files.map((file, index) => [`${fileFieldPrefix}_${index}`, file]),
|
||||
) as Record<string, File>;
|
||||
await onSubmit(payload);
|
||||
setFiles([]);
|
||||
} catch {
|
||||
// Caller shows toast for upload errors.
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -319,9 +319,14 @@ export const contractsService = {
|
||||
finalizeExportClearance: (id: string) =>
|
||||
postContract<Freight.IContract>(C.CLEARANCE_FINALIZE_EXPORT(id)),
|
||||
|
||||
uploadTransportDocument: async (bookingId: string, file: File) => {
|
||||
uploadTransportDocument: async (
|
||||
bookingId: string,
|
||||
files: Record<string, File | null>,
|
||||
) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
for (const [key, file] of Object.entries(files)) {
|
||||
if (file) form.append(key, file);
|
||||
}
|
||||
const response = await client.post(C.BOOKING_TRANSPORT_DOCUMENT(bookingId), form, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user