You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
834 lines
29 KiB
834 lines
29 KiB
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input";
|
|
import { DEFAULT_PRIORITY1_DEMO } from "@/workers/rpa/priority1/demo-input";
|
|
import type {
|
|
ExpeditedTrailerType,
|
|
FtlAdditionalService,
|
|
FtlTrailerType,
|
|
Priority1ShipmentType,
|
|
} from "@/workers/rpa/priority1/shipment-types";
|
|
import {
|
|
P1_COMMON_UI,
|
|
P1_DELIVERY_ACCESSORIALS,
|
|
P1_EXP_TRAILERS,
|
|
P1_EXPEDITED_UI,
|
|
P1_FREQUENCY_OPTIONS,
|
|
P1_FREIGHT_CLASS_OPTIONS,
|
|
P1_FTL_ADDITIONAL,
|
|
P1_FTL_LIFTGATE_SERVICE,
|
|
P1_FTL_TRAILERS,
|
|
P1_FTL_UI,
|
|
P1_LTL_UI,
|
|
P1_LOCATION_OPTIONS,
|
|
P1_OPEN_DECK_OPTIONS,
|
|
P1_PACKAGING_OPTIONS,
|
|
P1_PICKUP_ACCESSORIALS,
|
|
P1_SHIPMENT_CARDS,
|
|
P1_SPECIAL_HANDLING,
|
|
P1_STEP2_HEADING,
|
|
P1_STEP2_SUBTITLE,
|
|
} from "@/lib/priority1/ui-labels";
|
|
import {
|
|
FeatheryCard,
|
|
FeatheryCardGrid,
|
|
P1FieldLabel,
|
|
P1InstantQuotesButton,
|
|
P1Step2Progress,
|
|
P1StickyActionBar,
|
|
P1WeightField,
|
|
ShipmentTypeCard,
|
|
} from "@/components/priority1/feathery-card";
|
|
import { P1DateField } from "@/components/priority1/p1-date-field";
|
|
import { P1PhoneField } from "@/components/priority1/p1-phone-field";
|
|
import { findPhoneCountry } from "@/lib/priority1/phone-countries";
|
|
import { pickupDateValidationMessage } from "@/lib/priority1/pickup-date";
|
|
import { SelectField } from "@/components/ui/select-field";
|
|
|
|
export interface Priority1SimulatorProps {
|
|
disabled?: boolean;
|
|
loading?: boolean;
|
|
onSubmit: (input: Priority1DemoInput) => void;
|
|
}
|
|
|
|
function toggleInList(list: string[], value: string): string[] {
|
|
return list.includes(value)
|
|
? list.filter((v) => v !== value)
|
|
: [...list, value];
|
|
}
|
|
|
|
function toggleService(
|
|
list: FtlAdditionalService[],
|
|
svc: FtlAdditionalService,
|
|
): FtlAdditionalService[] {
|
|
return list.includes(svc) ? list.filter((s) => s !== svc) : [...list, svc];
|
|
}
|
|
|
|
function P1FeatherySelect({
|
|
id,
|
|
label,
|
|
required,
|
|
value,
|
|
disabled,
|
|
options,
|
|
onChange,
|
|
}: {
|
|
id: string;
|
|
label: string;
|
|
required?: boolean;
|
|
value: string;
|
|
disabled?: boolean;
|
|
options: Array<{ value: string; label: string }>;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1">
|
|
<P1FieldLabel required={required} htmlFor={id}>
|
|
{label}
|
|
</P1FieldLabel>
|
|
<select
|
|
id={id}
|
|
disabled={disabled}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className="h-11 w-full rounded-sm border border-neutral-400 bg-neutral-100 px-3 text-sm focus:border-neutral-600 focus:outline-none disabled:opacity-60"
|
|
>
|
|
{options.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function P1AccessorialGrid({
|
|
legend,
|
|
options,
|
|
selected,
|
|
disabled,
|
|
onToggle,
|
|
}: {
|
|
legend: string;
|
|
options: Array<{ value: string; label: string }>;
|
|
selected: string[];
|
|
disabled?: boolean;
|
|
onToggle: (value: string) => void;
|
|
}) {
|
|
return (
|
|
<fieldset className="space-y-2">
|
|
<legend className="text-sm font-medium italic text-neutral-900">{legend}</legend>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
{options.map((opt) => (
|
|
<label
|
|
key={opt.value}
|
|
className="flex cursor-pointer items-center gap-2 rounded border border-neutral-300 bg-neutral-50 px-3 py-2 text-sm"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
disabled={disabled}
|
|
checked={selected.includes(opt.value)}
|
|
onChange={() => onToggle(opt.value)}
|
|
/>
|
|
{opt.label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
function P1TextInput({
|
|
id,
|
|
label,
|
|
required,
|
|
value,
|
|
disabled,
|
|
error,
|
|
type = "text",
|
|
onChange,
|
|
}: {
|
|
id: string;
|
|
label: string;
|
|
required?: boolean;
|
|
value: string;
|
|
disabled?: boolean;
|
|
error?: string;
|
|
type?: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1">
|
|
<P1FieldLabel required={required} htmlFor={id}>
|
|
{label}
|
|
</P1FieldLabel>
|
|
<input
|
|
id={id}
|
|
type={type}
|
|
disabled={disabled}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className={`h-11 w-full rounded-sm border bg-neutral-100 px-3 text-sm focus:border-neutral-600 focus:outline-none disabled:opacity-60 ${
|
|
error ? "border-red-500" : "border-neutral-400"
|
|
}`}
|
|
/>
|
|
{error ? <p className="text-xs text-red-600">{error}</p> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 仿 Priority1 两步动态表单(与 form-logic-map 分支一致) */
|
|
export function Priority1Simulator({
|
|
disabled,
|
|
loading,
|
|
onSubmit,
|
|
}: Priority1SimulatorProps) {
|
|
const [step, setStep] = useState<1 | 2>(1);
|
|
const [form, setForm] = useState<Priority1DemoInput>({
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
});
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const patch = useCallback((partial: Partial<Priority1DemoInput>) => {
|
|
setForm((prev) => ({ ...prev, ...partial }));
|
|
}, []);
|
|
|
|
const validateStep1 = (): boolean => {
|
|
const next: Record<string, string> = {};
|
|
if (!/^\d{5}$/.test(form.originZip.trim())) {
|
|
next.originZip = "请输入 5 位起运邮编";
|
|
}
|
|
if (!/^\d{5}$/.test(form.destinationZip.trim())) {
|
|
next.destinationZip = "请输入 5 位目的邮编";
|
|
}
|
|
if (form.originZip === form.destinationZip) {
|
|
next.destinationZip = "起运与目的邮编不能相同";
|
|
}
|
|
if (!/^\d{2}\/\d{2}\/\d{4}$/.test(form.pickupDate.trim())) {
|
|
next.pickupDate = "请选择提货日";
|
|
} else {
|
|
const pickupErr = pickupDateValidationMessage(form.pickupDate);
|
|
if (pickupErr) next.pickupDate = pickupErr;
|
|
}
|
|
if (!form.email.trim()) next.email = "请填写邮箱";
|
|
const phoneDigits = form.phone.replace(/\D/g, "");
|
|
const country = findPhoneCountry(form.phoneCountry);
|
|
if (country.code === "US" || country.code === "CA") {
|
|
if (phoneDigits.length !== 10) next.phone = "请填写 10 位电话号码";
|
|
} else if (phoneDigits.length < 6 || phoneDigits.length > 15) {
|
|
next.phone = "请填写有效电话号码";
|
|
}
|
|
setErrors(next);
|
|
return Object.keys(next).length === 0;
|
|
};
|
|
|
|
const validateStep2 = (): boolean => {
|
|
const next: Record<string, string> = {};
|
|
if (form.shipmentType === "ltl" && !form.weightLb.trim()) {
|
|
next.weightLb = "请填写总重量";
|
|
}
|
|
if (form.shipmentType === "ftl" || form.shipmentType === "expedited") {
|
|
if (!form.commodity.trim()) next.commodity = "请填写货描";
|
|
if (!form.weightLb.trim()) next.weightLb = "请填写总重量";
|
|
}
|
|
setErrors(next);
|
|
return Object.keys(next).length === 0;
|
|
};
|
|
|
|
const handleStep1Next = () => {
|
|
if (!validateStep1()) return;
|
|
setErrors({});
|
|
setStep(2);
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
};
|
|
|
|
const handleFinalSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!validateStep2()) return;
|
|
onSubmit({
|
|
...form,
|
|
phone: form.phone.replace(/\D/g, "").slice(-10),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="relative rounded-md border border-neutral-200 bg-white p-4 md:p-6">
|
|
<div className="mb-6 flex items-center gap-2 text-xs font-medium text-neutral-500">
|
|
<span
|
|
className={`rounded-full px-2.5 py-1 ${step === 1 ? "bg-black text-white" : "bg-neutral-200"}`}
|
|
>
|
|
{P1_COMMON_UI.step1}
|
|
</span>
|
|
<span className="h-px flex-1 bg-neutral-300" />
|
|
<span
|
|
className={`rounded-full px-2.5 py-1 ${step === 2 ? "bg-black text-white" : "bg-neutral-200"}`}
|
|
>
|
|
{P1_COMMON_UI.step2}
|
|
</span>
|
|
</div>
|
|
|
|
{step === 1 ? (
|
|
<div className="space-y-6 pb-2">
|
|
<div>
|
|
<P1FieldLabel required>{P1_COMMON_UI.shipmentType}</P1FieldLabel>
|
|
<div className="mt-2 grid gap-3 md:grid-cols-3">
|
|
{P1_SHIPMENT_CARDS.map((card) => (
|
|
<ShipmentTypeCard
|
|
key={card.id}
|
|
type={card.id}
|
|
title={card.title}
|
|
description={card.description}
|
|
selected={form.shipmentType === card.id}
|
|
disabled={disabled}
|
|
onSelect={() => patch({ shipmentType: card.id })}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<P1TextInput
|
|
id="origin-zip"
|
|
label={P1_COMMON_UI.originZip}
|
|
required
|
|
value={form.originZip}
|
|
disabled={disabled}
|
|
error={errors.originZip}
|
|
onChange={(v) => patch({ originZip: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="destination-zip"
|
|
label={P1_COMMON_UI.destZip}
|
|
required
|
|
value={form.destinationZip}
|
|
disabled={disabled}
|
|
error={errors.destinationZip}
|
|
onChange={(v) => patch({ destinationZip: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<P1FieldLabel required htmlFor="shipment-frequency">
|
|
{P1_COMMON_UI.frequency}
|
|
</P1FieldLabel>
|
|
<select
|
|
id="shipment-frequency"
|
|
disabled={disabled}
|
|
value={form.shipmentFrequency}
|
|
onChange={(e) => patch({ shipmentFrequency: e.target.value })}
|
|
className="mt-1 h-11 w-full max-w-md rounded-sm border border-neutral-400 bg-neutral-100 px-3 text-sm focus:border-neutral-600 focus:outline-none disabled:opacity-60"
|
|
>
|
|
{P1_FREQUENCY_OPTIONS.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<P1DateField
|
|
label={P1_COMMON_UI.pickupDate}
|
|
value={form.pickupDate}
|
|
disabled={disabled}
|
|
error={errors.pickupDate}
|
|
onChange={(v) => patch({ pickupDate: v })}
|
|
/>
|
|
|
|
<P1TextInput
|
|
id="business-email"
|
|
label={P1_COMMON_UI.email}
|
|
required
|
|
type="email"
|
|
value={form.email}
|
|
disabled={disabled}
|
|
error={errors.email}
|
|
onChange={(v) => patch({ email: v })}
|
|
/>
|
|
|
|
<P1PhoneField
|
|
country={form.phoneCountry}
|
|
value={form.phone}
|
|
disabled={disabled}
|
|
error={errors.phone}
|
|
onCountryChange={(c) => patch({ phoneCountry: c })}
|
|
onChange={(v) => patch({ phone: v })}
|
|
/>
|
|
|
|
<P1StickyActionBar>
|
|
<P1InstantQuotesButton
|
|
disabled={disabled}
|
|
loading={loading}
|
|
onClick={handleStep1Next}
|
|
>
|
|
{P1_COMMON_UI.getQuotes}
|
|
</P1InstantQuotesButton>
|
|
</P1StickyActionBar>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleFinalSubmit} className="space-y-6 pb-2">
|
|
<div className="mb-2">
|
|
<h3 className="text-lg font-bold uppercase tracking-wide text-neutral-900">
|
|
{P1_STEP2_HEADING[form.shipmentType]}
|
|
</h3>
|
|
{form.shipmentType !== "ltl" ? (
|
|
<p className="mt-1 text-sm text-neutral-600">
|
|
{form.shipmentType === "expedited"
|
|
? P1_EXPEDITED_UI.subtitle
|
|
: form.shipmentType === "ftl"
|
|
? P1_FTL_UI.subtitle
|
|
: P1_STEP2_SUBTITLE}
|
|
</p>
|
|
) : null}
|
|
<P1Step2Progress label={P1_COMMON_UI.step2Of2} />
|
|
</div>
|
|
|
|
{form.shipmentType === "ltl" ? (
|
|
<>
|
|
<P1FieldLabel required>{P1_LTL_UI.freightSection}</P1FieldLabel>
|
|
<div className="flex flex-wrap items-end gap-2">
|
|
<div className="grid min-w-[12rem] flex-1 grid-cols-3 gap-2">
|
|
<P1TextInput
|
|
id="length"
|
|
label={P1_LTL_UI.length}
|
|
required
|
|
value={form.lengthIn}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ lengthIn: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="width"
|
|
label={P1_LTL_UI.width}
|
|
required
|
|
value={form.widthIn}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ widthIn: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="height"
|
|
label={P1_LTL_UI.height}
|
|
required
|
|
value={form.heightIn}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ heightIn: v })}
|
|
/>
|
|
</div>
|
|
<span className="mb-0 flex h-11 shrink-0 items-center rounded-sm bg-neutral-700 px-3 text-sm font-medium text-white">
|
|
{P1_LTL_UI.dimensionUnit}
|
|
</span>
|
|
<div className="min-w-[10rem] flex-1">
|
|
<P1WeightField
|
|
id="weight"
|
|
label={P1_LTL_UI.totalWeight}
|
|
required
|
|
value={form.weightLb}
|
|
disabled={disabled}
|
|
error={errors.weightLb}
|
|
unitLabel={P1_LTL_UI.weightUnit}
|
|
onChange={(v) => patch({ weightLb: v })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<P1FeatherySelect
|
|
id="packaging"
|
|
label={P1_LTL_UI.packaging}
|
|
required
|
|
value={form.packaging}
|
|
disabled={disabled}
|
|
options={P1_PACKAGING_OPTIONS}
|
|
onChange={(v) => patch({ packaging: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="pallets"
|
|
label={P1_LTL_UI.palletCount}
|
|
required
|
|
value={form.palletCount}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ palletCount: v })}
|
|
/>
|
|
</div>
|
|
|
|
<P1FeatherySelect
|
|
id="freight-class"
|
|
label={P1_LTL_UI.freightClass}
|
|
required
|
|
value={form.freightClass}
|
|
disabled={disabled}
|
|
options={P1_FREIGHT_CLASS_OPTIONS}
|
|
onChange={(v) => patch({ freightClass: v })}
|
|
/>
|
|
|
|
<P1FieldLabel required>{P1_LTL_UI.pickupSection}</P1FieldLabel>
|
|
<P1FeatherySelect
|
|
id="pickup-location"
|
|
label={P1_LTL_UI.pickupLocation}
|
|
required
|
|
value={form.pickupLocation}
|
|
disabled={disabled}
|
|
options={P1_LOCATION_OPTIONS}
|
|
onChange={(v) => patch({ pickupLocation: v })}
|
|
/>
|
|
<P1AccessorialGrid
|
|
legend={P1_LTL_UI.pickupServices}
|
|
options={P1_PICKUP_ACCESSORIALS}
|
|
selected={form.pickupServices}
|
|
disabled={disabled}
|
|
onToggle={(value) =>
|
|
patch({
|
|
pickupServices: toggleInList(form.pickupServices, value),
|
|
})
|
|
}
|
|
/>
|
|
|
|
<P1FieldLabel required>{P1_LTL_UI.deliverySection}</P1FieldLabel>
|
|
<P1FeatherySelect
|
|
id="delivery-location"
|
|
label={P1_LTL_UI.deliveryLocation}
|
|
required
|
|
value={form.deliveryLocation}
|
|
disabled={disabled}
|
|
options={P1_LOCATION_OPTIONS}
|
|
onChange={(v) => patch({ deliveryLocation: v })}
|
|
/>
|
|
<P1AccessorialGrid
|
|
legend={P1_LTL_UI.deliveryServices}
|
|
options={P1_DELIVERY_ACCESSORIALS}
|
|
selected={form.deliveryServices}
|
|
disabled={disabled}
|
|
onToggle={(value) =>
|
|
patch({
|
|
deliveryServices: toggleInList(
|
|
form.deliveryServices,
|
|
value,
|
|
),
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{form.shipmentType === "ftl" ? (
|
|
<>
|
|
<P1FieldLabel required>{P1_FTL_UI.selectTrailer}</P1FieldLabel>
|
|
<FeatheryCardGrid cols={2}>
|
|
{P1_FTL_TRAILERS.map((t) => (
|
|
<FeatheryCard
|
|
key={t.id}
|
|
label={t.label}
|
|
description={t.desc}
|
|
selected={form.ftlTrailer === t.id}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
const next = t.id as FtlTrailerType;
|
|
const partial: Partial<Priority1DemoInput> = {
|
|
ftlTrailer: next,
|
|
};
|
|
if (next === "ftl_expedited") {
|
|
partial.ftlAdditionalServices = [];
|
|
}
|
|
patch(partial);
|
|
}}
|
|
/>
|
|
))}
|
|
</FeatheryCardGrid>
|
|
|
|
{form.ftlTrailer === "open_deck" ? (
|
|
<SelectField
|
|
label={P1_FTL_UI.openDeckType}
|
|
name="openDeckSubtype"
|
|
required
|
|
value={form.openDeckSubtype}
|
|
disabled={disabled}
|
|
options={P1_OPEN_DECK_OPTIONS}
|
|
onChange={(e) => patch({ openDeckSubtype: e.target.value })}
|
|
/>
|
|
) : null}
|
|
|
|
{form.ftlTrailer === "ftl_expedited" ? (
|
|
<>
|
|
<P1FieldLabel required>
|
|
{P1_FTL_UI.selectExpeditedTrailer}
|
|
</P1FieldLabel>
|
|
<FeatheryCardGrid cols={3}>
|
|
{P1_EXP_TRAILERS.map((t) => (
|
|
<FeatheryCard
|
|
key={t.id}
|
|
label={t.label}
|
|
description={t.desc}
|
|
selected={form.ftlExpeditedTrailer === t.id}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
const next = t.id as ExpeditedTrailerType;
|
|
patch({
|
|
ftlExpeditedTrailer: next,
|
|
...(next !== "large_straight"
|
|
? { expeditedLiftgate: false }
|
|
: {}),
|
|
});
|
|
}}
|
|
/>
|
|
))}
|
|
</FeatheryCardGrid>
|
|
|
|
{form.ftlExpeditedTrailer === "large_straight" ? (
|
|
<>
|
|
<P1FieldLabel>{P1_FTL_UI.additionalServices}</P1FieldLabel>
|
|
<FeatheryCardGrid cols={2}>
|
|
<FeatheryCard
|
|
label={P1_FTL_LIFTGATE_SERVICE.label}
|
|
description={P1_FTL_LIFTGATE_SERVICE.desc}
|
|
selected={form.expeditedLiftgate}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
patch({
|
|
expeditedLiftgate: !form.expeditedLiftgate,
|
|
})
|
|
}
|
|
/>
|
|
</FeatheryCardGrid>
|
|
</>
|
|
) : null}
|
|
|
|
<P1DateField
|
|
label={P1_COMMON_UI.pickupDate}
|
|
value={form.pickupDate}
|
|
disabled={disabled}
|
|
error={errors.pickupDate}
|
|
onChange={(v) => patch({ pickupDate: v })}
|
|
/>
|
|
|
|
<p className="text-sm font-semibold text-neutral-800">
|
|
{P1_FTL_UI.freightSection}
|
|
</p>
|
|
<P1TextInput
|
|
id="commodity"
|
|
label={P1_FTL_UI.commodity}
|
|
required
|
|
value={form.commodity}
|
|
disabled={disabled}
|
|
error={errors.commodity}
|
|
onChange={(v) => patch({ commodity: v })}
|
|
/>
|
|
<fieldset className="space-y-2">
|
|
<legend className="text-sm font-medium italic text-neutral-900">
|
|
{P1_FTL_UI.specialHandling}
|
|
</legend>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
{P1_SPECIAL_HANDLING.map((opt) => (
|
|
<label
|
|
key={opt.value}
|
|
className="flex cursor-pointer items-center gap-2 rounded border border-neutral-300 bg-neutral-50 px-3 py-2 text-sm"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
disabled={disabled}
|
|
checked={form.specialHandlingServices.includes(
|
|
opt.value,
|
|
)}
|
|
onChange={() =>
|
|
patch({
|
|
specialHandlingServices: toggleInList(
|
|
form.specialHandlingServices,
|
|
opt.value,
|
|
),
|
|
})
|
|
}
|
|
/>
|
|
{opt.label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
<P1WeightField
|
|
id="ftl-weight"
|
|
label={P1_FTL_UI.totalWeight}
|
|
required
|
|
value={form.weightLb}
|
|
disabled={disabled}
|
|
error={errors.weightLb}
|
|
unitLabel={P1_FTL_UI.weightUnit}
|
|
onChange={(v) => patch({ weightLb: v })}
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
<P1FieldLabel>{P1_FTL_UI.additionalServices}</P1FieldLabel>
|
|
<FeatheryCardGrid cols={3}>
|
|
{P1_FTL_ADDITIONAL.map((s) => (
|
|
<FeatheryCard
|
|
key={s.id}
|
|
label={s.label}
|
|
description={s.desc}
|
|
selected={form.ftlAdditionalServices.includes(s.id)}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
patch({
|
|
ftlAdditionalServices: toggleService(
|
|
form.ftlAdditionalServices,
|
|
s.id,
|
|
),
|
|
})
|
|
}
|
|
/>
|
|
))}
|
|
</FeatheryCardGrid>
|
|
|
|
{form.ftlTrailer === "temperature_controlled" ? (
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<P1TextInput
|
|
id="temp-min"
|
|
label={P1_FTL_UI.tempMin}
|
|
required
|
|
value={form.tempMinF}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ tempMinF: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="temp-max"
|
|
label={P1_FTL_UI.tempMax}
|
|
required
|
|
value={form.tempMaxF}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ tempMaxF: v })}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
<p className="text-sm font-semibold text-neutral-800">
|
|
{P1_FTL_UI.freightSection}
|
|
</p>
|
|
<P1TextInput
|
|
id="commodity"
|
|
label={P1_FTL_UI.commodity}
|
|
required
|
|
value={form.commodity}
|
|
disabled={disabled}
|
|
error={errors.commodity}
|
|
onChange={(v) => patch({ commodity: v })}
|
|
/>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<P1WeightField
|
|
id="ftl-weight"
|
|
label={P1_FTL_UI.totalWeight}
|
|
required
|
|
value={form.weightLb}
|
|
disabled={disabled}
|
|
error={errors.weightLb}
|
|
unitLabel={P1_FTL_UI.weightUnit}
|
|
onChange={(v) => patch({ weightLb: v })}
|
|
/>
|
|
<P1TextInput
|
|
id="ftl-pallets"
|
|
label={P1_FTL_UI.palletCount}
|
|
required
|
|
value={form.palletCount}
|
|
disabled={disabled}
|
|
onChange={(v) => patch({ palletCount: v })}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
) : null}
|
|
|
|
{form.shipmentType === "expedited" ? (
|
|
<>
|
|
<P1FieldLabel required>{P1_EXPEDITED_UI.selectTrailer}</P1FieldLabel>
|
|
<FeatheryCardGrid cols={2}>
|
|
{P1_EXP_TRAILERS.map((t) => (
|
|
<FeatheryCard
|
|
key={t.id}
|
|
label={t.label}
|
|
description={t.desc}
|
|
selected={form.expeditedTrailer === t.id}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
const next = t.id as ExpeditedTrailerType;
|
|
patch({
|
|
expeditedTrailer: next,
|
|
...(next !== "large_straight"
|
|
? { expeditedLiftgate: false }
|
|
: {}),
|
|
});
|
|
}}
|
|
/>
|
|
))}
|
|
</FeatheryCardGrid>
|
|
|
|
{form.expeditedTrailer === "large_straight" ? (
|
|
<>
|
|
<P1FieldLabel>{P1_EXPEDITED_UI.additionalServices}</P1FieldLabel>
|
|
<FeatheryCardGrid cols={2}>
|
|
<FeatheryCard
|
|
label={P1_FTL_LIFTGATE_SERVICE.label}
|
|
description={P1_FTL_LIFTGATE_SERVICE.desc}
|
|
selected={form.expeditedLiftgate}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
patch({ expeditedLiftgate: !form.expeditedLiftgate })
|
|
}
|
|
/>
|
|
</FeatheryCardGrid>
|
|
</>
|
|
) : null}
|
|
|
|
<p className="text-sm font-semibold text-neutral-800">
|
|
{P1_EXPEDITED_UI.freightSection}
|
|
</p>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<P1TextInput
|
|
id="commodity"
|
|
label={P1_EXPEDITED_UI.commodity}
|
|
required
|
|
value={form.commodity}
|
|
disabled={disabled}
|
|
error={errors.commodity}
|
|
onChange={(v) => patch({ commodity: v })}
|
|
/>
|
|
<P1WeightField
|
|
id="exp-weight"
|
|
label={P1_EXPEDITED_UI.totalWeight}
|
|
required
|
|
value={form.weightLb}
|
|
disabled={disabled}
|
|
error={errors.weightLb}
|
|
unitLabel={P1_FTL_UI.weightUnit}
|
|
onChange={(v) => patch({ weightLb: v })}
|
|
/>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
|
|
<P1StickyActionBar>
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
className="rounded-sm border border-neutral-400 px-4 py-3 text-sm text-neutral-700 hover:bg-neutral-100 sm:w-auto"
|
|
onClick={() => {
|
|
setStep(1);
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
}}
|
|
>
|
|
← {P1_COMMON_UI.backStep1}
|
|
</button>
|
|
<div className="flex-1">
|
|
<P1InstantQuotesButton
|
|
type="submit"
|
|
loading={loading}
|
|
disabled={disabled}
|
|
>
|
|
{P1_COMMON_UI.getQuotes}
|
|
</P1InstantQuotesButton>
|
|
</div>
|
|
</div>
|
|
</P1StickyActionBar>
|
|
</form>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|