|
|
"use client";
|
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
import Link from "next/link";
|
|
|
import { useRouter } from "next/navigation";
|
|
|
import {
|
|
|
Alert,
|
|
|
App,
|
|
|
Button,
|
|
|
Checkbox,
|
|
|
Radio,
|
|
|
Space,
|
|
|
Typography,
|
|
|
} from "antd";
|
|
|
import { LeftOutlined } from "@ant-design/icons";
|
|
|
import {
|
|
|
CcForecastFormOrder,
|
|
|
buildCcForecastFormValue,
|
|
|
type CcForecastFormValue,
|
|
|
} from "@/components/CcForecastFormOrder";
|
|
|
import { ConfirmImportModal, ConflictModal } from "@/components/ConfirmImportModal";
|
|
|
import { ConfirmCcCustomerModal } from "@/components/ConfirmCcCustomerModal";
|
|
|
import {
|
|
|
CcCustomerProfileSelect,
|
|
|
useCcCustomerProfileSelect,
|
|
|
} from "@/components/CcCustomerProfileSelect";
|
|
|
import { useAuthSession } from "@/components/providers/AuthSessionProvider";
|
|
|
import { UI_COPY } from "@/constants/ui-copy";
|
|
|
import { describeCcFailureForOperator } from "@/constants/error-copy";
|
|
|
import { buildImportIdempotencyKey } from "@/utils/idempotency-client";
|
|
|
import { withVersionConflictRetry } from "@/utils/mail-submit-version";
|
|
|
import { useImport } from "@/hooks/useImport";
|
|
|
import { healthApi, listShippingLinesApi, parseForecastPackingApi } from "@/lib/client-api";
|
|
|
import { collectForecastImportIssues } from "@/services/cc/forecast-confirm-gate";
|
|
|
import { isInstructionImported } from "@/services/parse/instruction-execution-state";
|
|
|
import {
|
|
|
matchShippingLineId,
|
|
|
shippingLineHintFromBl,
|
|
|
} from "@/services/parse/extract-container-header";
|
|
|
import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template";
|
|
|
import { extractOpsSubjectFromMail } from "@/services/parse/ops-subject-template";
|
|
|
import {
|
|
|
normalizeParseShipments,
|
|
|
reindexShipments,
|
|
|
revalidateShipmentRow,
|
|
|
} from "@/services/parse/revalidate-shipment-row";
|
|
|
import type { MailInstructionUnit } from "@/services/parse/split-instructions";
|
|
|
import type { MailMessage } from "@/types";
|
|
|
import type { MailRecordModules, ShipmentRow } from "@/types/mail";
|
|
|
|
|
|
export function ForecastConfirmView({
|
|
|
mail,
|
|
|
unit,
|
|
|
}: {
|
|
|
mail: MailMessage;
|
|
|
unit: MailInstructionUnit;
|
|
|
}) {
|
|
|
const router = useRouter();
|
|
|
const { message, modal } = App.useApp();
|
|
|
const { user } = useAuthSession();
|
|
|
const { submitting, submit } = useImport(mail.id);
|
|
|
const ccProfile = useCcCustomerProfileSelect({ mailId: mail.id });
|
|
|
const instructionId = unit.id;
|
|
|
const [submitVersion, setSubmitVersion] = useState(mail.version);
|
|
|
useEffect(() => {
|
|
|
setSubmitVersion(mail.version);
|
|
|
}, [mail.version]);
|
|
|
|
|
|
const [shipments, setShipments] = useState<ShipmentRow[]>(() =>
|
|
|
normalizeParseShipments(mail.parse_result?.shipments),
|
|
|
);
|
|
|
useEffect(() => {
|
|
|
setShipments(normalizeParseShipments(mail.parse_result?.shipments));
|
|
|
}, [mail.id, mail.version, mail.parse_result?.shipments]);
|
|
|
|
|
|
const customerName =
|
|
|
mail.parse_result?.mail_record?.modules?.customer_name ||
|
|
|
(
|
|
|
mail.parse_result?.lineage?.mail_record as
|
|
|
| { modules?: { customer_name?: string } }
|
|
|
| undefined
|
|
|
)?.modules?.customer_name ||
|
|
|
"";
|
|
|
|
|
|
const [form, setForm] = useState<CcForecastFormValue>(() =>
|
|
|
buildCcForecastFormValue({}),
|
|
|
);
|
|
|
const [step, setStep] = useState(0);
|
|
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
|
|
const [ackUnmapped, setAckUnmapped] = useState(false);
|
|
|
const [customerConfirmOpen, setCustomerConfirmOpen] = useState(false);
|
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
|
const [conflictOpen, setConflictOpen] = useState(false);
|
|
|
const [conflictReason, setConflictReason] = useState("CONFLICT_CONTAINER");
|
|
|
const [conflictMessage, setConflictMessage] = useState("");
|
|
|
const [conflictKind, setConflictKind] = useState<string | undefined>();
|
|
|
const [conflictDetail, setConflictDetail] = useState<string | undefined>();
|
|
|
const [containerSource, setContainerSource] = useState<
|
|
|
"body" | "attachment" | null
|
|
|
>(null);
|
|
|
const [shippingLines, setShippingLines] = useState<
|
|
|
{ value: string; label: string }[]
|
|
|
>([]);
|
|
|
const [shippingLineRows, setShippingLineRows] = useState<
|
|
|
Array<{ id: string; name: string; code?: string; prefix?: string }>
|
|
|
>([]);
|
|
|
const isAdmin = user.role === "admin";
|
|
|
const [forceImportEnabled, setForceImportEnabled] = useState(false);
|
|
|
|
|
|
const softLimitExceeded = Boolean(
|
|
|
mail.parse_result?.lineage?.soft_limit_exceeded,
|
|
|
);
|
|
|
const containerConflict = mail.parse_result?.lineage?.CONTAINER_NO_CONFLICT as
|
|
|
| { body?: string; attachment?: string }
|
|
|
| undefined;
|
|
|
const hasContainerConflict = Boolean(
|
|
|
containerConflict?.body && containerConflict?.attachment,
|
|
|
);
|
|
|
const unitImported = isInstructionImported(
|
|
|
mail.parse_result?.lineage,
|
|
|
instructionId,
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
void healthApi().then((healthRes) => {
|
|
|
if (healthRes.ok) {
|
|
|
setForceImportEnabled(Boolean(healthRes.data.enable_force_import));
|
|
|
}
|
|
|
});
|
|
|
}, []);
|
|
|
|
|
|
const [shippingFetchLoading, setShippingFetchLoading] = useState(false);
|
|
|
const [packingImportLoading, setPackingImportLoading] = useState(false);
|
|
|
|
|
|
const loadShippingLines = async (opts?: { refresh?: boolean }) => {
|
|
|
const res = await listShippingLinesApi(
|
|
|
opts?.refresh ? { refresh: true } : undefined,
|
|
|
);
|
|
|
if (!res.ok) {
|
|
|
message.error(res.error.message);
|
|
|
return null;
|
|
|
}
|
|
|
setShippingLineRows(res.data.items);
|
|
|
setShippingLines(
|
|
|
res.data.items.map((item) => ({ value: item.id, label: item.name })),
|
|
|
);
|
|
|
return res.data.items;
|
|
|
};
|
|
|
|
|
|
const matchShippingLineFromBl = (
|
|
|
bl: string,
|
|
|
rows: typeof shippingLineRows,
|
|
|
): string | null => {
|
|
|
const hint = shippingLineHintFromBl(bl);
|
|
|
if (!hint && !bl.trim()) return null;
|
|
|
return matchShippingLineId(
|
|
|
hint,
|
|
|
rows.map((l) => ({
|
|
|
F_Id: l.id,
|
|
|
F_FullName: l.name,
|
|
|
F_EnCode: l.code,
|
|
|
F_Prefix: l.prefix,
|
|
|
})),
|
|
|
{ blNo: bl },
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const onFetchShippingLine = async () => {
|
|
|
setShippingFetchLoading(true);
|
|
|
try {
|
|
|
const items = await loadShippingLines({ refresh: true });
|
|
|
if (!items) return;
|
|
|
const bl = form.F_BLCopyCode || "";
|
|
|
if (!bl.trim()) {
|
|
|
message.warning("请先填写提单号,再按提单前缀匹配船司");
|
|
|
return;
|
|
|
}
|
|
|
const id = matchShippingLineFromBl(bl, items);
|
|
|
if (!id) {
|
|
|
message.warning(
|
|
|
`船司列表已刷新(${items.length} 条),但未能根据提单「${bl}」自动匹配,请手工选择`,
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
setForm((p) => ({ ...p, F_ShippingLineId: id }));
|
|
|
const name = items.find((x) => x.id === id)?.name || id;
|
|
|
message.success(`已匹配船司:${name}`);
|
|
|
} finally {
|
|
|
setShippingFetchLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const onImportPackingFile = async (file: File) => {
|
|
|
if (pageReadonly) return;
|
|
|
setPackingImportLoading(true);
|
|
|
try {
|
|
|
const res = await parseForecastPackingApi(mail.id, file, {
|
|
|
containerNo: form.F_ContainerNo,
|
|
|
});
|
|
|
if (!res.ok) {
|
|
|
modal.error({
|
|
|
title: "导入装箱表失败",
|
|
|
content:
|
|
|
res.error.message ||
|
|
|
"无法解析该文件。请上传卡派清单等含「渠道 / 件数 / 仓库」表头的 Excel(xlsx/xls)。",
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
if (!res.data.shipments.length) {
|
|
|
modal.warning({
|
|
|
title: "未识别到货件行",
|
|
|
content:
|
|
|
"文件已上传,但没有解析出货件。请确认表头包含渠道/派送方式、件数、体积、毛重等列。",
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
const imported = reindexShipments(
|
|
|
res.data.shipments.map((s, i) =>
|
|
|
revalidateShipmentRow({ ...s, row_index: i }),
|
|
|
),
|
|
|
);
|
|
|
setShipments(imported);
|
|
|
setSelected(
|
|
|
new Set(
|
|
|
imported
|
|
|
.filter((s) => s.row_status === "VALID")
|
|
|
.map((s) => s.row_index),
|
|
|
),
|
|
|
);
|
|
|
if (
|
|
|
res.data.container_no &&
|
|
|
!form.F_ContainerNo.trim() &&
|
|
|
!hasContainerConflict
|
|
|
) {
|
|
|
setForm((p) => ({
|
|
|
...p,
|
|
|
F_ContainerNo: res.data.container_no!.toUpperCase(),
|
|
|
}));
|
|
|
}
|
|
|
message.success(
|
|
|
`已导入 ${res.data.row_count} 行货件(有效 ${res.data.valid_count})`,
|
|
|
);
|
|
|
if (!isAdmin && step === 0) setStep(1);
|
|
|
} catch {
|
|
|
modal.error({
|
|
|
title: "导入装箱表失败",
|
|
|
content: "上传或解析时出错,请确认文件为有效的 Excel(xlsx/xls)后重试。",
|
|
|
});
|
|
|
} finally {
|
|
|
setPackingImportLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
void loadShippingLines();
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
}, []);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!form.F_BLCopyCode || !shippingLineRows.length || form.F_ShippingLineId) {
|
|
|
return;
|
|
|
}
|
|
|
const id = matchShippingLineFromBl(form.F_BLCopyCode, shippingLineRows);
|
|
|
if (id) {
|
|
|
setForm((p) =>
|
|
|
p.F_ShippingLineId === id ? p : { ...p, F_ShippingLineId: id },
|
|
|
);
|
|
|
}
|
|
|
}, [form.F_BLCopyCode, form.F_ShippingLineId, shippingLineRows]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
const record =
|
|
|
mail.parse_result?.mail_record ||
|
|
|
(
|
|
|
mail.parse_result?.lineage as
|
|
|
| { mail_record?: { modules?: MailRecordModules } }
|
|
|
| undefined
|
|
|
)?.mail_record;
|
|
|
const ops = extractOpsSubjectFromMail(mail.subject);
|
|
|
const bl = ops
|
|
|
? null
|
|
|
: extractBlPlusFromMail(mail.subject, mail.body_text || "");
|
|
|
const modules: MailRecordModules = {
|
|
|
...(bl?.modules || {}),
|
|
|
...(ops?.modules || {}),
|
|
|
...(record?.modules || {}),
|
|
|
};
|
|
|
setForm(
|
|
|
buildCcForecastFormValue({
|
|
|
customerName,
|
|
|
header: mail.parse_result?.container_header,
|
|
|
modules,
|
|
|
}),
|
|
|
);
|
|
|
}, [mail, customerName]);
|
|
|
|
|
|
const validRows = useMemo(
|
|
|
() => shipments.filter((s) => s.row_status === "VALID"),
|
|
|
[shipments],
|
|
|
);
|
|
|
const validRowKey = useMemo(
|
|
|
() => validRows.map((r) => r.row_index).join(","),
|
|
|
[validRows],
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (softLimitExceeded) {
|
|
|
setSelected((prev) => (prev.size === 0 ? prev : new Set()));
|
|
|
return;
|
|
|
}
|
|
|
setSelected((prev) => {
|
|
|
const nextIds = validRowKey
|
|
|
? validRowKey.split(",").map((x) => Number(x))
|
|
|
: [];
|
|
|
if (
|
|
|
prev.size === nextIds.length &&
|
|
|
nextIds.every((rowId) => prev.has(rowId))
|
|
|
) {
|
|
|
return prev;
|
|
|
}
|
|
|
return new Set(nextIds);
|
|
|
});
|
|
|
}, [mail.id, validRowKey, softLimitExceeded]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
setContainerSource(null);
|
|
|
setStep(0);
|
|
|
}, [mail.id]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!hasContainerConflict || !containerConflict || !containerSource) return;
|
|
|
const next =
|
|
|
containerSource === "body"
|
|
|
? containerConflict.body || ""
|
|
|
: containerConflict.attachment || "";
|
|
|
const upper = next.toUpperCase();
|
|
|
setForm((p) =>
|
|
|
p.F_ContainerNo === upper ? p : { ...p, F_ContainerNo: upper },
|
|
|
);
|
|
|
}, [
|
|
|
containerSource,
|
|
|
hasContainerConflict,
|
|
|
containerConflict?.body,
|
|
|
containerConflict?.attachment,
|
|
|
]);
|
|
|
|
|
|
const selectedValid = Array.from(selected).filter((idx) =>
|
|
|
validRows.some((r) => r.row_index === idx),
|
|
|
);
|
|
|
|
|
|
const hasUnmappedSelected = selectedValid.some((idx) => {
|
|
|
const row = shipments.find((s) => s.row_index === idx);
|
|
|
return row?.warnings?.includes("CHANNEL_UNMAPPED");
|
|
|
});
|
|
|
|
|
|
const pageReadonly =
|
|
|
unitImported ||
|
|
|
mail.status === "IMPORTING" ||
|
|
|
mail.status === "SUCCESS";
|
|
|
|
|
|
const importIssues = useMemo(
|
|
|
() =>
|
|
|
collectForecastImportIssues({
|
|
|
transMode: form.F_TransMode,
|
|
|
operationType: form.F_OperationType,
|
|
|
containerNo: form.F_ContainerNo,
|
|
|
cabinetType: form.F_CabinetType,
|
|
|
hasContainerConflict,
|
|
|
containerSource,
|
|
|
validRowCount: validRows.length,
|
|
|
selectedValidCount: selectedValid.length,
|
|
|
invalidRows: shipments.filter((s) => s.row_status === "INVALID"),
|
|
|
shipmentCount: shipments.length,
|
|
|
hasUnmappedSelected,
|
|
|
ackUnmapped,
|
|
|
softLimitExceeded,
|
|
|
portal: "client",
|
|
|
step,
|
|
|
}),
|
|
|
[
|
|
|
form.F_TransMode,
|
|
|
form.F_OperationType,
|
|
|
form.F_ContainerNo,
|
|
|
form.F_CabinetType,
|
|
|
hasContainerConflict,
|
|
|
containerSource,
|
|
|
validRows.length,
|
|
|
selectedValid.length,
|
|
|
shipments,
|
|
|
hasUnmappedSelected,
|
|
|
ackUnmapped,
|
|
|
softLimitExceeded,
|
|
|
step,
|
|
|
],
|
|
|
);
|
|
|
const errors = importIssues.errors;
|
|
|
const headerReady =
|
|
|
!errors.transMode &&
|
|
|
!errors.operationType &&
|
|
|
!errors.containerNo &&
|
|
|
!errors.cabinetType &&
|
|
|
!errors.containerSource;
|
|
|
|
|
|
const onStepChange = (s: number) => {
|
|
|
if (pageReadonly) {
|
|
|
setStep(s);
|
|
|
return;
|
|
|
}
|
|
|
if (s === 1 && !headerReady) {
|
|
|
message.error("请先修正基础信息");
|
|
|
return;
|
|
|
}
|
|
|
setStep(s);
|
|
|
};
|
|
|
|
|
|
const onPrimary = () => {
|
|
|
if (pageReadonly) return;
|
|
|
if (importIssues.blockers.length) {
|
|
|
message.error(importIssues.blockers[0]);
|
|
|
return;
|
|
|
}
|
|
|
if (!ccProfile.profileId || !ccProfile.selected) {
|
|
|
message.error("请先选择 CC 客户");
|
|
|
return;
|
|
|
}
|
|
|
setCustomerConfirmOpen(true);
|
|
|
};
|
|
|
|
|
|
const onCustomerConfirmOk = () => {
|
|
|
setCustomerConfirmOpen(false);
|
|
|
setConfirmOpen(true);
|
|
|
};
|
|
|
|
|
|
const onConfirmSubmit = async (forceSkip = false) => {
|
|
|
const transMode = form.F_TransMode;
|
|
|
const operationType = form.F_OperationType;
|
|
|
if (transMode !== 0 && transMode !== 1 && transMode !== 3) return;
|
|
|
if (operationType !== 0 && operationType !== 2 && operationType !== 4) {
|
|
|
return;
|
|
|
}
|
|
|
if (!ccProfile.profileId) {
|
|
|
message.error("请先选择 CC 客户");
|
|
|
return;
|
|
|
}
|
|
|
const containerNo = form.F_ContainerNo.toUpperCase();
|
|
|
const basePayload = {
|
|
|
instruction_id: instructionId,
|
|
|
idempotency_key: buildImportIdempotencyKey({
|
|
|
mailId: mail.id,
|
|
|
instructionId,
|
|
|
containerNo,
|
|
|
rowIndexes: selectedValid,
|
|
|
forceSkip,
|
|
|
}),
|
|
|
F_TransMode: transMode,
|
|
|
F_OperationType: operationType,
|
|
|
container_header: {
|
|
|
F_ContainerNo: containerNo,
|
|
|
F_CabinetType: form.F_CabinetType,
|
|
|
F_Classis: form.F_Classis,
|
|
|
F_ETA: form.F_ETA || null,
|
|
|
F_ETD: form.F_ETD || null,
|
|
|
F_LoadPort: form.F_LoadPort,
|
|
|
F_Dock: form.F_Dock,
|
|
|
F_BLCopyCode: form.F_BLCopyCode,
|
|
|
F_MemoRemark: form.F_MemoRemark,
|
|
|
F_Instruction: form.F_Instruction,
|
|
|
F_ShippingLineId: form.F_ShippingLineId,
|
|
|
},
|
|
|
selected_row_indexes: selectedValid,
|
|
|
ack_unmapped_channels: ackUnmapped,
|
|
|
cc_profile_id: ccProfile.profileId,
|
|
|
shipments,
|
|
|
...(forceSkip ? { force_skip_conflict: true } : {}),
|
|
|
};
|
|
|
|
|
|
const slowHint = window.setTimeout(() => {
|
|
|
message.loading({
|
|
|
content: "正在提交到 CarrierCentral,请稍候…",
|
|
|
key: "cc-import",
|
|
|
duration: 0,
|
|
|
});
|
|
|
}, 600);
|
|
|
try {
|
|
|
const res = await withVersionConflictRetry({
|
|
|
version: submitVersion,
|
|
|
onVersion: setSubmitVersion,
|
|
|
submit: (version) => submit({ ...basePayload, version }),
|
|
|
});
|
|
|
|
|
|
if (!res.ok) {
|
|
|
if (
|
|
|
res.error.code === "CONFLICT_CONTAINER" ||
|
|
|
res.error.code === "CONFLICT_CHECK_FAILED"
|
|
|
) {
|
|
|
setConfirmOpen(false);
|
|
|
const details = res.error.details as
|
|
|
| { kind?: string; error?: string }
|
|
|
| undefined;
|
|
|
const rawDetail =
|
|
|
details?.error || res.error.message || undefined;
|
|
|
const described = describeCcFailureForOperator(rawDetail);
|
|
|
setConflictReason(res.error.code);
|
|
|
setConflictMessage(described.message || res.error.message || "");
|
|
|
setConflictKind(details?.kind || described.kind);
|
|
|
setConflictDetail(described.detail || details?.error);
|
|
|
setConflictOpen(true);
|
|
|
return;
|
|
|
}
|
|
|
message.error(res.error.message);
|
|
|
setConfirmOpen(false);
|
|
|
setConflictOpen(false);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
setConfirmOpen(false);
|
|
|
setConflictOpen(false);
|
|
|
if (res.data.mail_status === "SUCCESS") {
|
|
|
message.success("导入成功");
|
|
|
router.push(`/logs?mail_id=${mail.id}`);
|
|
|
} else if (res.data.mail_status === "PARTIAL_SUCCESS") {
|
|
|
message.success("本条预报已导入,请继续确认其余指令");
|
|
|
router.push(`/mails/${mail.id}`);
|
|
|
} else {
|
|
|
const rawErr =
|
|
|
res.data.imports?.[0]?.last_error ||
|
|
|
mail.last_error ||
|
|
|
"导入失败,可修改后重试";
|
|
|
const described = describeCcFailureForOperator(rawErr);
|
|
|
setConflictReason("IMPORT_FAILED");
|
|
|
setConflictMessage(described.message);
|
|
|
setConflictKind(described.kind);
|
|
|
setConflictDetail(described.detail ?? undefined);
|
|
|
if (/柜型/.test(rawErr) || /柜型/.test(described.message)) {
|
|
|
setStep(0);
|
|
|
}
|
|
|
setConflictOpen(true);
|
|
|
}
|
|
|
} finally {
|
|
|
window.clearTimeout(slowHint);
|
|
|
message.destroy("cc-import");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const canForceSkip =
|
|
|
isAdmin &&
|
|
|
forceImportEnabled &&
|
|
|
(conflictReason === "CONFLICT_CONTAINER" ||
|
|
|
conflictReason === "CONFLICT_CHECK_FAILED");
|
|
|
|
|
|
return (
|
|
|
<Space
|
|
|
direction="vertical"
|
|
|
size={24}
|
|
|
style={{ width: "100%", paddingBottom: 96 }}
|
|
|
>
|
|
|
<Space
|
|
|
style={{ width: "100%", justifyContent: "space-between" }}
|
|
|
align="start"
|
|
|
>
|
|
|
<Space direction="vertical" size={8}>
|
|
|
<Link href={`/mails/${mail.id}`} prefetch>
|
|
|
<Button type="default" icon={<LeftOutlined />}>
|
|
|
{UI_COPY.backToDetail}
|
|
|
</Button>
|
|
|
</Link>
|
|
|
<Typography.Title
|
|
|
level={2}
|
|
|
style={{ margin: 0, maxWidth: "100%" }}
|
|
|
ellipsis={{ tooltip: mail.subject }}
|
|
|
>
|
|
|
{unit.label}
|
|
|
</Typography.Title>
|
|
|
<Typography.Text type="secondary">{mail.subject}</Typography.Text>
|
|
|
<Typography.Text
|
|
|
style={{
|
|
|
fontFamily: "monospace",
|
|
|
fontSize: 20,
|
|
|
color: "var(--color-primary)",
|
|
|
}}
|
|
|
>
|
|
|
{form.F_ContainerNo || "-"}
|
|
|
</Typography.Text>
|
|
|
</Space>
|
|
|
<Space direction="vertical" align="end" size={4}>
|
|
|
<CcCustomerProfileSelect
|
|
|
mailId={mail.id}
|
|
|
items={ccProfile.items}
|
|
|
loading={ccProfile.loading}
|
|
|
value={ccProfile.profileId}
|
|
|
onChange={ccProfile.setProfileId}
|
|
|
/>
|
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
客户端 · SaveContainer
|
|
|
</Typography.Text>
|
|
|
</Space>
|
|
|
</Space>
|
|
|
|
|
|
{mail.status === "IMPORTING" ? (
|
|
|
<Alert type="warning" showIcon message={UI_COPY.alertImporting} />
|
|
|
) : null}
|
|
|
|
|
|
{unitImported ? (
|
|
|
<Alert type="success" showIcon message="该预报指令已导入,当前只读" />
|
|
|
) : null}
|
|
|
|
|
|
{softLimitExceeded ? (
|
|
|
<Alert type="warning" showIcon message={UI_COPY.softLimitAlert} />
|
|
|
) : null}
|
|
|
|
|
|
{!pageReadonly && importIssues.blockers.length ? (
|
|
|
<Alert
|
|
|
type="error"
|
|
|
showIcon
|
|
|
message="当前无法确认导入"
|
|
|
description={
|
|
|
<ul style={{ margin: "4px 0 0", paddingLeft: 18 }}>
|
|
|
{importIssues.blockers.map((item) => (
|
|
|
<li key={item}>{item}</li>
|
|
|
))}
|
|
|
</ul>
|
|
|
}
|
|
|
/>
|
|
|
) : null}
|
|
|
|
|
|
{!pageReadonly &&
|
|
|
!importIssues.blockers.length &&
|
|
|
importIssues.notes.length ? (
|
|
|
<Alert
|
|
|
type="warning"
|
|
|
showIcon
|
|
|
message={importIssues.notes.join(" ")}
|
|
|
/>
|
|
|
) : null}
|
|
|
|
|
|
{hasContainerConflict ? (
|
|
|
<Alert
|
|
|
type="error"
|
|
|
showIcon
|
|
|
message={UI_COPY.containerConflictAlert}
|
|
|
description={
|
|
|
<Radio.Group
|
|
|
disabled={pageReadonly}
|
|
|
value={containerSource || undefined}
|
|
|
onChange={(e) => setContainerSource(e.target.value)}
|
|
|
style={{ marginTop: 8 }}
|
|
|
>
|
|
|
<Space direction="vertical">
|
|
|
<Radio value="body">
|
|
|
正文柜号:
|
|
|
<span className="mono">{containerConflict?.body}</span>
|
|
|
</Radio>
|
|
|
<Radio value="attachment">
|
|
|
附件柜号:
|
|
|
<span className="mono">{containerConflict?.attachment}</span>
|
|
|
</Radio>
|
|
|
</Space>
|
|
|
</Radio.Group>
|
|
|
}
|
|
|
/>
|
|
|
) : null}
|
|
|
{errors.containerSource ? (
|
|
|
<Typography.Text type="danger" style={{ fontSize: 12 }}>
|
|
|
{errors.containerSource}
|
|
|
</Typography.Text>
|
|
|
) : null}
|
|
|
|
|
|
{hasUnmappedSelected ? (
|
|
|
<div className={`cc-fo-ack${errors.ack ? " is-invalid" : ""}`}>
|
|
|
<Checkbox
|
|
|
disabled={pageReadonly}
|
|
|
checked={ackUnmapped}
|
|
|
onChange={(e) => setAckUnmapped(e.target.checked)}
|
|
|
>
|
|
|
{UI_COPY.ackUnmapped}
|
|
|
</Checkbox>
|
|
|
{errors.ack ? (
|
|
|
<div className="cc-fo-error" style={{ marginTop: 4 }}>
|
|
|
{errors.ack}
|
|
|
</div>
|
|
|
) : null}
|
|
|
</div>
|
|
|
) : null}
|
|
|
|
|
|
<CcForecastFormOrder
|
|
|
mode={pageReadonly ? "readonly" : "edit"}
|
|
|
portal="client"
|
|
|
value={form}
|
|
|
shipments={shipments}
|
|
|
shippingLines={shippingLines}
|
|
|
onFetchShippingLine={pageReadonly ? undefined : onFetchShippingLine}
|
|
|
fetchShippingLineLoading={shippingFetchLoading}
|
|
|
selected={selected}
|
|
|
onSelectedChange={setSelected}
|
|
|
onChange={(patch) => setForm((p) => ({ ...p, ...patch }))}
|
|
|
onShipmentsChange={pageReadonly ? undefined : setShipments}
|
|
|
packingImportLoading={packingImportLoading}
|
|
|
onImportPackingFile={pageReadonly ? undefined : onImportPackingFile}
|
|
|
errors={errors}
|
|
|
step={step}
|
|
|
onStepChange={onStepChange}
|
|
|
softLimitExceeded={softLimitExceeded}
|
|
|
containerNoLocked={hasContainerConflict}
|
|
|
onFinish={onPrimary}
|
|
|
finishLoading={submitting}
|
|
|
finishDisabled={
|
|
|
importIssues.blockers.length > 0 || !ccProfile.profileId
|
|
|
}
|
|
|
finishBlockers={pageReadonly ? [] : importIssues.blockers}
|
|
|
finishNotes={pageReadonly ? [] : importIssues.notes}
|
|
|
/>
|
|
|
|
|
|
<div className="sticky-footer">
|
|
|
<Typography.Text>
|
|
|
已选 <b>{selectedValid.length}</b> / 有效 <b>{validRows.length}</b>
|
|
|
{!pageReadonly && importIssues.blockers.length ? (
|
|
|
<span style={{ color: "var(--color-error)", marginLeft: 12 }}>
|
|
|
{importIssues.blockers[0]}
|
|
|
</span>
|
|
|
) : null}
|
|
|
</Typography.Text>
|
|
|
<Link href={`/mails/${mail.id}`} prefetch>
|
|
|
<Button>取消</Button>
|
|
|
</Link>
|
|
|
</div>
|
|
|
|
|
|
<ConfirmCcCustomerModal
|
|
|
open={customerConfirmOpen}
|
|
|
customerName={ccProfile.selected?.name || ""}
|
|
|
operationLabel="新增预报"
|
|
|
onCancel={() => setCustomerConfirmOpen(false)}
|
|
|
onOk={onCustomerConfirmOk}
|
|
|
/>
|
|
|
<ConfirmImportModal
|
|
|
open={confirmOpen}
|
|
|
loading={submitting}
|
|
|
containerNo={form.F_ContainerNo}
|
|
|
selectedCount={selectedValid.length}
|
|
|
onCancel={() => setConfirmOpen(false)}
|
|
|
onConfirm={() => void onConfirmSubmit(false)}
|
|
|
/>
|
|
|
<ConflictModal
|
|
|
open={conflictOpen}
|
|
|
reason={conflictReason}
|
|
|
message={conflictMessage}
|
|
|
failureKind={conflictKind}
|
|
|
detailError={conflictDetail}
|
|
|
canForceSkip={canForceSkip}
|
|
|
loading={submitting}
|
|
|
onClose={() => setConflictOpen(false)}
|
|
|
onForceSkip={() => void onConfirmSubmit(true)}
|
|
|
/>
|
|
|
</Space>
|
|
|
);
|
|
|
}
|