diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx
index bb50168ad..d8c0577fd 100644
--- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx
@@ -44,9 +44,9 @@ const STATUS_META: Record<
Freight.DocumentReviewStatus,
{ label: string; color: string }
> = {
- APPROVED: { label: "Approved", color: "edr-blue" },
+ APPROVED: { label: "Approved", color: "edr-green" },
QUERIED: { label: "Queried", color: "red" },
- PENDING: { label: "Pending", color: "edr-accent" },
+ PENDING: { label: "Pending", color: "gray" },
};
/**
@@ -142,7 +142,7 @@ export function ClearanceReviewSection({
if (isLoading || !clearance) {
return (
-
+
Loading clearance…
);
@@ -165,15 +165,15 @@ export function ClearanceReviewSection({
-
+
-
+
)}
@@ -219,13 +219,13 @@ export function ClearanceReviewSection({
icon={Upload}
title="Customs output documents"
subtitle="Upload the cleared/customs paperwork to hand back to the customer."
- accent="edr-accent"
+ accent="edr-green"
>
{glDocs.map((doc) => (
-
+
{doc.label}
{doc.required ? " *" : ""}
@@ -239,7 +239,7 @@ export function ClearanceReviewSection({
href={doc.file.url}
target="_blank"
rel="noreferrer"
- c="edr-accent"
+ c="edr-green"
style={{ display: "flex" }}
>
@@ -261,7 +261,7 @@ export function ClearanceReviewSection({
{...props}
size="compact-xs"
variant="light"
- color="edr-accent"
+ color="edr-green"
leftSection={}
>
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
@@ -275,7 +275,7 @@ export function ClearanceReviewSection({
}
disabled={Object.keys(outputFiles).length === 0}
@@ -301,7 +301,7 @@ export function ClearanceReviewSection({
@@ -314,7 +314,7 @@ export function ClearanceReviewSection({
}
disabled={!clearance.allApproved}
@@ -391,7 +391,7 @@ function DocReviewCard({
status === "QUERIED"
? "var(--mantine-color-red-2)"
: status === "APPROVED"
- ? "var(--mantine-color-edr-blue-2)"
+ ? "var(--mantine-color-edr-green-2)"
: "var(--mantine-color-edr-border-6)",
}}
>
@@ -399,7 +399,7 @@ function DocReviewCard({
@@ -471,7 +471,7 @@ function DocReviewCard({
}
disabled={busy}
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
index fa18a440d..72679aea8 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
@@ -64,7 +64,7 @@ export default function DocumentClearanceDetailPage() {
return (
-
+
Loading clearance…
@@ -103,7 +103,7 @@ export default function DocumentClearanceDetailPage() {
clearance.allApproved ? (
}
>
@@ -112,7 +112,7 @@ export default function DocumentClearanceDetailPage() {
) : (
}
>
@@ -133,13 +133,17 @@ export default function DocumentClearanceDetailPage() {
{/* RIGHT — sticky progress gauge */}
-
+
@@ -153,7 +157,7 @@ export default function DocumentClearanceDetailPage() {
/>
@@ -163,7 +167,7 @@ export default function DocumentClearanceDetailPage() {
value={stats.queried}
/>
@@ -199,7 +203,7 @@ function ClearanceHero({
-
+
@@ -210,7 +214,7 @@ function ClearanceHero({
{direction}
@@ -219,7 +223,7 @@ function ClearanceHero({
}
>
@@ -248,7 +252,7 @@ function ClearanceHero({
{stats.approved}/{stats.total}
-
+
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
index 475d04749..4d04e4c9c 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceListPage.tsx
@@ -15,6 +15,7 @@ import {
Text,
TextInput,
ThemeIcon,
+ Tooltip,
} from "@mantine/core";
import {
ArrowRight,
@@ -101,8 +102,29 @@ function formatDate(iso?: string): string {
});
}
-function directionColor(direction: string): string {
- return direction === "IMPORT" ? "edr-blue" : "edr-accent";
+/**
+ * Icon-only chip for a booking's trade direction — Truck for import, ShipWheel
+ * for export — on a light background, matching the "awaiting review" badge
+ * styling. Keeps the cards within the white / light-gray / green palette and
+ * drops the text label in favour of a tooltip.
+ */
+function DirectionIcon({ direction }: { direction: string }) {
+ const isImport = direction === "IMPORT";
+ const Icon = isImport ? Truck : ShipWheel;
+ const label = isImport ? "Import" : direction === "EXPORT" ? "Export" : "—";
+ return (
+
+
+
+
+
+ );
}
export default function DocumentClearanceListPage() {
@@ -204,15 +226,8 @@ export default function DocumentClearanceListPage() {
{r.destinationLabel}
-
-
- {r.tradeDirection}
-
+
+
{r.freightType}
@@ -235,7 +250,7 @@ export default function DocumentClearanceListPage() {
id: "status",
header: () => Status,
cell: () => (
-
+
Under review
),
@@ -296,13 +311,13 @@ export default function DocumentClearanceListPage() {
label: "Import",
value: tabCounts.import,
icon: Truck,
- color: "edr-blue",
+ color: "edr-green",
},
{
label: "Export",
value: tabCounts.export,
icon: ShipWheel,
- color: "edr-accent",
+ color: "gray",
},
]}
/>
@@ -528,7 +543,7 @@ function ClearanceCard({
-
+
Under review
@@ -554,28 +569,23 @@ function ClearanceCard({
-
-
- {row.tradeDirection}
-
+
+
{row.freightType}
{row.hasCustoms ? (
- }
- >
- Customs
-
+
+
+
+
+
) : null}
diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx
index 54bc644ca..ab26c9365 100644
--- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx
+++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx
@@ -5,6 +5,10 @@ import { Stepper } from "./Stepper";
import { PayNowButton } from "@/pages/bookings/payments/PayNowButton";
import { BookingActionButton } from "@/pages/bookings/clearance/BookingActionButton";
import { bookingHasInlineAction } from "@/pages/bookings/clearance/bookingNextAction";
+import {
+ ContractSignButton,
+ bookingIsSignable,
+} from "@/pages/bookings/contract/ContractSignButton";
interface BookingRowProps {
booking: any;
@@ -28,6 +32,9 @@ export const BookingRow = memo(function BookingRow({
// Clearance/operation steps + changes-requested resubmit can be done in place
// via a modal on the row.
const hasInlineAction = bookingHasInlineAction(booking);
+ // Contract ready for signature → "View & sign" jumps straight to the
+ // full-page contract viewer where the signature flow lives.
+ const canSign = bookingIsSignable(booking);
const origin = booking.originYard?.label ?? booking.originYard?.code ?? "—";
const dest =
booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—";
@@ -90,6 +97,8 @@ export const BookingRow = memo(function BookingRow({
{canPay ? (
+ ) : canSign ? (
+
) : hasInlineAction ? (
) : (
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
index 2838d872c..0599d685e 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
@@ -1,6 +1,14 @@
-import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core";
+import {
+ Alert,
+ Button,
+ Group,
+ Modal,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { Send, XCircle } from "lucide-react";
+import { AlertCircle, Send, XCircle } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
@@ -91,12 +99,23 @@ export function ChangesRequestedView({
Your documents
- These are the documents you submitted for this booking. Replace
- any you need to update, then resubmit for review.
+ Update the documents for this booking, then resubmit for review.
+ Replace any that changed and attach any that are still required.
+ {flow.validationError && (
+ }
+ mt="md"
+ >
+ {flow.validationError}
+
+ )}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitBookingModal.tsx b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitBookingModal.tsx
index 1481440c9..fd46cbe97 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitBookingModal.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitBookingModal.tsx
@@ -72,15 +72,13 @@ function ResubmitBookingModalBody({
)}
-
-
- Your documents
-
-
- Replace any document you need to update, then resubmit.
-
-
-
+
+
+ {flow.validationError && (
+ }>
+ {flow.validationError}
+
+ )}
{flow.mutations.some((m) => m.isError) && (
}>
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitDocuments.tsx b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitDocuments.tsx
index 745c0767c..b6eaaac81 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitDocuments.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/ResubmitDocuments.tsx
@@ -1,97 +1,123 @@
-import { ActionIcon, Box, Button, Group, Text } from "@mantine/core";
-import { Download, Upload, X } from "lucide-react";
-import { useRef } from "react";
+import { Box, Group, Loader, Stack, Text } from "@mantine/core";
+import { SmartFileInput } from "@edr/ui-common";
+import { CheckCircle2, Download, FileText } from "lucide-react";
-import { DocRow, IconSquare } from "../BookingDetailPage/components/Documents";
+import { IconSquare } from "../BookingDetailPage/components/Documents";
+import { labelForDocCode } from "./resubmitDocs";
import type { ResubmitFlowController } from "./useResubmitFlow";
/**
- * Document list for resubmitting a CHANGES_REQUESTED booking. Renders exactly
- * the files the customer originally submitted (`booking.files`, surfaced through
- * the flow controller) and lets them replace any of them before resubmitting.
+ * Document section for resubmitting a CHANGES_REQUESTED booking.
*
- * No fixed required list and no "X of N" gate — the customer updates what they
- * actually provided.
+ * Mirrors the new-booking document step: the fields come from the company's
+ * onboarding document setting (TIN, license, ID, passport, …) rendered via
+ * SmartFileInput. Documents already submitted on the booking are shown as an
+ * "on file" reference; the customer uploads here only to replace one, or to fill
+ * any required field that has nothing on file yet (those block resubmit).
*/
export function ResubmitDocuments({ flow }: { flow: ResubmitFlowController }) {
- const { rows, replacements, setReplacement } = flow;
- const inputRefs = useRef>({});
+ const { files, setting, settingLoading, documents, setDocuments, fieldErrors } =
+ flow;
- if (rows.length === 0) {
- return (
-
- No documents were submitted on this booking.
-
- );
- }
+ // One reference row per distinct doc already on the booking (latest upload).
+ const onFile = dedupeLatestByCode(files);
return (
-
- {rows.map((row, i) => {
- const replaced = replacements[row.key];
- return (
-
- }
- />
- {
- inputRefs.current[row.key] = el;
- }}
- type="file"
- accept=".pdf,.jpg,.jpeg,.png"
- style={{ display: "none" }}
- onChange={(e) =>
- setReplacement(row.key, e.target.files?.[0] ?? null)
- }
- />
-
- }
- onClick={() => inputRefs.current[row.key]?.click()}
- styles={{
- root: {
- height: 34,
- paddingInline: 13,
- border: "1.5px solid #CBD5E1",
- },
- label: {
- fontSize: 12.5,
- fontWeight: 700,
- color: "#334155",
- },
- }}
- >
- {replaced ? "Change" : "Replace"}
-
- {replaced && (
- setReplacement(row.key, null)}
- style={{ color: "#C0392B" }}
- >
-
-
- )}
+
+ {onFile.length > 0 && (
+
+
+ Already submitted
+
+ {onFile.map((file) => (
+
+
+
+
+
+
+ {labelForDocCode(file.code)}
+
+
+ {file.name}
+
+
+
+
+
+
+ On file
+
- >
- }
+ }
+ />
+
+
+ ))}
+
+ )}
+
+
+
+ Update documents
+
+
+ Replace any document you need to change. Documents marked required must
+ be on file before you can resubmit.
+
+
+ {settingLoading ? (
+
+
+
+ ) : setting ? (
+
- );
- })}
-
+ ) : (
+
+ No document requirements are configured for your account. You can
+ resubmit using the documents already on file.
+
+ )}
+
+
);
}
+
+type BookingFile = ResubmitFlowController["files"][number];
+
+/** Keep one row per code (the most recent upload, i.e. last in the array). */
+function dedupeLatestByCode(files: BookingFile[]): BookingFile[] {
+ const order: string[] = [];
+ const latest = new Map();
+ for (const file of files) {
+ if (!latest.has(file.code)) order.push(file.code);
+ latest.set(file.code, file);
+ }
+ return order.map((code) => latest.get(code)!);
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/index.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/index.ts
index da5e67fcb..a6a5094bc 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/index.ts
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/index.ts
@@ -1,13 +1,13 @@
export { PriceChangeModal } from "./PriceChangeModal";
export { ResubmitBookingModal } from "./ResubmitBookingModal";
export { ResubmitDocuments } from "./ResubmitDocuments";
+export { labelForDocCode, type BookingFile } from "./resubmitDocs";
export {
- getResubmitDocRows,
- labelForDocCode,
- type BookingFile,
- type ResubmitDocRow,
-} from "./resubmitDocs";
+ documentSettingCode,
+ useBookingDocumentSetting,
+} from "./useBookingDocumentSetting";
export {
useResubmitFlow,
+ type DocumentsValue,
type ResubmitFlowController,
} from "./useResubmitFlow";
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts
index f4096ddb9..d8b660413 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts
@@ -5,24 +5,23 @@ import { REQUIRED_DOC_FIELDS } from "../BookingDetailPage/constants";
/** A single uploaded file on a booking. */
export type BookingFile = NonNullable[number];
-/** A document the customer can replace when resubmitting after changes. */
-export interface ResubmitDocRow {
- /** Stable file code used as the multipart field name on update. */
- key: string;
- /** Human-readable label shown in the row. */
- label: string;
- /** The currently-submitted file for this row. */
- file: BookingFile;
-}
-
-const LABEL_BY_CODE = new Map(
- REQUIRED_DOC_FIELDS.map((d) => [d.key, d.label]),
-);
+/** Human labels for known document codes (shipment + onboarding documents). */
+const LABEL_BY_CODE = new Map([
+ ...REQUIRED_DOC_FIELDS.map((d) => [d.key, d.label] as const),
+ // Company onboarding document codes (see file-upload-settings seeder).
+ ["tin_certificate", "TIN Certificate"],
+ ["commercial_license", "Commercial License"],
+ ["business_license", "Business License / Trade License"],
+ ["investment_license", "Investment License"],
+ ["national_id", "National ID"],
+ ["national_id_passport", "National ID / Passport"],
+ ["passport", "Passport"],
+]);
/**
- * Turn a file `code` (e.g. "commercial_invoice", "custom_172..._0") into a
- * human label. Known booking-document codes use their configured label; ad-hoc
- * / unknown codes are title-cased from the code itself.
+ * Turn a file `code` (e.g. "tin_certificate", "commercial_invoice",
+ * "custom_172..._0") into a human label. Known codes use their configured label;
+ * ad-hoc / unknown codes are title-cased from the code itself.
*/
export function labelForDocCode(code: string): string {
const known = LABEL_BY_CODE.get(code);
@@ -32,32 +31,3 @@ export function labelForDocCode(code: string): string {
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
-
-/**
- * Documents to show when a customer is updating a booking that staff returned
- * with `CHANGES_REQUESTED`. These are exactly the files the customer submitted
- * during the booking process (`booking.files`) — not a fixed required list — so
- * the customer updates what they actually provided and resubmits.
- *
- * The API appends a new file record on every (re)upload without removing the
- * old one, so `booking.files` can hold several rows for the same `code`. We show
- * one row per code using the most recent upload (the last occurrence in the
- * array) while preserving the original first-seen order for stable rendering.
- */
-export function getResubmitDocRows(
- booking: Pick,
-): ResubmitDocRow[] {
- const files = booking.files ?? [];
-
- const order: string[] = [];
- const latestByCode = new Map();
- for (const file of files) {
- if (!latestByCode.has(file.code)) order.push(file.code);
- latestByCode.set(file.code, file); // last write wins → most recent upload
- }
-
- return order.map((code) => {
- const file = latestByCode.get(code)!;
- return { key: code, label: labelForDocCode(code), file };
- });
-}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts
new file mode 100644
index 000000000..65774f97f
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts
@@ -0,0 +1,35 @@
+import { useQuery } from "@tanstack/react-query";
+
+import useAuth from "@/hooks/useAuth";
+import { api } from "@/services/api";
+
+/** Onboarding document setting code for the company's nationality. */
+export function documentSettingCode(
+ nationality: string | null | undefined,
+): string {
+ return nationality === "foreign"
+ ? "company_onboarding_documents_foreign"
+ : "company_onboarding_documents_ethiopian";
+}
+
+/**
+ * Fetches the FileUploadSetting that describes the documents a booking requires
+ * (TIN, license, national ID, passport, …) — the same setting the new-booking
+ * document step uses, resolved from the company's nationality.
+ *
+ * Shared by the resubmit modal and the changes-requested detail view so both
+ * render an identical document section.
+ */
+export function useBookingDocumentSetting() {
+ const auth = useAuth();
+ const nationality = auth.company?.company?.nationality as
+ | string
+ | null
+ | undefined;
+
+ return useQuery(
+ api.fileUploadSettings.getByCode.queryOptions({
+ input: { code: documentSettingCode(nationality) },
+ }),
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useResubmitFlow.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useResubmitFlow.ts
index 5b2170c54..18d09595b 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useResubmitFlow.ts
+++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useResubmitFlow.ts
@@ -1,50 +1,87 @@
-import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { api } from "@/services/api";
import type { SubmitBookingResponse } from "@/services/bookings.service";
import type { Freight } from "@edr/types";
-import { getResubmitDocRows } from "./resubmitDocs";
+import { useBookingDocumentSetting } from "./useBookingDocumentSetting";
+
+export type DocumentsValue = Record;
+
+/** True when a SmartFileInput value holds at least one file for a key. */
+function hasFile(value: File | File[] | null | undefined): boolean {
+ if (!value) return false;
+ return Array.isArray(value) ? value.length > 0 : true;
+}
/**
* Drives the "update documents and resubmit" flow for a booking that staff
- * returned with `CHANGES_REQUESTED`. The document set is the files the customer
- * actually submitted (`booking.files`); they may replace any of them, then
- * resubmit. Shared by the booking detail page and the home-page modal.
+ * returned with `CHANGES_REQUESTED`.
+ *
+ * The document section mirrors the new-booking step: it's driven by the
+ * company's onboarding document setting (TIN, license, ID, passport, …) via
+ * SmartFileInput. Fields already present on the booking are treated as on file;
+ * any required field with neither an existing file nor a freshly-picked one
+ * blocks resubmit.
+ *
+ * Shared by the booking detail page and the home-page modal.
*/
export function useResubmitFlow(
booking: Freight.IBooking,
opts?: { onResubmitted?: () => void },
) {
const queryClient = useQueryClient();
+ const settingQuery = useBookingDocumentSetting();
- const rows = useMemo(() => getResubmitDocRows(booking), [booking]);
-
- // Replacement files keyed by document code; only changed docs are sent.
- const [replacements, setReplacements] = useState>(
- {},
+ // The booking may arrive from the lightweight list endpoint, which omits
+ // `files`. Fetch the full record so the already-submitted documents (and the
+ // required-field check that depends on them) are accurate everywhere.
+ const filesAlreadyLoaded = booking.files !== undefined;
+ const detailQuery = useQuery(
+ api.bookings.get.queryOptions({
+ input: { id: booking.id },
+ enabled: !filesAlreadyLoaded,
+ }),
);
+ const detailed = detailQuery.data ?? booking;
+ const files = detailed.files ?? [];
+
+ // Freshly-selected files keyed by fileKey (SmartFileInput value).
+ const [documents, setDocuments] = useState({});
const [priceChange, setPriceChange] = useState(
null,
);
+ const [validationError, setValidationError] = useState("");
+ // Only surface per-field "required" errors once the user has tried to submit.
+ const [showErrors, setShowErrors] = useState(false);
- const hasReplacements = Object.values(replacements).some(Boolean);
+ // Codes already attached to the booking from the original submission.
+ const existingCodes = useMemo(
+ () => new Set(files.map((f) => f.code)),
+ [files],
+ );
+
+ const fields = settingQuery.data?.fields ?? [];
+
+ // Required fields that have neither an existing file nor a newly-picked one.
+ const missingRequiredKeys = useMemo(() => {
+ return fields
+ .filter((f) => f.isRequired)
+ .filter(
+ (f) => !existingCodes.has(f.fileKey) && !hasFile(documents[f.fileKey]),
+ )
+ .map((f) => f.fileKey);
+ }, [fields, existingCodes, documents]);
+
+ const hasNewFiles = Object.values(documents).some(hasFile);
const invalidateLists = () =>
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
const updateMutation = useMutation({
- mutationFn: (files: Record) =>
+ mutationFn: (files: DocumentsValue) =>
api.bookings.update.call({ id: booking.id, dto: {}, documents: files }),
- onSuccess: () => {
- setReplacements({});
- queryClient.invalidateQueries({
- queryKey: api.bookings.get.queryKey({ id: booking.id }),
- });
- invalidateLists();
- opts?.onResubmitted?.();
- },
});
const submitMutation = useMutation({
@@ -74,22 +111,32 @@ export function useResubmitFlow(
opts?.onResubmitted?.();
}
- const setReplacement = (key: string, file: File | null) =>
- setReplacements((prev) => ({ ...prev, [key]: file }));
-
/**
- * Upload any replaced documents (if any), then resubmit the booking for
- * review. Replacing files is optional — staff may have asked for a non-doc
- * change — so an empty replacement set still submits.
+ * Validate required documents, upload any newly-selected ones, then resubmit
+ * the booking for review.
*/
function resubmit() {
- const changed: Record = {};
- for (const [key, file] of Object.entries(replacements)) {
- if (file) changed[key] = file;
+ if (missingRequiredKeys.length > 0) {
+ setShowErrors(true);
+ setValidationError(
+ "Please attach all required documents before resubmitting.",
+ );
+ return;
}
- if (Object.keys(changed).length > 0) {
- updateMutation.mutate(changed, {
- onSuccess: () => submitMutation.mutate(),
+ setShowErrors(false);
+ setValidationError("");
+
+ const files: DocumentsValue = {};
+ for (const [key, value] of Object.entries(documents)) {
+ if (hasFile(value)) files[key] = value;
+ }
+
+ if (Object.keys(files).length > 0) {
+ updateMutation.mutate(files, {
+ onSuccess: () => {
+ setDocuments({});
+ submitMutation.mutate();
+ },
});
} else {
submitMutation.mutate();
@@ -97,15 +144,28 @@ export function useResubmitFlow(
}
const isBusy =
+ settingQuery.isLoading ||
updateMutation.isPending ||
submitMutation.isPending ||
confirmSubmitMutation.isPending;
return {
- rows,
- replacements,
- hasReplacements,
- setReplacement,
+ booking: detailed,
+ /** Documents already attached to the booking from the original submission. */
+ files,
+ setting: settingQuery.data,
+ settingLoading: settingQuery.isLoading || detailQuery.isLoading,
+ existingCodes,
+ documents,
+ setDocuments,
+ hasNewFiles,
+ missingRequiredKeys,
+ /** Per-field errors for SmartFileInput; only set after a failed submit. */
+ fieldErrors: showErrors
+ ? Object.fromEntries(missingRequiredKeys.map((k) => [k, "Required"]))
+ : {},
+ canResubmit: missingRequiredKeys.length === 0,
+ validationError,
resubmit,
isBusy,
priceChange,