mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
Merge pull request #54 from Tria-plc/freight/feature/ui-sync
onboarding and bookin ui page
This commit is contained in:
@@ -20,9 +20,11 @@ import {
|
|||||||
|
|
||||||
const userSchema = z.object({
|
const userSchema = z.object({
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
|
||||||
countryCode: z.string().min(1, "Country code is required"),
|
countryCode: z.string().min(1, "Country code is required"),
|
||||||
phone: z.string().min(9, "Phone number is too short").max(9, "Phone number is too long"),
|
phone: z
|
||||||
|
.string()
|
||||||
|
.min(9, "Phone number is too short")
|
||||||
|
.max(9, "Phone number is too long"),
|
||||||
userType: z.string(),
|
userType: z.string(),
|
||||||
name: z.object({
|
name: z.object({
|
||||||
en: z.string().min(2, "Name is required"),
|
en: z.string().min(2, "Name is required"),
|
||||||
@@ -46,7 +48,6 @@ export default function SignupPage() {
|
|||||||
resolver: zodResolver(userSchema),
|
resolver: zodResolver(userSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
username: "",
|
|
||||||
countryCode: "+251",
|
countryCode: "+251",
|
||||||
phone: "",
|
phone: "",
|
||||||
userType: userType.individual,
|
userType: userType.individual,
|
||||||
@@ -58,10 +59,12 @@ export default function SignupPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const normalizedPhone = data.phone.startsWith("0") ? data.phone.slice(1) : data.phone;
|
const normalizedPhone = data.phone.startsWith("0")
|
||||||
|
? data.phone.slice(1)
|
||||||
|
: data.phone;
|
||||||
const payload: SignupPayload = {
|
const payload: SignupPayload = {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
username: data.username,
|
username: data.email,
|
||||||
phoneNumber: `${data.countryCode}${normalizedPhone}`,
|
phoneNumber: `${data.countryCode}${normalizedPhone}`,
|
||||||
userType: data.userType,
|
userType: data.userType,
|
||||||
name: { en: data.name.en, am: data.name.am ?? "" },
|
name: { en: data.name.en, am: data.name.am ?? "" },
|
||||||
@@ -92,7 +95,12 @@ export default function SignupPage() {
|
|||||||
"Enterprise-grade operations",
|
"Enterprise-grade operations",
|
||||||
"Multi-corridor freight monitoring",
|
"Multi-corridor freight monitoring",
|
||||||
],
|
],
|
||||||
stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
|
stats: {
|
||||||
|
label: "Active Corridors",
|
||||||
|
value: "24+",
|
||||||
|
footer: "Operational",
|
||||||
|
progress: "w-[95%]",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -125,31 +133,17 @@ export default function SignupPage() {
|
|||||||
<FieldError errors={[errors.name?.en]} />
|
<FieldError errors={[errors.name?.en]} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<Field data-invalid={Boolean(errors.email)}>
|
||||||
<Field data-invalid={Boolean(errors.username)}>
|
<FieldLabel>Email Address</FieldLabel>
|
||||||
<FieldLabel>Username</FieldLabel>
|
<Input
|
||||||
<Input
|
type="email"
|
||||||
type="text"
|
placeholder="john@example.com"
|
||||||
placeholder="john_doe"
|
disabled={loading}
|
||||||
disabled={loading}
|
aria-invalid={Boolean(errors.email)}
|
||||||
aria-invalid={Boolean(errors.username)}
|
{...register("email")}
|
||||||
{...register("username")}
|
/>
|
||||||
/>
|
<FieldError errors={[errors.email]} />
|
||||||
<FieldError errors={[errors.username]} />
|
</Field>
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field data-invalid={Boolean(errors.email)}>
|
|
||||||
<FieldLabel>Email Address</FieldLabel>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="john@example.com"
|
|
||||||
disabled={loading}
|
|
||||||
aria-invalid={Boolean(errors.email)}
|
|
||||||
{...register("email")}
|
|
||||||
/>
|
|
||||||
<FieldError errors={[errors.email]} />
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -160,12 +154,7 @@ export default function SignupPage() {
|
|||||||
/>
|
/>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" disabled={loading} size="lg" className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Check, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { Button } from "@edr/ui-common";
|
import { Button } from "@edr/ui-common";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { CreateBookingPayload } from "@/services/bookings.service";
|
import type { CreateBookingPayload } from "@/services/bookings.service";
|
||||||
import {
|
import {
|
||||||
MOCK_VALID_CONTRACTS,
|
|
||||||
STEPS,
|
STEPS,
|
||||||
bookingFormSchema,
|
bookingFormSchema,
|
||||||
calcWagons,
|
calcWagons,
|
||||||
@@ -22,11 +21,8 @@ import { StepIndicator } from "./new-booking-form/StepIndicator";
|
|||||||
import {
|
import {
|
||||||
Step1ContractType,
|
Step1ContractType,
|
||||||
Step2ServiceType,
|
Step2ServiceType,
|
||||||
Step3FirstLastMile,
|
|
||||||
Step4Route,
|
Step4Route,
|
||||||
Step5CargoDetails,
|
Step5CargoDetails,
|
||||||
Step6WagonAllocation,
|
|
||||||
Step7Documents,
|
|
||||||
Step8Review,
|
Step8Review,
|
||||||
} from "./new-booking-form/steps";
|
} from "./new-booking-form/steps";
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
@@ -35,8 +31,6 @@ export default function NewBookingPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [renewalValidating, setRenewalValidating] = useState(false);
|
|
||||||
const [renewalValid, setRenewalValid] = useState<boolean | null>(null);
|
|
||||||
const { customer } = useAuth();
|
const { customer } = useAuth();
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (payload: CreateBookingPayload) =>
|
mutationFn: (payload: CreateBookingPayload) =>
|
||||||
@@ -56,7 +50,6 @@ export default function NewBookingPage() {
|
|||||||
const originYard = form.watch("originYard");
|
const originYard = form.watch("originYard");
|
||||||
const destinationYard = form.watch("destinationYard");
|
const destinationYard = form.watch("destinationYard");
|
||||||
const containers = form.watch("containers");
|
const containers = form.watch("containers");
|
||||||
const previousContractRef = form.watch("previousContractRef");
|
|
||||||
|
|
||||||
const direction = useMemo(
|
const direction = useMemo(
|
||||||
() => getRouteDirection(originYard, destinationYard),
|
() => getRouteDirection(originYard, destinationYard),
|
||||||
@@ -68,63 +61,18 @@ export default function NewBookingPage() {
|
|||||||
return calcWagons(containers);
|
return calcWagons(containers);
|
||||||
}, [containers]);
|
}, [containers]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRenewalValid(null);
|
|
||||||
}, [previousContractRef]);
|
|
||||||
|
|
||||||
function validateRenewal() {
|
|
||||||
const previousContractRef = form.getValues("previousContractRef").trim();
|
|
||||||
|
|
||||||
if (!previousContractRef) {
|
|
||||||
form.setError("previousContractRef", {
|
|
||||||
type: "manual",
|
|
||||||
message: "Enter a previous contract reference.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRenewalValidating(true);
|
|
||||||
setRenewalValid(null);
|
|
||||||
setTimeout(() => {
|
|
||||||
const valid = MOCK_VALID_CONTRACTS.includes(
|
|
||||||
previousContractRef.toUpperCase(),
|
|
||||||
);
|
|
||||||
setRenewalValidating(false);
|
|
||||||
setRenewalValid(valid);
|
|
||||||
if (!valid) {
|
|
||||||
form.setError("previousContractRef", {
|
|
||||||
type: "manual",
|
|
||||||
message: "Contract Reference Number not found or unauthorized.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.clearErrors("previousContractRef");
|
|
||||||
}
|
|
||||||
}, 1200);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleContinue() {
|
async function handleContinue() {
|
||||||
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
|
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
if (step === 1 && form.getValues("contractType") === "renewal") {
|
|
||||||
if (renewalValid !== true) {
|
|
||||||
form.setError("previousContractRef", {
|
|
||||||
type: "manual",
|
|
||||||
message:
|
|
||||||
"Validate the previous contract reference before continuing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
|
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
if (data.contractType === "renewal" && renewalValid !== true) {
|
if (data.contractType === "renewal" && !data.previousContractRef) {
|
||||||
form.setError("previousContractRef", {
|
form.setError("previousContractRef", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: "Validate the previous contract reference before submitting.",
|
message: "Select a previous contract reference.",
|
||||||
});
|
});
|
||||||
setStep(1);
|
setStep(1);
|
||||||
return;
|
return;
|
||||||
@@ -149,19 +97,22 @@ export default function NewBookingPage() {
|
|||||||
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
||||||
previousContractId: data.previousContractRef || undefined,
|
previousContractId: data.previousContractRef || undefined,
|
||||||
serviceType:
|
serviceType:
|
||||||
data.serviceType === "rail" ? "RAIL_ONLY" : "RAIL_AND_FORWARDING",
|
data.service.serviceType === "rail"
|
||||||
firstMileEnabled: data.firstMileEnabled,
|
? "RAIL_ONLY"
|
||||||
firstMilePickupAddress: data.firstMileEnabled
|
: "RAIL_AND_FORWARDING",
|
||||||
? data.pickUpAddress
|
...(data.service.serviceType === "rail"
|
||||||
: undefined,
|
? {}
|
||||||
lastMileEnabled: data.lastMileEnabled,
|
: {
|
||||||
lastMileDeliveryAddress: data.lastMileEnabled
|
firstMileEnabled: data.firstMile.enabled,
|
||||||
? data.deliveryAddress
|
firstMilePickupAddress: data.firstMile.pickUpAddress ?? undefined,
|
||||||
: undefined,
|
lastMileEnabled: data.lastMile.enabled,
|
||||||
equipmentReturn:
|
lastMileDeliveryAddress: data.lastMile.deliveryAddress ?? undefined,
|
||||||
data.equipmentReturn === "with_return"
|
equipmentReturn:
|
||||||
? ("WITH_RETURN" as const)
|
data.equipmentReturn === "with_return"
|
||||||
: ("WITHOUT_RETURN" as const),
|
? ("WITH_RETURN" as const)
|
||||||
|
: ("WITHOUT_RETURN" as const),
|
||||||
|
customsClearingEnabled: data.customsClearingEnabled,
|
||||||
|
}),
|
||||||
originStation: data.originYard,
|
originStation: data.originYard,
|
||||||
destinationStation: data.destinationYard,
|
destinationStation: data.destinationYard,
|
||||||
cargoTotalWeightVgm: totalWeight,
|
cargoTotalWeightVgm: totalWeight,
|
||||||
@@ -220,43 +171,22 @@ export default function NewBookingPage() {
|
|||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<div className="sticky top-0 z-20 border-b border-border bg-background">
|
<div className="sticky top-0 z-20">
|
||||||
<div className="mx-auto max-w-4xl space-y-3 px-6 py-3">
|
<div className="mx-auto max-w-4xl space-y-3 px-6 pt-4">
|
||||||
<Breadcrumbs
|
|
||||||
items={[
|
|
||||||
{ label: "Bookings", href: "/bookings" },
|
|
||||||
{ label: "New Contract Request" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<StepIndicator step={step} />
|
<StepIndicator step={step} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="mx-auto max-w-4xl px-6 py-8">
|
<div className="mx-auto max-w-4xl px-6 py-8">
|
||||||
{step === 1 && (
|
{step === 1 && <Step1ContractType form={form} />}
|
||||||
<Step1ContractType
|
|
||||||
form={form}
|
|
||||||
renewalValid={renewalValid}
|
|
||||||
renewalValidating={renewalValidating}
|
|
||||||
onValidate={validateRenewal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{step === 2 && <Step2ServiceType form={form} />}
|
{step === 2 && <Step2ServiceType form={form} />}
|
||||||
{step === 3 && <Step3FirstLastMile form={form} />}
|
{step === 3 && <Step4Route form={form} />}
|
||||||
{step === 4 && <Step4Route form={form} />}
|
{step === 4 && (
|
||||||
{step === 5 && (
|
|
||||||
<Step5CargoDetails form={form} direction={direction} />
|
<Step5CargoDetails form={form} direction={direction} />
|
||||||
)}
|
)}
|
||||||
{step === 6 && <Step6WagonAllocation form={form} wagons={wagons} />}
|
{step === 5 && (
|
||||||
{step === 7 && <Step7Documents form={form} />}
|
<Step8Review form={form} setStep={setStep} direction={direction} />
|
||||||
{step === 8 && (
|
|
||||||
<Step8Review
|
|
||||||
form={form}
|
|
||||||
setStep={setStep}
|
|
||||||
wagons={wagons}
|
|
||||||
direction={direction}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,11 +207,11 @@ export default function NewBookingPage() {
|
|||||||
{step < STEPS.length ? (
|
{step < STEPS.length ? (
|
||||||
<Button type="button" onClick={handleContinue}>
|
<Button type="button" onClick={handleContinue}>
|
||||||
Continue
|
Continue
|
||||||
<ChevronRight className="ml-1 h-4 w-4" />
|
<ChevronRight />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="submit" form="new-booking-form">
|
<Button type="submit" form="new-booking-form">
|
||||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
<Check />
|
||||||
Submit Contract Request
|
Submit Contract Request
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DeepPartial, Path } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
export const STATIONS = [
|
export const STATIONS = [
|
||||||
@@ -42,123 +43,69 @@ export const MOCK_VALID_CONTRACTS = [
|
|||||||
"EDR-2022-55442",
|
"EDR-2022-55442",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const REQUIRED_DOC_KEYS = [
|
export const CONTAINER_TYPES = [
|
||||||
"tin_certificate",
|
"Dry Container",
|
||||||
"business_license",
|
"High Cubic",
|
||||||
"business_registration",
|
"Reefer Container",
|
||||||
// "national_id",
|
"Open Top",
|
||||||
|
"Flat Rack",
|
||||||
|
"Tank Container",
|
||||||
|
"Open Side",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SHIPPING_LINES = [
|
||||||
|
"MSC",
|
||||||
|
"CMA CGM",
|
||||||
|
"Evergreen",
|
||||||
|
"COSCO",
|
||||||
|
"Hapag-Lloyd",
|
||||||
|
"ONE",
|
||||||
|
"Yang Ming",
|
||||||
|
"ZIM",
|
||||||
|
"Messina Line",
|
||||||
|
"Safmarine",
|
||||||
|
"Wan Hai",
|
||||||
|
"Ethiopian Shipping Lines (ESLSE)",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const STEPS = [
|
export const STEPS = [
|
||||||
{ id: 1, label: "Contract Type", short: "Contract" },
|
{ id: 1, label: "Contract Type", short: "Contract" },
|
||||||
{ id: 2, label: "Service Type", short: "Service" },
|
{ id: 2, label: "Service Type & Mile", short: "Service" },
|
||||||
{ id: 3, label: "First & Last Mile", short: "Mile" },
|
{ id: 3, label: "Route", short: "Route" },
|
||||||
{ id: 4, label: "Route", short: "Route" },
|
{ id: 4, label: "Cargo Details", short: "Cargo" },
|
||||||
{ id: 5, label: "Cargo Details", short: "Cargo" },
|
{ id: 5, label: "Review & Submit", short: "Submit" },
|
||||||
{ id: 6, label: "Wagon Allocation", short: "Wagons" },
|
|
||||||
{ id: 7, label: "Documents", short: "Docs" },
|
|
||||||
{ id: 8, label: "Review & Submit", short: "Submit" },
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const BOOKING_DOCS_SETTING = {
|
|
||||||
id: "booking-compliance",
|
|
||||||
createdAt: "",
|
|
||||||
updatedAt: "",
|
|
||||||
deletedAt: null,
|
|
||||||
code: "booking_compliance_docs",
|
|
||||||
label: "Compliance Documents",
|
|
||||||
description:
|
|
||||||
"Upload your company's legal credentials. All mandatory documents must be submitted before the contract request can be reviewed by EDR Line Staff.",
|
|
||||||
entity: "booking" as const,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: "f1",
|
|
||||||
createdAt: "",
|
|
||||||
updatedAt: "",
|
|
||||||
deletedAt: null,
|
|
||||||
settingId: "booking-compliance",
|
|
||||||
fileKey: "tin_certificate",
|
|
||||||
fileLabel: "TIN Certificate",
|
|
||||||
helpText:
|
|
||||||
"Tax Identification Number certificate issued by ERCA (10-digit TIN).",
|
|
||||||
isRequired: true,
|
|
||||||
isMultiple: false,
|
|
||||||
maxFiles: 1,
|
|
||||||
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
|
|
||||||
maxSizeMb: 5,
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "f2",
|
|
||||||
createdAt: "",
|
|
||||||
updatedAt: "",
|
|
||||||
deletedAt: null,
|
|
||||||
settingId: "booking-compliance",
|
|
||||||
fileKey: "business_license",
|
|
||||||
fileLabel: "Business / Investment License",
|
|
||||||
helpText:
|
|
||||||
"Current business or investment license issued by the relevant government authority.",
|
|
||||||
isRequired: true,
|
|
||||||
isMultiple: false,
|
|
||||||
maxFiles: 1,
|
|
||||||
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
|
|
||||||
maxSizeMb: 5,
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "f3",
|
|
||||||
createdAt: "",
|
|
||||||
updatedAt: "",
|
|
||||||
deletedAt: null,
|
|
||||||
settingId: "booking-compliance",
|
|
||||||
fileKey: "business_registration",
|
|
||||||
fileLabel: "Business Registration Certificate",
|
|
||||||
helpText: "Certificate of registration from the relevant authority.",
|
|
||||||
isRequired: true,
|
|
||||||
isMultiple: false,
|
|
||||||
maxFiles: 1,
|
|
||||||
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
|
|
||||||
maxSizeMb: 5,
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "f5",
|
|
||||||
createdAt: "",
|
|
||||||
updatedAt: "",
|
|
||||||
deletedAt: null,
|
|
||||||
settingId: "booking-compliance",
|
|
||||||
fileKey: "power_of_attorney",
|
|
||||||
fileLabel: "Power of Attorney (PoA)",
|
|
||||||
helpText:
|
|
||||||
"Required only if a representative is signing on behalf of the company.",
|
|
||||||
isRequired: false,
|
|
||||||
isMultiple: false,
|
|
||||||
maxFiles: 1,
|
|
||||||
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
|
|
||||||
maxSizeMb: 5,
|
|
||||||
order: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileValueSchema = z.union([
|
|
||||||
z.custom<File>(),
|
|
||||||
z.array(z.custom<File>()),
|
|
||||||
z.null(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const bookingFormSchema = z
|
export const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
contractType: z.enum(["new", "renewal"], "Select a contract type."),
|
contractType: z.enum(["new", "renewal"], "Select a contract type."),
|
||||||
previousContractRef: z.string(),
|
previousContractRef: z.string(),
|
||||||
serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
|
serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
|
||||||
firstMileEnabled: z.boolean(),
|
firstMile: z
|
||||||
pickUpAddress: z.string(),
|
.object({
|
||||||
lastMileEnabled: z.boolean(),
|
enabled: z.boolean().default(false),
|
||||||
deliveryAddress: z.string(),
|
pickUpAddress: z.string(),
|
||||||
equipmentReturn: z.enum(["with_return", "without_return"]),
|
})
|
||||||
originYard: z.string(),
|
.refine((data) => !(data.enabled && !data.pickUpAddress.trim()), {
|
||||||
destinationYard: z.string(),
|
message: "Enter the pick-up address.",
|
||||||
|
path: ["pickUpAddress"],
|
||||||
|
}),
|
||||||
|
lastMile: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().default(false),
|
||||||
|
deliveryAddress: z.string(),
|
||||||
|
})
|
||||||
|
.refine((data) => !(data.enabled && !data.deliveryAddress.trim()), {
|
||||||
|
message: "Enter the delivery address.",
|
||||||
|
path: ["deliveryAddress"],
|
||||||
|
}),
|
||||||
|
equipmentReturn: z
|
||||||
|
.enum(["with_return", "without_return"])
|
||||||
|
.default("with_return"),
|
||||||
|
customsClearingEnabled: z.boolean().default(false),
|
||||||
|
originYard: z.string().min(1, "Select an origin yard."),
|
||||||
|
destinationYard: z.string().min(1, "Select a destination yard."),
|
||||||
|
shippingLine: z.string(),
|
||||||
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
|
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
|
||||||
cargoWeight: z.string(),
|
cargoWeight: z.string(),
|
||||||
freightType: z.enum(["bulk", "break_bulk"]).optional(),
|
freightType: z.enum(["bulk", "break_bulk"]).optional(),
|
||||||
@@ -171,142 +118,116 @@ export const bookingFormSchema = z
|
|||||||
containers: z.array(
|
containers: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["20ft", "40ft"]),
|
type: z.enum(["20ft", "40ft"]),
|
||||||
|
containerType: z.string().min(1, "Select a container type."),
|
||||||
qty: z
|
qty: z
|
||||||
.string()
|
.string()
|
||||||
|
.refine((q) => q.length !== 0, "Quantity is required.")
|
||||||
.refine((q) => !isNaN(+q), "Enter a valid Number")
|
.refine((q) => !isNaN(+q), "Enter a valid Number")
|
||||||
.refine((qty) => Number(qty) >= 1, "Must be greater than 0"),
|
.refine((qty) => Number(qty) >= 1, "Must be greater than 0"),
|
||||||
vgm: z
|
vgm: z
|
||||||
.string()
|
.string()
|
||||||
|
.refine((vgm) => vgm.length !== 0, "VGM is required.")
|
||||||
.refine((vgm) => !isNaN(+vgm), "Enter a valid Number")
|
.refine((vgm) => !isNaN(+vgm), "Enter a valid Number")
|
||||||
.refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"),
|
.refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
consolidationEnabled: z.boolean(),
|
consolidationEnabled: z.boolean(),
|
||||||
documents: z.record(z.string(), fileValueSchema),
|
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
termsAccepted: z.boolean(),
|
termsAccepted: z.boolean(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.refine(
|
||||||
if (data.contractType === "renewal" && !data.previousContractRef.trim()) {
|
(data) =>
|
||||||
ctx.addIssue({
|
!(data.contractType === "renewal" && !data.previousContractRef.trim()),
|
||||||
code: "custom",
|
{
|
||||||
path: ["previousContractRef"],
|
message: "Enter a previous contract reference.",
|
||||||
message: "Enter a previous contract reference.",
|
path: ["previousContractRef"],
|
||||||
});
|
},
|
||||||
}
|
)
|
||||||
|
.refine((data) => data.originYard !== "", {
|
||||||
if (data.firstMileEnabled && !data.pickUpAddress.trim()) {
|
message: "Select an origin yard.",
|
||||||
ctx.addIssue({
|
path: ["originYard"],
|
||||||
code: "custom",
|
})
|
||||||
path: ["pickUpAddress"],
|
.refine((data) => data.destinationYard !== "", {
|
||||||
message: "Enter the pick-up address.",
|
message: "Select a destination yard.",
|
||||||
});
|
path: ["destinationYard"],
|
||||||
}
|
})
|
||||||
|
.refine(
|
||||||
if (data.lastMileEnabled && !data.deliveryAddress.trim()) {
|
(data) =>
|
||||||
ctx.addIssue({
|
!(
|
||||||
code: "custom",
|
data.originYard &&
|
||||||
path: ["deliveryAddress"],
|
data.destinationYard &&
|
||||||
message: "Enter the delivery address.",
|
data.originYard === data.destinationYard
|
||||||
});
|
),
|
||||||
}
|
{
|
||||||
|
message: "Destination must be different from origin.",
|
||||||
if (!data.originYard) {
|
path: ["destinationYard"],
|
||||||
ctx.addIssue({
|
},
|
||||||
code: "custom",
|
)
|
||||||
path: ["originYard"],
|
.refine((data) => !(data.cargoType === "bulk" && !data.freightType), {
|
||||||
message: "Select an origin yard.",
|
message: "Select a freight type.",
|
||||||
});
|
path: ["freightType"],
|
||||||
}
|
})
|
||||||
|
.refine(
|
||||||
if (!data.destinationYard) {
|
(data) =>
|
||||||
ctx.addIssue({
|
!(
|
||||||
code: "custom",
|
data.cargoType === "bulk" &&
|
||||||
path: ["destinationYard"],
|
data.freightType === "bulk" &&
|
||||||
message: "Select a destination yard.",
|
!data.bulkCommodity
|
||||||
});
|
),
|
||||||
}
|
{ message: "Select a commodity.", path: ["bulkCommodity"] },
|
||||||
|
)
|
||||||
if (
|
.refine(
|
||||||
data.originYard &&
|
(data) =>
|
||||||
data.destinationYard &&
|
!(
|
||||||
data.originYard === data.destinationYard
|
data.cargoType === "bulk" &&
|
||||||
) {
|
data.freightType === "bulk" &&
|
||||||
ctx.addIssue({
|
data.bulkCommodity === "Others" &&
|
||||||
code: "custom",
|
!data.bulkCommodityOther.trim()
|
||||||
path: ["destinationYard"],
|
),
|
||||||
message: "Destination must be different from origin.",
|
{ message: "Specify the commodity.", path: ["bulkCommodityOther"] },
|
||||||
});
|
)
|
||||||
}
|
.refine(
|
||||||
|
(data) =>
|
||||||
if (data.cargoType === "bulk") {
|
!(
|
||||||
if (!data.freightType) {
|
data.cargoType === "bulk" &&
|
||||||
ctx.addIssue({
|
data.freightType === "break_bulk" &&
|
||||||
code: "custom",
|
!data.breakBulkType
|
||||||
path: ["freightType"],
|
),
|
||||||
message: "Select a freight type.",
|
{ message: "Select a break-bulk type.", path: ["breakBulkType"] },
|
||||||
});
|
)
|
||||||
}
|
.refine(
|
||||||
|
(data) =>
|
||||||
if (data.freightType === "bulk") {
|
!(
|
||||||
if (!data.bulkCommodity) {
|
data.cargoType === "bulk" &&
|
||||||
ctx.addIssue({
|
data.freightType === "break_bulk" &&
|
||||||
code: "custom",
|
data.breakBulkType === "Others" &&
|
||||||
path: ["bulkCommodity"],
|
!data.breakBulkTypeOther.trim()
|
||||||
message: "Select a commodity.",
|
),
|
||||||
});
|
{
|
||||||
}
|
message: "Specify the break-bulk type.",
|
||||||
if (
|
path: ["breakBulkTypeOther"],
|
||||||
data.bulkCommodity === "Others" &&
|
},
|
||||||
!data.bulkCommodityOther.trim()
|
)
|
||||||
) {
|
.refine(
|
||||||
ctx.addIssue({
|
(data) => {
|
||||||
code: "custom",
|
if (data.cargoType !== "bulk") return true;
|
||||||
path: ["bulkCommodityOther"],
|
|
||||||
message: "Specify the commodity.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.freightType === "break_bulk") {
|
|
||||||
if (!data.breakBulkType) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
path: ["breakBulkType"],
|
|
||||||
message: "Select a break-bulk type.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
data.breakBulkType === "Others" &&
|
|
||||||
!data.breakBulkTypeOther.trim()
|
|
||||||
) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
path: ["breakBulkTypeOther"],
|
|
||||||
message: "Specify the break-bulk type.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cargoWeight = Number(data.cargoWeight);
|
const cargoWeight = Number(data.cargoWeight);
|
||||||
if (!data.cargoWeight || Number.isNaN(cargoWeight) || cargoWeight <= 0) {
|
return (
|
||||||
ctx.addIssue({
|
!!data.cargoWeight && !Number.isNaN(cargoWeight) && cargoWeight > 0
|
||||||
code: "custom",
|
);
|
||||||
path: ["cargoWeight"],
|
},
|
||||||
message: "Enter a cargo weight greater than 0.",
|
{ message: "Enter a cargo weight greater than 0.", path: ["cargoWeight"] },
|
||||||
});
|
)
|
||||||
}
|
.refine(
|
||||||
}
|
(data) => !(data.cargoType === "container" && data.containers.length === 0),
|
||||||
|
{ message: "Add at least one container.", path: ["containers"] },
|
||||||
|
)
|
||||||
|
.refine((data) => data.termsAccepted, {
|
||||||
|
message: "Accept the freight contract terms to submit.",
|
||||||
|
path: ["termsAccepted"],
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
if (data.cargoType === "container") {
|
if (data.cargoType === "container") {
|
||||||
if (data.containers.length === 0) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
path: ["containers"],
|
|
||||||
message: "Add at least one container.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
data.containers.forEach((c, i) => {
|
data.containers.forEach((c, i) => {
|
||||||
if (!c.qty || +c.qty < 1) {
|
if (!c.qty || +c.qty < 1) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -325,39 +246,26 @@ export const bookingFormSchema = z
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of REQUIRED_DOC_KEYS) {
|
|
||||||
const value = data.documents[key];
|
|
||||||
const hasFile = Array.isArray(value) ? value.length > 0 : Boolean(value);
|
|
||||||
if (!hasFile) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
path: ["documents", key],
|
|
||||||
message: "Upload this required document.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.termsAccepted) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
path: ["termsAccepted"],
|
|
||||||
message: "Accept the freight contract terms to submit.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
export type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
||||||
|
|
||||||
export const initialBookingFormValues: Partial<BookingFormValues> = {
|
export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
|
||||||
previousContractRef: "",
|
previousContractRef: "",
|
||||||
firstMileEnabled: false,
|
|
||||||
pickUpAddress: "",
|
firstMile: {
|
||||||
lastMileEnabled: false,
|
enabled: false,
|
||||||
deliveryAddress: "",
|
pickUpAddress: "",
|
||||||
|
},
|
||||||
|
lastMile: {
|
||||||
|
enabled: false,
|
||||||
|
deliveryAddress: "",
|
||||||
|
},
|
||||||
equipmentReturn: "with_return",
|
equipmentReturn: "with_return",
|
||||||
|
customsClearingEnabled: false,
|
||||||
originYard: "",
|
originYard: "",
|
||||||
destinationYard: "",
|
destinationYard: "",
|
||||||
|
shippingLine: "",
|
||||||
cargoWeight: "",
|
cargoWeight: "",
|
||||||
bulkCommodity: "",
|
bulkCommodity: "",
|
||||||
bulkCommodityOther: "",
|
bulkCommodityOther: "",
|
||||||
@@ -365,25 +273,29 @@ export const initialBookingFormValues: Partial<BookingFormValues> = {
|
|||||||
breakBulkTypeOther: "",
|
breakBulkTypeOther: "",
|
||||||
isHazardous: false,
|
isHazardous: false,
|
||||||
isRefrigerated: false,
|
isRefrigerated: false,
|
||||||
containers: [{ type: "20ft", qty: "1", vgm: "" }],
|
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
||||||
consolidationEnabled: false,
|
consolidationEnabled: false,
|
||||||
documents: {},
|
|
||||||
notes: "",
|
notes: "",
|
||||||
termsAccepted: false,
|
termsAccepted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stepFields: Record<number, Array<keyof BookingFormValues>> = {
|
export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
||||||
1: ["contractType", "previousContractRef"],
|
1: ["contractType", "previousContractRef"],
|
||||||
2: ["serviceType"],
|
2: [
|
||||||
3: [
|
"serviceType",
|
||||||
"firstMileEnabled",
|
"firstMile",
|
||||||
"pickUpAddress",
|
"lastMile",
|
||||||
"lastMileEnabled",
|
|
||||||
"deliveryAddress",
|
|
||||||
"equipmentReturn",
|
"equipmentReturn",
|
||||||
|
"customsClearingEnabled",
|
||||||
],
|
],
|
||||||
4: ["originYard", "destinationYard", "isHazardous", "isRefrigerated"],
|
3: [
|
||||||
5: [
|
"originYard",
|
||||||
|
"destinationYard",
|
||||||
|
"isHazardous",
|
||||||
|
"isRefrigerated",
|
||||||
|
"shippingLine",
|
||||||
|
],
|
||||||
|
4: [
|
||||||
"cargoType",
|
"cargoType",
|
||||||
"cargoWeight",
|
"cargoWeight",
|
||||||
"freightType",
|
"freightType",
|
||||||
@@ -392,16 +304,16 @@ export const stepFields: Record<number, Array<keyof BookingFormValues>> = {
|
|||||||
"breakBulkType",
|
"breakBulkType",
|
||||||
"breakBulkTypeOther",
|
"breakBulkTypeOther",
|
||||||
"containers",
|
"containers",
|
||||||
|
"consolidationEnabled",
|
||||||
],
|
],
|
||||||
6: ["consolidationEnabled"],
|
5: ["notes", "termsAccepted"],
|
||||||
7: ["documents"],
|
|
||||||
8: ["notes", "termsAccepted"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RouteDirection = "import" | "export" | "domestic" | null;
|
export type RouteDirection = "import" | "export" | "domestic" | null;
|
||||||
|
|
||||||
export interface ContainerConfig {
|
export interface ContainerConfig {
|
||||||
type: "20ft" | "40ft";
|
type: "20ft" | "40ft";
|
||||||
|
containerType: string;
|
||||||
qty: string;
|
qty: string;
|
||||||
vgm: string;
|
vgm: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
import type { BookingFormValues } from "./schema";
|
import type { BookingFormValues } from "./schema";
|
||||||
import { REQUIRED_DOC_KEYS } from "./schema";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function getUploadedRequiredCount(
|
|
||||||
documents: BookingFormValues["documents"],
|
|
||||||
) {
|
|
||||||
return REQUIRED_DOC_KEYS.filter((key) => {
|
|
||||||
const file = documents[key];
|
|
||||||
return Array.isArray(file) ? file.length > 0 : Boolean(file);
|
|
||||||
}).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OptionFieldError({ error }: { error?: { message?: string } }) {
|
export function OptionFieldError({ error }: { error?: { message?: string } }) {
|
||||||
return <FieldError errors={[error]} />;
|
return <FieldError errors={[error]} />;
|
||||||
}
|
}
|
||||||
@@ -160,6 +150,8 @@ export function SelectField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { SelectItem };
|
||||||
|
|
||||||
export function SelectOptions({ options }: { options: readonly string[] }) {
|
export function SelectOptions({ options }: { options: readonly string[] }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||||
import { FileText, Loader2, RefreshCw } from "lucide-react";
|
import { FileText, RefreshCw } from "lucide-react";
|
||||||
import { Button, Field, FieldLabel, Input } from "@edr/ui-common";
|
import { Field } from "@edr/ui-common";
|
||||||
import { type BookingFormValues } from "./schema";
|
import { MOCK_VALID_CONTRACTS, type BookingFormValues } from "./schema";
|
||||||
import { AlertBox, OptionCard, OptionFieldError, StepHeader } from "./shared";
|
import {
|
||||||
|
AlertBox,
|
||||||
|
OptionCard,
|
||||||
|
OptionFieldError,
|
||||||
|
SelectField,
|
||||||
|
SelectItem,
|
||||||
|
StepHeader,
|
||||||
|
} from "./shared";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||||
|
|
||||||
export function Step1ContractType({
|
export function Step1ContractType({ form }: { form: BookingForm }) {
|
||||||
form,
|
|
||||||
renewalValid,
|
|
||||||
renewalValidating,
|
|
||||||
onValidate,
|
|
||||||
}: {
|
|
||||||
form: BookingForm;
|
|
||||||
renewalValid: boolean | null;
|
|
||||||
renewalValidating: boolean;
|
|
||||||
onValidate: () => void;
|
|
||||||
}) {
|
|
||||||
const contractType = form.watch("contractType");
|
const contractType = form.watch("contractType");
|
||||||
const previousContractRef = form.watch("previousContractRef");
|
const previousContractRef = form.watch("previousContractRef");
|
||||||
|
|
||||||
@@ -38,6 +35,7 @@ export function Step1ContractType({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
field.onChange("new");
|
field.onChange("new");
|
||||||
form.clearErrors(["contractType", "previousContractRef"]);
|
form.clearErrors(["contractType", "previousContractRef"]);
|
||||||
|
form.setValue("previousContractRef", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
||||||
@@ -45,7 +43,7 @@ export function Step1ContractType({
|
|||||||
</div>
|
</div>
|
||||||
<p className="font-semibold">New Contract</p>
|
<p className="font-semibold">New Contract</p>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Blank contract form. A draft ID is auto-generated.
|
Create a new contract.
|
||||||
</p>
|
</p>
|
||||||
</OptionCard>
|
</OptionCard>
|
||||||
|
|
||||||
@@ -61,7 +59,7 @@ export function Step1ContractType({
|
|||||||
</div>
|
</div>
|
||||||
<p className="font-semibold">Contract Renewal</p>
|
<p className="font-semibold">Contract Renewal</p>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Enter a previous reference to auto-populate historical
|
Select a previous reference to auto-populate historical
|
||||||
parameters.
|
parameters.
|
||||||
</p>
|
</p>
|
||||||
</OptionCard>
|
</OptionCard>
|
||||||
@@ -77,46 +75,26 @@ export function Step1ContractType({
|
|||||||
name="previousContractRef"
|
name="previousContractRef"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Field data-invalid={fieldState.invalid}>
|
<SelectField
|
||||||
<FieldLabel htmlFor="previousContractRef">
|
field={field}
|
||||||
Previous Contract Reference Number
|
error={fieldState.error}
|
||||||
</FieldLabel>
|
label="Previous Contract Reference Number"
|
||||||
<div className="flex gap-2">
|
placeholder="Select a contract..."
|
||||||
<Input
|
>
|
||||||
{...field}
|
{MOCK_VALID_CONTRACTS.map((ref) => (
|
||||||
id="previousContractRef"
|
<SelectItem key={ref} value={ref}>
|
||||||
aria-invalid={fieldState.invalid}
|
{ref}
|
||||||
placeholder="e.g. EDR-2024-10001"
|
</SelectItem>
|
||||||
className="font-mono"
|
))}
|
||||||
/>
|
</SelectField>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onValidate}
|
|
||||||
disabled={!previousContractRef || renewalValidating}
|
|
||||||
>
|
|
||||||
{renewalValidating ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Validate"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{renewalValid === true && (
|
{previousContractRef && (
|
||||||
<AlertBox tone="success">
|
<AlertBox tone="success">
|
||||||
<strong>Contract found.</strong> Company details, route, and wagon
|
<strong>Contract found.</strong> Company details, route, and wagon
|
||||||
preferences will be pre-filled.
|
preferences will be pre-filled.
|
||||||
</AlertBox>
|
</AlertBox>
|
||||||
)}
|
)}
|
||||||
{renewalValid === false && (
|
|
||||||
<AlertBox tone="error">
|
|
||||||
Contract Reference Number not found or unauthorized. Try{" "}
|
|
||||||
<span className="font-mono">EDR-2024-10001</span>.
|
|
||||||
</AlertBox>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||||
import { Package, Train } from "lucide-react";
|
import { FileText, Package, Train, Truck } from "lucide-react";
|
||||||
import { Badge, Field, FieldError } from "@edr/ui-common";
|
import { Badge, Field, FieldError, Input, Switch } from "@edr/ui-common";
|
||||||
import { type BookingFormValues } from "./schema";
|
import { type BookingFormValues } from "./schema";
|
||||||
import { OptionCard, OptionFieldError, StepHeader } from "./shared";
|
import { OptionCard, OptionFieldError, StepHeader } from "./shared";
|
||||||
|
|
||||||
@@ -8,12 +9,67 @@ type BookingForm = UseFormReturn<BookingFormValues>;
|
|||||||
|
|
||||||
export function Step2ServiceType({ form }: { form: BookingForm }) {
|
export function Step2ServiceType({ form }: { form: BookingForm }) {
|
||||||
const serviceType = form.watch("serviceType");
|
const serviceType = form.watch("serviceType");
|
||||||
|
const firstMileEnabled = form.watch("firstMile.enabled");
|
||||||
|
const lastMileEnabled = form.watch("lastMile.enabled");
|
||||||
|
|
||||||
|
const prevServiceType = useRef(serviceType);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevServiceType.current;
|
||||||
|
prevServiceType.current = serviceType;
|
||||||
|
|
||||||
|
if (!prev || prev === serviceType) return;
|
||||||
|
|
||||||
|
if (serviceType === "rail") {
|
||||||
|
form.setValue(
|
||||||
|
"firstMile",
|
||||||
|
{ enabled: false, pickUpAddress: "" },
|
||||||
|
{ shouldDirty: true, shouldValidate: true },
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"lastMile",
|
||||||
|
{ enabled: false, deliveryAddress: "" },
|
||||||
|
{ shouldDirty: true, shouldValidate: true },
|
||||||
|
);
|
||||||
|
form.setValue("equipmentReturn", "with_return", {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
form.setValue("customsClearingEnabled", false, {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
} else if (serviceType === "rail_forwarding") {
|
||||||
|
form.setValue(
|
||||||
|
"firstMile",
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
pickUpAddress: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldDirty: false,
|
||||||
|
shouldValidate: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"lastMile",
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
deliveryAddress: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldDirty: false,
|
||||||
|
shouldValidate: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [serviceType, form]);
|
||||||
|
|
||||||
|
const showServiceSections = serviceType === "rail_forwarding";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<StepHeader
|
<StepHeader
|
||||||
title="Service Type"
|
title="Service Type"
|
||||||
description="Select the service combination you require."
|
description="Select the service combination and configure trucking options."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
@@ -46,9 +102,7 @@ export function Step2ServiceType({ form }: { form: BookingForm }) {
|
|||||||
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
||||||
<Package className="h-4 w-4 text-primary" />
|
<Package className="h-4 w-4 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">Logistics</p>
|
||||||
Rail Transport & Freight Forwarding
|
|
||||||
</p>
|
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Rail transport plus documentation, customs liaison, and a
|
Rail transport plus documentation, customs liaison, and a
|
||||||
dedicated coordinator.
|
dedicated coordinator.
|
||||||
@@ -63,10 +117,172 @@ export function Step2ServiceType({ form }: { form: BookingForm }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
{showServiceSections && (
|
||||||
Customs and Clearance Service cannot be selected independently. It must
|
<div className="divide-y divide-border rounded-xl border border-border">
|
||||||
be bundled with a Rail Transport service.
|
<div className="p-4">
|
||||||
</p>
|
<Controller
|
||||||
|
name="firstMile.enabled"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Truck className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
First Mile - Pick-up
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
Truck pick-up from your premises (Door to Port) to the
|
||||||
|
origin rail yard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
if (!value) {
|
||||||
|
form.setValue("firstMile.pickUpAddress", "", {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{firstMileEnabled && (
|
||||||
|
<Controller
|
||||||
|
name="firstMile.pickUpAddress"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field className="mt-3" data-invalid={fieldState.invalid}>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
placeholder="Pick-up address *"
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<Controller
|
||||||
|
name="lastMile.enabled"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Truck className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Last Mile - Delivery
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
Truck delivery from the destination rail yard to the
|
||||||
|
final address (Port to Door).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
if (!value) {
|
||||||
|
form.setValue("lastMile.deliveryAddress", "", {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
form.setValue("equipmentReturn", "with_return", {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{lastMileEnabled && (
|
||||||
|
<Controller
|
||||||
|
name="lastMile.deliveryAddress"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field className="mt-3" data-invalid={fieldState.invalid}>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
placeholder="Delivery address *"
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lastMileEnabled && (
|
||||||
|
<div className="p-4">
|
||||||
|
<Controller
|
||||||
|
name="equipmentReturn"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Equipment Return</p>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
{field.value === "with_return"
|
||||||
|
? "Container returned to EDR after unloading."
|
||||||
|
: "Container retained by the customer after delivery."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={field.value === "with_return"}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
field.onChange(
|
||||||
|
value ? "with_return" : "without_return",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<Controller
|
||||||
|
name="customsClearingEnabled"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Customs Clearing Service
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
EDR handles customs documentation and clearance on your
|
||||||
|
behalf.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||||
import { Field, FieldError, Input, Switch } from "@edr/ui-common";
|
import { Field, FieldError, Input, Switch } from "@edr/ui-common";
|
||||||
import { type BookingFormValues } from "./schema";
|
import { type BookingFormValues } from "./schema";
|
||||||
import { OptionCard, StepHeader } from "./shared";
|
import { StepHeader } from "./shared";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ export function Step3FirstLastMile({ form }: { form: BookingForm }) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">First Mile - Pick-up</p>
|
<p className="text-sm font-medium">First Mile - Pick-up</p>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Truck pick-up from your premises to the origin rail yard.
|
Truck pick-up from your premises (Door to Port) to the
|
||||||
|
origin rail yard.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -73,7 +74,7 @@ export function Step3FirstLastMile({ form }: { form: BookingForm }) {
|
|||||||
<p className="text-sm font-medium">Last Mile - Delivery</p>
|
<p className="text-sm font-medium">Last Mile - Delivery</p>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Truck delivery from the destination rail yard to the final
|
Truck delivery from the destination rail yard to the final
|
||||||
address.
|
address (Port to Door).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -107,40 +108,35 @@ export function Step3FirstLastMile({ form }: { form: BookingForm }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4">
|
{lastMileEnabled && (
|
||||||
<p className="mb-3 text-sm font-medium">Equipment Return</p>
|
<div className="mt-4 border-t border-border pt-4">
|
||||||
<p className="mb-3 text-xs text-muted-foreground">
|
<Controller
|
||||||
Declare whether the container asset will be returned after
|
name="equipmentReturn"
|
||||||
unloading.
|
control={form.control}
|
||||||
</p>
|
render={({ field }) => (
|
||||||
<Controller
|
<div className="flex items-start justify-between gap-4">
|
||||||
name="equipmentReturn"
|
<div>
|
||||||
control={form.control}
|
<p className="text-sm font-medium">Equipment Return</p>
|
||||||
render={({ field }) => (
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
{field.value === "with_return"
|
||||||
<OptionCard
|
? "Container returned to EDR after unloading."
|
||||||
selected={equipmentReturn === "with_return"}
|
: "Container retained by the customer after delivery."}
|
||||||
onClick={() => field.onChange("with_return")}
|
</p>
|
||||||
>
|
</div>
|
||||||
<p className="text-sm font-semibold">With Return</p>
|
<Switch
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
checked={field.value === "with_return"}
|
||||||
Container returned to EDR after unloading.
|
onCheckedChange={(value) => {
|
||||||
</p>
|
field.onChange(
|
||||||
</OptionCard>
|
value ? "with_return" : "without_return",
|
||||||
<OptionCard
|
);
|
||||||
selected={equipmentReturn === "without_return"}
|
}}
|
||||||
onClick={() => field.onChange("without_return")}
|
/>
|
||||||
>
|
</div>
|
||||||
<p className="text-sm font-semibold">Without Return</p>
|
)}
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
/>
|
||||||
Container retained by the customer after delivery.
|
</div>
|
||||||
</p>
|
)}
|
||||||
</OptionCard>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||||
import { Flame, MapPin, Snowflake } from "lucide-react";
|
import { Flame, MapPin, Snowflake } from "lucide-react";
|
||||||
import { Field, SelectItem, Separator, Switch } from "@edr/ui-common";
|
import { Field, SelectItem, Separator, Switch } from "@edr/ui-common";
|
||||||
import { type BookingFormValues, getRouteDirection, STATIONS } from "./schema";
|
import {
|
||||||
|
SHIPPING_LINES,
|
||||||
|
type BookingFormValues,
|
||||||
|
getRouteDirection,
|
||||||
|
STATIONS,
|
||||||
|
} from "./schema";
|
||||||
import {
|
import {
|
||||||
AlertBox,
|
AlertBox,
|
||||||
SelectField,
|
SelectField,
|
||||||
@@ -11,6 +16,7 @@ import {
|
|||||||
} from "./shared";
|
} from "./shared";
|
||||||
import { useDropdownSettingByCode } from "@/hooks/useDropdownSettings";
|
import { useDropdownSettingByCode } from "@/hooks/useDropdownSettings";
|
||||||
import { DropdownOption } from "@/types/dropdownSettings";
|
import { DropdownOption } from "@/types/dropdownSettings";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||||
|
|
||||||
@@ -39,6 +45,12 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
|||||||
};
|
};
|
||||||
const stationSelectDisabled = stationsLoading || stationOptions.length === 0;
|
const stationSelectDisabled = stationsLoading || stationOptions.length === 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (direction === "domestic") {
|
||||||
|
form.setValue("shippingLine", "", { shouldDirty: true });
|
||||||
|
}
|
||||||
|
}, [direction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<StepHeader
|
<StepHeader
|
||||||
@@ -106,6 +118,22 @@ export function Step4Route({ form }: { form: BookingForm }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{direction && direction != "domestic" && (
|
||||||
|
<Controller
|
||||||
|
name="shippingLine"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<SelectField
|
||||||
|
field={field}
|
||||||
|
error={fieldState.error}
|
||||||
|
label="Shipping Line"
|
||||||
|
placeholder="Select shipping line..."
|
||||||
|
>
|
||||||
|
<SelectOptions options={SHIPPING_LINES} />
|
||||||
|
</SelectField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="space-y-0 divide-y divide-border">
|
<div className="space-y-0 divide-y divide-border">
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||||
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
|
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
|
||||||
import {
|
import { Button, Field, FieldError, FieldLabel, Input } from "@edr/ui-common";
|
||||||
Button,
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
Input,
|
|
||||||
Separator,
|
|
||||||
} from "@edr/ui-common";
|
|
||||||
import {
|
import {
|
||||||
BREAK_BULK_TYPES,
|
BREAK_BULK_TYPES,
|
||||||
BULK_COMMODITIES,
|
BULK_COMMODITIES,
|
||||||
|
CONTAINER_TYPES,
|
||||||
|
calcWagons,
|
||||||
type BookingFormValues,
|
type BookingFormValues,
|
||||||
type RouteDirection,
|
type RouteDirection,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
@@ -81,7 +76,6 @@ export function Step5CargoDetails({
|
|||||||
form.setValue("freightType", undefined, {
|
form.setValue("freightType", undefined, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
});
|
});
|
||||||
form.setValue("cargoWeight", "", { shouldDirty: true });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
||||||
@@ -96,11 +90,7 @@ export function Step5CargoDetails({
|
|||||||
selected={cargoType === "bulk"}
|
selected={cargoType === "bulk"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
field.onChange("bulk");
|
field.onChange("bulk");
|
||||||
form.setValue(
|
form.setValue("containers", [], { shouldDirty: true });
|
||||||
"containers",
|
|
||||||
[{ type: "20ft", qty: "1", vgm: "0" }],
|
|
||||||
{ shouldDirty: true },
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-amber-100">
|
<div className="mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-amber-100">
|
||||||
@@ -118,173 +108,162 @@ export function Step5CargoDetails({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<StepLabel>Weight</StepLabel>
|
||||||
|
<Controller
|
||||||
|
name="cargoWeight"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<FieldLabel htmlFor="cargoWeight">
|
||||||
|
Total Cargo Weight(Tons)*
|
||||||
|
</FieldLabel>
|
||||||
|
<div className="relative">
|
||||||
|
<Weight className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="cargoWeight"
|
||||||
|
type="number"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
placeholder="0.00"
|
||||||
|
className="pl-9"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{cargoType === "bulk" && (
|
{cargoType === "bulk" && (
|
||||||
<>
|
<div className="space-y-3">
|
||||||
<Separator />
|
<StepLabel>Freight Type *</StepLabel>
|
||||||
<div className="space-y-3">
|
<Controller
|
||||||
<StepLabel>Freight Type *</StepLabel>
|
name="freightType"
|
||||||
<Controller
|
control={form.control}
|
||||||
name="freightType"
|
render={({ field, fieldState }) => (
|
||||||
control={form.control}
|
<Field data-invalid={fieldState.invalid}>
|
||||||
render={({ field, fieldState }) => (
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<Field data-invalid={fieldState.invalid}>
|
<OptionCard
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
selected={freightType === "bulk"}
|
||||||
<OptionCard
|
onClick={() => field.onChange("bulk")}
|
||||||
selected={freightType === "bulk"}
|
>
|
||||||
onClick={() => field.onChange("bulk")}
|
<p className="font-semibold">Bulk</p>
|
||||||
>
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
<p className="font-semibold">Bulk</p>
|
Coffee, fertilizer, grain, ore, etc.
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
</p>
|
||||||
Coffee, fertilizer, grain, ore, etc.
|
</OptionCard>
|
||||||
</p>
|
<OptionCard
|
||||||
</OptionCard>
|
selected={freightType === "break_bulk"}
|
||||||
<OptionCard
|
onClick={() => field.onChange("break_bulk")}
|
||||||
selected={freightType === "break_bulk"}
|
>
|
||||||
onClick={() => field.onChange("break_bulk")}
|
<p className="font-semibold">Break-Bulk</p>
|
||||||
>
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
<p className="font-semibold">Break-Bulk</p>
|
Machinery, vehicles, project cargo, etc.
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
</p>
|
||||||
Machinery, vehicles, project cargo, etc.
|
</OptionCard>
|
||||||
</p>
|
</div>
|
||||||
</OptionCard>
|
<FieldError errors={[fieldState.error]} />
|
||||||
</div>
|
</Field>
|
||||||
<FieldError errors={[fieldState.error]} />
|
)}
|
||||||
</Field>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{freightType === "bulk" && (
|
{freightType === "bulk" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<Controller
|
||||||
|
name="bulkCommodity"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<SelectField
|
||||||
|
field={field}
|
||||||
|
error={fieldState.error}
|
||||||
|
label="Commodity *"
|
||||||
|
placeholder="Select commodity *"
|
||||||
|
>
|
||||||
|
<SelectOptions options={BULK_COMMODITIES} />
|
||||||
|
</SelectField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{bulkCommodity === "Others" && (
|
||||||
<Controller
|
<Controller
|
||||||
name="bulkCommodity"
|
name="bulkCommodityOther"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SelectField
|
<Field data-invalid={fieldState.invalid}>
|
||||||
field={field}
|
<Input
|
||||||
error={fieldState.error}
|
{...field}
|
||||||
label="Commodity *"
|
aria-invalid={fieldState.invalid}
|
||||||
placeholder="Select commodity *"
|
placeholder="Specify commodity *"
|
||||||
>
|
/>
|
||||||
<SelectOptions options={BULK_COMMODITIES} />
|
<FieldError errors={[fieldState.error]} />
|
||||||
</SelectField>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{bulkCommodity === "Others" && (
|
)}
|
||||||
<Controller
|
</div>
|
||||||
name="bulkCommodityOther"
|
)}
|
||||||
control={form.control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field data-invalid={fieldState.invalid}>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
placeholder="Specify commodity *"
|
|
||||||
/>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{freightType === "break_bulk" && (
|
{freightType === "break_bulk" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<Controller
|
||||||
|
name="breakBulkType"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<SelectField
|
||||||
|
field={field}
|
||||||
|
error={fieldState.error}
|
||||||
|
label="Break-bulk type *"
|
||||||
|
placeholder="Select type *"
|
||||||
|
>
|
||||||
|
<SelectOptions options={BREAK_BULK_TYPES} />
|
||||||
|
</SelectField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{breakBulkType === "Others" && (
|
||||||
<Controller
|
<Controller
|
||||||
name="breakBulkType"
|
name="breakBulkTypeOther"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SelectField
|
<Field data-invalid={fieldState.invalid}>
|
||||||
field={field}
|
<Input
|
||||||
error={fieldState.error}
|
{...field}
|
||||||
label="Break-bulk type *"
|
aria-invalid={fieldState.invalid}
|
||||||
placeholder="Select type *"
|
placeholder="Specify break-bulk type *"
|
||||||
>
|
/>
|
||||||
<SelectOptions options={BREAK_BULK_TYPES} />
|
<FieldError errors={[fieldState.error]} />
|
||||||
</SelectField>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{breakBulkType === "Others" && (
|
|
||||||
<Controller
|
|
||||||
name="breakBulkTypeOther"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field data-invalid={fieldState.invalid}>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
placeholder="Specify break-bulk type *"
|
|
||||||
/>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<StepLabel>Weight</StepLabel>
|
|
||||||
<Controller
|
|
||||||
name="cargoWeight"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field data-invalid={fieldState.invalid}>
|
|
||||||
<FieldLabel htmlFor="cargoWeight">
|
|
||||||
Total Cargo Weight - VGM (Tons) *
|
|
||||||
</FieldLabel>
|
|
||||||
<div className="relative">
|
|
||||||
<Weight className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
id="cargoWeight"
|
|
||||||
type="number"
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
placeholder="0.00"
|
|
||||||
className="pl-9"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cargoType === "container" && (
|
{cargoType === "container" && (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<StepLabel>Container Configuration</StepLabel>
|
<StepLabel>Containers</StepLabel>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => append({ type: "20ft", qty: "1", vgm: "0" })}
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
type: "20ft",
|
||||||
|
containerType: "",
|
||||||
|
qty: "1",
|
||||||
|
vgm: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3.5 w-3.5" />
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||||
Add Container
|
Add Container
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{direction && (
|
|
||||||
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<MapPin className="h-3.5 w-3.5" />
|
|
||||||
Route detected as{" "}
|
|
||||||
<span className="font-medium capitalize text-foreground">
|
|
||||||
{direction}
|
|
||||||
</span>{" "}
|
|
||||||
workflow
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const containerType = containers[index]?.type;
|
const containerType = containers[index]?.type;
|
||||||
const vgm = containers[index]?.vgm ?? 0;
|
const vgm = containers[index]?.vgm ?? 0;
|
||||||
@@ -293,12 +272,9 @@ export function Step5CargoDetails({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className="space-y-3 rounded-xl border border-border p-4"
|
className="rounded-xl flex flex-col border border-border p-3 gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
||||||
Container #{index + 1}
|
|
||||||
</p>
|
|
||||||
{fields.length > 1 && (
|
{fields.length > 1 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -315,7 +291,6 @@ export function Step5CargoDetails({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field: typeField, fieldState }) => (
|
render={({ field: typeField, fieldState }) => (
|
||||||
<Field data-invalid={fieldState.invalid}>
|
<Field data-invalid={fieldState.invalid}>
|
||||||
<FieldLabel>Container Type *</FieldLabel>
|
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -352,7 +327,7 @@ export function Step5CargoDetails({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<Controller
|
<Controller
|
||||||
name={`containers.${index}.qty`}
|
name={`containers.${index}.qty`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -407,7 +382,7 @@ export function Step5CargoDetails({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field: vgmField, fieldState }) => (
|
render={({ field: vgmField, fieldState }) => (
|
||||||
<Field data-invalid={fieldState.invalid}>
|
<Field data-invalid={fieldState.invalid}>
|
||||||
<FieldLabel>VGM (Tons) *</FieldLabel>
|
<FieldLabel>Tons*</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
value={vgmField.value ?? 0}
|
value={vgmField.value ?? 0}
|
||||||
onChange={(e) => vgmField.onChange(e.target.value)}
|
onChange={(e) => vgmField.onChange(e.target.value)}
|
||||||
@@ -422,6 +397,21 @@ export function Step5CargoDetails({
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name={`containers.${index}.containerType`}
|
||||||
|
control={form.control}
|
||||||
|
render={({ field: ctField, fieldState }) => (
|
||||||
|
<SelectField
|
||||||
|
field={ctField}
|
||||||
|
error={fieldState.error}
|
||||||
|
label="Container Type *"
|
||||||
|
placeholder="Select type..."
|
||||||
|
>
|
||||||
|
<SelectOptions options={CONTAINER_TYPES} />
|
||||||
|
</SelectField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{alert && (
|
{alert && (
|
||||||
@@ -433,6 +423,29 @@ export function Step5CargoDetails({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const result = calcWagons(containers ?? []);
|
||||||
|
if (result.hasOddUnit) {
|
||||||
|
return (
|
||||||
|
<AlertBox tone="warning">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Unpaired 20ft Container</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
One 20ft container occupies only half a wagon. The wagon
|
||||||
|
will depart once a co-loader is found to fill the
|
||||||
|
remaining slot, which{" "}
|
||||||
|
<strong>may delay departure</strong> beyond the standard
|
||||||
|
lead time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,24 +11,21 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
import {
|
import {
|
||||||
REQUIRED_DOC_KEYS,
|
|
||||||
type BookingFormValues,
|
type BookingFormValues,
|
||||||
type RouteDirection,
|
type RouteDirection,
|
||||||
type WagonCalcResult,
|
type WagonCalcResult,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
import { getUploadedRequiredCount, StepHeader } from "./shared";
|
import { StepHeader } from "./shared";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||||
|
|
||||||
export function Step8Review({
|
export function Step8Review({
|
||||||
form,
|
form,
|
||||||
setStep,
|
setStep,
|
||||||
wagons,
|
|
||||||
direction,
|
direction,
|
||||||
}: {
|
}: {
|
||||||
form: BookingForm;
|
form: BookingForm;
|
||||||
setStep: (step: number) => void;
|
setStep: (step: number) => void;
|
||||||
wagons: WagonCalcResult | null;
|
|
||||||
direction: RouteDirection;
|
direction: RouteDirection;
|
||||||
}) {
|
}) {
|
||||||
const values = form.watch();
|
const values = form.watch();
|
||||||
@@ -63,13 +60,16 @@ export function Step8Review({
|
|||||||
const containerSummary =
|
const containerSummary =
|
||||||
values.cargoType === "container" && values.containers.length > 0
|
values.cargoType === "container" && values.containers.length > 0
|
||||||
? values.containers
|
? values.containers
|
||||||
.filter((c) => c.qty > 0)
|
.filter((c) => +c.qty > 0)
|
||||||
.map((c) => `${c.qty} × ${c.type}`)
|
.map((c) => `${c.qty} × ${c.type}`)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
: "";
|
: "";
|
||||||
const totalVgm =
|
const totalVgm =
|
||||||
values.cargoType === "container"
|
values.cargoType === "container"
|
||||||
? values.containers.reduce((sum, c) => sum + (c.qty || 0) * (c.vgm || 0), 0)
|
? values.containers.reduce(
|
||||||
|
(sum, c) => sum + (+c.qty || 0) * (+c.vgm || 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const cargoValue =
|
const cargoValue =
|
||||||
@@ -80,8 +80,6 @@ export function Step8Review({
|
|||||||
: values.freightType === "break_bulk"
|
: values.freightType === "break_bulk"
|
||||||
? `Break-Bulk - ${values.breakBulkType === "Others" ? values.breakBulkTypeOther : values.breakBulkType}`
|
? `Break-Bulk - ${values.breakBulkType === "Others" ? values.breakBulkTypeOther : values.breakBulkType}`
|
||||||
: "";
|
: "";
|
||||||
const uploadedCount = getUploadedRequiredCount(values.documents);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<StepHeader
|
<StepHeader
|
||||||
@@ -91,7 +89,7 @@ export function Step8Review({
|
|||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Card className="gap-0 overflow-hidden py-0">
|
<Card className="gap-0 overflow-hidden py-0">
|
||||||
<CardHeader className="border-b px-5 py-3">
|
<CardHeader className="border-b px-5 py-3!">
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Contract & Service
|
Contract & Service
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -102,11 +100,6 @@ export function Step8Review({
|
|||||||
value={values.contractType === "new" ? "New Contract" : "Renewal"}
|
value={values.contractType === "new" ? "New Contract" : "Renewal"}
|
||||||
target={1}
|
target={1}
|
||||||
/>
|
/>
|
||||||
<Row
|
|
||||||
label="Contract ID"
|
|
||||||
value={values.draftContractId || values.previousContractRef}
|
|
||||||
target={1}
|
|
||||||
/>
|
|
||||||
<Row
|
<Row
|
||||||
label="Service"
|
label="Service"
|
||||||
value={
|
value={
|
||||||
@@ -122,7 +115,7 @@ export function Step8Review({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="gap-0 overflow-hidden py-0">
|
<Card className="gap-0 overflow-hidden py-0">
|
||||||
<CardHeader className="border-b px-5 py-3">
|
<CardHeader className="border-b px-5 py-3!">
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
First & Last Mile
|
First & Last Mile
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -131,18 +124,20 @@ export function Step8Review({
|
|||||||
<Row
|
<Row
|
||||||
label="First Mile"
|
label="First Mile"
|
||||||
value={
|
value={
|
||||||
values.firstMileEnabled ? values.pickUpAddress : "Not requested"
|
values.firstMile.enabled
|
||||||
|
? values.firstMile.pickUpAddress
|
||||||
|
: "Not requested"
|
||||||
}
|
}
|
||||||
target={3}
|
target={2}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label="Last Mile"
|
label="Last Mile"
|
||||||
value={
|
value={
|
||||||
values.lastMileEnabled
|
values.lastMile.enabled
|
||||||
? values.deliveryAddress
|
? values.lastMile.deliveryAddress
|
||||||
: "Not requested"
|
: "Not requested"
|
||||||
}
|
}
|
||||||
target={3}
|
target={2}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label="Equipment Return"
|
label="Equipment Return"
|
||||||
@@ -151,13 +146,20 @@ export function Step8Review({
|
|||||||
? "With Return"
|
? "With Return"
|
||||||
: "Without Return"
|
: "Without Return"
|
||||||
}
|
}
|
||||||
target={3}
|
target={2}
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
label="Customs Clearing"
|
||||||
|
value={
|
||||||
|
values.customsClearingEnabled ? "Enabled" : "Not requested"
|
||||||
|
}
|
||||||
|
target={2}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="gap-0 overflow-hidden py-0">
|
<Card className="gap-0 overflow-hidden py-0">
|
||||||
<CardHeader className="border-b px-5 py-3">
|
<CardHeader className="border-b px-5 py-3!">
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Route & Cargo
|
Route & Cargo
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -166,7 +168,7 @@ export function Step8Review({
|
|||||||
<Row
|
<Row
|
||||||
label="Route"
|
label="Route"
|
||||||
value={`${values.originYard} -> ${values.destinationYard}`}
|
value={`${values.originYard} -> ${values.destinationYard}`}
|
||||||
target={4}
|
target={3}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label="Workflow"
|
label="Workflow"
|
||||||
@@ -175,14 +177,14 @@ export function Step8Review({
|
|||||||
? direction.charAt(0).toUpperCase() + direction.slice(1)
|
? direction.charAt(0).toUpperCase() + direction.slice(1)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
target={4}
|
target={3}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label="Weight (VGM)"
|
label="Weight (VGM)"
|
||||||
value={values.cargoWeight ? `${values.cargoWeight} tons` : ""}
|
value={values.cargoWeight ? `${values.cargoWeight} tons` : ""}
|
||||||
target={5}
|
target={4}
|
||||||
/>
|
/>
|
||||||
<Row label="Cargo" value={cargoValue} target={5} />
|
<Row label="Cargo" value={cargoValue} target={4} />
|
||||||
<Row
|
<Row
|
||||||
label="Modifiers"
|
label="Modifiers"
|
||||||
value={
|
value={
|
||||||
@@ -193,13 +195,13 @@ export function Step8Review({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(", ") || "None"
|
.join(", ") || "None"
|
||||||
}
|
}
|
||||||
target={4}
|
target={3}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="gap-0 overflow-hidden py-0">
|
<Card className="gap-0 overflow-hidden py-0">
|
||||||
<CardHeader className="border-b px-5 py-3">
|
<CardHeader className="border-b px-5 py-3!">
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<CardTitle className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Container & Wagons
|
Container & Wagons
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -208,26 +210,12 @@ export function Step8Review({
|
|||||||
<Row
|
<Row
|
||||||
label="Containers"
|
label="Containers"
|
||||||
value={containerSummary || "-"}
|
value={containerSummary || "-"}
|
||||||
target={5}
|
target={4}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label="Total VGM"
|
label="Total VGM"
|
||||||
value={totalVgm > 0 ? `${totalVgm.toFixed(1)} tons` : ""}
|
value={totalVgm > 0 ? `${totalVgm.toFixed(1)} tons` : ""}
|
||||||
target={5}
|
target={4}
|
||||||
/>
|
|
||||||
<Row
|
|
||||||
label="Wagons"
|
|
||||||
value={
|
|
||||||
wagons
|
|
||||||
? `${wagons.totalWagons} wagon${wagons.totalWagons > 1 ? "s" : ""}`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
target={6}
|
|
||||||
/>
|
|
||||||
<Row
|
|
||||||
label="Documents"
|
|
||||||
value={`${uploadedCount}/${REQUIRED_DOC_KEYS.length} mandatory uploaded`}
|
|
||||||
target={7}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
export { Step1ContractType } from "./step1-contract-type";
|
export { Step1ContractType } from "./step1-contract-type";
|
||||||
export { Step2ServiceType } from "./step2-service-type";
|
export { Step2ServiceType } from "./step2-service-type";
|
||||||
export { Step3FirstLastMile } from "./step3-first-last-mile";
|
|
||||||
export { Step4Route } from "./step4-route";
|
export { Step4Route } from "./step4-route";
|
||||||
export { Step5CargoDetails } from "./step5-cargo-details";
|
export { Step5CargoDetails } from "./step5-cargo-details";
|
||||||
export { Step6WagonAllocation } from "./step6-wagon-allocation";
|
|
||||||
export { Step7Documents } from "./step7-documents";
|
|
||||||
export { Step8Review } from "./step8-review";
|
export { Step8Review } from "./step8-review";
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export interface CreateBookingDto {
|
|||||||
lastMileDeliveryAddress?: string;
|
lastMileDeliveryAddress?: string;
|
||||||
|
|
||||||
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
|
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
|
||||||
|
customsClearingEnabled?: boolean;
|
||||||
originStation: string;
|
originStation: string;
|
||||||
destinationStation: string;
|
destinationStation: string;
|
||||||
cargoTotalWeightVgm: number;
|
cargoTotalWeightVgm: number;
|
||||||
|
|||||||
5
tasks.md
Normal file
5
tasks.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
- In ./apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx the input field for the previous contract ref should be a select field with the list of contracts from the API
|
||||||
|
- the "Equipment Return" field in ./apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step3-first-last-mile.tsx should be a toggle like Last Mile - Delivery and should appear only if the last mile is toggled. and also add "( Door to Port)" to first mile description and vice versa to the last mile
|
||||||
|
- add Type of container- Dry container, high cubic containers , reefer containers, open top containers, flat rack, tank container, open side containers to ./apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx
|
||||||
|
- merge the "Unpaired 20ft Container" from ./apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx to step 4 and fully remove the step 5
|
||||||
|
- add Type of Shipping lines - MSC, CMA CGM, Evergreen, COSCO, Hapag-Lloyd, ONE, Yang Ming, ZIM, Messina Line, Safmarine, Wan Hai, Ethiopian Shipping Lines (ESLSE) to ./apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx
|
||||||
Reference in New Issue
Block a user