/** * Rewrite MailBusinessSummary.tsx fully in UTF-8. * ASCII-only script source; Chinese via \\u escapes only. * Run: pnpm exec tsx scripts/rewrite-mail-business-summary.ts */ import fs from "fs"; import path from "path"; const out = path.join(process.cwd(), "src", "components", "MailBusinessSummary.tsx"); const ZH = { cardTitle: "\u8fd9\u5c01\u90ae\u4ef6\u5728\u5e72\u4ec0\u4e48", splitPrefix: "\u5df2\u62c6\u51fa ", splitSuffix: " \u6761\u6307\u4ee4\uff1a", from: "\u53d1\u4ef6\u4eba", action: "\u4e1a\u52a1\u52a8\u4f5c", hint: "\u90ae\u4ef6\u8868\u8ff0", next: "\u7cfb\u7edf\u4e0b\u4e00\u6b65", vessel: "\u8239\u540d\u822a\u6b21 ", woMock: "\u5de5\u5355\u5df2\u63d0\u4ea4\uff08mock\uff09", woOk: "\u5de5\u5355\u5df2\u63d0\u4ea4", doMock: "DO \u5df2\u767b\u8bb0\uff08mock\uff09", doOk: "DO \u5df2\u4e0a\u4f20", needFba: "\u8bf7\u586b\u5199\u76ee\u6807 FBACode", xferMock: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4\uff08mock\uff09", xferOk: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4", msgType: "\u62c6\u67dc\u65f6\u81ea\u52a8\u8f6c\u4ed3", gate: "\u95f8\u95e8\uff1a", semi: "\uff1b", actXfer: "\u8f6c\u4ed3", btnWo: "\u786e\u8ba4\u63d0\u4ea4\u5de5\u5355", btnXfer: "\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3", btnDo: "\u786e\u8ba4\u4e0a\u4f20 DO", }; const file = `"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { Alert, App, Button, Card, Descriptions, Space, Tag, Typography, } from "antd"; import { CcDoUploadPanel, buildCcDoUploadFormValue, } from "@/components/CcDoUploadPanel"; import { CcForecastFormOrder, buildCcForecastFormValue, } from "@/components/CcForecastFormOrder"; import { CcCustomerInstructionPanel } from "@/components/CcCustomerInstructionPanel"; import { InstructionUnitShell } from "@/components/MailInstructionUnitShell"; import { CcTransferIndexTruck, buildCcTransferFormValue, } from "@/components/CcTransferIndexTruck"; import { CcWorkOrderForm, buildCcWorkOrderFormValue, } from "@/components/CcWorkOrderForm"; import { TypeTag } from "@/components/TypeTag"; import { confirmDoUploadApi, confirmTransferApi, confirmWorkOrderApi, getTransferGateApi, listShippingLinesApi, } from "@/lib/client-api"; import { buildMailIntent, extractTransferPairs, filterTransferShipments, } from "@/services/parse/mail-intent"; import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template"; import { extractForecastInstructionText, extractMailInstructions, } from "@/services/parse/split-instructions"; import type { MailMessage } from "@/types"; import type { ContainerHeader, MailRecord, MailType, TypeEvidence, } from "@/types/mail"; import type { TransferGateDecision } from "@/services/cc/container-status"; function asHeader(mail: MailMessage): ContainerHeader | null { return mail.parse_result?.container_header ?? null; } function asMailRecord(mail: MailMessage): MailRecord | null { const direct = mail.parse_result?.mail_record; if (direct) return direct; const lineage = mail.parse_result?.lineage as | { mail_record?: MailRecord } | undefined; return lineage?.mail_record ?? null; } type EvidenceExt = TypeEvidence & { mailbox?: { from?: string; inbox?: string; extracted?: string[] }; }; export function MailBusinessSummary({ mail, onConfirmed, }: { mail: MailMessage; onConfirmed?: () => void; }) { const { message } = App.useApp(); const header = asHeader(mail); const record = asMailRecord(mail); const evidence = (mail.type_evidence || { total: 0, mail_type: mail.mail_type, signals: [], }) as EvidenceExt; // Stable deps for useEffect const shipmentsRaw = mail.parse_result?.shipments; const attachmentsRaw = mail.attachments; const shipmentsKey = JSON.stringify(shipmentsRaw ?? []); const filenamesKey = (attachmentsRaw || []).map((a) => a.filename).join("?"); const shipments = useMemo( () => shipmentsRaw ?? [], // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by shipmentsKey [shipmentsKey], ); const filenames = useMemo( () => (attachmentsRaw || []).map((a) => a.filename), // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by filenamesKey [filenamesKey], ); const bodyText = mail.body_text || mail.ocr_text || ""; const containerNo = header?.F_ContainerNo || mail.container_no || ""; const workOrderActionsKey = (record?.work_order_actions || []).join("|"); const intentSignalsKey = JSON.stringify(evidence.signals ?? []); const intent = useMemo( () => evidence.intent || buildMailIntent({ mailType: mail.mail_type as MailType, subject: mail.subject, body: bodyText, filenames, shipments, evidence, containerNo, }), // eslint-disable-next-line react-hooks/exhaustive-deps -- evidence default object unstable [ evidence.intent, evidence.mail_type, evidence.total, intentSignalsKey, mail.mail_type, mail.subject, bodyText, filenames, shipments, containerNo, ], ); const summaryText = record?.summary || intent.narrative; const transferPairs = useMemo( () => intent.transfer_pairs?.length ? intent.transfer_pairs : extractTransferPairs(shipments), [intent.transfer_pairs, shipments], ); const transferShipments = useMemo( () => filterTransferShipments(shipments), [shipments], ); const instructions = useMemo( () => extractMailInstructions({ subject: mail.subject, body: bodyText, filenames, forceForecast: record?.kind === "BL_FORECAST", workOrderActions: record?.work_order_actions, }), [ mail.subject, bodyText, filenames, record?.kind, workOrderActionsKey, ], ); const forecastInstruction = useMemo(() => { const vessel = record?.modules.vessel_voyage; return extractForecastInstructionText({ subject: mail.subject, body: bodyText, modulesInstruction: vessel ? \`${ZH.vessel}\${vessel}\` : header?.F_Instruction || null, }); }, [ mail.subject, bodyText, header?.F_Instruction, record?.modules.vessel_voyage, ]); const hasForecastUi = instructions.some((u) => u.uiKind === "forecast"); const hasTransferUi = instructions.some((u) => u.uiKind === "transfer"); const canConfirmOps = mail.status === "PENDING_CONFIRM" || mail.status === "FAILED"; const blPlusModules = useMemo( () => extractBlPlusFromMail(mail.subject, bodyText)?.modules ?? null, [mail.subject, bodyText], ); const forecastValue = useMemo(() => { const modules = { ...(blPlusModules || {}), ...(record?.modules || {}), }; const base = buildCcForecastFormValue({ customerName: record?.modules.customer_name || blPlusModules?.customer_name || undefined, header, modules, }); return { ...base, F_Instruction: forecastInstruction || base.F_Instruction, }; }, [ header, record?.modules, record?.modules.customer_name, blPlusModules, forecastInstruction, ]); const transferValue = useMemo( () => buildCcTransferFormValue({ customerName: record?.modules.customer_name, containerNo, modules: record?.modules, shipments: transferShipments, transferPairs, }), [ record?.modules.customer_name, containerNo, record?.modules, transferShipments, transferPairs, ], ); const currentWorkOrderUnit = useMemo( () => instructions.find((u) => u.uiKind === "work_order" && u.isCurrent), [instructions], ); const [workOrderValue, setWorkOrderValue] = useState(() => buildCcWorkOrderFormValue({ subject: currentWorkOrderUnit?.segmentSubject || mail.subject, body: currentWorkOrderUnit?.segmentText || bodyText, filenames, containerNo, actions: record?.work_order_actions, }), ); const [doValue, setDoValue] = useState(() => buildCcDoUploadFormValue({ subject: mail.subject, body: bodyText, containerNo, filenames, }), ); const [gate, setGate] = useState(null); const [submitting, setSubmitting] = useState(false); // Reset forms when mail / current work-order segment changes useEffect(() => { setWorkOrderValue( buildCcWorkOrderFormValue({ subject: currentWorkOrderUnit?.segmentSubject || mail.subject, body: currentWorkOrderUnit?.segmentText || bodyText, filenames, containerNo, actions: record?.work_order_actions, }), ); setDoValue( buildCcDoUploadFormValue({ subject: mail.subject, body: bodyText, containerNo, filenames, }), ); }, [ mail.id, mail.version, mail.subject, bodyText, containerNo, workOrderActionsKey, filenames, currentWorkOrderUnit?.id, currentWorkOrderUnit?.segmentText, ]); useEffect(() => { if (mail.mail_type !== "TRANSFER" && !hasTransferUi) return; let cancelled = false; void getTransferGateApi(mail.id).then((res) => { if (!cancelled && res.ok) setGate(res.data.gate); }); return () => { cancelled = true; }; }, [mail.id, mail.mail_type, hasTransferUi]); const [shippingLines, setShippingLines] = useState< { value: string; label: string }[] >([]); useEffect(() => { if (!hasForecastUi) return; let cancelled = false; void listShippingLinesApi().then((res) => { if (!cancelled && res.ok) { setShippingLines( res.data.items.map((item) => ({ value: item.id, label: item.name })), ); } }); return () => { cancelled = true; }; }, [hasForecastUi]); const submitWorkOrder = async () => { setSubmitting(true); const res = await confirmWorkOrderApi(mail.id, { version: mail.version, entity: workOrderValue, }); setSubmitting(false); if (!res.ok) { message.error(res.error.message); return; } message.success( res.data.mode === "mock" ? "${ZH.woMock}" : "${ZH.woOk}", ); onConfirmed?.(); }; const submitDo = async () => { setSubmitting(true); const res = await confirmDoUploadApi(mail.id, { version: mail.version, container_no: doValue.F_ContainerNo, container_task_id: doValue.F_ContainerTaskId || undefined, filenames: doValue.filenames, }); setSubmitting(false); if (!res.ok) { message.error(res.error.message); return; } message.success( res.data.mode === "mock" ? "${ZH.doMock}" : "${ZH.doOk}", ); onConfirmed?.(); }; const submitTransfer = async () => { const target = transferValue.targetFBACode || transferValue.F_ConvertFBACode || ""; if (!target) { message.error("${ZH.needFba}"); return; } const rows = transferShipments.length ? transferShipments : shipments.slice(0, 1); setSubmitting(true); const res = await confirmTransferApi(mail.id, { version: mail.version, container_no: transferValue.F_ContainerNo || header?.F_ContainerNo || mail.container_no || "", payload: { keyValue: gate && "containerTaskId" in gate ? gate.containerTaskId || "" : "", strDetialList: rows.map((r) => ({ F_FBACode: r.F_FBACode || "", F_ConvertFBACode: target, })), F_ConvertChargeMode: transferValue.F_ConvertChargeMode || "2", F_RemovePlanDetail: transferValue.F_RemovePlanDetail ? "1" : "0", }, }); setSubmitting(false); if (!res.ok) { message.error(res.error.message); return; } message.success( res.data.mode === "mock" ? "${ZH.xferMock}" : "${ZH.xferOk}", ); onConfirmed?.(); }; return ( <> {summaryText} {instructions.length > 1 ? (
${ZH.splitPrefix}{instructions.length}${ZH.splitSuffix} {instructions.map((u) => ( {u.label} ))}
) : null} {evidence.mailbox?.from || mail.from_addr || "-"} ), }, { key: "action", label: "${ZH.action}", children: ( {intent.action} ), }, { key: "hint", label: "${ZH.hint}", children: intent.content_hint || "-", }, { key: "next", label: "${ZH.next}", span: { xs: 1, sm: 2, md: 2 }, children: intent.next_step || "-", }, ]} />
{instructions.map((unit, index) => { let body: ReactNode = null; let actions: ReactNode = null; if (unit.uiKind === "forecast") { body = ( ); } else if (unit.uiKind === "transfer") { const blocked = gate && !gate.canTransfer; body = ( <> {gate ? ( ) : null} {blocked ? ( setWorkOrderValue((v) => ({ ...v, ...p })) } /> ) : ( )} ); if (canConfirmOps && unit.isCurrent) { actions = blocked ? ( ) : ( ); } } else if (unit.uiKind === "work_order") { const woForUnit = unit.isCurrent ? workOrderValue : buildCcWorkOrderFormValue({ subject: unit.segmentSubject || mail.subject, body: unit.segmentText || bodyText, filenames, containerNo, }); body = ( setWorkOrderValue((v) => ({ ...v, ...p }))} /> ); if (canConfirmOps && unit.isCurrent) { actions = ( ); } } else if (unit.uiKind === "do_upload") { const doForUnit = buildCcDoUploadFormValue({ subject: unit.segmentSubject || mail.subject, body: unit.segmentText || bodyText, containerNo, filenames: unit.source === "attachment" ? [unit.segmentText] : filenames, }); body = ( setDoValue((v) => ({ ...v, ...p }))} /> ); if (canConfirmOps && unit.isCurrent) { actions = ( ); } } else if (unit.uiKind === "customer_instruction") { body = ( ); } return ( {body} {actions ? (
{actions}
) : null}
); })} ); } `; fs.writeFileSync(out, file, "utf8"); const check = fs.readFileSync(out, "utf8"); const bad = [...check.matchAll(/\?{3,}/g)].filter((m) => { const i = m.index || 0; const line = check.slice(check.lastIndexOf("\n", i) + 1, check.indexOf("\n", i)); return !line.includes('join("?")'); }); console.log( JSON.stringify( { bytes: Buffer.byteLength(check, "utf8"), hasTitle: check.includes(ZH.cardTitle), hasFrom: check.includes(ZH.from), hasBtnWo: check.includes(ZH.btnWo), hasDearLogic: check.includes("currentWorkOrderUnit"), leftoverQ: bad.length, }, null, 2, ), ); if (!check.includes(ZH.cardTitle) || bad.length) process.exit(1);