feat: setted up the renewal contract

This commit is contained in:
Nathnael
2026-06-16 08:51:23 +00:00
parent 05da2ce27a
commit 570c5ee125
3 changed files with 226 additions and 12 deletions

View File

@@ -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 (
<Box
style={{
padding: "28px",
minHeight: "calc(100dvh - var(--app-shell-header-height, 56px))",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Alert
color="orange"
icon={<AlertCircle size={20} />}
radius="md"
style={{ maxWidth: "500px" }}
mb="lg"
>
<Text size="lg" fw={600} mb="md">
Complete Your Company Setup
</Text>
<Text size="sm" mb="md">
You need to complete your company onboarding before you can create
bookings. Please follow the onboarding process to get started.
</Text>
<Button
color="orange"
onClick={() => navigate("/onboarding")}
mt="md"
>
Go to Onboarding
</Button>
</Alert>
</Box>
);
}
const createMutation = useMutation({
mutationFn: async (payload: CreateBookingPayload) => {
const booking = await api.bookings.create.call(payload);
@@ -264,7 +304,7 @@ export default function NewBookingPage() {
</Alert>
)}
{step === 1 && <Step1ContractType form={form} />}
{step === 1 && <Step1ContractType form={form} referenceData={referenceData} />}
{step === 2 && (
<Step2ServiceType referenceData={referenceData} form={form} />
)}

View File

@@ -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<BookingFormInputValues>;
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 (
<Input.Wrapper label={label} error={error?.message}>
<Combobox store={combobox} disabled={disabled}>
<Combobox.Target>
<InputBase
placeholder={placeholder}
disabled={disabled}
value={searchQuery || selectedLabel}
onChange={(e) => {
onSearchChange(e.currentTarget.value);
combobox.openDropdown();
}}
onFocus={() => combobox.openDropdown()}
onBlur={() => {
field.onBlur();
combobox.closeDropdown();
if (!selectedLabel) {
onSearchChange("");
}
}}
rightSection={
isLoading ? <Loader size={14} /> : <Combobox.Chevron />
}
/>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
{isLoading ? (
<Combobox.Empty>Loading contracts...</Combobox.Empty>
) : options.length === 0 ? (
<Combobox.Empty>No contracts found</Combobox.Empty>
) : (
options.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
onClick={() => handleSelectOption(option.value)}
>
{option.label}
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
</Input.Wrapper>
);
}

View File

@@ -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<BookingFormInputValues, any, BookingFormValues>;
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<PreviousContractOption[]>(() => {
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 (
<div className="space-y-6">
@@ -73,23 +148,32 @@ export function Step1ContractType({ form }: { form: BookingForm }) {
{contractType === "renewal" && (
<div className="space-y-3 pt-1">
{error && (
<AlertBox tone="error">
Failed to load previous contracts. Please try again later.
</AlertBox>
)}
<Controller
name="previousContractRef"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
<AsyncComboboxField
field={field}
error={fieldState.error}
label="Previous Contract Reference Number"
placeholder="Select a contract..."
data={MOCK_VALID_CONTRACTS}
placeholder="Search by reference or route..."
options={contractOptions}
isLoading={isLoading}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
onSelect={handleSelectContract}
/>
)}
/>
{previousContractRef && (
<AlertBox tone="success">
<strong>Contract found.</strong> Company details, route, and wagon
preferences will be pre-filled.
<strong>Contract found.</strong> Route, service type, and cargo
details will be pre-filled.
</AlertBox>
)}
</div>