fix(intake): preserve confirmation after factory edits

This commit is contained in:
WuYongJie 2026-08-27 15:13:20 +08:00
parent 01a9d1e2c8
commit 48e496b01b
6 changed files with 272 additions and 67 deletions

View File

@ -528,6 +528,147 @@ describe("NonIntegratedDetailClient factory and SKU editing", () => {
});
});
it("keeps basic information confirmed after a factory set count change", async () => {
const actor = userEvent.setup();
const productId = "ZWJ0218-factory-preserves-basic-confirmation";
const basicInfoApprovedAt = "2026-08-27T03:00:00.000Z";
saveNonIntegratedProduct(
createProductRecord({
id: productId,
spu: "ZWJ0218",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
documentWorkspaceConfirmations: {
...createDefaultDocumentWorkspaceConfirmations(),
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true,
},
documentWorkspaceApprovedAt: {
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: basicInfoApprovedAt,
},
skuItems: [
createSkuItem({
id: "sku-factory-preserves-basic-confirmation-1",
skuSequence: 1,
model: "ZWJ0218K01",
skuCode: "ZWJ021801",
setCount: "1",
barcode: "ZWJ021801N396V01",
manufacturer: "曹县嘉欢",
manufacturerCode: "N396",
installMethod: "整装",
source: "sku_workbook",
sourceTable: "sku_workbook",
contentLocked: true,
}),
],
}),
);
render(<NonIntegratedDetailClient mode="edit" productId={productId} />);
await actor.click(
screen.getByTestId("document-workspace-trigger-产品定厂"),
);
const factoryWorkspace = screen.getByTestId("product-factory-workspace");
const setCountInput =
within(factoryWorkspace).getByPlaceholderText("请输入套件量");
fireEvent.change(setCountInput, { target: { value: "2" } });
fireEvent.blur(setCountInput);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.skuItems.map((item) => item.setCount)).toEqual([
"2",
"2",
]);
expect(
savedProduct?.documentWorkspaceConfirmations?.[
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
],
).toBe(true);
expect(
savedProduct?.documentWorkspaceApprovedAt?.[
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
],
).toBe(basicInfoApprovedAt);
});
});
it("keeps basic information confirmed after a factory install method change", async () => {
const actor = userEvent.setup();
const productId = "ZWJ0218-factory-install-preserves-confirmation";
const basicInfoApprovedAt = "2026-08-27T03:00:00.000Z";
saveNonIntegratedProduct(
createProductRecord({
id: productId,
spu: "ZWJ0218",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
documentWorkspaceConfirmations: {
...createDefaultDocumentWorkspaceConfirmations(),
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true,
},
documentWorkspaceApprovedAt: {
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: basicInfoApprovedAt,
},
skuItems: [
createSkuItem({
id: "sku-factory-install-preserves-confirmation-1",
skuSequence: 1,
model: "ZWJ0218K01",
skuCode: "ZWJ021801",
setCount: "1",
barcode: "ZWJ021801N396V01",
manufacturer: "曹县嘉欢",
manufacturerCode: "N396",
installMethod: "整装",
source: "sku_workbook",
sourceTable: "sku_workbook",
contentLocked: true,
}),
],
}),
);
render(<NonIntegratedDetailClient mode="edit" productId={productId} />);
await actor.click(
screen.getByTestId("document-workspace-trigger-产品定厂"),
);
const factoryWorkspace = screen.getByTestId("product-factory-workspace");
const installMethodSelect = within(factoryWorkspace).getByRole(
"combobox",
{ name: "安装方式" },
);
await actor.click(installMethodSelect);
fireEvent.mouseDown(screen.getByRole("option", { name: "拆装" }));
await waitFor(() => expect(installMethodSelect).toHaveTextContent("拆装"));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.skuItems.map((item) => item.installMethod)).toEqual([
"拆装",
]);
expect(
savedProduct?.factoryRows.map((row) => row.installMethod),
).toEqual(["拆装"]);
expect(
savedProduct?.styleDetails.map((detail) => detail.installMethod),
).toEqual(["拆装"]);
expect(
savedProduct?.documentWorkspaceConfirmations?.[
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
],
).toBe(true);
expect(
savedProduct?.documentWorkspaceApprovedAt?.[
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
],
).toBe(basicInfoApprovedAt);
});
});
it("fills the first warehouse selection by SPU and keeps later warehouse changes per SKU", async () => {
const actor = userEvent.setup();

View File

@ -271,6 +271,7 @@ export function useNonIntegratedFormActionControllers({
productFactoryModelAutofillRef: workflowState.productFactoryModelAutofillRef,
productFactoryReopenField: workflowState.productFactoryReopenField,
productFactoryRows: workflowRows.productFactoryRows,
scheduleAutoSave,
setDocumentWorkspaceApprovedAt: documentState.setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations:
documentState.setDocumentWorkspaceConfirmations,
@ -281,6 +282,9 @@ export function useNonIntegratedFormActionControllers({
setSaveMessageType: overlayState.setSaveMessageType,
setSkuItems: skuState.setSkuItems,
setStyleDetails: artifactState.setStyleDetails,
skuBatchChangeTypes: skuState.skuBatchChangeTypes,
skuBatchCount: skuState.skuBatchCount,
skuBatchFlowNos: skuState.skuBatchFlowNos,
supplierCodeByName: directoryOptions.supplierCodeByName,
workflowSkuItems: seriesDerived.workflowSkuItems,
});

View File

@ -98,7 +98,7 @@ describe("useNonIntegratedWorkflowConfirmActionController", () => {
);
});
it("still blocks confirmation when the active batch itself is incomplete", async () => {
it("still blocks confirmation when the active batch itself is missing a required field", async () => {
const params = createControllerParams({
derivedSkuItems: [
createSkuItem({
@ -109,7 +109,7 @@ describe("useNonIntegratedWorkflowConfirmActionController", () => {
}),
createSkuItem({
id: "sku-batch-2",
installMethod: "",
material: "",
}),
],
});
@ -126,7 +126,29 @@ describe("useNonIntegratedWorkflowConfirmActionController", () => {
expect(params.persistEditedProduct).not.toHaveBeenCalled();
expect(params.setSaveMessage).toHaveBeenCalledWith(
"基础信息未填写完整,SKU XG001254 缺少:安装方式。请补全后再确认。",
"基础信息未填写完整,SKU XG001254 缺少:材质。请补全后再确认。",
);
});
it("allows the active batch to confirm without an install method", async () => {
const params = createControllerParams({
derivedSkuItems: [createSkuItem({ installMethod: "" })],
});
jest.spyOn(window, "confirm").mockReturnValue(true);
const { result } = renderHook(() =>
useNonIntegratedWorkflowConfirmActionController(params),
);
await act(async () => {
await result.current.handleConfirmWorkflowNode(
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
);
});
expect(params.persistEditedProduct).toHaveBeenCalledTimes(1);
expect(params.setSaveMessage).not.toHaveBeenCalledWith(
expect.stringContaining("安装方式"),
);
});
});

View File

@ -86,4 +86,13 @@ describe("buildSkuContentRevisionSignature", () => {
buildSignature(createSku()),
);
});
it("keeps install method and set count revision-sensitive for direct basic information edits", () => {
expect(buildSignature(createSku({ installMethod: "整装" }))).not.toBe(
buildSignature(createSku()),
);
expect(buildSignature(createSku({ setCount: "2" }))).not.toBe(
buildSignature(createSku()),
);
});
});

View File

@ -5,11 +5,12 @@ import type {
} from "react";
import type { PersistEditedProductOverrides } from "../../nonIntegratedDetailTypes";
import type {
NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap,
NonIntegratedFactoryRow,
NonIntegratedSkuItem,
import {
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
type NonIntegratedDocumentWorkspaceApprovalMap,
type NonIntegratedDocumentWorkspaceConfirmationMap,
type NonIntegratedFactoryRow,
type NonIntegratedSkuItem,
} from "../../nonIntegratedStorage";
import { updateStyleDetailEntry, type StyleDetailForm } from "../style-assets/styleDetailState";
import {
@ -32,6 +33,7 @@ import {
updateProductFactoryRowsForSku,
} from "./productFactoryRowUpdateHelpers";
import { getProductFactoryTargetSkuIds } from "./productFactoryTargetSkuHelpers";
import { buildSkuContentRevisionSignature } from "../preview/previewState";
type SaveMessageType = "success" | "error";
@ -71,6 +73,7 @@ type UseProductFactoryRowControllerParams = {
>;
productFactoryReopenField: ProductFactoryEditableKey;
productFactoryRows: NonIntegratedFactoryRow[];
scheduleAutoSave: () => void;
setDocumentWorkspaceApprovedAt: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceApprovalMap>
>;
@ -83,6 +86,9 @@ type UseProductFactoryRowControllerParams = {
setSaveMessageType: Dispatch<SetStateAction<SaveMessageType>>;
setSkuItems: Dispatch<SetStateAction<NonIntegratedSkuItem[]>>;
setStyleDetails: Dispatch<SetStateAction<Record<string, StyleDetailForm>>>;
skuBatchChangeTypes: Record<string, string>;
skuBatchCount: number;
skuBatchFlowNos: Record<string, string>;
supplierCodeByName: ReadonlyMap<string, string>;
workflowSkuItems: NonIntegratedSkuItem[];
};
@ -106,6 +112,7 @@ export function useProductFactoryRowController({
productFactoryModelAutofillRef,
productFactoryReopenField,
productFactoryRows,
scheduleAutoSave,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setFactoryRows,
@ -114,9 +121,33 @@ export function useProductFactoryRowController({
setSaveMessageType,
setSkuItems,
setStyleDetails,
skuBatchChangeTypes,
skuBatchCount,
skuBatchFlowNos,
supplierCodeByName,
workflowSkuItems,
}: UseProductFactoryRowControllerParams) {
function setFactoryProjectedSkuItems(
update: (current: NonIntegratedSkuItem[]) => NonIntegratedSkuItem[],
) {
setSkuItems((current) => {
const nextSkuItems = update(current);
if (documentWorkspaceRevisionSignatureRef.current) {
documentWorkspaceRevisionSignatureRef.current = {
...documentWorkspaceRevisionSignatureRef.current,
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]:
buildSkuContentRevisionSignature({
skuItems: nextSkuItems,
skuBatchCount,
skuBatchFlowNos,
skuBatchChangeTypes,
}),
};
}
return nextSkuItems;
});
}
function updateProductFactoryRow(
skuId: string,
key: ProductFactoryEditableKey,
@ -135,81 +166,82 @@ export function useProductFactoryRowController({
);
const supplierName = supplier.name;
setSkuItems((current) => {
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
return current.map((item, index) =>
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
setFactoryProjectedSkuItems((current) =>
current.map((item, index) =>
targetSkuIds.has(item.id)
? applySkuSupplierName(item, index, supplierName)
: item,
);
});
),
);
}
if (key === "setCount") {
setSkuItems((current) => {
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
return current.map((item) =>
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
setFactoryProjectedSkuItems((current) =>
current.map((item) =>
targetSkuIds.has(item.id) ? { ...item, setCount: value } : item,
);
});
),
);
}
if (key === "minimumOrderQuantity") {
setSkuItems((current) => {
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
productFactoryRows,
value,
productFactoryModelAutofillRef.current,
);
return current.map((item) =>
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
productFactoryRows,
value,
productFactoryModelAutofillRef.current,
);
setFactoryProjectedSkuItems((current) =>
current.map((item) =>
targetSkuIds.has(item.id)
? { ...item, minimumOrderQuantity: value }
: item,
);
});
),
);
}
if (key === "installMethod") {
setSkuItems((current) => {
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
const targetStyleKeys = new Set(
workflowSkuItems
.filter((item) => targetSkuIds.has(item.id))
.map((item) => getStyleKeyFromModel(item.model))
.filter(Boolean),
);
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
const targetStyleKeys = new Set(
workflowSkuItems
.filter((item) => targetSkuIds.has(item.id))
.map((item) => getStyleKeyFromModel(item.model))
.filter(Boolean),
);
if (targetStyleKeys.size > 0) {
setStyleDetails((currentDetails) => {
let nextDetails = currentDetails;
targetStyleKeys.forEach((styleKey) => {
nextDetails = updateStyleDetailEntry(nextDetails, styleKey, {
installMethod: value,
});
if (targetStyleKeys.size > 0) {
setStyleDetails((currentDetails) => {
let nextDetails = currentDetails;
targetStyleKeys.forEach((styleKey) => {
nextDetails = updateStyleDetailEntry(nextDetails, styleKey, {
installMethod: value,
});
return nextDetails;
});
}
return nextDetails;
});
}
return current.map((item) =>
setFactoryProjectedSkuItems((current) =>
current.map((item) =>
targetSkuIds.has(item.id) ? { ...item, installMethod: value } : item,
);
});
),
);
scheduleAutoSave();
}
setFactoryRows((current) =>
@ -238,7 +270,7 @@ export function useProductFactoryRowController({
const supplierName = supplier.name;
if (supplierName) {
setSkuItems((current) =>
setFactoryProjectedSkuItems((current) =>
current.map((item, index) =>
item.id === skuId
? applySkuSupplierName(item, index, supplierName)
@ -390,7 +422,7 @@ export function useProductFactoryRowController({
);
if (currentFactoryRow?.confirmed) return;
setSkuItems((current) =>
setFactoryProjectedSkuItems((current) =>
buildSkuItemsAfterSetCountCommit({
allowImportedContentLocked: true,
current,

View File

@ -11,7 +11,6 @@ type BasicInfoRequiredSkuField =
| "versionNo"
| "attributeCode"
| "drawingId"
| "installMethod"
| "executeStandard"
| "nationalStandardCompliant"
| "batchNo";
@ -30,7 +29,6 @@ type BasicInfoConfirmationSku = {
versionNo: string;
attributeCode: string;
drawingId: string;
installMethod: string;
executeStandard: string;
nationalStandardCompliant: string;
batchNo: number;
@ -52,7 +50,6 @@ export const BASIC_INFO_CONFIRMATION_REQUIRED_FIELDS: ReadonlyArray<{
{ key: "versionNo", label: "版本号" },
{ key: "attributeCode", label: "属性代码" },
{ key: "drawingId", label: "图纸ID" },
{ key: "installMethod", label: "安装方式" },
{ key: "executeStandard", label: "执行标准" },
{ key: "nationalStandardCompliant", label: "符合国标" },
{ key: "batchNo", label: "创建批次" },