fix transi permit file upload

This commit is contained in:
marshal
2026-07-02 12:22:07 +03:00
parent 4fefe4f827
commit 56bb75c9a8
9 changed files with 271 additions and 123 deletions

View File

@@ -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;
}
}}
/>

View File

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