refactor(booking): Migrate new booking logic to react-query, deprecate mock service and draft IDs

This commit is contained in:
ghost2023
2026-05-26 10:09:27 +03:00
parent 32e552e13b
commit 8039f70deb
3 changed files with 27 additions and 103 deletions

View File

@@ -1,11 +1,11 @@
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@edr/ui-common";
import Breadcrumbs from "@/components/Breadcrumbs";
import { addBooking } from "./bookings.mock";
import { getCurrentCustomer } from "@/lib/currentCustomer";
import { api } from "@/services/api";
import type { CreateBookingPayload } from "@/services/bookings.service";
@@ -33,10 +33,18 @@ import {
export default function NewBookingPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [step, setStep] = useState(1);
const [renewalValidating, setRenewalValidating] = useState(false);
const [renewalValid, setRenewalValid] = useState<boolean | null>(null);
const [submitted, setSubmitted] = useState(false);
const createMutation = useMutation({
mutationFn: (payload: CreateBookingPayload) =>
api.bookings.create.call(payload),
onSuccess: (booking) => {
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
setTimeout(() => navigate(`/bookings/${booking.id}`), 2500);
},
});
const form = useForm<BookingFormValues>({
defaultValues: initialBookingFormValues,
@@ -48,8 +56,6 @@ export default function NewBookingPage() {
const destinationYard = form.watch("destinationYard");
const containers = form.watch("containers");
const previousContractRef = form.watch("previousContractRef");
const contractId =
form.watch("draftContractId") || form.watch("previousContractRef");
const direction = useMemo(
() => getRouteDirection(originYard, destinationYard),
@@ -113,7 +119,7 @@ export default function NewBookingPage() {
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
}
function handleSubmit(data: BookingFormValues) {
const handleSubmit = form.handleSubmit((data) => {
if (data.contractType === "renewal" && renewalValid !== true) {
form.setError("previousContractRef", {
type: "manual",
@@ -124,15 +130,7 @@ export default function NewBookingPage() {
}
const me = getCurrentCustomer();
const reference =
data.draftContractId ||
data.previousContractRef ||
`EDR-DRAFT-${Date.now()}`;
const qtyCount =
data.cargoType === "container"
? data.containers.reduce((acc, c) => acc + Number(c.qty || 0), 0)
: 1;
const reference = data.previousContractRef;
const totalWeight =
data.cargoType === "container"
@@ -142,48 +140,13 @@ export default function NewBookingPage() {
)
: Number(data.cargoWeight || 0);
const description =
data.cargoType === "container"
? data.containers.map((c) => `${c.qty} × ${c.type}`).join(", ")
: data.freightType === "bulk"
? `Bulk - ${data.bulkCommodity === "Others" ? data.bulkCommodityOther : data.bulkCommodity}`
: `Break-Bulk - ${data.breakBulkType === "Others" ? data.breakBulkTypeOther : data.breakBulkType}`;
const newBooking = {
id: Date.now(),
reference,
customerId: me.id,
customer: me.company,
cargoType: (data.cargoType === "container"
? "Containerized"
: "Bulk") as any,
originStation: data.originYard,
destinationStation: data.destinationYard,
transportMode: (data.serviceType === "rail"
? "Rail"
: "Multimodal") as any,
containerType: (data.cargoType === "container" &&
data.containers[0]?.type === "40ft"
? "40FT"
: "20FT") as any,
containerCount: qtyCount,
weightTons: totalWeight,
requestedDate: new Date().toISOString().slice(0, 10),
priority: (data.isHazardous ? "High" : "Normal") as any,
cargoDescription: description,
specialInstructions: data.notes || "Standard handling required",
status: "Pending" as any,
};
addBooking(newBooking);
// Call API using api.bookings.create.call
const apiPayload = {
reference,
customerId: String(me.id),
scheduledDate: new Date().toISOString().slice(0, 10),
totalAmount: 0,
contractType: data.contractType.toUpperCase(),
contractType:
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
previousContractId: data.previousContractRef || undefined,
serviceType:
data.serviceType === "rail" ? "RAIL_ONLY" : "RAIL_AND_FORWARDING",
@@ -197,8 +160,8 @@ export default function NewBookingPage() {
: undefined,
equipmentReturn:
data.equipmentReturn === "with_return"
? "WITH_RETURN"
: "WITHOUT_RETURN",
? ("WITH_RETURN" as const)
: ("WITHOUT_RETURN" as const),
originStation: data.originYard,
destinationStation: data.destinationYard,
cargoTotalWeightVgm: totalWeight,
@@ -220,31 +183,18 @@ export default function NewBookingPage() {
...(data.cargoType === "container" && data.containers.length > 0
? {
containers: data.containers.map((c) => ({
type: c.type === "40ft" ? "40FT" as const : "20FT" as const,
type: c.type === "40ft" ? ("40FT" as const) : ("20FT" as const),
qty: Number(c.qty || 1),
vgm: Number(c.vgm || 0),
})),
}
: {}),
};
} satisfies CreateBookingPayload;
api.bookings.create
.call(apiPayload as CreateBookingPayload)
.then((created) => {
console.log("Successfully created booking via API:", created);
})
.catch((err) => {
console.warn(
"API call failed (expected if API server is offline), falling back to mock storage:",
err,
);
});
createMutation.mutate(apiPayload);
});
setSubmitted(true);
setTimeout(() => navigate("/bookings"), 2500);
}
if (submitted) {
if (createMutation.isSuccess && createMutation.data) {
return (
<div className="flex min-h-screen items-center justify-center p-6">
<div className="w-full max-w-sm rounded-2xl border border-border bg-card p-10 text-center shadow-sm">
@@ -257,7 +207,7 @@ export default function NewBookingPage() {
notified once approved.
</p>
<p className="mt-4 font-mono text-sm font-semibold text-primary">
{contractId}
{createMutation.data.reference}
</p>
</div>
</div>
@@ -268,7 +218,7 @@ export default function NewBookingPage() {
<form
id="new-booking-form"
className="flex flex-col"
onSubmit={form.handleSubmit(handleSubmit)}
onSubmit={handleSubmit}
>
<div className="sticky top-0 z-20 border-b border-border bg-background">
<div className="mx-auto max-w-4xl space-y-3 px-6 py-3">

View File

@@ -151,7 +151,6 @@ export const bookingFormSchema = z
.object({
contractType: z.enum(["new", "renewal"], "Select a contract type."),
previousContractRef: z.string(),
draftContractId: z.string(),
serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
firstMileEnabled: z.boolean(),
pickUpAddress: z.string(),
@@ -162,7 +161,7 @@ export const bookingFormSchema = z
destinationYard: z.string(),
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
cargoWeight: z.string(),
freightType: z.enum(["bulk", "break_bulk", ""]).default(""),
freightType: z.enum(["bulk", "break_bulk"]),
bulkCommodity: z.string(),
bulkCommodityOther: z.string(),
breakBulkType: z.string(),
@@ -188,14 +187,6 @@ export const bookingFormSchema = z
termsAccepted: z.boolean(),
})
.superRefine((data, ctx) => {
if (data.contractType === "new" && !data.draftContractId.trim()) {
ctx.addIssue({
code: "custom",
path: ["draftContractId"],
message: "A draft contract ID is required.",
});
}
if (data.contractType === "renewal" && !data.previousContractRef.trim()) {
ctx.addIssue({
code: "custom",
@@ -360,7 +351,6 @@ export type BookingFormValues = z.infer<typeof bookingFormSchema>;
export const initialBookingFormValues: Partial<BookingFormValues> = {
previousContractRef: "",
draftContractId: "",
firstMileEnabled: false,
pickUpAddress: "",
lastMileEnabled: false,
@@ -383,7 +373,7 @@ export const initialBookingFormValues: Partial<BookingFormValues> = {
};
export const stepFields: Record<number, Array<keyof BookingFormValues>> = {
1: ["contractType", "previousContractRef", "draftContractId"],
1: ["contractType", "previousContractRef"],
2: ["serviceType"],
3: [
"firstMileEnabled",

View File

@@ -1,7 +1,7 @@
import { Controller, type UseFormReturn } from "react-hook-form";
import { FileText, Loader2, RefreshCw } from "lucide-react";
import { Button, Field, FieldError, FieldLabel, Input } from "@edr/ui-common";
import { type BookingFormValues, genContractId } from "./schema";
import { Button, Field, FieldLabel, Input } from "@edr/ui-common";
import { type BookingFormValues } from "./schema";
import { AlertBox, OptionCard, OptionFieldError, StepHeader } from "./shared";
type BookingForm = UseFormReturn<BookingFormValues>;
@@ -18,9 +18,7 @@ export function Step1ContractType({
onValidate: () => void;
}) {
const contractType = form.watch("contractType");
const draftContractId = form.watch("draftContractId");
const previousContractRef = form.watch("previousContractRef");
const errors = form.formState.errors;
return (
<div className="space-y-6">
@@ -40,12 +38,6 @@ export function Step1ContractType({
onClick={() => {
field.onChange("new");
form.clearErrors(["contractType", "previousContractRef"]);
if (!draftContractId) {
form.setValue("draftContractId", genContractId(), {
shouldDirty: true,
shouldValidate: true,
});
}
}}
>
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
@@ -55,11 +47,6 @@ export function Step1ContractType({
<p className="mt-0.5 text-xs text-muted-foreground">
Blank contract form. A draft ID is auto-generated.
</p>
{field.value === "new" && draftContractId && (
<p className="mt-2 font-mono text-xs font-semibold text-primary">
{draftContractId}
</p>
)}
</OptionCard>
<OptionCard
@@ -115,9 +102,6 @@ export function Step1ContractType({
)}
</Button>
</div>
<FieldError
errors={[fieldState.error, errors.draftContractId]}
/>
</Field>
)}
/>