mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(api): Add endpoint for booking reference data
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Check, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import { api } from "@/services/api";
|
||||
import type { CreateBookingPayload } from "@/services/bookings.service";
|
||||
@@ -32,6 +33,10 @@ export default function NewBookingPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [step, setStep] = useState(1);
|
||||
const { customer } = useAuth();
|
||||
const { data: referenceData } = useQuery(
|
||||
api.bookings.referenceData.queryOptions(),
|
||||
);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (payload: CreateBookingPayload) =>
|
||||
api.bookings.create.call(payload),
|
||||
@@ -49,18 +54,12 @@ export default function NewBookingPage() {
|
||||
|
||||
const originYard = form.watch("originYard");
|
||||
const destinationYard = form.watch("destinationYard");
|
||||
const containers = form.watch("containers");
|
||||
|
||||
const direction = useMemo(
|
||||
() => getRouteDirection(originYard, destinationYard),
|
||||
[originYard, destinationYard],
|
||||
);
|
||||
|
||||
const wagons = useMemo(() => {
|
||||
if (!containers || containers.length === 0) return null;
|
||||
return calcWagons(containers);
|
||||
}, [containers]);
|
||||
|
||||
async function handleContinue() {
|
||||
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
|
||||
if (!valid) return;
|
||||
@@ -181,9 +180,15 @@ export default function NewBookingPage() {
|
||||
<div className="mx-auto max-w-4xl px-6 py-8">
|
||||
{step === 1 && <Step1ContractType form={form} />}
|
||||
{step === 2 && <Step2ServiceType form={form} />}
|
||||
{step === 3 && <Step4Route form={form} />}
|
||||
{step === 3 && (
|
||||
<Step4Route form={form} referenceData={referenceData} />
|
||||
)}
|
||||
{step === 4 && (
|
||||
<Step5CargoDetails form={form} direction={direction} />
|
||||
<Step5CargoDetails
|
||||
form={form}
|
||||
direction={direction}
|
||||
referenceData={referenceData}
|
||||
/>
|
||||
)}
|
||||
{step === 5 && (
|
||||
<Step8Review form={form} setStep={setStep} direction={direction} />
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { Flame, MapPin, Snowflake } from "lucide-react";
|
||||
import { Field, SelectItem, Separator, Switch } from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
import {
|
||||
SHIPPING_LINES,
|
||||
type BookingFormValues,
|
||||
getRouteDirection,
|
||||
STATIONS,
|
||||
} from "./schema";
|
||||
import {
|
||||
AlertBox,
|
||||
SelectField,
|
||||
SelectOptions,
|
||||
StepHeader,
|
||||
StepLabel,
|
||||
} from "./shared";
|
||||
import { useDropdownSettingByCode } from "@/hooks/useDropdownSettings";
|
||||
import { DropdownOption } from "@/types/dropdownSettings";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||
|
||||
const STATION_DROPDOWN_CODE = "stations_ter";
|
||||
|
||||
export function Step4Route({ form }: { form: BookingForm }) {
|
||||
export function Step4Route({
|
||||
form,
|
||||
referenceData,
|
||||
}: {
|
||||
form: BookingForm;
|
||||
referenceData?: Freight.BookingReferenceData;
|
||||
}) {
|
||||
const originYard = form.watch("originYard");
|
||||
const destinationYard = form.watch("destinationYard");
|
||||
const {
|
||||
data: stationSetting,
|
||||
isLoading: stationsLoading,
|
||||
isError: stationsError,
|
||||
error: stationsFetchError,
|
||||
} = useDropdownSettingByCode(STATION_DROPDOWN_CODE);
|
||||
const stationOptions = getStationOptions(stationSetting?.children);
|
||||
|
||||
const yardOptions = useMemo(() => {
|
||||
if (!referenceData?.yard) return [];
|
||||
return referenceData.yard.map((y) => ({
|
||||
value: y.name,
|
||||
label: y.name,
|
||||
country: y.country,
|
||||
}));
|
||||
}, [referenceData]);
|
||||
|
||||
const shippingLineOptions = useMemo(() => {
|
||||
if (!referenceData?.shipping_line) return [];
|
||||
return referenceData.shipping_line.map((sl) => ({
|
||||
value: sl.name,
|
||||
label: sl.name,
|
||||
}));
|
||||
}, [referenceData]);
|
||||
|
||||
const direction = getRouteDirection(originYard, destinationYard);
|
||||
const directionStyle: Record<string, string> = {
|
||||
export: "bg-sky-50 text-sky-800 border-sky-200",
|
||||
@@ -43,7 +54,6 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
import: "Import workflow (outside country to inside country)",
|
||||
domestic: "Domestic corridor",
|
||||
};
|
||||
const stationSelectDisabled = stationsLoading || stationOptions.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (direction === "domestic") {
|
||||
@@ -51,6 +61,8 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
}
|
||||
}, [direction]);
|
||||
|
||||
const stationSelectDisabled = yardOptions.length === 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader
|
||||
@@ -72,10 +84,9 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
placeholder="Select origin..."
|
||||
disabled={stationSelectDisabled}
|
||||
>
|
||||
<StationSelectOptions
|
||||
options={stationOptions}
|
||||
<YardSelectOptions
|
||||
options={yardOptions}
|
||||
excludeValue={destinationYard}
|
||||
isLoading={stationsLoading}
|
||||
/>
|
||||
</SelectField>
|
||||
)}
|
||||
@@ -91,21 +102,17 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
placeholder="Select destination..."
|
||||
disabled={stationSelectDisabled}
|
||||
>
|
||||
<StationSelectOptions
|
||||
options={stationOptions}
|
||||
<YardSelectOptions
|
||||
options={yardOptions}
|
||||
excludeValue={originYard}
|
||||
isLoading={stationsLoading}
|
||||
/>
|
||||
</SelectField>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{stationsError && (
|
||||
<AlertBox tone="error">
|
||||
Failed to load stations from the API.{" "}
|
||||
{stationsFetchError instanceof Error
|
||||
? stationsFetchError.message
|
||||
: "Try again later."}
|
||||
{!referenceData && (
|
||||
<AlertBox tone="warning">
|
||||
Loading reference data...
|
||||
</AlertBox>
|
||||
)}
|
||||
{direction && (
|
||||
@@ -129,7 +136,11 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
label="Shipping Line"
|
||||
placeholder="Select shipping line..."
|
||||
>
|
||||
<SelectOptions options={SHIPPING_LINES} />
|
||||
{shippingLineOptions.map((sl) => (
|
||||
<SelectItem key={sl.value} value={sl.value}>
|
||||
{sl.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectField>
|
||||
)}
|
||||
/>
|
||||
@@ -179,23 +190,17 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getStationOptions(options?: DropdownOption[]): DropdownOption[] {
|
||||
return [...(options ?? [])].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
function StationSelectOptions({
|
||||
function YardSelectOptions({
|
||||
options,
|
||||
excludeValue,
|
||||
isLoading,
|
||||
}: {
|
||||
options: DropdownOption[];
|
||||
options: Array<{ value: string; label: string; country: string }>;
|
||||
excludeValue: string;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
if (isLoading) {
|
||||
if (options.length === 0) {
|
||||
return (
|
||||
<SelectItem value="__stations_loading" disabled>
|
||||
Loading stations...
|
||||
<SelectItem value="__yards_empty" disabled>
|
||||
No yards available
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
@@ -204,22 +209,10 @@ function StationSelectOptions({
|
||||
(option) => option.value !== excludeValue,
|
||||
);
|
||||
|
||||
if (availableOptions.length === 0) {
|
||||
return (
|
||||
<SelectItem value="__stations_empty" disabled>
|
||||
No stations available
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{availableOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.id}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
|
||||
import { Button, Field, FieldError, FieldLabel, Input } from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
import {
|
||||
BREAK_BULK_TYPES,
|
||||
BULK_COMMODITIES,
|
||||
CONTAINER_TYPES,
|
||||
calcWagons,
|
||||
type BookingFormValues,
|
||||
type RouteDirection,
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
AlertBox,
|
||||
OptionCard,
|
||||
SelectField,
|
||||
SelectOptions,
|
||||
SelectItem,
|
||||
StepHeader,
|
||||
StepLabel,
|
||||
} from "./shared";
|
||||
@@ -23,9 +22,11 @@ type BookingForm = UseFormReturn<BookingFormValues>;
|
||||
export function Step5CargoDetails({
|
||||
form,
|
||||
direction,
|
||||
referenceData,
|
||||
}: {
|
||||
form: BookingForm;
|
||||
direction: RouteDirection;
|
||||
referenceData?: Freight.BookingReferenceData;
|
||||
}) {
|
||||
const cargoType = form.watch("cargoType");
|
||||
const freightType = form.watch("freightType");
|
||||
@@ -38,6 +39,20 @@ export function Step5CargoDetails({
|
||||
name: "containers",
|
||||
});
|
||||
|
||||
const containerTypeOptions = useMemo(() => {
|
||||
if (!referenceData?.containers) return [];
|
||||
return referenceData.containers.flatMap((group) =>
|
||||
group.types.map((t) => t.name),
|
||||
);
|
||||
}, [referenceData]);
|
||||
|
||||
const bulkCommodityOptions = useMemo(() => {
|
||||
if (!referenceData?.cargo_type) return [];
|
||||
return referenceData.cargo_type.flatMap((group) =>
|
||||
group.children?.map((c) => c.name) ?? [],
|
||||
);
|
||||
}, [referenceData]);
|
||||
|
||||
function getOverweightAlert(
|
||||
type: "20ft" | "40ft",
|
||||
vgm: number,
|
||||
@@ -181,7 +196,11 @@ export function Step5CargoDetails({
|
||||
label="Commodity *"
|
||||
placeholder="Select commodity *"
|
||||
>
|
||||
<SelectOptions options={BULK_COMMODITIES} />
|
||||
{bulkCommodityOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectField>
|
||||
)}
|
||||
/>
|
||||
@@ -216,7 +235,11 @@ export function Step5CargoDetails({
|
||||
label="Break-bulk type *"
|
||||
placeholder="Select type *"
|
||||
>
|
||||
<SelectOptions options={BREAK_BULK_TYPES} />
|
||||
{bulkCommodityOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectField>
|
||||
)}
|
||||
/>
|
||||
@@ -408,7 +431,11 @@ export function Step5CargoDetails({
|
||||
label="Container Type *"
|
||||
placeholder="Select type..."
|
||||
>
|
||||
<SelectOptions options={CONTAINER_TYPES} />
|
||||
{containerTypeOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const bookingsService = {
|
||||
return data.data;
|
||||
},
|
||||
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
|
||||
const { data } = await client.get("/bookings/reference-data");
|
||||
const { data } = await client.get("/api/bookings/reference-data");
|
||||
return data.data;
|
||||
},
|
||||
remove: async (id: string): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user