Merge pull request #62 from Tria-plc/freight/feature/booking-api

WIP booking api integration
This commit is contained in:
yaschalew10
2026-06-01 16:45:14 +03:00
committed by GitHub
12 changed files with 459 additions and 214 deletions

View File

@@ -1,14 +1,23 @@
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 {
AlertCircle,
Check,
CheckCircle2,
ChevronLeft,
ChevronRight,
LoaderCircle,
} 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";
import {
BookingFormInputValues,
STEPS,
bookingFormSchema,
calcWagons,
@@ -32,6 +41,10 @@ export default function NewBookingPage() {
const queryClient = useQueryClient();
const [step, setStep] = useState(1);
const { customer } = useAuth();
const { data: referenceData, isLoading: refDataLoading } = useQuery(
api.bookings.referenceData.queryOptions(),
);
const createMutation = useMutation({
mutationFn: (payload: CreateBookingPayload) =>
api.bookings.create.call(payload),
@@ -41,7 +54,7 @@ export default function NewBookingPage() {
},
});
const form = useForm<BookingFormValues>({
const form = useForm<BookingFormInputValues, any, BookingFormValues>({
defaultValues: initialBookingFormValues,
resolver: zodResolver(bookingFormSchema),
mode: "onChange",
@@ -49,18 +62,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;
@@ -78,8 +85,6 @@ export default function NewBookingPage() {
return;
}
const reference = data.previousContractRef;
const totalWeight =
data.cargoType === "container"
? data.containers.reduce(
@@ -88,59 +93,115 @@ export default function NewBookingPage() {
)
: Number(data.cargoWeight || 0);
const apiPayload = {
reference,
customerId: customer!.id,
scheduledDate: new Date().toISOString().slice(0, 10),
totalAmount: 0,
contractType:
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
previousContractId: data.previousContractRef || undefined,
serviceType:
data.service.serviceType === "rail"
? "RAIL_ONLY"
: "RAIL_AND_FORWARDING",
...(data.service.serviceType === "rail"
? {}
: {
firstMileEnabled: data.firstMile.enabled,
firstMilePickupAddress: data.firstMile.pickUpAddress ?? undefined,
lastMileEnabled: data.lastMile.enabled,
lastMileDeliveryAddress: data.lastMile.deliveryAddress ?? undefined,
equipmentReturn:
data.equipmentReturn === "with_return"
? ("WITH_RETURN" as const)
: ("WITHOUT_RETURN" as const),
customsClearingEnabled: data.customsClearingEnabled,
}),
originStation: data.originYard,
destinationStation: data.destinationYard,
cargoTotalWeightVgm: totalWeight,
freightType: data.cargoType === "container" ? "BREAK_BULK" : "BULK",
freightSubtype:
data.cargoType === "container"
? undefined
: data.freightType === "bulk"
// ── Reference data lookups ──────────────────────────────────────────
const yards = referenceData?.yard ?? [];
const services = referenceData?.service ?? [];
const shippingLines = referenceData?.shipping_line ?? [];
const cargoTree = referenceData?.cargo_type ?? [];
const containerGroups = referenceData?.containers ?? [];
const findYardId = (name: string): string =>
yards.find((y) => y.name === name)?.id ?? "";
const findServiceTypeId = (): string => {
const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING";
return services.find((s) => s.code === code)?.id ?? services[0]?.id ?? "";
};
const findShippingLineId = (name: string): string | undefined =>
shippingLines.find((l) => l.name === name)?.id;
const findCargoTypeId = (name: string): string | undefined => {
for (const group of cargoTree) {
const child = group.children?.find((c) => c.name === name);
if (child) return child.id;
}
return undefined;
};
const findContainerCargoTypeId = (): string => {
const group = cargoTree.find(
(g) => g.code === "CONTAINER" || /container/i.test(g.name),
);
console.log(group, cargoTree);
return group?.id ?? "";
};
const findContainerTypeId = (name: string): string => {
for (const group of containerGroups) {
const ct = group.types.find((t) => t.name === name);
if (ct) return ct.id;
}
return "";
};
const cargoTypeId =
data.cargoType === "container"
? findContainerCargoTypeId()
: (findCargoTypeId(
data.freightType === "bulk"
? data.bulkCommodity
: data.breakBulkType,
isHazardous: data.isHazardous,
isRefrigerated: data.isRefrigerated,
) ?? "");
const cargoFreeText =
data.cargoType === "container"
? undefined
: data.freightType === "bulk" && data.bulkCommodity === "Others"
? data.bulkCommodityOther
: data.freightType === "break_bulk" && data.breakBulkType === "Others"
? data.breakBulkTypeOther
: undefined;
// ── Build API payload ───────────────────────────────────────────────
const apiPayload: CreateBookingPayload = {
scheduledDate: new Date().toISOString().slice(0, 10),
contractType:
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
serviceTypeId: findServiceTypeId(),
equipmentReturn:
data.equipmentReturn === "with_return"
? "WITH_RETURN"
: "WITHOUT_RETURN",
originYardId: findYardId(data.originYard),
destinationYardId: findYardId(data.destinationYard),
tradeDirection:
getRouteDirection(data.originYard, data.destinationYard) === "export"
direction === "export"
? "EXPORT"
: "IMPORT",
: direction === "domestic"
? "DOMESTIC"
: "IMPORT",
cargoTypeId,
cargoTotalWeightVgm: totalWeight,
isHazardous: data.isHazardous,
paymentCurrency: "USD",
allowConsolidation: data.consolidationEnabled,
...(data.cargoType === "container" && data.containers.length > 0
? {
containers: data.containers.map((c) => ({
type: c.type === "40ft" ? ("40FT" as const) : ("20FT" as const),
qty: Number(c.qty || 1),
vgm: Number(c.vgm || 0),
})),
}
containers:
data.cargoType === "container"
? data.containers.map((c) => ({
containerTypeId: findContainerTypeId(c.containerType),
quantity: Number(c.qty || 1),
vgmPerUnitTons: Number(c.vgm || 0),
}))
: [],
...(customer ? { customerId: customer.id } : {}),
...(data.previousContractRef
? { previousContractId: data.previousContractRef }
: {}),
} satisfies CreateBookingPayload;
...(data.contractType === "renewal" && data.previousContractRef
? { pnrCode: data.previousContractRef }
: {}),
...(data.serviceType === "rail_forwarding" && data.firstMile.enabled
? { firstMilePickupAddress: data.firstMile.pickUpAddress }
: {}),
...(data.serviceType === "rail_forwarding" && data.lastMile.enabled
? { lastMileDeliveryAddress: data.lastMile.deliveryAddress }
: {}),
...(data.shippingLine
? { shippingLineId: findShippingLineId(data.shippingLine) }
: {}),
...(cargoFreeText ? { cargoFreeText } : {}),
};
createMutation.mutate(apiPayload);
});
@@ -179,11 +240,35 @@ export default function NewBookingPage() {
<div className="flex-1">
<div className="mx-auto max-w-4xl px-6 py-8">
{createMutation.isError && (
<div className="mb-6 flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
<div>
<p className="font-semibold">Submission failed</p>
<p className="mt-1 text-red-600">
{createMutation.error instanceof Error
? createMutation.error.message
: "An unexpected error occurred. Please try again."}
</p>
</div>
</div>
)}
{step === 1 && <Step1ContractType form={form} />}
{step === 2 && <Step2ServiceType form={form} />}
{step === 3 && <Step4Route form={form} />}
{step === 3 && (
<Step4Route
form={form}
referenceData={referenceData}
isLoading={refDataLoading}
/>
)}
{step === 4 && (
<Step5CargoDetails form={form} direction={direction} />
<Step5CargoDetails
form={form}
direction={direction}
referenceData={referenceData}
isLoading={refDataLoading}
/>
)}
{step === 5 && (
<Step8Review form={form} setStep={setStep} direction={direction} />
@@ -210,9 +295,19 @@ export default function NewBookingPage() {
<ChevronRight />
</Button>
) : (
<Button type="submit" form="new-booking-form">
<Check />
Submit Contract Request
<Button
type="submit"
form="new-booking-form"
disabled={createMutation.isPending}
>
{createMutation.isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Check />
)}
{createMutation.isPending
? "Submitting..."
: "Submit Contract Request"}
</Button>
)}
</div>

View File

@@ -249,6 +249,7 @@ export const bookingFormSchema = z
});
export type BookingFormValues = z.infer<typeof bookingFormSchema>;
export type BookingFormInputValues = z.input<typeof bookingFormSchema>;
export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
previousContractRef: "",

View File

@@ -21,7 +21,7 @@ import {
SelectTrigger,
SelectValue,
} from "@edr/ui-common";
import type { BookingFormValues } from "./schema";
import type { BookingFormInputValues, BookingFormValues } from "./schema";
import { cn } from "@/lib/utils";
export function OptionFieldError({ error }: { error?: { message?: string } }) {
@@ -122,7 +122,7 @@ export function SelectField({
disabled,
children,
}: {
field: ControllerRenderProps<BookingFormValues>;
field: ControllerRenderProps<BookingFormInputValues>;
error?: RhfFieldError;
label: string;
placeholder: string;

View File

@@ -1,7 +1,11 @@
import { Controller, type UseFormReturn } from "react-hook-form";
import { FileText, RefreshCw } from "lucide-react";
import { Field } from "@edr/ui-common";
import { MOCK_VALID_CONTRACTS, type BookingFormValues } from "./schema";
import {
BookingFormInputValues,
MOCK_VALID_CONTRACTS,
type BookingFormValues,
} from "./schema";
import {
AlertBox,
OptionCard,
@@ -11,7 +15,11 @@ import {
StepHeader,
} from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
type BookingForm = UseFormReturn<
BookingFormInputValues,
any,
BookingFormValues
>;
export function Step1ContractType({ form }: { form: BookingForm }) {
const contractType = form.watch("contractType");

View File

@@ -2,10 +2,14 @@ import { useEffect, useRef } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { FileText, Package, Train, Truck } from "lucide-react";
import { Badge, Field, FieldError, Input, Switch } from "@edr/ui-common";
import { type BookingFormValues } from "./schema";
import { BookingFormInputValues, type BookingFormValues } from "./schema";
import { OptionCard, OptionFieldError, StepHeader } from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
type BookingForm = UseFormReturn<
BookingFormInputValues,
any,
BookingFormValues
>;
export function Step2ServiceType({ form }: { form: BookingForm }) {
const serviceType = form.watch("serviceType");

View File

@@ -1,37 +1,50 @@
import { useEffect, useMemo } from "react";
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 { Field, SelectItem, Separator, Skeleton, Switch } from "@edr/ui-common";
import type { Freight } from "@edr/types";
import {
SHIPPING_LINES,
BookingFormInputValues,
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 { SelectField, StepHeader, StepLabel } from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
type BookingForm = UseFormReturn<
BookingFormInputValues,
any,
BookingFormValues
>;
const STATION_DROPDOWN_CODE = "stations_ter";
export function Step4Route({ form }: { form: BookingForm }) {
export function Step4Route({
form,
referenceData,
isLoading,
}: {
form: BookingForm;
referenceData?: Freight.BookingReferenceData;
isLoading?: boolean;
}) {
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 +56,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 +63,8 @@ export function Step4Route({ form }: { form: BookingForm }) {
}
}, [direction]);
const stationSelectDisabled = yardOptions.length === 0;
return (
<div className="space-y-6">
<StepHeader
@@ -58,65 +72,59 @@ export function Step4Route({ form }: { form: BookingForm }) {
description="Select the origin and destination yards."
/>
<div className="space-y-3">
<StepLabel>Route</StepLabel>
<div className="grid gap-3 sm:grid-cols-2">
<Controller
name="originYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin Yard*"
placeholder="Select origin..."
disabled={stationSelectDisabled}
>
<StationSelectOptions
options={stationOptions}
excludeValue={destinationYard}
isLoading={stationsLoading}
/>
</SelectField>
)}
/>
<Controller
name="destinationYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination Yard *"
placeholder="Select destination..."
disabled={stationSelectDisabled}
>
<StationSelectOptions
options={stationOptions}
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."}
</AlertBox>
)}
{direction && (
<div
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium ${directionStyle[direction]}`}
>
<MapPin className="h-3.5 w-3.5 shrink-0" />
{directionLabel[direction]}
{isLoading ? (
<LoadingSkeleton />
) : (
<div className="space-y-3">
<StepLabel>Route</StepLabel>
<div className="grid gap-3 sm:grid-cols-2">
<Controller
name="originYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin Yard*"
placeholder="Select origin..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={destinationYard}
/>
</SelectField>
)}
/>
<Controller
name="destinationYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination Yard *"
placeholder="Select destination..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={originYard}
/>
</SelectField>
)}
/>
</div>
)}
</div>
{direction && (
<div
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium ${directionStyle[direction]}`}
>
<MapPin className="h-3.5 w-3.5 shrink-0" />
{directionLabel[direction]}
</div>
)}
</div>
)}
{direction && direction != "domestic" && (
<Controller
@@ -129,7 +137,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 +191,35 @@ export function Step4Route({ form }: { form: BookingForm }) {
);
}
function getStationOptions(options?: DropdownOption[]): DropdownOption[] {
return [...(options ?? [])].sort((a, b) => a.order - b.order);
function LoadingSkeleton() {
return (
<div className="space-y-4 rounded-xl border border-border p-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<Skeleton className="h-8 w-full" />
</div>
);
}
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 +228,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,17 @@
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 {
BREAK_BULK_TYPES,
BULK_COMMODITIES,
CONTAINER_TYPES,
Button,
Field,
FieldError,
FieldLabel,
Input,
Skeleton,
} from "@edr/ui-common";
import type { Freight } from "@edr/types";
import {
BookingFormInputValues,
calcWagons,
type BookingFormValues,
type RouteDirection,
@@ -13,19 +20,27 @@ import {
AlertBox,
OptionCard,
SelectField,
SelectOptions,
SelectItem,
StepHeader,
StepLabel,
} from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
type BookingForm = UseFormReturn<
BookingFormInputValues,
any,
BookingFormValues
>;
export function Step5CargoDetails({
form,
direction,
referenceData,
isLoading,
}: {
form: BookingForm;
direction: RouteDirection;
referenceData?: Freight.BookingReferenceData;
isLoading?: boolean;
}) {
const cargoType = form.watch("cargoType");
const freightType = form.watch("freightType");
@@ -38,6 +53,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,
@@ -54,6 +83,26 @@ export function Step5CargoDetails({
return null;
}
if (isLoading) {
return (
<div className="space-y-6">
<StepHeader
title="Cargo Details"
description="Define your cargo type, weight, and container configuration."
/>
<div className="space-y-4 rounded-xl border border-border p-4">
<Skeleton className="h-4 w-24" />
<div className="grid gap-3 sm:grid-cols-2">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-1/3" />
</div>
</div>
);
}
return (
<div className="space-y-6">
<StepHeader
@@ -181,7 +230,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 +269,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 +465,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

@@ -11,13 +11,17 @@ import {
Textarea,
} from "@edr/ui-common";
import {
BookingFormInputValues,
type BookingFormValues,
type RouteDirection,
type WagonCalcResult,
} from "./schema";
import { StepHeader } from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
type BookingForm = UseFormReturn<
BookingFormInputValues,
any,
BookingFormValues
>;
export function Step8Review({
form,

View File

@@ -137,6 +137,12 @@ export const api = {
bookingsService.create,
),
referenceData: endpoint<void, Freight.BookingReferenceData>(
"bookings",
"referenceData",
bookingsService.getReferenceData,
),
remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) =>
bookingsService.remove(id),
),

View File

@@ -17,6 +17,10 @@ export const bookingsService = {
const { data } = await client.post("/api/bookings", payload);
return data.data;
},
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
const { data } = await client.get("/api/bookings/reference-data");
return data.data;
},
remove: async (id: string): Promise<void> => {
await client.delete(`/bookings/${id}`);
},

View File

@@ -216,45 +216,94 @@ export interface IInvoice extends BaseEntity {
dueAt: string;
}
// ── Reference Data (booking form catalog) ──────────────────────────────────────
export interface BookingReferenceYard {
id: string;
name: string;
code: string;
country: string;
}
export interface BookingReferenceContainerType {
id: string;
name: string;
code: string;
is_reefer: boolean;
wagons_per_unit: number;
}
export interface BookingReferenceContainerSizeGroup {
size: string;
types: BookingReferenceContainerType[];
}
export interface BookingReferenceService {
id: string;
name: string;
code: string;
}
export interface BookingReferenceShippingLine {
id: string;
name: string;
code: string;
}
export interface BookingReferenceCargoTypeChild {
id: string;
name: string;
code: string;
show_free_text_box: boolean;
}
export interface BookingReferenceCargoTypeGroup {
id: string;
name: string;
code: string;
children?: BookingReferenceCargoTypeChild[];
}
export interface BookingReferenceData {
yard: BookingReferenceYard[];
containers: BookingReferenceContainerSizeGroup[];
service: BookingReferenceService[];
shipping_line: BookingReferenceShippingLine[];
cargo_type: BookingReferenceCargoTypeGroup[];
}
// ── DTOs ───────────────────────────────────────────────────────────────────────
export interface CreateBookingContainerDto {
containerTypeId: string;
quantity: number;
vgmPerUnitTons: number;
}
export interface CreateBookingDto {
reference: string;
customerId: string;
reference?: string;
customerId?: string;
trainId?: string;
scheduledDate: string;
totalAmount: number;
paymentStatus?: string;
contractType: "NEW" | "RENEWAL";
previousContractId?: string;
serviceType: "RAIL_ONLY" | "RAIL_AND_FORWARDING";
firstMileEnabled?: boolean;
serviceTypeId: string;
firstMilePickupAddress?: string;
lastMileEnabled?: boolean;
lastMileDeliveryAddress?: string;
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
customsClearingEnabled?: boolean;
originStation: string;
destinationStation: string;
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN" | "NA";
originYardId: string;
destinationYardId: string;
tradeDirection: "IMPORT" | "EXPORT" | "DOMESTIC";
cargoTypeId: string;
cargoFreeText?: string;
shippingLineId?: string;
cargoTotalWeightVgm: number;
freightType: "BULK" | "BREAK_BULK";
freightSubtype?: string;
isHazardous?: boolean;
isRefrigerated?: boolean;
tradeDirection: "IMPORT" | "EXPORT";
paymentCurrency: string;
allowConsolidation?: boolean;
paymentCurrency: "ETB" | "USD";
pnrCode?: string;
startDate?: string;
endDate?: string;
financialTerms?: string;
containers?: Array<{
type: "20FT" | "40FT";
qty: number;
vgm: number;
}>;
containers: CreateBookingContainerDto[];
allowConsolidation?: boolean;
}

View File

@@ -23,6 +23,7 @@ export type {
export * from "./components/button";
export * from "./components/input";
export * from "./components/skeleton";
export * from "./components/textarea";
export * from "./components/label";
export * from "./components/card";