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.

161 lines
5.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { isValidContainerNo } from "@/utils/iso6346";
/**
* 这一条「预报」指令是否具备进入确认页的数据与状态。
* 不看整封邮件主类型:同封还可以有转仓/工单/DO,互不拦截。
* 仅柜头预报(有柜号、暂无货件行)也可进入确认页补录/空货件导入。
*/
export function canEnterForecastConfirm(input: {
status: string;
validShipmentCount: number;
/** 已解析出可用柜号(ISO 柜头),允许无货件行进入确认 */
hasContainerHeader?: boolean;
/** 详情页仅待确认类状态可点导入;确认页还允许多种导入后回看状态 */
allowReadonlyStatuses?: boolean;
}): boolean {
const { status, validShipmentCount } = input;
const hasRows = validShipmentCount >= 1;
const headerOnly = Boolean(input.hasContainerHeader) && !hasRows;
if (!hasRows && !headerOnly) return false;
if (input.allowReadonlyStatuses) {
return [
"PENDING_CONFIRM",
"FAILED",
"IMPORTING",
"SUCCESS",
"PARTIAL_SUCCESS",
].includes(status);
}
return (
status === "PENDING_CONFIRM" ||
status === "FAILED" ||
status === "PARTIAL_SUCCESS"
);
}
export type ForecastImportIssues = {
/** 字段级错误,挂到对应表单项 */
errors: Record<string, string>;
/** 阻断「确认导入」的原因(按钮灰掉时必须展示) */
blockers: string[];
/** 不阻断提交的提示(如仅柜头、货件全无效) */
notes: string[];
};
function summarizeInvalidRows(
rows: Array<{ row_index: number; invalid_reasons?: string[] }>,
): string | null {
if (!rows.length) return null;
const counts = new Map<string, number>();
for (const row of rows) {
const reasons = row.invalid_reasons?.length
? row.invalid_reasons
: ["未标明原因"];
for (const reason of reasons) {
counts.set(reason, (counts.get(reason) || 0) + 1);
}
}
const parts = [...counts.entries()].map(([reason, n]) =>
n > 1 ? `${reason}(${n}行)` : reason,
);
return `货件有 ${rows.length} 行未通过校验:${parts.join("、")}。请在本页补全必填项或导入装箱表。`;
}
/**
* 实时收集确认导入页的缺项 / 错项,供灰按钮旁提示与字段红字共用。
* 必填以 SaveContainer 为准:柜头 F_TransMode / F_OperationType / F_ContainerNo / F_CabinetType;
* 货件行 F_FBACode / F_Transporter / F_CTNS(由 VALID 行保证)。无有效货件不可导入。
*/
export function collectForecastImportIssues(input: {
transMode: number | null | undefined;
operationType: number | null | undefined;
containerNo: string;
/** 柜型(SaveContainer 必填) */
cabinetType?: string | null;
hasContainerConflict: boolean;
containerSource: "body" | "attachment" | null;
validRowCount: number;
selectedValidCount: number;
invalidRows?: Array<{ row_index: number; invalid_reasons?: string[] }>;
shipmentCount?: number;
hasUnmappedSelected: boolean;
ackUnmapped: boolean;
softLimitExceeded?: boolean;
portal?: "admin" | "client";
step?: number;
}): ForecastImportIssues {
const errors: Record<string, string> = {};
const blockers: string[] = [];
const notes: string[] = [];
const invalidRows = input.invalidRows ?? [];
const shipmentCount = input.shipmentCount ?? 0;
if (input.transMode !== 0 && input.transMode !== 1 && input.transMode !== 3) {
const msg = "请选择运输方式(海运 / 空运 / 自送柜)";
errors.transMode = msg;
blockers.push(msg);
}
if (
input.operationType !== 0 &&
input.operationType !== 2 &&
input.operationType !== 4
) {
const msg = "请选择业务类型(提拆派 / 直送 / 只清关)";
errors.operationType = msg;
blockers.push(msg);
}
const containerNo = (input.containerNo || "").trim();
if (!containerNo) {
const msg = "柜号为空,请填写 4 位字母 + 7 位数字(ISO 6346)";
errors.containerNo = msg;
blockers.push(msg);
} else if (!isValidContainerNo(containerNo)) {
const msg = `柜号「${containerNo.toUpperCase()}」不符合 ISO 6346 校验位,请核对后改正`;
errors.containerNo = msg;
blockers.push(msg);
}
if (!(input.cabinetType || "").trim()) {
const msg = "请选择柜型(标准:20GP / 40GP / 40HQ / 40HR / 45HQ / 53HQ 等)";
errors.cabinetType = msg;
blockers.push(msg);
}
if (input.hasContainerConflict && !input.containerSource) {
const msg = "正文与附件柜号不一致,请先选定使用哪一个";
errors.containerSource = msg;
blockers.push(msg);
}
if (input.validRowCount <= 0) {
const msg =
shipmentCount > 0 || invalidRows.length > 0
? "没有可导入的有效货件行(必填:派送方式、件数、体积、毛重)。请在本页修改或导入装箱表"
: "未解析到货件信息。请在本页新增货件,或导入卡派清单(xlsx)后再确认导入";
errors.rows = msg;
blockers.push(msg);
} else if (input.selectedValidCount === 0) {
const msg = input.softLimitExceeded
? "货件超过软限,默认未全选;请筛选后至少勾选一行有效货件"
: "请至少勾选一行有效货件";
errors.rows = msg;
blockers.push(msg);
}
if (input.hasUnmappedSelected && !input.ackUnmapped) {
const msg = "所选货件含未映射渠道,请勾选「已知晓存在未映射渠道,仍提交原值」";
errors.ack = msg;
blockers.push(msg);
}
const invalidSummary = summarizeInvalidRows(invalidRows);
if (input.validRowCount === 0 && invalidSummary) {
notes.push(invalidSummary);
}
return { errors, blockers, notes };
}