Files
inquiry_robot/inquiry-backend/src/views/inquiry/workOrder/demandFields.ts
T

231 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 工单详情「询价需求」格子:按运输方式 + 陆运运输类型展示不同必填项。
*
* 口径对齐销售侧询价确认卡(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(" · ");
}