|
|
"use client";
|
|
|
|
|
|
import { buildPriority1CalendarGrid, weekdayLabelsZh } from "@/lib/priority1/calendar-grid";
|
|
|
import { priority1CustomerDisplayUsd } from "@/lib/priority1/display-price";
|
|
|
import { P1_RESULT_UI } from "@/lib/priority1/ui-labels";
|
|
|
import type { Priority1DisplayLine } from "@/modules/priority1/quote-storage";
|
|
|
|
|
|
export type Priority1ShipmentSummary = {
|
|
|
originZip: string;
|
|
|
destinationZip: string;
|
|
|
pickupDate: string;
|
|
|
weightLb?: number;
|
|
|
itemsLabel?: string;
|
|
|
};
|
|
|
|
|
|
type Props = {
|
|
|
lines: Priority1DisplayLine[];
|
|
|
shipment: Priority1ShipmentSummary;
|
|
|
onRequestNewQuote?: () => void;
|
|
|
};
|
|
|
|
|
|
function formatUsd(n: number): string {
|
|
|
return new Intl.NumberFormat("en-US", {
|
|
|
minimumFractionDigits: 2,
|
|
|
maximumFractionDigits: 2,
|
|
|
}).format(n);
|
|
|
}
|
|
|
|
|
|
function lineDisplayUsd(line: Priority1DisplayLine): number {
|
|
|
return priority1CustomerDisplayUsd(line);
|
|
|
}
|
|
|
|
|
|
export function Priority1FtlScheduleCalendar({
|
|
|
lines,
|
|
|
shipment,
|
|
|
onRequestNewQuote,
|
|
|
}: Props) {
|
|
|
const { monthLabel, cells } = buildPriority1CalendarGrid(
|
|
|
shipment.pickupDate,
|
|
|
lines,
|
|
|
);
|
|
|
const weekdays = weekdayLabelsZh();
|
|
|
const lowest = lines.length
|
|
|
? Math.min(...lines.map((l) => lineDisplayUsd(l)))
|
|
|
: null;
|
|
|
|
|
|
return (
|
|
|
<div className="flex min-h-[520px] bg-black text-white">
|
|
|
<aside className="flex w-[280px] shrink-0 flex-col border-r border-neutral-800 bg-neutral-950 p-4">
|
|
|
<div className="mb-4 flex h-36 items-center justify-center rounded border border-neutral-800 bg-neutral-900 text-xs text-neutral-500">
|
|
|
路线地图({shipment.originZip} → {shipment.destinationZip})
|
|
|
</div>
|
|
|
<div className="mb-4 rounded border border-neutral-800 bg-neutral-900 p-3 text-sm">
|
|
|
<p className="mb-2 text-xs font-bold tracking-wider text-neutral-400">
|
|
|
{P1_RESULT_UI.shippingDetails}
|
|
|
</p>
|
|
|
<dl className="space-y-1 text-neutral-200">
|
|
|
<div className="flex justify-between gap-2">
|
|
|
<dt className="text-neutral-500">{P1_RESULT_UI.from}</dt>
|
|
|
<dd>{shipment.originZip}</dd>
|
|
|
</div>
|
|
|
<div className="flex justify-between gap-2">
|
|
|
<dt className="text-neutral-500">{P1_RESULT_UI.to}</dt>
|
|
|
<dd>{shipment.destinationZip}</dd>
|
|
|
</div>
|
|
|
<div className="flex justify-between gap-2">
|
|
|
<dt className="text-neutral-500">{P1_RESULT_UI.date}</dt>
|
|
|
<dd>{shipment.pickupDate}</dd>
|
|
|
</div>
|
|
|
{shipment.weightLb != null && shipment.weightLb > 0 ? (
|
|
|
<div className="flex justify-between gap-2">
|
|
|
<dt className="text-neutral-500">{P1_RESULT_UI.weight}</dt>
|
|
|
<dd>{shipment.weightLb} lb</dd>
|
|
|
</div>
|
|
|
) : null}
|
|
|
{shipment.itemsLabel ? (
|
|
|
<div className="flex justify-between gap-2">
|
|
|
<dt className="text-neutral-500">{P1_RESULT_UI.items}</dt>
|
|
|
<dd className="text-right">{shipment.itemsLabel}</dd>
|
|
|
</div>
|
|
|
) : null}
|
|
|
</dl>
|
|
|
</div>
|
|
|
{onRequestNewQuote ? (
|
|
|
<div className="mt-auto">
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={onRequestNewQuote}
|
|
|
className="w-full border border-neutral-600 bg-neutral-800 py-3 text-xs font-bold tracking-wide text-white hover:bg-neutral-700"
|
|
|
>
|
|
|
{P1_RESULT_UI.requestNewQuote}
|
|
|
</button>
|
|
|
</div>
|
|
|
) : null}
|
|
|
</aside>
|
|
|
|
|
|
<div className="flex flex-1 flex-col p-6">
|
|
|
<h2 className="mb-4 text-2xl font-bold tracking-wide">
|
|
|
{P1_RESULT_UI.ftlTitle}
|
|
|
</h2>
|
|
|
<div className="mb-6 rounded border border-neutral-800 bg-neutral-900/80 p-4 text-sm text-neutral-300">
|
|
|
{P1_RESULT_UI.ftlIntro}
|
|
|
{lowest != null ? (
|
|
|
<span className="mt-1 block text-[#f5d000]">
|
|
|
{P1_RESULT_UI.ftlLowest(formatUsd(lowest))}
|
|
|
</span>
|
|
|
) : null}
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded border border-neutral-300 bg-white p-4 text-black">
|
|
|
<div className="mb-3 flex items-baseline justify-between">
|
|
|
<h3 className="text-xl font-bold text-[#c9a000]">{P1_RESULT_UI.schedule}</h3>
|
|
|
<span className="text-2xl font-light text-neutral-700">{monthLabel}</span>
|
|
|
</div>
|
|
|
<div className="grid grid-cols-7 gap-px bg-neutral-200">
|
|
|
{weekdays.map((d) => (
|
|
|
<div
|
|
|
key={d}
|
|
|
className="bg-white py-2 text-center text-xs font-semibold text-neutral-600"
|
|
|
>
|
|
|
{d}
|
|
|
</div>
|
|
|
))}
|
|
|
{cells.map((cell) => {
|
|
|
const day = cell.date.getDate();
|
|
|
const price =
|
|
|
cell.line != null
|
|
|
? lineDisplayUsd(cell.line)
|
|
|
: cell.priceUsd;
|
|
|
const hasPrice = cell.inMonth && price != null && price > 0;
|
|
|
return (
|
|
|
<div
|
|
|
key={cell.date.toISOString()}
|
|
|
className={`min-h-[72px] bg-white p-1 text-center ${
|
|
|
cell.inMonth ? "" : "opacity-40"
|
|
|
}`}
|
|
|
>
|
|
|
<div className="text-sm font-medium text-neutral-800">{day}</div>
|
|
|
{hasPrice ? (
|
|
|
<div className="mt-1 text-xs font-semibold text-neutral-900">
|
|
|
{formatUsd(price!)}
|
|
|
</div>
|
|
|
) : cell.inMonth ? (
|
|
|
<div className="mt-1 text-[10px] text-neutral-400">—</div>
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|