mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 13:28:11 +00:00
fix transi permit file upload
This commit is contained in:
@@ -863,14 +863,14 @@ export class ContractsController {
|
|||||||
|
|
||||||
@Post('bookings/:bookingId/transport-document')
|
@Post('bookings/:bookingId/transport-document')
|
||||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(AnyFilesInterceptor())
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
@ApiOperation({ summary: 'GL ET uploads export transport document after wagon allocation' })
|
@ApiOperation({ summary: 'GL ET uploads export transit permit documents (multi-file)' })
|
||||||
uploadTransportDocument(
|
uploadTransportDocument(
|
||||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFiles() files: Express.Multer.File[],
|
||||||
) {
|
) {
|
||||||
return this.glOperationsService.uploadTransportDocument(bookingId, file);
|
return this.glOperationsService.uploadTransportDocument(bookingId, files ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bookings/:bookingId/documents')
|
@Post('bookings/:bookingId/documents')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
IncidentType,
|
IncidentType,
|
||||||
} from './entities/clearance-incident.entity';
|
} from './entities/clearance-incident.entity';
|
||||||
import { ClearanceMilestoneService } from './clearance-milestone.service';
|
import { ClearanceMilestoneService } from './clearance-milestone.service';
|
||||||
|
import { persistExportTransportUploads } from './phased-clearance.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a GL post-booking document `code` to the milestone it auto-completes when
|
* Maps a GL post-booking document `code` to the milestone it auto-completes when
|
||||||
@@ -165,7 +166,7 @@ export class GlOperationsService {
|
|||||||
*/
|
*/
|
||||||
async uploadTransportDocument(
|
async uploadTransportDocument(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
file: Express.Multer.File,
|
files: Express.Multer.File[],
|
||||||
): Promise<{ uploaded: boolean; milestoneCompleted: boolean }> {
|
): Promise<{ uploaded: boolean; milestoneCompleted: boolean }> {
|
||||||
const booking = await this.getBooking(bookingId);
|
const booking = await this.getBooking(bookingId);
|
||||||
if (booking.tradeDirection !== 'EXPORT') {
|
if (booking.tradeDirection !== 'EXPORT') {
|
||||||
@@ -182,14 +183,11 @@ export class GlOperationsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file) throw new BadRequestException('No transport document uploaded');
|
if (files.length === 0) {
|
||||||
|
throw new BadRequestException('No transit permit documents uploaded');
|
||||||
|
}
|
||||||
|
|
||||||
await this.filesService.upsertByCode({
|
await persistExportTransportUploads(this.filesService, bookingId, files);
|
||||||
resourceId: bookingId,
|
|
||||||
resource: 'bookings',
|
|
||||||
code: 'export_transport_document',
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (wagonAllocated && wagonAllocated.status !== 'COMPLETED') {
|
if (wagonAllocated && wagonAllocated.status !== 'COMPLETED') {
|
||||||
await this.milestoneService.completeForBooking(bookingId, 'WAGON_ALLOCATED');
|
await this.milestoneService.completeForBooking(bookingId, 'WAGON_ALLOCATED');
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
declarationFileLabel,
|
declarationFileLabel,
|
||||||
isDeclarationFileCode,
|
isDeclarationFileCode,
|
||||||
isImportTransitPermitFileCode,
|
isImportTransitPermitFileCode,
|
||||||
|
isExportTransportFileCode,
|
||||||
|
exportTransportFileLabel,
|
||||||
transitPermitFileLabel,
|
transitPermitFileLabel,
|
||||||
type ClearanceWorkflowFile,
|
type ClearanceWorkflowFile,
|
||||||
} from '@edr/types';
|
} from '@edr/types';
|
||||||
@@ -114,6 +116,50 @@ export async function persistTransitPermitUploads(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Require at least one export transport document in the upload batch. */
|
||||||
|
export function assertExportTransportFiles(files: Express.Multer.File[]): void {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new BadRequestException('No transit permit documents uploaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeExportTransportFieldNames(
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
): Express.Multer.File[] {
|
||||||
|
return files.map((file, index) => ({
|
||||||
|
...file,
|
||||||
|
fieldname: `export_transport_document_${index}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace all export transport documents on a booking with a new multi-file batch. */
|
||||||
|
export async function persistExportTransportUploads(
|
||||||
|
store: DeclarationFileStore,
|
||||||
|
bookingId: string,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
): Promise<void> {
|
||||||
|
const normalized = normalizeExportTransportFieldNames(files);
|
||||||
|
assertExportTransportFiles(normalized);
|
||||||
|
|
||||||
|
const existing = await store.findByResource(bookingId, 'bookings');
|
||||||
|
await Promise.all(
|
||||||
|
existing
|
||||||
|
.filter((f) => f.code && isExportTransportFileCode(f.code))
|
||||||
|
.map((f) => store.deleteByCode(bookingId, 'bookings', f.code!)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
normalized.map((file, index) =>
|
||||||
|
store.upload({
|
||||||
|
resourceId: bookingId,
|
||||||
|
resource: 'bookings',
|
||||||
|
code: `export_transport_document_${index}`,
|
||||||
|
file,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseDutyRequiredForm(value: string | boolean | undefined): boolean {
|
export function parseDutyRequiredForm(value: string | boolean | undefined): boolean {
|
||||||
if (typeof value === 'boolean') return value;
|
if (typeof value === 'boolean') return value;
|
||||||
if (value === undefined || value === '') return false;
|
if (value === undefined || value === '') return false;
|
||||||
@@ -251,5 +297,23 @@ export function buildWorkflowFiles(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tradeDirection === 'EXPORT') {
|
||||||
|
const extraExportTransport = files
|
||||||
|
.filter((f) => f.code && isExportTransportFileCode(f.code) && !included.has(f.code))
|
||||||
|
.sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));
|
||||||
|
|
||||||
|
extraExportTransport.forEach((file, index) => {
|
||||||
|
if (!file.code) return;
|
||||||
|
included.add(file.code);
|
||||||
|
out.push({
|
||||||
|
code: file.code,
|
||||||
|
label: exportTransportFileLabel(file.code, index),
|
||||||
|
uploadedBy: 'gl_et',
|
||||||
|
category: 'transit',
|
||||||
|
file: { id: file.id, name: file.name, url: file.url },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class PaymentClientService {
|
|||||||
private readonly logger = new Logger(PaymentClientService.name);
|
private readonly logger = new Logger(PaymentClientService.name);
|
||||||
private readonly baseUrl = (
|
private readonly baseUrl = (
|
||||||
// process.env.PAYMENT_API_URL ??
|
// process.env.PAYMENT_API_URL ??
|
||||||
// "https://paymentcallback.triaplc.com"
|
"https://paymentcallback.triaplc.com"
|
||||||
"http://localhost:3003"
|
// "http://localhost:3003"
|
||||||
).replace(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
private readonly serviceToken = process.env.SERVICE_AUTH_TOKEN ?? "";
|
private readonly serviceToken = process.env.SERVICE_AUTH_TOKEN ?? "";
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { PaymentEntity } from "./entities/payment.entity";
|
|||||||
import { PaymentRepository } from "./payment.repository";
|
import { PaymentRepository } from "./payment.repository";
|
||||||
import { PaymentClientService } from "./payment-client.service";
|
import { PaymentClientService } from "./payment-client.service";
|
||||||
import { BillingService } from "../billing/billing.service";
|
import { BillingService } from "../billing/billing.service";
|
||||||
import { FirstMileService } from "../first-mile/first-mile.service";
|
|
||||||
import { BookingBatchService } from "../train-scheduling/booking-batch.service";
|
|
||||||
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
@@ -108,9 +106,6 @@ export class PaymentService {
|
|||||||
private readonly paymentClient: PaymentClientService,
|
private readonly paymentClient: PaymentClientService,
|
||||||
@Inject(forwardRef(() => BillingService))
|
@Inject(forwardRef(() => BillingService))
|
||||||
private readonly billing: BillingService,
|
private readonly billing: BillingService,
|
||||||
@Inject(forwardRef(() => BookingBatchService))
|
|
||||||
private readonly bookingBatchService: BookingBatchService,
|
|
||||||
private readonly firstMileService: FirstMileService,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async getAll(filters: {
|
async getAll(filters: {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||||
|
import {
|
||||||
|
TransitPermitMultiUpload,
|
||||||
|
type TransitPermitUploadedRow,
|
||||||
|
} from "@/components/contracts/TransitPermitMultiUpload";
|
||||||
import { DateInput } from "@mantine/dates";
|
import { DateInput } from "@mantine/dates";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -332,7 +336,7 @@ export function PhasedClearanceActionPanel({
|
|||||||
>
|
>
|
||||||
{showEt &&
|
{showEt &&
|
||||||
canEt &&
|
canEt &&
|
||||||
!clearance.bookingReady &&
|
!bookingCreated &&
|
||||||
(activeStep >= 4 ||
|
(activeStep >= 4 ||
|
||||||
isMilestoneDone(clearance.milestones, "TRANSIT_PERMIT_UPLOADED")) ? (
|
isMilestoneDone(clearance.milestones, "TRANSIT_PERMIT_UPLOADED")) ? (
|
||||||
<TransitPermitStep
|
<TransitPermitStep
|
||||||
@@ -839,79 +843,30 @@ function TransitPermitStep({
|
|||||||
onViewFile?: (file: { name: string; url: string }) => void;
|
onViewFile?: (file: { name: string; url: string }) => void;
|
||||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||||
}) {
|
}) {
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const uploaded: TransitPermitUploadedRow[] = importTransitFilesFromWorkflow(workflowFiles);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const uploaded = importTransitFilesFromWorkflow(workflowFiles);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<TransitPermitMultiUpload
|
||||||
<Text size="sm" fw={700}>
|
uploaded={uploaded}
|
||||||
Transit Permit
|
replaceMode={replaceMode}
|
||||||
</Text>
|
fileFieldPrefix="transit_permit"
|
||||||
|
onViewFile={onViewFile}
|
||||||
{uploaded.length > 0 ? (
|
onDownloadFile={onDownloadFile}
|
||||||
<Stack gap={8}>
|
onSubmit={async (payload) => {
|
||||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
|
try {
|
||||||
Current file{uploaded.length > 1 ? "s" : ""}
|
if (isBooking) {
|
||||||
</Text>
|
await bookingsService.uploadTransitPermit(entityId, payload);
|
||||||
{uploaded.map((row) => (
|
} else {
|
||||||
<PhasedUploadedFileRow
|
await contractsService.uploadContractTransitPermit(entityId, payload);
|
||||||
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)."
|
|
||||||
}
|
}
|
||||||
value={files}
|
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
||||||
onChange={setFiles}
|
onChanged?.();
|
||||||
replaceMode={replaceMode}
|
} catch (e) {
|
||||||
disabled={loading}
|
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||||
/>
|
throw e;
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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({
|
function ExportTransitPermitStep({
|
||||||
bookingId,
|
bookingId,
|
||||||
workflowFiles = [],
|
workflowFiles = [],
|
||||||
@@ -1335,50 +1302,43 @@ function ExportTransitPermitStep({
|
|||||||
onViewFile?: (file: { name: string; url: string }) => void;
|
onViewFile?: (file: { name: string; url: string }) => void;
|
||||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||||
}) {
|
}) {
|
||||||
const [files, setFiles] = useState<Record<string, File | null>>({
|
const uploaded = exportTransitFilesFromWorkflow(workflowFiles);
|
||||||
export_transport_document: null,
|
const hasUploaded = uploaded.length > 0;
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const hasFile = Boolean(files.export_transport_document);
|
|
||||||
const existing = findWorkflowFile(workflowFiles, "export_transport_document");
|
|
||||||
|
|
||||||
if (existing && !replaceMode) {
|
if (hasUploaded && !replaceMode) {
|
||||||
return (
|
return (
|
||||||
<PhasedUploadedFileRow
|
<Stack gap="sm">
|
||||||
label="Transit permit"
|
<Text size="sm" fw={700}>
|
||||||
file={existing}
|
Transit Permit
|
||||||
onView={onViewFile}
|
</Text>
|
||||||
onDownload={onDownloadFile}
|
{uploaded.map((row) => (
|
||||||
/>
|
<PhasedUploadedFileRow
|
||||||
|
key={row.code}
|
||||||
|
label={row.label}
|
||||||
|
file={row.file}
|
||||||
|
onView={onViewFile}
|
||||||
|
onDownload={onDownloadFile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhasedDocumentUploadField
|
<TransitPermitMultiUpload
|
||||||
fields={[{ key: "export_transport_document", label: "Transit permit" }]}
|
replaceMode={replaceMode || hasUploaded}
|
||||||
files={files}
|
uploaded={uploaded}
|
||||||
onChange={(key, file) => setFiles((prev) => ({ ...prev, [key]: file }))}
|
fileFieldPrefix="export_transport_document"
|
||||||
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"}
|
|
||||||
onViewFile={onViewFile}
|
onViewFile={onViewFile}
|
||||||
onDownloadFile={onDownloadFile}
|
onDownloadFile={onDownloadFile}
|
||||||
onSubmit={async () => {
|
onSubmit={async (payload) => {
|
||||||
const file = files.export_transport_document;
|
|
||||||
if (!file) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
await contractsService.uploadTransportDocument(bookingId, file);
|
await contractsService.uploadTransportDocument(bookingId, payload);
|
||||||
setFiles({ export_transport_document: null });
|
|
||||||
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
toast.success(replaceMode ? "Transit permit updated" : "Transit permit uploaded");
|
||||||
onChanged?.();
|
onChanged?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||||
} finally {
|
throw e;
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) =>
|
finalizeExportClearance: (id: string) =>
|
||||||
postContract<Freight.IContract>(C.CLEARANCE_FINALIZE_EXPORT(id)),
|
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();
|
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, {
|
const response = await client.post(C.BOOKING_TRANSPORT_DOCUMENT(bookingId), form, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,6 +101,26 @@ export function transitPermitFileLabel(code: string, index?: number): string {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const LEGACY_EXPORT_TRANSPORT_CODE = "export_transport_document";
|
||||||
|
|
||||||
|
export function isExportTransportFileCode(code: string | null | undefined): boolean {
|
||||||
|
if (!code) return false;
|
||||||
|
const lower = code.toLowerCase();
|
||||||
|
return (
|
||||||
|
lower === LEGACY_EXPORT_TRANSPORT_CODE ||
|
||||||
|
lower.startsWith(`${LEGACY_EXPORT_TRANSPORT_CODE}_`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportTransportFileLabel(code: string, index?: number): string {
|
||||||
|
const lower = code.toLowerCase();
|
||||||
|
if (lower === LEGACY_EXPORT_TRANSPORT_CODE) return "Transit Permit";
|
||||||
|
if (lower.startsWith(`${LEGACY_EXPORT_TRANSPORT_CODE}_`)) {
|
||||||
|
return index != null ? `Transit permit ${index + 1}` : "Transit Permit";
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
export function catalogEntriesForTradeDirection(
|
export function catalogEntriesForTradeDirection(
|
||||||
tradeDirection: string,
|
tradeDirection: string,
|
||||||
): ClearanceWorkflowFileCatalogEntry[] {
|
): ClearanceWorkflowFileCatalogEntry[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user