mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 20:38:17 +00:00
feat: add document upload to booking creation form
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
|||||||
Step2ServiceType,
|
Step2ServiceType,
|
||||||
Step4Route,
|
Step4Route,
|
||||||
Step5CargoDetails,
|
Step5CargoDetails,
|
||||||
|
StepDocuments,
|
||||||
Step8Review,
|
Step8Review,
|
||||||
} from "./new-booking-form/steps";
|
} from "./new-booking-form/steps";
|
||||||
|
|
||||||
@@ -34,8 +35,25 @@ export default function NewBookingPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (payload: CreateBookingPayload) =>
|
mutationFn: async (payload: CreateBookingPayload) => {
|
||||||
api.bookings.create.call(payload),
|
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) => {
|
onSuccess: (booking) => {
|
||||||
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
|
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
|
||||||
navigate(`/bookings/${booking.id}`);
|
navigate(`/bookings/${booking.id}`);
|
||||||
@@ -282,7 +300,8 @@ export default function NewBookingPage() {
|
|||||||
isLoading={refDataLoading}
|
isLoading={refDataLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 5 && (
|
{step === 5 && <StepDocuments form={form} />}
|
||||||
|
{step === 6 && (
|
||||||
<Step8Review
|
<Step8Review
|
||||||
form={form}
|
form={form}
|
||||||
setStep={setStep}
|
setStep={setStep}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Freight } from "@edr/types";
|
||||||
import { DeepPartial, Path } from "react-hook-form";
|
import { DeepPartial, Path } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
@@ -22,9 +23,61 @@ export const STEPS = [
|
|||||||
{ id: 2, label: "Service Type & Mile", short: "Service" },
|
{ id: 2, label: "Service Type & Mile", short: "Service" },
|
||||||
{ id: 3, label: "Route", short: "Route" },
|
{ id: 3, label: "Route", short: "Route" },
|
||||||
{ id: 4, label: "Cargo Details", short: "Cargo" },
|
{ 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;
|
] 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
|
export const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
contractType: z.enum(["new", "renewal"], "Select a contract type."),
|
contractType: z.enum(["new", "renewal"], "Select a contract type."),
|
||||||
@@ -78,6 +131,7 @@ export const bookingFormSchema = z
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
consolidationEnabled: z.boolean(),
|
consolidationEnabled: z.boolean(),
|
||||||
|
documents: z.record(z.string(), z.any()).default({}),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
termsAccepted: z.boolean(),
|
termsAccepted: z.boolean(),
|
||||||
})
|
})
|
||||||
@@ -187,6 +241,7 @@ export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
|
|||||||
isRefrigerated: false,
|
isRefrigerated: false,
|
||||||
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
||||||
consolidationEnabled: false,
|
consolidationEnabled: false,
|
||||||
|
documents: {},
|
||||||
notes: "",
|
notes: "",
|
||||||
termsAccepted: false,
|
termsAccepted: false,
|
||||||
};
|
};
|
||||||
@@ -215,7 +270,8 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
|||||||
"containers",
|
"containers",
|
||||||
"consolidationEnabled",
|
"consolidationEnabled",
|
||||||
],
|
],
|
||||||
5: ["notes", "termsAccepted"],
|
5: ["documents"],
|
||||||
|
6: ["notes", "termsAccepted"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RouteDirection = "import" | "export" | "domestic" | null;
|
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 { Step2ServiceType } from "./step2-service-type";
|
||||||
export { Step4Route } from "./step4-route";
|
export { Step4Route } from "./step4-route";
|
||||||
export { Step5CargoDetails } from "./step5-cargo-details";
|
export { Step5CargoDetails } from "./step5-cargo-details";
|
||||||
|
export { StepDocuments } from "./step-documents";
|
||||||
export { Step8Review } from "./step8-review";
|
export { Step8Review } from "./step8-review";
|
||||||
|
|||||||
Reference in New Issue
Block a user