mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
feat: add document upload to booking creation form
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
Step2ServiceType,
|
||||
Step4Route,
|
||||
Step5CargoDetails,
|
||||
StepDocuments,
|
||||
Step8Review,
|
||||
} from "./new-booking-form/steps";
|
||||
|
||||
@@ -34,8 +35,25 @@ export default function NewBookingPage() {
|
||||
);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (payload: CreateBookingPayload) =>
|
||||
api.bookings.create.call(payload),
|
||||
mutationFn: async (payload: CreateBookingPayload) => {
|
||||
const booking = await api.bookings.create.call(payload);
|
||||
|
||||
// Documents can't ride along with creation — upload them against the
|
||||
// new booking id once it exists. Optional here; the booking detail page
|
||||
// remains the catch-all for any docs the user skips.
|
||||
const documents = form.getValues("documents") ?? {};
|
||||
const hasDocuments = Object.values(documents).some((value) =>
|
||||
Array.isArray(value) ? value.length > 0 : Boolean(value),
|
||||
);
|
||||
if (hasDocuments) {
|
||||
await api.bookings.uploadDocuments.call({
|
||||
id: booking.id,
|
||||
files: documents,
|
||||
});
|
||||
}
|
||||
|
||||
return booking;
|
||||
},
|
||||
onSuccess: (booking) => {
|
||||
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
|
||||
navigate(`/bookings/${booking.id}`);
|
||||
@@ -282,7 +300,8 @@ export default function NewBookingPage() {
|
||||
isLoading={refDataLoading}
|
||||
/>
|
||||
)}
|
||||
{step === 5 && (
|
||||
{step === 5 && <StepDocuments form={form} />}
|
||||
{step === 6 && (
|
||||
<Step8Review
|
||||
form={form}
|
||||
setStep={setStep}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Freight } from "@edr/types";
|
||||
import { DeepPartial, Path } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
@@ -22,9 +23,61 @@ export const STEPS = [
|
||||
{ id: 2, label: "Service Type & Mile", short: "Service" },
|
||||
{ id: 3, label: "Route", short: "Route" },
|
||||
{ id: 4, label: "Cargo Details", short: "Cargo" },
|
||||
{ id: 5, label: "Review & Submit", short: "Submit" },
|
||||
{ id: 5, label: "Documents", short: "Documents" },
|
||||
{ id: 6, label: "Review & Submit", short: "Submit" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Shipment documents collected during booking creation. The fileKeys mirror
|
||||
* `REQUIRED_DOC_FIELDS` in BookingDetailPage/constants.ts so anything attached
|
||||
* here shows up as "Uploaded" on the booking detail page. All optional in this
|
||||
* flow — the detail page remains the catch-all for uploading them later.
|
||||
*/
|
||||
const DOC_SETTING_TS = "2024-01-01T00:00:00.000Z";
|
||||
|
||||
function docField(
|
||||
fileKey: string,
|
||||
fileLabel: string,
|
||||
order: number,
|
||||
): Freight.IFileUploadField {
|
||||
return {
|
||||
id: fileKey,
|
||||
settingId: "booking_documents",
|
||||
createdAt: DOC_SETTING_TS,
|
||||
updatedAt: DOC_SETTING_TS,
|
||||
deletedAt: null,
|
||||
fileKey,
|
||||
fileLabel,
|
||||
helpText: null,
|
||||
isRequired: false,
|
||||
isMultiple: false,
|
||||
maxFiles: 1,
|
||||
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
|
||||
maxSizeMb: 10,
|
||||
order,
|
||||
};
|
||||
}
|
||||
|
||||
export const BOOKING_DOCS_SETTING: Freight.IFileUploadSetting = {
|
||||
id: "booking_documents",
|
||||
createdAt: DOC_SETTING_TS,
|
||||
updatedAt: DOC_SETTING_TS,
|
||||
deletedAt: null,
|
||||
code: "booking_documents",
|
||||
label: "Booking Documents",
|
||||
description:
|
||||
"Attach your shipment documents now, or skip and upload them later from the booking page.",
|
||||
entity: "booking",
|
||||
fields: [
|
||||
docField("commercial_invoice", "Commercial Invoice", 1),
|
||||
docField("packing_list", "Packing List", 2),
|
||||
docField("certificate_of_origin", "Certificate of Origin", 3),
|
||||
docField("letter_of_credit", "Letter of Credit / LC", 4),
|
||||
],
|
||||
};
|
||||
|
||||
export type BookingDocuments = Record<string, File | File[] | null>;
|
||||
|
||||
export const bookingFormSchema = z
|
||||
.object({
|
||||
contractType: z.enum(["new", "renewal"], "Select a contract type."),
|
||||
@@ -78,6 +131,7 @@ export const bookingFormSchema = z
|
||||
}),
|
||||
),
|
||||
consolidationEnabled: z.boolean(),
|
||||
documents: z.record(z.string(), z.any()).default({}),
|
||||
notes: z.string(),
|
||||
termsAccepted: z.boolean(),
|
||||
})
|
||||
@@ -187,6 +241,7 @@ export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
|
||||
isRefrigerated: false,
|
||||
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
||||
consolidationEnabled: false,
|
||||
documents: {},
|
||||
notes: "",
|
||||
termsAccepted: false,
|
||||
};
|
||||
@@ -215,7 +270,8 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
||||
"containers",
|
||||
"consolidationEnabled",
|
||||
],
|
||||
5: ["notes", "termsAccepted"],
|
||||
5: ["documents"],
|
||||
6: ["notes", "termsAccepted"],
|
||||
};
|
||||
|
||||
export type RouteDirection = "import" | "export" | "domestic" | null;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { SmartFileInput } from "@edr/ui-common";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
|
||||
import {
|
||||
BOOKING_DOCS_SETTING,
|
||||
type BookingDocuments,
|
||||
type BookingFormValues,
|
||||
} from "./schema";
|
||||
import { StepHeader } from "./shared";
|
||||
|
||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||
|
||||
function countAttached(documents: BookingDocuments): number {
|
||||
return BOOKING_DOCS_SETTING.fields.filter((f) => {
|
||||
const value = documents[f.fileKey];
|
||||
return Array.isArray(value) ? value.length > 0 : Boolean(value);
|
||||
}).length;
|
||||
}
|
||||
|
||||
export function StepDocuments({ form }: { form: BookingForm }) {
|
||||
const documents = (form.watch("documents") ?? {}) as BookingDocuments;
|
||||
const attached = countAttached(documents);
|
||||
const total = BOOKING_DOCS_SETTING.fields.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader
|
||||
title="Shipment Documents"
|
||||
description="Attach your shipment documents now, or skip this step and upload them later from the booking page."
|
||||
/>
|
||||
|
||||
<Group
|
||||
gap={10}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
className="rounded-xl"
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-edr-border-0)",
|
||||
backgroundColor: "var(--mantine-color-gray-0)",
|
||||
padding: "12px 16px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 32,
|
||||
height: 32,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
backgroundColor: attached === total ? "#ECF6F1" : "#EAF1FB",
|
||||
color: attached === total ? "#0A6F4D" : "#2E5B96",
|
||||
}}
|
||||
>
|
||||
{attached === total ? <CheckCircle2 size={16} /> : `${attached}/${total}`}
|
||||
</Box>
|
||||
<Text size="sm" c="dimmed">
|
||||
{attached === 0
|
||||
? "All documents are optional here — you can upload them later from the booking page."
|
||||
: `${attached} of ${total} attached. You can finish the rest later from the booking page.`}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Controller
|
||||
name="documents"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<SmartFileInput
|
||||
file={BOOKING_DOCS_SETTING}
|
||||
value={(field.value ?? {}) as BookingDocuments}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export { Step1ContractType } from "./step1-contract-type";
|
||||
export { Step2ServiceType } from "./step2-service-type";
|
||||
export { Step4Route } from "./step4-route";
|
||||
export { Step5CargoDetails } from "./step5-cargo-details";
|
||||
export { StepDocuments } from "./step-documents";
|
||||
export { Step8Review } from "./step8-review";
|
||||
|
||||
Reference in New Issue
Block a user