From 570c5ee12516d19011ddc5976377dc5f52e21ab9 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 16 Jun 2026 08:51:23 +0000 Subject: [PATCH] feat: setted up the renewal contract --- .../src/pages/bookings/NewBookingPage.tsx | 42 +++++++- .../bookings/new-booking-form/shared.tsx | 94 +++++++++++++++- .../new-booking-form/step1-contract-type.tsx | 102 ++++++++++++++++-- 3 files changed, 226 insertions(+), 12 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 7de208301..513b93a45 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -7,6 +7,7 @@ import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; +import useAuth from "@/hooks/useAuth"; import { BookingFormInputValues, STEPS, @@ -31,10 +32,49 @@ export default function NewBookingPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const [step, setStep] = useState(1); + const auth = useAuth(); const { data: referenceData, isLoading: refDataLoading } = useQuery( api.bookings.referenceData.queryOptions(), ); + if (!auth.isPending && !auth.company) { + return ( + + } + radius="md" + style={{ maxWidth: "500px" }} + mb="lg" + > + + Complete Your Company Setup + + + You need to complete your company onboarding before you can create + bookings. Please follow the onboarding process to get started. + + + + + ); + } + const createMutation = useMutation({ mutationFn: async (payload: CreateBookingPayload) => { const booking = await api.bookings.create.call(payload); @@ -264,7 +304,7 @@ export default function NewBookingPage() { )} - {step === 1 && } + {step === 1 && } {step === 2 && ( )} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx index e4b53cfd1..55b6eda44 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx @@ -1,7 +1,8 @@ +import { Alert, Combobox, Input, InputBase, Select, Text, Title, useCombobox } from "@mantine/core"; +import { AlertTriangle, Check, CheckCircle2, Info, Loader, XCircle } from "lucide-react"; import type { ReactNode } from "react"; +import { useMemo } from "react"; import type { ControllerRenderProps, FieldError as RhfFieldError } from "react-hook-form"; -import { AlertTriangle, Check, CheckCircle2, Info, XCircle } from "lucide-react"; -import { Alert, Select, Text, Title } from "@mantine/core"; import type { BookingFormInputValues } from "./schema"; export function OptionFieldError({ error }: { error?: { message?: string } }) { @@ -124,3 +125,92 @@ export function SelectField({ /> ); } + +interface AsyncComboboxOption { + value: string; + label: string; +} + +export function AsyncComboboxField({ + field, + error, + label, + placeholder, + options, + isLoading, + searchQuery, + onSearchChange, + onSelect, + disabled, +}: { + field: ControllerRenderProps; + error?: RhfFieldError; + label: string; + placeholder: string; + options: AsyncComboboxOption[]; + isLoading?: boolean; + searchQuery: string; + onSearchChange: (query: string) => void; + onSelect: (value: string) => void; + disabled?: boolean; +}) { + const combobox = useCombobox(); + + const selectedLabel = useMemo(() => { + return options.find((opt) => opt.value === field.value)?.label || ""; + }, [field.value, options]); + + const handleSelectOption = (val: string) => { + onSelect(val); + combobox.closeDropdown(); + }; + + return ( + + + + { + onSearchChange(e.currentTarget.value); + combobox.openDropdown(); + }} + onFocus={() => combobox.openDropdown()} + onBlur={() => { + field.onBlur(); + combobox.closeDropdown(); + if (!selectedLabel) { + onSearchChange(""); + } + }} + rightSection={ + isLoading ? : + } + /> + + + + + {isLoading ? ( + Loading contracts... + ) : options.length === 0 ? ( + No contracts found + ) : ( + options.map((option) => ( + handleSelectOption(option.value)} + > + {option.label} + + )) + )} + + + + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx index 1b4e91e3a..6070f6157 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx @@ -1,23 +1,98 @@ -import { Controller, type UseFormReturn } from "react-hook-form"; +import { api } from "@/services/api"; +import type { Freight } from "@edr/types"; +import { useQuery } from "@tanstack/react-query"; import { FileText, RefreshCw } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Controller, type UseFormReturn } from "react-hook-form"; import { BookingFormInputValues, - MOCK_VALID_CONTRACTS, type BookingFormValues, } from "./schema"; import { AlertBox, + AsyncComboboxField, OptionCard, OptionFieldError, - SelectField, StepHeader, } from "./shared"; type BookingForm = UseFormReturn; -export function Step1ContractType({ form }: { form: BookingForm }) { +interface PreviousContractOption { + value: string; + label: string; + booking: Freight.IBooking; +} + +export function Step1ContractType({ + form, + referenceData, +}: { + form: BookingForm; + referenceData?: Freight.BookingReferenceData; +}) { const contractType = form.watch("contractType"); const previousContractRef = form.watch("previousContractRef"); + const [searchQuery, setSearchQuery] = useState(""); + + const { data: bookings, isLoading, error } = useQuery( +api.bookings.list.queryOptions({ + input: { + + page: 1, + pageSize: 100, + sortBy: "createdAt", + sortOrder: "DESC", + } + }) + ); + + const contractOptions = useMemo(() => { + console.log("Bookings data:", bookings); + if(!bookings) return [] + + return bookings?.items + .map((booking) => { + const origin = booking.originYard?.label || "Unknown"; + const destination = booking.destinationYard?.label || "Unknown"; + return { + value: booking.reference, + label: `${booking.reference} - Route: ${origin} to ${destination}`, + booking, + }; + }) + .filter((opt) => + opt.label.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [bookings, searchQuery]); + + const handleSelectContract = async (contractId: string) => { + const selected = contractOptions.find((opt) => opt.value === contractId); + if (!selected) return; + + form.setValue("previousContractRef", contractId); + + // Auto-fill from previous contract + const booking = selected.booking; + if (booking) { + form.setValue("originYard", booking.originYardId); + form.setValue("destinationYard", booking.destinationYardId); + form.setValue("serviceTypeId", booking.serviceTypeId); + form.setValue("cargoType", booking.freightType === "CONTAINER" ? "container" : "bulk"); + form.setValue("equipmentReturn", booking.equipmentReturn === "WITH_RETURN" ? "with_return" : "without_return"); + if (booking.isHazardous) form.setValue("isHazardous", booking.isHazardous); + + // Look up shipping line name from reference data + if (booking.shippingLineId && referenceData?.shipping_line) { + const shippingLine = referenceData.shipping_line.find( + (sl) => sl.id === booking.shippingLineId, + ); + if (shippingLine) { + form.setValue("shippingLine", shippingLine.name); + } + } + } + } return (
@@ -73,23 +148,32 @@ export function Step1ContractType({ form }: { form: BookingForm }) { {contractType === "renewal" && (
+ {error && ( + + Failed to load previous contracts. Please try again later. + + )} ( - )} /> {previousContractRef && ( - Contract found. Company details, route, and wagon - preferences will be pre-filled. + Contract found. Route, service type, and cargo + details will be pre-filled. )}