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: "创建批次" },