231 lines
9.5 KiB
TypeScript
231 lines
9.5 KiB
TypeScript
/**
|
||
* 工单详情「询价需求」格子:按运输方式 + 陆运运输类型展示不同必填项。
|
||
*
|
||
* 口径对齐销售侧询价确认卡(inquiry_field_lines):
|
||
* 空运看件数/重量/体积/包装;海运看货量/整柜拼柜/箱型箱量;
|
||
* 陆运拼车看体积重量,整车/中港零担看车型数量,中港整车不强制车型。
|
||
* 只做展示,不改六态、不写主账。
|
||
*/
|
||
|
||
/** 详情格子一行:标签 + 值(空则「-」) */
|
||
export type DemandField = { label: string; value: string };
|
||
|
||
const LAND_FTL_TYPES = new Set(["国内运输整车", "跨境整车", "中港零担/集拼"]);
|
||
const LAND_CUSTOMS_TYPES = new Set(["跨境集拼", "跨境整车", "中港整车", "中港零担/集拼"]);
|
||
|
||
const LAND_LABEL: Record<string, string> = {
|
||
起运港: "始发站",
|
||
目的港: "目的地",
|
||
品名: "货物品名",
|
||
毛重: "重量(KG)",
|
||
体积: "体积(CBM)",
|
||
"车型/数量": "车型/数量",
|
||
通关口岸: "通关口岸",
|
||
报价日期: "报价日期",
|
||
};
|
||
|
||
/**
|
||
* JSON 字符串 / 对象都解开成扁平键值。坏 JSON 当空。
|
||
* 协同快照里的车型等也要能展示,调用方自己决定合并顺序。
|
||
*/
|
||
export function parseFactsMap(raw: unknown): Record<string, string> {
|
||
if (raw == null) return {};
|
||
let obj: any = raw;
|
||
if (typeof raw === "string" && raw.trim()) {
|
||
try {
|
||
obj = JSON.parse(raw);
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {};
|
||
const out: Record<string, string> = {};
|
||
for (const key of Object.keys(obj)) {
|
||
const val = obj[key];
|
||
if (val == null) continue;
|
||
if (typeof val === "object") continue;
|
||
const text = String(val).trim();
|
||
if (text) out[key] = text;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 询价事实优先,协同只补空键(中港整车车型常在协同里)。 */
|
||
export function mergeInquiryFacts(ticket: any): Record<string, string> {
|
||
const inquiry = parseFactsMap(ticket?.factsJson ?? ticket?.facts);
|
||
const collab = parseFactsMap(ticket?.collabFactsJson ?? ticket?.collabFacts);
|
||
const out: Record<string, string> = { ...inquiry };
|
||
for (const [k, v] of Object.entries(collab)) {
|
||
if (!out[k]) out[k] = v;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function pick(facts: Record<string, string>, ...keys: string[]): string {
|
||
for (const key of keys) {
|
||
const v = (facts[key] || "").trim();
|
||
if (v) return v;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function dash(v: unknown): string {
|
||
const s = (v ?? "").toString().trim();
|
||
return s ? s : "-";
|
||
}
|
||
|
||
function columnText(v: unknown): string {
|
||
if (v == null || v === "") return "";
|
||
const s = String(v).trim();
|
||
if (!s || s === "-") return "";
|
||
return s;
|
||
}
|
||
|
||
function businessLineOf(ticket: any): "AIR" | "SEA" | "LAND" | "MULTI" | "" {
|
||
const raw = String(ticket?.businessLine ?? "").trim().toUpperCase();
|
||
if (raw === "AIR" || raw === "SEA" || raw === "LAND" || raw === "MULTI") return raw;
|
||
const label = String(ticket?.bizTypeLabel ?? ticket?.bizType ?? "").trim();
|
||
if (label.includes("空运")) return "AIR";
|
||
if (label.includes("海运")) return "SEA";
|
||
if (label.includes("陆运")) return "LAND";
|
||
if (label.includes("多段") || label.includes("联运")) return "MULTI";
|
||
return "";
|
||
}
|
||
|
||
function transportModeLabel(ticket: any, line: string): string {
|
||
const label = String(ticket?.bizTypeLabel ?? "").trim();
|
||
if (label) return label;
|
||
if (line === "AIR") return "空运";
|
||
if (line === "SEA") return "海运";
|
||
if (line === "LAND") return "陆运";
|
||
if (line === "MULTI") return "多段联运";
|
||
return "";
|
||
}
|
||
|
||
/** 当前陆运类型要展示的货物内部键。中港整车不要把车型当成询价必填。 */
|
||
function landCargoKeys(landType: string): string[] {
|
||
if (landType === "中港整车") {
|
||
return ["起运港", "目的港", "品名", "报价日期"];
|
||
}
|
||
if (LAND_FTL_TYPES.has(landType)) {
|
||
return ["起运港", "目的港", "品名", "车型/数量", "报价日期"];
|
||
}
|
||
return ["起运港", "目的港", "品名", "体积", "毛重", "报价日期"];
|
||
}
|
||
|
||
function push(fields: DemandField[], label: string, value: string) {
|
||
fields.push({ label, value: dash(value) });
|
||
}
|
||
|
||
/**
|
||
* 工单详情询价需求格子。
|
||
* 列字段(origin / pieces / containerSpec 等)作 facts 空时的兜底,兼容旧单。
|
||
*/
|
||
export function buildDemandFields(ticket: any): DemandField[] {
|
||
const facts = mergeInquiryFacts(ticket);
|
||
const line = businessLineOf(ticket);
|
||
const mode = transportModeLabel(ticket, line);
|
||
const origin = pick(facts, "起运港", "起运地") || columnText(ticket?.origin);
|
||
const dest = pick(facts, "目的港", "目的地") || columnText(ticket?.destination);
|
||
const goods = pick(facts, "品名", "货物品名") || columnText(ticket?.goodsName);
|
||
const quoteDate =
|
||
pick(facts, "报价日期") || columnText(ticket?.quoteDate)?.toString().slice(0, 10) || "";
|
||
const customer = pick(facts, "客户名称") || columnText(ticket?.customerName);
|
||
const fields: DemandField[] = [];
|
||
|
||
push(fields, "运输方式", mode);
|
||
|
||
if (line === "SEA") {
|
||
push(fields, "起运港", origin);
|
||
push(fields, "目的港", dest);
|
||
push(fields, "品名", goods);
|
||
push(fields, "货物数量", pick(facts, "货量", "货物数量", "件数") || columnText(ticket?.cargoQty) || columnText(ticket?.pieces));
|
||
push(fields, "整柜或拼柜", pick(facts, "整柜或拼柜") || columnText(ticket?.fclLcl));
|
||
push(fields, "箱型箱量", pick(facts, "箱型箱量") || columnText(ticket?.containerSpec));
|
||
push(fields, "贸易条款", pick(facts, "贸易条款"));
|
||
push(fields, "运输分类", pick(facts, "运输分类"));
|
||
push(fields, "报价日期", quoteDate);
|
||
} else if (line === "LAND") {
|
||
const landType = pick(facts, "运输类型");
|
||
push(fields, "运输类型", landType);
|
||
push(fields, "线路类别", pick(facts, "线路类别"));
|
||
push(fields, "运输分类", pick(facts, "运输分类"));
|
||
const cargoVals: Record<string, string> = {
|
||
起运港: origin,
|
||
目的港: dest,
|
||
品名: goods,
|
||
体积: pick(facts, "体积") || columnText(ticket?.volumeCbm),
|
||
毛重: pick(facts, "毛重", "重量") || columnText(ticket?.weightKg),
|
||
"车型/数量": pick(facts, "车型/数量", "车型"),
|
||
报价日期: quoteDate,
|
||
};
|
||
for (const key of landCargoKeys(landType)) {
|
||
push(fields, LAND_LABEL[key] || key, cargoVals[key] || pick(facts, key));
|
||
}
|
||
if (LAND_CUSTOMS_TYPES.has(landType)) {
|
||
push(fields, "通关口岸", pick(facts, "通关口岸"));
|
||
}
|
||
// 中港整车询价不强制车型,协同里有值仍展示,避免后台看不到 53 尺这类原文。
|
||
if (landType === "中港整车" && pick(facts, "车型/数量", "车型")) {
|
||
push(fields, "车型/数量", pick(facts, "车型/数量", "车型"));
|
||
}
|
||
} else if (line === "AIR") {
|
||
push(fields, "起运地", origin);
|
||
push(fields, "目的地", dest);
|
||
push(fields, "品名", goods);
|
||
push(fields, "件数", pick(facts, "件数") || columnText(ticket?.pieces));
|
||
push(fields, "重量(KG)", pick(facts, "毛重", "重量") || columnText(ticket?.weightKg));
|
||
push(fields, "体积(CBM)", pick(facts, "体积") || columnText(ticket?.volumeCbm));
|
||
push(fields, "包装类型", pick(facts, "包装方式", "包装类型"));
|
||
push(fields, "报价日期", quoteDate);
|
||
} else {
|
||
// 多段或未识别:至少把路线和货量列出来,避免整块空白。
|
||
push(fields, "起运 / 目的", origin || dest ? `${origin || "-"} → ${dest || "-"}` : "");
|
||
push(fields, "品名", goods);
|
||
push(fields, "件数", pick(facts, "件数") || columnText(ticket?.pieces));
|
||
push(fields, "重量(KG)", pick(facts, "毛重", "重量") || columnText(ticket?.weightKg));
|
||
push(fields, "体积(CBM)", pick(facts, "体积") || columnText(ticket?.volumeCbm));
|
||
const box = pick(facts, "箱型箱量") || columnText(ticket?.containerSpec);
|
||
if (box) push(fields, "箱型箱量", box);
|
||
const vehicle = pick(facts, "车型/数量", "车型");
|
||
if (vehicle) push(fields, "车型/数量", vehicle);
|
||
}
|
||
|
||
push(fields, "客户", customer);
|
||
return fields;
|
||
}
|
||
|
||
/**
|
||
* 列表「询价需求」第二行货量:按方式只写该方式关心的量,避免陆运整车全是件数「-」。
|
||
*/
|
||
export function quantitySummary(ticket: any): string {
|
||
const facts = mergeInquiryFacts(ticket);
|
||
const line = businessLineOf(ticket);
|
||
const landType = pick(facts, "运输类型");
|
||
const parts: string[] = [];
|
||
const add = (label: string, raw: string) => {
|
||
const v = (raw || "").trim();
|
||
if (v && v !== "-") parts.push(`${label} ${v}`);
|
||
};
|
||
if (line === "SEA") {
|
||
add("货物数量", pick(facts, "货量", "货物数量") || columnText(ticket?.cargoQty));
|
||
add("整柜或拼柜", pick(facts, "整柜或拼柜") || columnText(ticket?.fclLcl));
|
||
add("箱型箱量", pick(facts, "箱型箱量") || columnText(ticket?.containerSpec));
|
||
return parts.join(" · ");
|
||
}
|
||
if (line === "LAND") {
|
||
if (landType) add("运输类型", landType);
|
||
if (LAND_FTL_TYPES.has(landType) || (landType === "中港整车" && pick(facts, "车型/数量"))) {
|
||
add("车型/数量", pick(facts, "车型/数量", "车型"));
|
||
} else {
|
||
add("体积(CBM)", pick(facts, "体积") || columnText(ticket?.volumeCbm));
|
||
add("重量(KG)", pick(facts, "毛重", "重量") || columnText(ticket?.weightKg));
|
||
}
|
||
return parts.join(" · ");
|
||
}
|
||
add("件数", pick(facts, "件数") || columnText(ticket?.pieces));
|
||
add("重量(KG)", pick(facts, "毛重", "重量") || columnText(ticket?.weightKg));
|
||
add("体积(CBM)", pick(facts, "体积") || columnText(ticket?.volumeCbm));
|
||
return parts.join(" · ");
|
||
}
|