|
|
"use client";
|
|
|
|
|
|
import { useCallback, useMemo, useState } from "react";
|
|
|
import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input";
|
|
|
import type { QuoteDetail, QuotePageStatus } from "@/lib/frontend/types";
|
|
|
import { SUBMIT_LOCK_MS } from "@/lib/frontend/constants";
|
|
|
import { resolvePriority1UserMessage } from "@/lib/priority1/api-errors";
|
|
|
import { hostCreatePriority1Quote, hostGetQuote } from "@/lib/frontend/api-client";
|
|
|
import { pollQuoteUntilDone } from "@/hooks/use-quote-polling";
|
|
|
import { formatQuoteErrorMessage } from "@/modules/quote/quote-error-messages";
|
|
|
import { isPriority1ManualFollowupDetail } from "@/modules/priority1/quote-outcome";
|
|
|
import { uuidV4 } from "@/lib/frontend/format";
|
|
|
import { Priority1Simulator } from "@/components/priority1/priority1-simulator";
|
|
|
import { Priority1QuoteResultPanel } from "@/components/priority1/priority1-quote-result-panel";
|
|
|
import type { Priority1ShipmentSummary } from "@/components/priority1/priority1-ftl-schedule-calendar";
|
|
|
|
|
|
export interface Priority1QuoteWidgetProps {
|
|
|
serviceToken?: string;
|
|
|
customerId: string;
|
|
|
apiBaseUrl?: string;
|
|
|
}
|
|
|
|
|
|
function toShipmentSummary(input: Priority1DemoInput): Priority1ShipmentSummary {
|
|
|
const weight = Number(input.weightLb);
|
|
|
return {
|
|
|
originZip: input.originZip,
|
|
|
destinationZip: input.destinationZip,
|
|
|
pickupDate: input.pickupDate,
|
|
|
weightLb: Number.isFinite(weight) && weight > 0 ? weight : undefined,
|
|
|
itemsLabel:
|
|
|
input.shipmentType === "ltl" ? input.packaging : input.commodity,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function shipmentFromQuote(
|
|
|
quote: QuoteDetail,
|
|
|
fallback?: Priority1ShipmentSummary,
|
|
|
): Priority1ShipmentSummary | undefined {
|
|
|
const meta = quote.priority1?.shipment_meta;
|
|
|
if (!meta?.origin_zip || !meta.destination_zip) {
|
|
|
return fallback;
|
|
|
}
|
|
|
return {
|
|
|
originZip: meta.origin_zip,
|
|
|
destinationZip: meta.destination_zip,
|
|
|
pickupDate: meta.pickup_date ?? fallback?.pickupDate ?? "",
|
|
|
weightLb: meta.weight_lb ?? fallback?.weightLb,
|
|
|
itemsLabel: meta.commodity ?? fallback?.itemsLabel,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/** Priority1 模拟填表 + RPA 真实询价 */
|
|
|
export function Priority1QuoteWidget({
|
|
|
serviceToken,
|
|
|
customerId,
|
|
|
apiBaseUrl = "",
|
|
|
}: Priority1QuoteWidgetProps) {
|
|
|
const [status, setStatus] = useState<QuotePageStatus>("idle");
|
|
|
const [quote, setQuote] = useState<QuoteDetail | null>(null);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [submitLocked, setSubmitLocked] = useState(false);
|
|
|
const [lastInput, setLastInput] = useState<Priority1DemoInput | null>(null);
|
|
|
|
|
|
const reset = useCallback(() => {
|
|
|
setStatus("idle");
|
|
|
setQuote(null);
|
|
|
setError(null);
|
|
|
}, []);
|
|
|
|
|
|
const finishQuote = useCallback((detail: QuoteDetail) => {
|
|
|
setQuote(detail);
|
|
|
if (detail.status === "failed") {
|
|
|
setStatus("error");
|
|
|
setError(
|
|
|
resolvePriority1UserMessage(
|
|
|
detail.error_code,
|
|
|
detail.error_message ??
|
|
|
formatQuoteErrorMessage(detail.error_code),
|
|
|
),
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
if (detail.status === "expired") {
|
|
|
setStatus("expired");
|
|
|
return;
|
|
|
}
|
|
|
if (isPriority1ManualFollowupDetail(detail)) {
|
|
|
setStatus("manual_followup");
|
|
|
setError(null);
|
|
|
return;
|
|
|
}
|
|
|
setStatus(detail.is_realtime === false ? "fallback" : "success");
|
|
|
setError(null);
|
|
|
}, []);
|
|
|
|
|
|
const handleSubmit = useCallback(
|
|
|
async (input: Priority1DemoInput) => {
|
|
|
if (submitLocked) return;
|
|
|
setSubmitLocked(true);
|
|
|
setTimeout(() => setSubmitLocked(false), SUBMIT_LOCK_MS);
|
|
|
|
|
|
setLastInput(input);
|
|
|
setStatus("validating");
|
|
|
setError(null);
|
|
|
|
|
|
const created = await hostCreatePriority1Quote(
|
|
|
apiBaseUrl,
|
|
|
serviceToken,
|
|
|
{
|
|
|
request_id: uuidV4(),
|
|
|
customer_id: customerId,
|
|
|
priority1_input: input,
|
|
|
},
|
|
|
);
|
|
|
|
|
|
if (created.code !== 0) {
|
|
|
setStatus("error");
|
|
|
setError(resolvePriority1UserMessage(created.code, created.message));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const { quote_id } = created.data;
|
|
|
setStatus("processing");
|
|
|
|
|
|
const pollResult = await pollQuoteUntilDone(
|
|
|
async () => {
|
|
|
try {
|
|
|
const res = await hostGetQuote(
|
|
|
apiBaseUrl,
|
|
|
serviceToken,
|
|
|
customerId,
|
|
|
quote_id,
|
|
|
);
|
|
|
if (res.code !== 0) {
|
|
|
return { ok: false, errorMessage: res.message };
|
|
|
}
|
|
|
return { ok: true, data: res.data };
|
|
|
} catch {
|
|
|
return { ok: false };
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
fastAfterMs: 5_000,
|
|
|
fastIntervalMs: 600,
|
|
|
},
|
|
|
);
|
|
|
|
|
|
if (pollResult.type === "done") {
|
|
|
finishQuote(pollResult.quote);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
setStatus("error");
|
|
|
setError(
|
|
|
pollResult.type === "timeout"
|
|
|
? "Priority1 询价超时,请稍后重试"
|
|
|
: resolvePriority1UserMessage("INTERNAL_ERROR", pollResult.message),
|
|
|
);
|
|
|
},
|
|
|
[
|
|
|
apiBaseUrl,
|
|
|
serviceToken,
|
|
|
customerId,
|
|
|
submitLocked,
|
|
|
finishQuote,
|
|
|
],
|
|
|
);
|
|
|
|
|
|
const formDisabled =
|
|
|
status === "validating" ||
|
|
|
status === "processing" ||
|
|
|
submitLocked;
|
|
|
|
|
|
const shipmentContext = useMemo(() => {
|
|
|
if (quote) {
|
|
|
return shipmentFromQuote(
|
|
|
quote,
|
|
|
lastInput ? toShipmentSummary(lastInput) : undefined,
|
|
|
);
|
|
|
}
|
|
|
return lastInput ? toShipmentSummary(lastInput) : undefined;
|
|
|
}, [quote, lastInput]);
|
|
|
|
|
|
const showWideResult =
|
|
|
quote?.priority1?.display_mode === "ftl_calendar" &&
|
|
|
quote.priority1.lines.length > 0;
|
|
|
|
|
|
return (
|
|
|
<div className="grid gap-6">
|
|
|
<div className={showWideResult ? "" : "lg:grid lg:grid-cols-5 lg:gap-6"}>
|
|
|
<div className={showWideResult ? "" : "lg:col-span-3"}>
|
|
|
<p className="mb-3 text-sm text-text-secondary">
|
|
|
按 Priority1 流程填写;第 1 步底部点击「获取即时报价」进入第 2 步,再次点击将提交真实询价。
|
|
|
</p>
|
|
|
<Priority1Simulator
|
|
|
disabled={formDisabled}
|
|
|
loading={status === "validating" || status === "processing"}
|
|
|
onSubmit={(input) => void handleSubmit(input)}
|
|
|
/>
|
|
|
</div>
|
|
|
{!showWideResult ? (
|
|
|
<div className="mt-6 lg:col-span-2 lg:mt-0">
|
|
|
<Priority1QuoteResultPanel
|
|
|
quote={quote}
|
|
|
loading={status === "validating" || status === "processing"}
|
|
|
error={
|
|
|
status === "error"
|
|
|
? error?.trim() || "Priority1 询价失败,请稍后重试"
|
|
|
: null
|
|
|
}
|
|
|
shipmentContext={shipmentContext}
|
|
|
onRequestNewQuote={reset}
|
|
|
/>
|
|
|
</div>
|
|
|
) : null}
|
|
|
</div>
|
|
|
{showWideResult ? (
|
|
|
<Priority1QuoteResultPanel
|
|
|
quote={quote}
|
|
|
loading={status === "validating" || status === "processing"}
|
|
|
error={
|
|
|
status === "error"
|
|
|
? error?.trim() || "Priority1 询价失败,请稍后重试"
|
|
|
: null
|
|
|
}
|
|
|
shipmentContext={shipmentContext}
|
|
|
onRequestNewQuote={reset}
|
|
|
/>
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|
|
|
}
|