diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.factory-sku.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.factory-sku.test.tsx
index 5e978f60..6c8d8e96 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.factory-sku.test.tsx
+++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.factory-sku.test.tsx
@@ -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();
+ 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();
+ 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();
diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedFormActionControllers.ts b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedFormActionControllers.ts
index b52c1a1a..e68adfc9 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedFormActionControllers.ts
+++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedFormActionControllers.ts
@@ -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,
});
diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedWorkflowConfirmActionController.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedWorkflowConfirmActionController.test.tsx
index d13c0703..b0a6c5a4 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedWorkflowConfirmActionController.test.tsx
+++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedWorkflowConfirmActionController.test.tsx
@@ -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("安装方式"),
);
});
});
diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/preview/previewState.test.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/preview/previewState.test.ts
index 66361f89..b8a4c752 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/preview/previewState.test.ts
+++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/preview/previewState.test.ts
@@ -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()),
+ );
+ });
});
diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/useProductFactoryRowController.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/useProductFactoryRowController.ts
index 01dd678e..9420f086 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/useProductFactoryRowController.ts
+++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/useProductFactoryRowController.ts
@@ -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
>;
@@ -83,6 +86,9 @@ type UseProductFactoryRowControllerParams = {
setSaveMessageType: Dispatch>;
setSkuItems: Dispatch>;
setStyleDetails: Dispatch>>;
+ skuBatchChangeTypes: Record;
+ skuBatchCount: number;
+ skuBatchFlowNos: Record;
supplierCodeByName: ReadonlyMap;
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,
diff --git a/furniture-3d-system/src/lib/open-product/basic-info-confirmation-validation.ts b/furniture-3d-system/src/lib/open-product/basic-info-confirmation-validation.ts
index 74e4f426..9dbdd5c0 100644
--- a/furniture-3d-system/src/lib/open-product/basic-info-confirmation-validation.ts
+++ b/furniture-3d-system/src/lib/open-product/basic-info-confirmation-validation.ts
@@ -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: "创建批次" },