mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 15:58:18 +00:00
feat(bookings): Implement unified hierarchical bulk cargo type selection via cargoTypePath
This commit is contained in:
@@ -121,12 +121,15 @@ export default function NewBookingPage() {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cargoTypePath = data.cargoTypePath ?? [];
|
||||||
|
const childId = cargoTypePath[1];
|
||||||
|
|
||||||
const bulkChild = cargoTree
|
const bulkChild = cargoTree
|
||||||
.flatMap((g) => g.children ?? [])
|
.flatMap((g) => g.children ?? [])
|
||||||
.find((c) => c.id === data.bulkCommoditytype);
|
.find((c) => c.id === childId);
|
||||||
|
|
||||||
const cargoTypeId =
|
const cargoTypeId =
|
||||||
data.cargoType === "bulk" ? data.bulkCommoditytype : undefined;
|
data.cargoType === "bulk" ? childId : undefined;
|
||||||
|
|
||||||
const cargoFreeText = bulkChild?.show_free_text_box
|
const cargoFreeText = bulkChild?.show_free_text_box
|
||||||
? data.cargoFreeText
|
? data.cargoFreeText
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ export const bookingFormSchema = z
|
|||||||
trainScheduleId: z.string().min(1, "Select a shipment date."),
|
trainScheduleId: z.string().min(1, "Select a shipment date."),
|
||||||
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
|
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
|
||||||
cargoWeight: z.string(),
|
cargoWeight: z.string(),
|
||||||
freightType: z.string(), // parent group
|
cargoTypePath: z.array(z.string()).default([]),
|
||||||
bulkCommoditytype: z.string(),
|
|
||||||
cargoFreeText: z.string(),
|
cargoFreeText: z.string(),
|
||||||
isHazardous: z.boolean(),
|
isHazardous: z.boolean(),
|
||||||
isRefrigerated: z.boolean(),
|
isRefrigerated: z.boolean(),
|
||||||
@@ -158,19 +157,6 @@ export const bookingFormSchema = z
|
|||||||
path: ["destinationYard"],
|
path: ["destinationYard"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine((data) => !(data.cargoType === "bulk" && !data.freightType), {
|
|
||||||
message: "Select a freight type.",
|
|
||||||
path: ["freightType"],
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) =>
|
|
||||||
!(
|
|
||||||
data.cargoType === "bulk" &&
|
|
||||||
data.freightType &&
|
|
||||||
!data.bulkCommoditytype
|
|
||||||
),
|
|
||||||
{ message: "Select a commodity.", path: ["bulkCommoditytype"] },
|
|
||||||
)
|
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.cargoType !== "bulk") return true;
|
if (data.cargoType !== "bulk") return true;
|
||||||
@@ -190,6 +176,21 @@ export const bookingFormSchema = z
|
|||||||
path: ["termsAccepted"],
|
path: ["termsAccepted"],
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.cargoType === "bulk") {
|
||||||
|
if (!data.cargoTypePath[0]) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["cargoTypePath"],
|
||||||
|
message: "Select a freight type.",
|
||||||
|
});
|
||||||
|
} else if (!data.cargoTypePath[1]) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["cargoTypePath"],
|
||||||
|
message: "Select a commodity.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (data.cargoType === "container") {
|
if (data.cargoType === "container") {
|
||||||
data.containers.forEach((c, i) => {
|
data.containers.forEach((c, i) => {
|
||||||
if (!c.qty || +c.qty < 1) {
|
if (!c.qty || +c.qty < 1) {
|
||||||
@@ -233,7 +234,7 @@ export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
|
|||||||
scheduledDate: "",
|
scheduledDate: "",
|
||||||
trainScheduleId: "",
|
trainScheduleId: "",
|
||||||
cargoWeight: "",
|
cargoWeight: "",
|
||||||
bulkCommoditytype: "",
|
cargoTypePath: [],
|
||||||
cargoFreeText: "",
|
cargoFreeText: "",
|
||||||
isHazardous: false,
|
isHazardous: false,
|
||||||
isRefrigerated: false,
|
isRefrigerated: false,
|
||||||
@@ -263,8 +264,7 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
|||||||
4: [
|
4: [
|
||||||
"cargoType",
|
"cargoType",
|
||||||
"cargoWeight",
|
"cargoWeight",
|
||||||
"freightType",
|
"cargoTypePath",
|
||||||
"bulkCommoditytype",
|
|
||||||
"containers",
|
"containers",
|
||||||
"consolidationEnabled",
|
"consolidationEnabled",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||||
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
|
import { Package, Plus, Trash2, Weight } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Button,
|
Button,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
InputLabel,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
@@ -42,8 +42,9 @@ export function Step5CargoDetails({
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const cargoType = form.watch("cargoType");
|
const cargoType = form.watch("cargoType");
|
||||||
const freightType = form.watch("freightType");
|
const cargoTypePath = form.watch("cargoTypePath") ?? [];
|
||||||
const bulkCommoditytype = form.watch("bulkCommoditytype");
|
const parentId = cargoTypePath[0];
|
||||||
|
const childId = cargoTypePath[1];
|
||||||
const containers = form.watch("containers");
|
const containers = form.watch("containers");
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
@@ -58,15 +59,17 @@ export function Step5CargoDetails({
|
|||||||
);
|
);
|
||||||
}, [referenceData]);
|
}, [referenceData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (parentId) {
|
||||||
|
form.setValue("cargoTypePath", [parentId, ""], { shouldDirty: true });
|
||||||
|
}
|
||||||
|
}, [parentId]);
|
||||||
|
|
||||||
const selectedCommodity = useMemo(() => {
|
const selectedCommodity = useMemo(() => {
|
||||||
if (!referenceData?.cargo_type || !freightType || !bulkCommoditytype) return null;
|
if (!referenceData?.cargo_type || !parentId || !childId) return null;
|
||||||
const group = referenceData.cargo_type.find(
|
const group = referenceData.cargo_type.find((g) => g.id === parentId);
|
||||||
(g) => g.code.toLowerCase() === freightType,
|
return group?.children?.find((c) => c.id === childId) ?? null;
|
||||||
);
|
}, [referenceData, parentId, childId]);
|
||||||
return group?.children?.find(
|
|
||||||
(c) => c.name === bulkCommoditytype,
|
|
||||||
) ?? null;
|
|
||||||
}, [referenceData, freightType, bulkCommoditytype]);
|
|
||||||
|
|
||||||
const freightTypeGroups = useMemo(() => {
|
const freightTypeGroups = useMemo(() => {
|
||||||
if (!referenceData?.cargo_type) return [];
|
if (!referenceData?.cargo_type) return [];
|
||||||
@@ -75,13 +78,25 @@ export function Step5CargoDetails({
|
|||||||
);
|
);
|
||||||
}, [referenceData]);
|
}, [referenceData]);
|
||||||
|
|
||||||
|
const freightTypeOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
freightTypeGroups.map((g) => ({
|
||||||
|
value: g.id,
|
||||||
|
label: g.name,
|
||||||
|
})),
|
||||||
|
[freightTypeGroups],
|
||||||
|
);
|
||||||
|
|
||||||
const commodityOptions = useMemo(() => {
|
const commodityOptions = useMemo(() => {
|
||||||
if (!referenceData?.cargo_type || !freightType) return [];
|
if (!referenceData?.cargo_type || !parentId) return [];
|
||||||
const group = referenceData.cargo_type.find(
|
const group = referenceData.cargo_type.find((g) => g.id === parentId);
|
||||||
(g) => g.code.toLowerCase() === freightType,
|
return (
|
||||||
|
group?.children?.map((c) => ({
|
||||||
|
value: c.id,
|
||||||
|
label: c.name,
|
||||||
|
})) ?? []
|
||||||
);
|
);
|
||||||
return group?.children?.map((c) => c.name) ?? [];
|
}, [referenceData, parentId]);
|
||||||
}, [referenceData, freightType]);
|
|
||||||
|
|
||||||
function getOverweightAlert(
|
function getOverweightAlert(
|
||||||
type: "20ft" | "40ft",
|
type: "20ft" | "40ft",
|
||||||
@@ -128,7 +143,7 @@ export function Step5CargoDetails({
|
|||||||
|
|
||||||
{/* Cargo Type */}
|
{/* Cargo Type */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<StepLabel>Cargo Type *</StepLabel>
|
<InputLabel>Cargo Type *</InputLabel>
|
||||||
<Controller
|
<Controller
|
||||||
name="cargoType"
|
name="cargoType"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -139,7 +154,7 @@ export function Step5CargoDetails({
|
|||||||
selected={cargoType === "container"}
|
selected={cargoType === "container"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
field.onChange("container");
|
field.onChange("container");
|
||||||
form.setValue("freightType", "", { shouldDirty: true });
|
form.setValue("cargoTypePath", [], { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-100">
|
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-100">
|
||||||
@@ -174,7 +189,6 @@ export function Step5CargoDetails({
|
|||||||
|
|
||||||
{/* Weight */}
|
{/* Weight */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<StepLabel>Weight</StepLabel>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="cargoWeight"
|
name="cargoWeight"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -198,45 +212,35 @@ export function Step5CargoDetails({
|
|||||||
{/* Bulk freight type */}
|
{/* Bulk freight type */}
|
||||||
{cargoType === "bulk" && (
|
{cargoType === "bulk" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<StepLabel>Freight Type *</StepLabel>
|
{freightTypeOptions.length > 0 ? (
|
||||||
<Controller
|
|
||||||
name="freightType"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
{freightTypeGroups.map((group) => {
|
|
||||||
const val = group.code.toLowerCase();
|
|
||||||
return (
|
|
||||||
<OptionCard
|
|
||||||
key={group.code}
|
|
||||||
selected={freightType === val}
|
|
||||||
onClick={() => {
|
|
||||||
field.onChange(val);
|
|
||||||
form.setValue("bulkCommoditytype", "", {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="font-semibold">{group.name}</p>
|
|
||||||
</OptionCard>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<OptionFieldError error={fieldState.error} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{freightType && commodityOptions.length > 0 && (
|
|
||||||
<Controller
|
<Controller
|
||||||
name="bulkCommoditytype"
|
name="cargoTypePath.0"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SelectField
|
<SelectField
|
||||||
field={field}
|
field={field}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
label="Cargo type *"
|
label="Bulk Cargo Type *"
|
||||||
|
placeholder="Select freight type..."
|
||||||
|
data={freightTypeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
No freight types available.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parentId && commodityOptions.length > 0 && (
|
||||||
|
<Controller
|
||||||
|
name="cargoTypePath.1"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<SelectField
|
||||||
|
field={field}
|
||||||
|
error={fieldState.error}
|
||||||
|
label=""
|
||||||
placeholder="Select type *"
|
placeholder="Select type *"
|
||||||
data={commodityOptions}
|
data={commodityOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user