diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx index 7d3d0241..264fd877 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx @@ -1920,6 +1920,69 @@ describe("NonIntegratedDetailClient", () => { ).not.toBeInTheDocument(); }); + it("keeps product data approved when adding a SKU in basic info", async () => { + const actor = userEvent.setup(); + const approvedAt = "2026-04-28T06:30:00.000Z"; + + window.localStorage.setItem( + NON_INTEGRATED_STORAGE_KEY, + JSON.stringify([ + createProductRecord({ + id: "DNZ0001-sku-independent-confirmation", + skuItems: [createSkuItem()], + uploadFiles: [ + { + id: "product-data-file-1", + name: "model.pdf", + category: "产品模型", + dataUrl: "data:application/pdf;base64,test-pdf", + mimeType: "application/pdf", + }, + ], + documentWorkspaceConfirmations: { + ...createDefaultDocumentWorkspaceConfirmations(), + [NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true, + 产品模型: true, + }, + documentWorkspaceApprovedAt: { + [NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: approvedAt, + 产品模型: approvedAt, + }, + }), + ]), + ); + + render( + , + ); + + const productDataTrigger = screen.getByTestId( + "document-workspace-trigger-产品模型", + ); + expect(productDataTrigger).toHaveAttribute( + "data-pending-confirmation", + "false", + ); + + await actor.click(screen.getByTestId("document-workspace-basic-info-trigger")); + await actor.click(screen.getByTestId("non-integrated-add-sku")); + + await waitFor(() => { + expect(productDataTrigger).toHaveAttribute( + "data-pending-confirmation", + "false", + ); + expect( + screen.getByTestId("document-workspace-basic-info-trigger"), + ).toHaveAttribute("data-pending-confirmation", "true"); + expect(screen.getByTestId("workflow-approve-current-node")).toBeEnabled(); + }); + }); + it("counts review data as zero until the first round has a filled issue", () => { window.localStorage.setItem( NON_INTEGRATED_STORAGE_KEY, diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx index 0eb65248..4db0810a 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -114,7 +114,6 @@ import { getNonIntegratedFormalSpu, getNonIntegratedProductById, getNonIntegratedProductDisplayName, - hasPendingDocumentWorkspaceConfirmation, normalizeNonIntegratedOpeningReference, normalizeNonIntegratedPatentApplication, normalizeNonIntegratedProjectReview, @@ -935,6 +934,24 @@ const DEFAULT_DOCUMENT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = const PRODUCT_PHOTO_CATEGORY: DocumentWorkspaceCategory = "产品实拍图"; const AUTO_SAVE_DELAY_MS = 500; + +function getChangedWorkflowNodeCategories( + previousSignatures: Record | null | undefined, + currentSignatures: Record, + projectStatus: NonIntegratedProjectStatus, +) { + if (!previousSignatures) return []; + + return NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES.filter((category) => { + const changed = previousSignatures[category] !== currentSignatures[category]; + if (!changed) return false; + + return !( + category === PROJECT_REVIEW_WORKSPACE_CATEGORY && + projectStatus === "approved" + ); + }); +} const PATENT_APPLICATION_STATUS_OPTIONS: NonIntegratedPatentApplication["patentStatus"][] = ["待确认", "已有专利", "申请中", "申请失败", "无专利", "放弃维护"]; const PATENT_APPLICATION_DECISION_OPTIONS: Array<{ @@ -1810,6 +1827,40 @@ function hasPackagingStyleStructuredContent(detail: { ); } +function buildInstallationRevisionSignature( + styleDetails: Record, +) { + return Object.fromEntries( + Object.entries(styleDetails).map(([styleKey, detail]) => [ + styleKey, + { + installMethod: detail.installMethod, + installSteps: detail.installSteps, + }, + ]), + ); +} + +function buildPackagingRevisionSignature( + styleDetails: Record, +) { + return Object.fromEntries( + Object.entries(styleDetails).map(([styleKey, detail]) => [ + styleKey, + { + packageDetail: detail.packageDetail, + craftDetail: detail.craftDetail, + outerPackagingType: detail.outerPackagingType, + innerPackagingItems: detail.innerPackagingItems, + packagingModel3dFileName: detail.packagingModel3dFileName, + packagingModel3dDataUrl: detail.packagingModel3dDataUrl, + packagingModel3dNodeOffsets: detail.packagingModel3dNodeOffsets, + packagingModel3dNodeRotations: detail.packagingModel3dNodeRotations, + }, + ]), + ); +} + function createEmptySkuItem(): NonIntegratedSkuItem { return { id: buildNonIntegratedSkuId(), @@ -7127,8 +7178,7 @@ export default function NonIntegratedDetailClient({ files: filesForCategory(OPENING_REFERENCE_WORKSPACE_CATEGORY), }); signatures[PRODUCT_DATA_WORKSPACE_CATEGORY] = JSON.stringify({ - formValues, - skuItems, + note: documentWorkspaceNotes[PRODUCT_DATA_WORKSPACE_CATEGORY], files: filesForCategory(PRODUCT_DATA_WORKSPACE_CATEGORY), }); signatures["设计卖点"] = JSON.stringify({ @@ -7152,20 +7202,20 @@ export default function NonIntegratedDetailClient({ files: filesForCategory(PRODUCT_PHOTO_CATEGORY), }); signatures[INSTALLATION_WORKSPACE_CATEGORY] = JSON.stringify({ - styleDetails, + styleDetails: buildInstallationRevisionSignature(styleDetails), files: filesForCategory(INSTALLATION_WORKSPACE_CATEGORY), }); signatures[PACKAGING_WORKSPACE_CATEGORY] = JSON.stringify({ - styleDetails, + styleDetails: buildPackagingRevisionSignature(styleDetails), files: filesForCategory(PACKAGING_WORKSPACE_CATEGORY), }); signatures[PRODUCT_FACTORY_WORKSPACE_CATEGORY] = - JSON.stringify(productFactoryRows); + JSON.stringify(factoryRows); signatures[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY] = JSON.stringify( - productSampleMeasurementRows, + sampleMeasurementRows, ); signatures[PRODUCT_PRICING_WORKSPACE_CATEGORY] = - JSON.stringify(productPricingRows); + JSON.stringify(pricingRows); signatures[DETECTION_WORKSPACE_CATEGORY] = JSON.stringify({ files: filesForCategory(DETECTION_WORKSPACE_CATEGORY).filter( (file) => resolveDetectionWorkspaceAssetType(file) === "quality-standard", @@ -7182,16 +7232,16 @@ export default function NonIntegratedDetailClient({ return signatures; }, [ documentWorkspaceNotes, + factoryRows, formValues, openingReference, patentApplication, - productFactoryRows, - productPricingRows, - productSampleMeasurementRows, projectReview, projectStatus, qualityInspectionSheet, + pricingRows, reviewRounds, + sampleMeasurementRows, skuItems, styleDetails, uploadFiles, @@ -7199,6 +7249,21 @@ export default function NonIntegratedDetailClient({ const activeDocumentWorkspaceHasContent = (documentWorkspaceCountByCategory[activeDocumentWorkspaceCategory] ?? 0) > 0; + const dirtyWorkflowNodeCategories = new Set( + getChangedWorkflowNodeCategories( + documentWorkspaceRevisionSignatureRef.current, + documentWorkspaceRevisionSignatures, + projectStatus, + ), + ); + const isWorkflowNodeConfirmed = ( + category: WorkflowNodeCategory, + hasContent: boolean, + ) => + hasContent && + documentWorkspaceConfirmations[category] === true && + Boolean(documentWorkspaceApprovedAt[category]) && + !dirtyWorkflowNodeCategories.has(category); const activeWorkflowNodeCategory: WorkflowNodeCategory = documentWorkspaceView === "detail" ? NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY @@ -7209,16 +7274,15 @@ export default function NonIntegratedDetailClient({ : activeDocumentWorkspaceHasContent; const activeWorkflowNodeApprovedAt = documentWorkspaceApprovedAt[activeWorkflowNodeCategory] ?? ""; - const activeWorkflowNodeIsApproved = - activeWorkflowNodeHasContent && - documentWorkspaceConfirmations[activeWorkflowNodeCategory] === true && - Boolean(activeWorkflowNodeApprovedAt); + const activeWorkflowNodeIsApproved = isWorkflowNodeConfirmed( + activeWorkflowNodeCategory, + activeWorkflowNodeHasContent, + ); const activeWorkflowNodeNeedsApproval = !activeWorkflowNodeIsApproved; - const basicInfoIsApproved = - basicInfoHasContent && - documentWorkspaceConfirmations[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY] === - true && - Boolean(documentWorkspaceApprovedAt[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]); + const basicInfoIsApproved = isWorkflowNodeConfirmed( + NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY, + basicInfoHasContent, + ); const basicInfoDisplayCount = basicInfoIsApproved ? 1 : 0; const basicInfoNeedsConfirmation = !basicInfoIsApproved; const isWorkflowCompleted = false; @@ -7232,18 +7296,10 @@ export default function NonIntegratedDetailClient({ return; } - const changedCategories = NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES.filter( - (category) => { - const changed = - previousSignatures[category] !== - documentWorkspaceRevisionSignatures[category]; - if (!changed) return false; - - return !( - category === PROJECT_REVIEW_WORKSPACE_CATEGORY && - projectStatus === "approved" - ); - }, + const changedCategories = getChangedWorkflowNodeCategories( + previousSignatures, + documentWorkspaceRevisionSignatures, + projectStatus, ); documentWorkspaceRevisionSignatureRef.current = documentWorkspaceRevisionSignatures; @@ -7273,19 +7329,20 @@ export default function NonIntegratedDetailClient({ return changed ? next : current; }); }, [documentWorkspaceRevisionSignatures, isWorkflowCompleted, projectStatus]); - const pendingDocumentWorkspaceCategories = useMemo( - () => - UPLOAD_CATEGORY_PANEL_ORDER.filter( - (category) => !documentWorkspaceConfirmations[category], + const pendingDocumentWorkspaceCategories = UPLOAD_CATEGORY_PANEL_ORDER.filter( + (category) => + !isWorkflowNodeConfirmed( + category, + (documentWorkspaceCountByCategory[category] ?? 0) > 0, ), - [documentWorkspaceConfirmations], ); const hasPendingDocumentWorkspaceCategories = - hasPendingDocumentWorkspaceConfirmation({ - documentWorkspaceConfirmations, - }); + pendingDocumentWorkspaceCategories.length > 0; const activeDocumentWorkspaceNeedsConfirmation = - !documentWorkspaceConfirmations[activeDocumentWorkspaceCategory]; + !isWorkflowNodeConfirmed( + activeDocumentWorkspaceCategory, + activeDocumentWorkspaceHasContent, + ); const activeDocumentWorkspaceCopy = DOCUMENT_WORKSPACE_COPY[activeDocumentWorkspaceCategory]; const detectionAttachmentFiles = useMemo( @@ -10369,10 +10426,10 @@ export default function NonIntegratedDetailClient({ ? basicInfoHasContent : (documentWorkspaceCountByCategory[category as DocumentWorkspaceCategory] ?? 0) > 0; - const isCategoryApproved = - hasCategoryContent && - documentWorkspaceConfirmations[category] === true && - Boolean(documentWorkspaceApprovedAt[category]); + const isCategoryApproved = isWorkflowNodeConfirmed( + category, + hasCategoryContent, + ); if (isReadOnly || isCategoryApproved) return; if (!hasCategoryContent) { @@ -10560,7 +10617,33 @@ export default function NonIntegratedDetailClient({ } function renderDocumentWorkspaceConfirmationAction() { - return null; + const category = activeDocumentWorkspaceCategory; + const hasContent = activeDocumentWorkspaceHasContent; + const isApproved = isWorkflowNodeConfirmed(category, hasContent); + const label = getDocumentWorkspaceCategoryLabel(category); + + if (isApproved) { + return ( + + 已确认 + + ); + } + + return ( + void handleConfirmDocumentWorkspaceCategory(category)} + > + 确认{label} + + ); } async function handleApproveCurrentWorkflowNode() { @@ -10626,7 +10709,7 @@ export default function NonIntegratedDetailClient({ : "无资料"; return ( - + {isReadOnly ? null : ( - + 新增SKU )} @@ -18905,12 +18988,10 @@ function renderFactoryQualityWorkspace() { documentWorkspaceCountByCategory[category] ?? files.length; const hasFiles = fileCount > 0; - const needsConfirmation = - !( - hasFiles && - documentWorkspaceConfirmations[category] === true && - Boolean(documentWorkspaceApprovedAt[category]) - ); + const needsConfirmation = !isWorkflowNodeConfirmed( + category, + hasFiles, + ); const shouldInsertBasicInfoTrigger = index === BASIC_INFO_INSERT_INDEX;