mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile
This commit is contained in:
@@ -146,3 +146,94 @@ describe('BookingTransitionService — finalizeClearance customs output gate', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first clearance submission (AWAITING_DOCUMENTS) must include every
|
||||||
|
* required input document; subsequent re-uploads during review only need the
|
||||||
|
* specific files being fixed, so already-uploaded required docs stay in place.
|
||||||
|
*/
|
||||||
|
describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => {
|
||||||
|
const inputSetting = {
|
||||||
|
code: 'clearance_import_container_without_customs',
|
||||||
|
fields: [
|
||||||
|
{ fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true },
|
||||||
|
{ fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeService(status: string, existingCodes: string[]) {
|
||||||
|
const bookingsRepository = {
|
||||||
|
upsertDocumentReviewPending: jest.fn().mockResolvedValue(undefined),
|
||||||
|
update: jest.fn().mockResolvedValue({ id: 'b-3' }),
|
||||||
|
};
|
||||||
|
const booking = {
|
||||||
|
id: 'b-3',
|
||||||
|
status,
|
||||||
|
tradeDirection: 'IMPORT',
|
||||||
|
freightType: 'CONTAINER',
|
||||||
|
serviceType: { includesCustoms: false },
|
||||||
|
};
|
||||||
|
const bookingsService = { findById: jest.fn().mockResolvedValue(booking) };
|
||||||
|
const fileUploadSettingsService = {
|
||||||
|
getByCode: jest.fn().mockResolvedValue(inputSetting),
|
||||||
|
};
|
||||||
|
const filesService = {
|
||||||
|
findByResource: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(existingCodes.map((code) => ({ code }))),
|
||||||
|
upsertByCode: jest.fn().mockResolvedValue({ id: 'file-rec' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new BookingTransitionService(
|
||||||
|
bookingsRepository as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
filesService as never,
|
||||||
|
fileUploadSettingsService as never,
|
||||||
|
{} as never,
|
||||||
|
bookingsService as never,
|
||||||
|
);
|
||||||
|
return { service, bookingsRepository, filesService };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeFile(fieldname: string): Express.Multer.File {
|
||||||
|
return { fieldname, originalname: `${fieldname}.pdf` } as Express.Multer.File;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('rejects the first submission when a required document is missing', async () => {
|
||||||
|
const { service } = makeService('AWAITING_DOCUMENTS', []);
|
||||||
|
await expect(
|
||||||
|
service.submitClearanceDocuments('b-3', [fakeFile('commercial_invoice')]),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts the first submission when every required document is provided', async () => {
|
||||||
|
const { service, bookingsRepository } = makeService('AWAITING_DOCUMENTS', []);
|
||||||
|
await service.submitClearanceDocuments('b-3', [
|
||||||
|
fakeFile('commercial_invoice'),
|
||||||
|
fakeFile('packing_list'),
|
||||||
|
]);
|
||||||
|
expect(bookingsRepository.update).toHaveBeenCalledWith(
|
||||||
|
'b-3',
|
||||||
|
expect.objectContaining({ status: 'DOCUMENTS_UNDER_REVIEW' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows re-uploading a single queried document during review without re-sending the rest', async () => {
|
||||||
|
// packing_list was already uploaded in the first round; the customer is now
|
||||||
|
// only re-uploading the queried commercial_invoice.
|
||||||
|
const { service, bookingsRepository } = makeService(
|
||||||
|
'DOCUMENTS_UNDER_REVIEW',
|
||||||
|
['packing_list'],
|
||||||
|
);
|
||||||
|
await service.submitClearanceDocuments('b-3', [
|
||||||
|
fakeFile('commercial_invoice'),
|
||||||
|
]);
|
||||||
|
// Only the re-uploaded doc is touched — no full re-gate, no rework on the rest.
|
||||||
|
expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledTimes(1);
|
||||||
|
expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ fileKey: 'commercial_invoice' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -645,6 +645,14 @@ export class BookingTransitionService {
|
|||||||
throw new BadRequestException('No documents uploaded');
|
throw new BadRequestException('No documents uploaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First submission (nothing in review yet): every required input field must
|
||||||
|
// be provided. Once review has started (DOCUMENTS_UNDER_REVIEW) the customer
|
||||||
|
// is only fixing queried/pending docs, so the already-uploaded required docs
|
||||||
|
// stay in place and we don't re-gate on the full required set.
|
||||||
|
if (booking.status === 'AWAITING_DOCUMENTS') {
|
||||||
|
await this.assertRequiredInputsPresent(bookingId, inputCode, files);
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const record = await this.filesService.upsertByCode({
|
const record = await this.filesService.upsertByCode({
|
||||||
resourceId: bookingId,
|
resourceId: bookingId,
|
||||||
@@ -670,6 +678,41 @@ export class BookingTransitionService {
|
|||||||
return this.bookingsService.findById(bookingId);
|
return this.bookingsService.findById(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for the first clearance submission: every required field of the
|
||||||
|
* booking's customer-input set must be covered, either by a file already on
|
||||||
|
* the booking or by one in this upload batch. Keeps the customer from starting
|
||||||
|
* review with required documents missing.
|
||||||
|
*/
|
||||||
|
private async assertRequiredInputsPresent(
|
||||||
|
bookingId: string,
|
||||||
|
inputCode: string,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
): Promise<void> {
|
||||||
|
let setting;
|
||||||
|
try {
|
||||||
|
setting = await this.fileUploadSettingsService.getByCode(inputCode);
|
||||||
|
} catch {
|
||||||
|
return; // setting not seeded — nothing to enforce
|
||||||
|
}
|
||||||
|
const required = (setting.fields ?? []).filter((f) => f.isRequired);
|
||||||
|
if (required.length === 0) return;
|
||||||
|
|
||||||
|
const existing = await this.filesService.findByResource(bookingId, 'bookings');
|
||||||
|
const presentKeys = new Set<string>([
|
||||||
|
...existing.map((f) => f.code),
|
||||||
|
...files.map((f) => f.fieldname),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const missing = required.filter((f) => !presentKeys.has(f.fileKey));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
const labels = missing.map((f) => f.fileLabel).join(', ');
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Please upload all required documents before submitting: ${labels}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** GL reviews a single document: APPROVED or QUERIED (with a note). */
|
/** GL reviews a single document: APPROVED or QUERIED (with a note). */
|
||||||
async reviewDocument(
|
async reviewDocument(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
|
|||||||
@@ -203,6 +203,19 @@ export const STATUS_CONFIG: Record<string, StageConfig> = {
|
|||||||
badgeDot: "edr-green.5",
|
badgeDot: "edr-green.5",
|
||||||
action: { label: "Schedule & proceed", kind: "amber", icon: ArrowRight },
|
action: { label: "Schedule & proceed", kind: "amber", icon: ArrowRight },
|
||||||
},
|
},
|
||||||
|
OPERATION_REQUEST_PENDING: {
|
||||||
|
stage: 3,
|
||||||
|
icon: FileCheck2,
|
||||||
|
iconColor: "edr-blue",
|
||||||
|
tile: "edr-blue-soft",
|
||||||
|
hint: "Operation request under review by operations",
|
||||||
|
step: "edr-blue-dot",
|
||||||
|
badgeLabel: "Op. review",
|
||||||
|
badgeBg: "edr-blue-soft",
|
||||||
|
badgeText: "edr-blue",
|
||||||
|
badgeDot: "edr-blue-dot",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
OPERATION_REQUESTED: {
|
OPERATION_REQUESTED: {
|
||||||
stage: 3,
|
stage: 3,
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
@@ -216,6 +229,45 @@ export const STATUS_CONFIG: Record<string, StageConfig> = {
|
|||||||
badgeDot: "edr-green.5",
|
badgeDot: "edr-green.5",
|
||||||
action: { label: "View", kind: "outline" },
|
action: { label: "View", kind: "outline" },
|
||||||
},
|
},
|
||||||
|
OPERATION_CHANGES_REQUESTED: {
|
||||||
|
stage: 3,
|
||||||
|
icon: FilePen,
|
||||||
|
iconColor: "edr-amber-text",
|
||||||
|
tile: "edr-amber-soft",
|
||||||
|
hint: "Operations requested changes · please review",
|
||||||
|
step: "edr-accent",
|
||||||
|
badgeLabel: "Revise",
|
||||||
|
badgeBg: "edr-amber-soft",
|
||||||
|
badgeText: "edr-amber-text",
|
||||||
|
badgeDot: "edr-accent",
|
||||||
|
action: { label: "Review", kind: "dark" },
|
||||||
|
},
|
||||||
|
OPERATION_PRICE_PENDING_CONFIRM: {
|
||||||
|
stage: 3,
|
||||||
|
icon: Wallet,
|
||||||
|
iconColor: "edr-amber-text",
|
||||||
|
tile: "edr-amber-soft",
|
||||||
|
hint: "Price adjusted · confirm to proceed",
|
||||||
|
step: "edr-accent",
|
||||||
|
badgeLabel: "Confirm price",
|
||||||
|
badgeBg: "edr-amber-soft",
|
||||||
|
badgeText: "edr-amber-text",
|
||||||
|
badgeDot: "edr-accent",
|
||||||
|
action: { label: "Confirm", kind: "amber", icon: ArrowRight },
|
||||||
|
},
|
||||||
|
ROAD_DISPATCH_PENDING: {
|
||||||
|
stage: 3,
|
||||||
|
icon: Truck,
|
||||||
|
iconColor: "edr-blue",
|
||||||
|
tile: "edr-blue-soft",
|
||||||
|
hint: "Accepted · awaiting truck dispatch",
|
||||||
|
step: "edr-blue-dot",
|
||||||
|
badgeLabel: "Awaiting dispatch",
|
||||||
|
badgeBg: "edr-blue-soft",
|
||||||
|
badgeText: "edr-blue",
|
||||||
|
badgeDot: "edr-blue-dot",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
PNR_GENERATED: {
|
PNR_GENERATED: {
|
||||||
stage: 3,
|
stage: 3,
|
||||||
icon: FileCheck2,
|
icon: FileCheck2,
|
||||||
@@ -372,6 +424,84 @@ export const STATUS_CONFIG: Record<string, StageConfig> = {
|
|||||||
badgeDot: "edr-green.5",
|
badgeDot: "edr-green.5",
|
||||||
action: { label: "View", kind: "outline" },
|
action: { label: "View", kind: "outline" },
|
||||||
},
|
},
|
||||||
|
PRICE_CHANGED_PENDING_CONFIRM: {
|
||||||
|
stage: 1,
|
||||||
|
icon: Wallet,
|
||||||
|
iconColor: "edr-amber-text",
|
||||||
|
tile: "edr-amber-soft",
|
||||||
|
hint: "Price changed · confirm to continue",
|
||||||
|
step: "edr-accent",
|
||||||
|
badgeLabel: "Confirm price",
|
||||||
|
badgeBg: "edr-amber-soft",
|
||||||
|
badgeText: "edr-amber-text",
|
||||||
|
badgeDot: "edr-accent",
|
||||||
|
action: { label: "Confirm", kind: "amber", icon: ArrowRight },
|
||||||
|
},
|
||||||
|
READY_FOR_ASSIGNMENT: {
|
||||||
|
stage: 2,
|
||||||
|
icon: FileCheck2,
|
||||||
|
iconColor: "edr-blue",
|
||||||
|
tile: "edr-blue-soft",
|
||||||
|
hint: "Approved · awaiting wagon assignment",
|
||||||
|
step: "edr-blue-dot",
|
||||||
|
badgeLabel: "Assigning",
|
||||||
|
badgeBg: "edr-blue-soft",
|
||||||
|
badgeText: "edr-blue",
|
||||||
|
badgeDot: "edr-blue-dot",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
|
WAGON_ASSIGNED: {
|
||||||
|
stage: 3,
|
||||||
|
icon: CheckCircle2,
|
||||||
|
iconColor: "edr-green.7",
|
||||||
|
tile: "edr-soft",
|
||||||
|
hint: "Wagon assigned · preparing for loading",
|
||||||
|
step: "edr-green.5",
|
||||||
|
badgeLabel: "Wagon assigned",
|
||||||
|
badgeBg: "edr-soft",
|
||||||
|
badgeText: "edr-green.7",
|
||||||
|
badgeDot: "edr-green.5",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
|
INVOICED: {
|
||||||
|
stage: 3,
|
||||||
|
icon: Wallet,
|
||||||
|
iconColor: "edr-blue",
|
||||||
|
tile: "edr-blue-soft",
|
||||||
|
hint: "Invoice issued · awaiting payment",
|
||||||
|
step: "edr-blue-dot",
|
||||||
|
badgeLabel: "Invoiced",
|
||||||
|
badgeBg: "edr-blue-soft",
|
||||||
|
badgeText: "edr-blue",
|
||||||
|
badgeDot: "edr-blue-dot",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
|
CONTRACT_ACTIVE: {
|
||||||
|
stage: 3,
|
||||||
|
icon: CheckCircle2,
|
||||||
|
iconColor: "edr-green.7",
|
||||||
|
tile: "edr-soft",
|
||||||
|
hint: "Contract active · accepting orders",
|
||||||
|
step: "edr-green.5",
|
||||||
|
badgeLabel: "Active",
|
||||||
|
badgeBg: "edr-soft",
|
||||||
|
badgeText: "edr-green.7",
|
||||||
|
badgeDot: "edr-green.5",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
|
CONTRACT_CLOSED: {
|
||||||
|
stage: 4,
|
||||||
|
icon: CheckCircle2,
|
||||||
|
iconColor: "edr-slate",
|
||||||
|
tile: "edr-slate-soft2",
|
||||||
|
hint: "Contract closed · quantity used or window elapsed",
|
||||||
|
step: "edr-step",
|
||||||
|
badgeLabel: "Closed",
|
||||||
|
badgeBg: "edr-slate-soft2",
|
||||||
|
badgeText: "edr-slate",
|
||||||
|
badgeDot: "edr-step",
|
||||||
|
action: { label: "View", kind: "outline" },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACTION_PROPS: Record<
|
export const ACTION_PROPS: Record<
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
|||||||
leftSection={<Upload size={16} />}
|
leftSection={<Upload size={16} />}
|
||||||
onClick={() => flow.submitDocuments()}
|
onClick={() => flow.submitDocuments()}
|
||||||
loading={flow.uploadMutation.isPending}
|
loading={flow.uploadMutation.isPending}
|
||||||
disabled={!flow.hasStagedFiles}
|
disabled={!flow.canSubmit}
|
||||||
>
|
>
|
||||||
Submit documents
|
Submit documents
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -177,6 +177,61 @@ export const STATUS_MAP: Record<
|
|||||||
description: "Cargo has been consolidated with a partner shipment.",
|
description: "Cargo has been consolidated with a partner shipment.",
|
||||||
stage: 5,
|
stage: 5,
|
||||||
},
|
},
|
||||||
|
AWAITING_DOCUMENTS: {
|
||||||
|
title: "Clearance documents needed",
|
||||||
|
description:
|
||||||
|
"Upload the required clearance documents so your shipment can be reviewed.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
DOCUMENTS_UNDER_REVIEW: {
|
||||||
|
title: "Documents under review",
|
||||||
|
description:
|
||||||
|
"Your clearance documents are being reviewed. Re-upload any queried documents to proceed.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
CLEARANCE_READY: {
|
||||||
|
title: "Cleared — choose a shipment day",
|
||||||
|
description:
|
||||||
|
"Clearance is complete. Pick a shipment day and proceed to operation.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
OPERATION_REQUESTED: {
|
||||||
|
title: "Operation requested",
|
||||||
|
description: "Operation requested. An operator will take your shipment forward.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
CONTRACT_ACTIVE: {
|
||||||
|
title: "Contract active",
|
||||||
|
description: "This general contract is active and accepting drawdown orders.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
CONTRACT_CLOSED: {
|
||||||
|
title: "Contract closed",
|
||||||
|
description:
|
||||||
|
"This general contract is closed — its reserved quantity has been used or its window has elapsed.",
|
||||||
|
stage: 7,
|
||||||
|
},
|
||||||
|
PRICE_CHANGED_PENDING_CONFIRM: {
|
||||||
|
title: "Price changed — confirm to proceed",
|
||||||
|
description:
|
||||||
|
"The price for this booking changed. Confirm the new price to continue.",
|
||||||
|
stage: 1,
|
||||||
|
},
|
||||||
|
READY_FOR_ASSIGNMENT: {
|
||||||
|
title: "Awaiting wagon assignment",
|
||||||
|
description: "Approved and queued for wagon assignment.",
|
||||||
|
stage: 2,
|
||||||
|
},
|
||||||
|
WAGON_ASSIGNED: {
|
||||||
|
title: "Wagon assigned",
|
||||||
|
description: "A wagon has been assigned and your cargo is being prepared for loading.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
|
INVOICED: {
|
||||||
|
title: "Invoice issued",
|
||||||
|
description: "An invoice has been issued for this booking.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
COMPLETED: {
|
COMPLETED: {
|
||||||
title: "Service complete",
|
title: "Service complete",
|
||||||
description: "Cargo delivered and service successfully terminated.",
|
description: "Cargo delivered and service successfully terminated.",
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function BookingActionModalBody({
|
|||||||
leftSection={<Upload size={16} />}
|
leftSection={<Upload size={16} />}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={flow.uploadMutation.isPending}
|
loading={flow.uploadMutation.isPending}
|
||||||
disabled={!flow.hasStagedFiles}
|
disabled={!flow.canSubmit}
|
||||||
>
|
>
|
||||||
Submit documents
|
Submit documents
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -90,9 +90,11 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
|
|||||||
glDocs,
|
glDocs,
|
||||||
isReady,
|
isReady,
|
||||||
canUpload,
|
canUpload,
|
||||||
|
isInitialUpload,
|
||||||
status,
|
status,
|
||||||
pending,
|
pending,
|
||||||
adHoc,
|
adHoc,
|
||||||
|
missingRequired,
|
||||||
stagePending,
|
stagePending,
|
||||||
addAdHocRow,
|
addAdHocRow,
|
||||||
setAdHocName,
|
setAdHocName,
|
||||||
@@ -116,14 +118,23 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
|
|||||||
) : status === "DOCUMENTS_UNDER_REVIEW" ? (
|
) : status === "DOCUMENTS_UNDER_REVIEW" ? (
|
||||||
<Alert color="blue" radius="md" icon={<Clock size={18} />} mb="md">
|
<Alert color="blue" radius="md" icon={<Clock size={18} />} mb="md">
|
||||||
{clearance.includesCustoms
|
{clearance.includesCustoms
|
||||||
? "Global Logistics is reviewing your documents and will clear your shipment. Queried documents below need to be re-uploaded."
|
? "Global Logistics is reviewing your documents and will clear your shipment. Only re-upload the documents flagged with a query below — approved documents stay as they are."
|
||||||
: "Our team is reviewing your documents. Queried documents below need to be re-uploaded."}
|
: "Our team is reviewing your documents. Only re-upload the documents flagged with a query below — approved documents stay as they are."}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Alert color="yellow" radius="md" icon={<AlertCircle size={18} />} mb="md">
|
<Alert color="yellow" radius="md" icon={<AlertCircle size={18} />} mb="md">
|
||||||
{clearance.includesCustoms
|
{clearance.includesCustoms
|
||||||
? "Upload the documents customs needs — Global Logistics will clear your shipment and return the cleared documents here."
|
? "Upload every required document customs needs (marked *) to start the review. Global Logistics will clear your shipment and return the cleared documents here."
|
||||||
: "Upload all the required clearance documents below to start the review."}
|
: "Upload every required clearance document (marked *) below to start the review."}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isInitialUpload && missingRequired.length > 0 && (
|
||||||
|
<Alert color="yellow" variant="light" radius="md" mb="md" p="xs">
|
||||||
|
<Text fz="12px" c="#9A5B00">
|
||||||
|
Still required:{" "}
|
||||||
|
{missingRequired.map((d) => d.label).join(", ")}
|
||||||
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,32 @@ export function useClearanceFlow(booking: Freight.IBooking) {
|
|||||||
const isReady = status === "CLEARANCE_READY";
|
const isReady = status === "CLEARANCE_READY";
|
||||||
const canUpload =
|
const canUpload =
|
||||||
status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW";
|
status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW";
|
||||||
|
// The very first upload (nothing in review yet). Here every required document
|
||||||
|
// must be provided. Once GL has started reviewing (DOCUMENTS_UNDER_REVIEW) the
|
||||||
|
// customer is only re-uploading queried/pending docs, so we don't re-gate on
|
||||||
|
// the full required set.
|
||||||
|
const isInitialUpload = status === "AWAITING_DOCUMENTS";
|
||||||
|
|
||||||
const hasStagedFiles =
|
const hasStagedFiles =
|
||||||
Object.keys(pending).length > 0 || adHoc.some((r) => r.file);
|
Object.keys(pending).length > 0 || adHoc.some((r) => r.file);
|
||||||
|
|
||||||
|
// A required customer document is satisfied when it already has an uploaded
|
||||||
|
// file or the customer has just staged one for this submission.
|
||||||
|
const missingRequired = useMemo(
|
||||||
|
() =>
|
||||||
|
customerDocs.filter(
|
||||||
|
(d) => d.required && !d.file && !pending[d.fileKey],
|
||||||
|
),
|
||||||
|
[customerDocs, pending],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial upload: block submit until every required field has a file (and at
|
||||||
|
// least one file is actually staged to send). Re-upload rounds only need at
|
||||||
|
// least one staged file — the customer fixes the specific queried documents.
|
||||||
|
const canSubmit = isInitialUpload
|
||||||
|
? hasStagedFiles && missingRequired.length === 0
|
||||||
|
: hasStagedFiles;
|
||||||
|
|
||||||
// --- staged-upload mutators ----------------------------------------------
|
// --- staged-upload mutators ----------------------------------------------
|
||||||
|
|
||||||
const stagePending = (fileKey: string, file: File) =>
|
const stagePending = (fileKey: string, file: File) =>
|
||||||
@@ -117,10 +139,13 @@ export function useClearanceFlow(booking: Freight.IBooking) {
|
|||||||
glDocs,
|
glDocs,
|
||||||
isReady,
|
isReady,
|
||||||
canUpload,
|
canUpload,
|
||||||
|
isInitialUpload,
|
||||||
// staged upload state
|
// staged upload state
|
||||||
pending,
|
pending,
|
||||||
adHoc,
|
adHoc,
|
||||||
hasStagedFiles,
|
hasStagedFiles,
|
||||||
|
missingRequired,
|
||||||
|
canSubmit,
|
||||||
stagePending,
|
stagePending,
|
||||||
addAdHocRow,
|
addAdHocRow,
|
||||||
setAdHocName,
|
setAdHocName,
|
||||||
|
|||||||
Reference in New Issue
Block a user