/** * MotherShip 登录后二级 Details:提货 / 送货 / 货物明细 * 字段与一级 Create shipment 一一对应并预填 */ "use client"; import { useMemo, useState } from "react"; import { CaretDown, Info, Plus, Warning, X } from "@phosphor-icons/react"; import { MS_CARGO_TYPES, MS_DEFAULT_READY_TIME, MS_DELIVERY_ACCESSORIALS, MS_PICKUP_ACCESSORIALS, MS_READY_TIMES, type MothershipLoggedInShipmentPayload, type MsCargoTypeId, } from "@/components/mothership/mothership-logged-in-shipment-form"; import { MothershipWeekdayDatePicker } from "@/components/mothership/mothership-weekday-date-picker"; import { snapMothershipReadyDateToWeekday, } from "@/lib/mothership/logged-in-constraints"; import { evaluateMothershipAccessorialCompat, MS_PICKUP_BLOCKED_ACCESSORIALS, toggleMothershipAccessorial, } from "@/lib/mothership/option-compat"; export type MsLocationDetails = { address: string; companyName: string; suite: string; contactFirst: string; contactLast: string; contactEmail: string; contactPhone: string; extension: string; reference: string; accessorials: string[]; notes: string; opensAt: string; closesAt: string; readyDate: string; readyTime: string; }; export type MsDetailsCargoLine = { key: string; cargoType: MsCargoTypeId; quantity: string; freightClass: string; weightLb: string; lengthIn: string; widthIn: string; heightIn: string; pieceCountType: string; pieceCountQty: string; description: string; nmfc: string; hazmat: boolean; alcohol: boolean; tobacco: boolean; }; export type MothershipLoggedInDetailsState = { pickup: MsLocationDetails; delivery: MsLocationDetails; requestDeliveryAppointment: boolean; fbaNumber: string; fbaPoNumber: string; cargo: MsDetailsCargoLine[]; }; export type MothershipLoggedInDetailsPayload = { pickup: { companyName: string; suite: string; contactFirst: string; contactLast: string; contactEmail: string; contactPhone: string; reference: string; notes: string; opensAt: string; closesAt: string; }; delivery: { companyName: string; suite: string; contactFirst: string; contactLast: string; contactEmail: string; contactPhone: string; reference: string; notes: string; opensAt: string; closesAt: string; }; requestDeliveryAppointment: boolean; fbaNumber: string; fbaPoNumber: string; cargo: Array<{ pieceCountType: string; pieceCountQty: number; description: string; nmfc: string; hazmat: boolean; alcohol: boolean; tobacco: boolean; }>; }; function RequiredMark() { return *; } function FieldLabel({ children, required, tip, }: { children: React.ReactNode; required?: boolean; tip?: boolean; }) { return ( {children} {required ? : null} {tip ? : null} ); } function TextInput({ value, onChange, placeholder, disabled, className = "", }: { value: string; onChange: (v: string) => void; placeholder?: string; disabled?: boolean; className?: string; }) { return ( onChange(e.target.value)} className={`h-10 w-full rounded-md border border-border bg-surface px-3 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:opacity-60 ${className}`} /> ); } function AccessorialSelect({ options, selected, disabled, onChange, hintPickUp, side, }: { options: readonly { id: string; label: string }[]; selected: string[]; disabled?: boolean; onChange: (next: string[]) => void; hintPickUp?: boolean; side: "pickup" | "delivery"; }) { const [open, setOpen] = useState(false); const [hint, setHint] = useState(null); const issues = useMemo( () => evaluateMothershipAccessorialCompat({ side, selected }), [side, selected], ); const summary = selected.length === 0 ? "选择" : selected.length === 1 ? (options.find((o) => o.id === selected[0])?.label ?? "已选择") : `${selected.length} 项已选`; return (
附加服务 避免追收附加费
{open && !disabled && (

下列附加服务建议勾选,以 {hintPickUp ? "避免提货问题" : "避免送货问题"} 并减少事后追收。住宅提货承运商不支持;预约与 Amazon 预约不能同选。

{options.map((opt) => { const checked = selected.includes(opt.id); const warn = opt.id === "liftgate" || opt.id === "residential" || opt.id === "limitedAccess"; const pickupBlocked = side === "pickup" && Boolean(MS_PICKUP_BLOCKED_ACCESSORIALS[opt.id]); return ( ); })}
)} {hint ? (

{hint}

) : null} {issues.length > 0 ? ( ) : null}
); } function locationFromL1( address: string, accessorials: string[], readyDate: string, readyTime: string, ): MsLocationDetails { return { address, companyName: "", suite: "", contactFirst: "", contactLast: "", contactEmail: "", contactPhone: "", extension: "", reference: "", accessorials: [...accessorials], notes: "", opensAt: "", closesAt: "", readyDate: snapMothershipReadyDateToWeekday(readyDate), readyTime, }; } function locationFromConfirmed( confirmed: { formatted_address: string; display_label: string; street: string; city: string; state: string; zip: string; }, fallbackQuery: string, accessorials: string[], readyDate: string, readyTime: string, ): MsLocationDetails { const label = confirmed.formatted_address || confirmed.display_label || fallbackQuery; const composed = [confirmed.street, confirmed.city, confirmed.state, confirmed.zip] .map((p) => p?.trim()) .filter(Boolean) .join(", "); return { ...locationFromL1(label || composed || fallbackQuery, accessorials, readyDate, readyTime), }; } export function buildDetailsStateFromL1( payload: MothershipLoggedInShipmentPayload, ): MothershipLoggedInDetailsState { const wantFba = payload.deliveryAccessorials.includes("fbaAppointment"); return { pickup: locationFromConfirmed( payload.pickupConfirmed, payload.pickupQuery, payload.pickupAccessorials, payload.readyDate, payload.readyTime, ), delivery: locationFromConfirmed( payload.deliveryConfirmed, payload.deliveryQuery, payload.deliveryAccessorials, payload.readyDate, payload.readyTime, ), requestDeliveryAppointment: wantFba || payload.deliveryAccessorials.includes("appointment"), fbaNumber: "", fbaPoNumber: "", cargo: payload.cargo.map((c, i) => ({ key: `d-${i}-${c.cargoType}`, cargoType: c.cargoType, quantity: String(c.quantity), freightClass: "", weightLb: String(c.weightLb), lengthIn: String(c.lengthIn), widthIn: String(c.widthIn), heightIn: String(c.heightIn), pieceCountType: "", pieceCountQty: "0", description: "", nmfc: "", hazmat: false, alcohol: false, tobacco: false, })), }; } function LocationSection({ title, value, disabled, accessorialOptions, hintPickUp, showFreightReady, onChange, }: { title: string; value: MsLocationDetails; disabled?: boolean; accessorialOptions: readonly { id: string; label: string }[]; hintPickUp?: boolean; showFreightReady?: boolean; onChange: (next: MsLocationDetails) => void; }) { const set = ( key: K, v: MsLocationDetails[K], ) => onChange({ ...value, [key]: v }); return (

{title}

set("accessorials", next)} />