mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
feat: setted up the renewal contract
This commit is contained in:
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user