feat(api): Add endpoint for booking reference data

This commit is contained in:
ghost2023
2026-06-01 16:01:46 +03:00
parent ff3cbed747
commit c880e8c7f9
4 changed files with 96 additions and 71 deletions

View File

@@ -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} />

View File

@@ -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>
))}

View File

@@ -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>
)}
/>

View File

@@ -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> => {