You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
680 lines
19 KiB
680 lines
19 KiB
/**
|
|
* 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<TransferGateDecision | null>(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 (
|
|
<>
|
|
<Card title="${ZH.cardTitle}">
|
|
<Typography.Paragraph style={{ marginBottom: 12, fontSize: 15 }}>
|
|
{summaryText}
|
|
</Typography.Paragraph>
|
|
{instructions.length > 1 ? (
|
|
<div style={{ marginBottom: 12 }}>
|
|
<Typography.Text type="secondary" style={{ marginRight: 8 }}>
|
|
${ZH.splitPrefix}{instructions.length}${ZH.splitSuffix}
|
|
</Typography.Text>
|
|
<Space wrap size={[4, 4]}>
|
|
{instructions.map((u) => (
|
|
<Tag key={u.id}>{u.label}</Tag>
|
|
))}
|
|
</Space>
|
|
</div>
|
|
) : null}
|
|
<Descriptions
|
|
bordered
|
|
size="small"
|
|
column={{ xs: 1, sm: 2, md: 2 }}
|
|
items={[
|
|
{
|
|
key: "from",
|
|
label: "${ZH.from}",
|
|
children: (
|
|
<span className="mono">
|
|
{evidence.mailbox?.from || mail.from_addr || "-"}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "action",
|
|
label: "${ZH.action}",
|
|
children: (
|
|
<Space>
|
|
<TypeTag type={mail.mail_type} />
|
|
<Typography.Text>{intent.action}</Typography.Text>
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
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 || "-",
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
|
|
{instructions.map((unit, index) => {
|
|
let body: ReactNode = null;
|
|
let actions: ReactNode = null;
|
|
|
|
if (unit.uiKind === "forecast") {
|
|
body = (
|
|
<CcForecastFormOrder
|
|
mode="readonly"
|
|
value={forecastValue}
|
|
shipments={shipments}
|
|
shippingLines={shippingLines}
|
|
/>
|
|
);
|
|
} else if (unit.uiKind === "transfer") {
|
|
const blocked = gate && !gate.canTransfer;
|
|
body = (
|
|
<>
|
|
{gate ? (
|
|
<Alert
|
|
style={{ marginBottom: 12 }}
|
|
type={gate.canTransfer ? "success" : "warning"}
|
|
showIcon
|
|
message={gate.reason}
|
|
/>
|
|
) : null}
|
|
{blocked ? (
|
|
<CcWorkOrderForm
|
|
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
|
value={{
|
|
...workOrderValue,
|
|
F_MessageType:
|
|
workOrderValue.F_MessageType || "${ZH.msgType}",
|
|
F_Remark: [
|
|
workOrderValue.F_Remark,
|
|
gate.reason ? \`${ZH.gate}\${gate.reason}\` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("${ZH.semi}"),
|
|
mailActions: workOrderValue.mailActions?.length
|
|
? workOrderValue.mailActions
|
|
: ["${ZH.actXfer}"],
|
|
}}
|
|
onChange={(p) =>
|
|
setWorkOrderValue((v) => ({ ...v, ...p }))
|
|
}
|
|
/>
|
|
) : (
|
|
<CcTransferIndexTruck
|
|
mode="readonly"
|
|
value={transferValue}
|
|
shipments={transferShipments}
|
|
transferPairs={transferPairs}
|
|
totalShipmentCount={shipments.length}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
if (canConfirmOps && unit.isCurrent) {
|
|
actions = blocked ? (
|
|
<Button
|
|
type="primary"
|
|
loading={submitting}
|
|
onClick={() => void submitWorkOrder()}
|
|
>
|
|
${ZH.btnWo}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="primary"
|
|
loading={submitting}
|
|
onClick={() => void submitTransfer()}
|
|
>
|
|
${ZH.btnXfer}
|
|
</Button>
|
|
);
|
|
}
|
|
} else if (unit.uiKind === "work_order") {
|
|
const woForUnit = unit.isCurrent
|
|
? workOrderValue
|
|
: buildCcWorkOrderFormValue({
|
|
subject: unit.segmentSubject || mail.subject,
|
|
body: unit.segmentText || bodyText,
|
|
filenames,
|
|
containerNo,
|
|
});
|
|
body = (
|
|
<CcWorkOrderForm
|
|
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
|
value={woForUnit}
|
|
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
|
|
/>
|
|
);
|
|
if (canConfirmOps && unit.isCurrent) {
|
|
actions = (
|
|
<Button
|
|
type="primary"
|
|
loading={submitting}
|
|
onClick={() => void submitWorkOrder()}
|
|
>
|
|
${ZH.btnWo}
|
|
</Button>
|
|
);
|
|
}
|
|
} 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 = (
|
|
<CcDoUploadPanel
|
|
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
|
value={unit.isCurrent ? doValue : doForUnit}
|
|
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
|
|
/>
|
|
);
|
|
if (canConfirmOps && unit.isCurrent) {
|
|
actions = (
|
|
<Button
|
|
type="primary"
|
|
loading={submitting}
|
|
onClick={() => void submitDo()}
|
|
>
|
|
${ZH.btnDo}
|
|
</Button>
|
|
);
|
|
}
|
|
} else if (unit.uiKind === "customer_instruction") {
|
|
body = (
|
|
<CcCustomerInstructionPanel
|
|
value={{
|
|
roundAt: unit.roundAt,
|
|
roundFrom: unit.roundFrom,
|
|
subject: unit.segmentSubject,
|
|
body: unit.segmentText || bodyText,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<InstructionUnitShell
|
|
key={unit.id}
|
|
unit={unit}
|
|
index={index}
|
|
total={instructions.length}
|
|
>
|
|
{body}
|
|
{actions ? (
|
|
<div style={{ marginTop: 12, textAlign: "right" }}>{actions}</div>
|
|
) : null}
|
|
</InstructionUnitShell>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
`;
|
|
|
|
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);
|