From e9f83b16ee7ac80bb6192235480fa74a5c59c9db Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Mon, 7 Sep 2026 17:12:59 +0800 Subject: [PATCH] feat(intake): add product factory workbook export and import by sku code the product factory workspace gains export/import template buttons (purchase/merchandiser already hold node download grants): the export brings out the sku code, set count and every factory fillable field, splitting set count 2 rows into their A/B package codes; the import matches rows by sku code, enforces a header layout identical to the exported template, validates supplier names against the supplier directory, warehouses against the warehouse directory, enum fields against system options and number/date formats, collapses or splits A/B packages when the imported set count changes, and rejects confirmed rows - any row error aborts the whole import with per-row line numbers and reasons. Size, color and material columns are reference only. --- .../buildNonIntegratedDetailChrome.test.tsx | 1 + .../buildNonIntegratedDetailChrome.tsx | 12 + .../buildNonIntegratedDetailRenderers.tsx | 1 + ...ntegratedDocumentWorkspaceHeaderActions.ts | 6 + ...useNonIntegratedDetailDomainControllers.ts | 1 + .../useNonIntegratedDetailInputRefs.ts | 2 + ...tegratedSkuReviewActionControllers.test.ts | 11 + ...NonIntegratedSkuReviewActionControllers.ts | 30 ++ .../ProductFactoryHeaderActions.tsx | 45 ++ .../productFactoryWorkbookHelpers.ts | 441 ++++++++++++++++++ .../useProductFactoryWorkbookController.ts | 192 ++++++++ .../DocumentWorkspaceHeaderActionsSlot.tsx | 21 + ...ntegrated-product-factory-workbook.test.ts | 132 ++++++ ...non-integrated-product-factory-workbook.ts | 263 +++++++++++ 14 files changed, 1158 insertions(+) create mode 100644 furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/productFactoryWorkbookHelpers.ts create mode 100644 furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/useProductFactoryWorkbookController.ts create mode 100644 furniture-3d-system/src/lib/non-integrated-product-factory-workbook.test.ts create mode 100644 furniture-3d-system/src/lib/non-integrated-product-factory-workbook.ts diff --git a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.test.tsx index d805d7f0..8fc9583e 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.test.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.test.tsx @@ -37,6 +37,7 @@ describe("buildNonIntegratedDetailChrome", () => { permissionState: {}, productContext: {}, productFactoryActions: {}, + productFactoryWorkbook: {}, productLimitWorkbook: {}, productPricingWorkbook: {}, productPhotoUploadActions: {}, diff --git a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.tsx b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.tsx index b9836bac..6c96d970 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailChrome.tsx @@ -63,6 +63,7 @@ type DetailChromeProps = Required< permissionState: ProductSetupState["permissionState"]; productContext: ProductSetupState["productContext"]; productFactoryActions: FormActionState["productFactoryActions"]; + productFactoryWorkbook: SkuReviewActionState["productFactoryWorkbook"]; productLimitWorkbook: SkuReviewActionState["productLimitWorkbook"]; productPricingWorkbook: SkuReviewActionState["productPricingWorkbook"]; productPhotoUploadActions: FileActionState["productPhotoUploadActions"]; @@ -110,6 +111,7 @@ export function buildNonIntegratedDetailChrome({ permissionState, productContext, productFactoryActions, + productFactoryWorkbook, productLimitWorkbook, productPricingWorkbook, productPhotoUploadActions, @@ -150,6 +152,8 @@ export function buildNonIntegratedDetailChrome({ onDownloadUploadFile: documentDownloads.handleDownloadUploadFile, onDownloadWorkspaceFiles: documentDownloads.handleDownloadWorkspaceFiles, + onExportProductFactoryWorkbook: + productFactoryWorkbook.handleExportProductFactoryWorkbook, onExportProductLimitWorkbook: productLimitWorkbook.handleExportProductLimitWorkbook, onExportProductPricingWorkbook: @@ -161,6 +165,8 @@ export function buildNonIntegratedDetailChrome({ onExportReviewSheet: reviewSheetAssets.handleExportReviewSheet, onFinishedSampleShipmentStatusClick: sampleWorkflowActions.handleFinishedSampleShipmentStatusClick, + onImportProductFactoryWorkbookChange: + productFactoryWorkbook.handleImportProductFactoryWorkbookChange, onImportProductLimitWorkbookChange: productLimitWorkbook.handleImportProductLimitWorkbookChange, onImportProductPricingWorkbookChange: @@ -242,6 +248,8 @@ export function buildNonIntegratedDetailChrome({ isCurrentUserAdmin: permissionState.isCurrentUserAdmin, isCurrentUserPlainReadOnlyViewer: permissionState.isCurrentUserPlainReadOnlyViewer, + isExportingProductFactory: + productFactoryWorkbook.isExportingProductFactory, isExportingProductLimit: productLimitWorkbook.isExportingProductLimit, isExportingProductPricing: @@ -251,6 +259,8 @@ export function buildNonIntegratedDetailChrome({ isExportingReviewSheet: reviewSheetAssets.isExportingReviewSheet, isExportingSampleMeasurement: sampleMeasurementWorkbook.isExportingSampleMeasurement, + isImportingProductFactory: + productFactoryWorkbook.isImportingProductFactory, isImportingProductLimit: productLimitWorkbook.isImportingProductLimit, isImportingProductPricing: @@ -266,6 +276,8 @@ export function buildNonIntegratedDetailChrome({ isReadOnly: editability.isReadOnly, productFactoryReopenField: workflowState.productFactoryReopenField, + productFactoryWorkbookInputRef: + inputRefs.productFactoryWorkbookInputRef, productLimitWorkbookInputRef: inputRefs.productLimitWorkbookInputRef, productPricingWorkbookInputRef: diff --git a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailRenderers.tsx b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailRenderers.tsx index 4bdd7ad4..f8345b58 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailRenderers.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDetailRenderers.tsx @@ -163,6 +163,7 @@ export function buildNonIntegratedDetailRenderers({ permissionState: setup.permissionState, productContext: setup.productContext, productFactoryActions: formActions.productFactoryActions, + productFactoryWorkbook: skuActions.productFactoryWorkbook, productLimitWorkbook: skuActions.productLimitWorkbook, productPricingWorkbook: skuActions.productPricingWorkbook, sampleMeasurementWorkbook: skuActions.sampleMeasurementWorkbook, diff --git a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDocumentWorkspaceHeaderActions.ts b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDocumentWorkspaceHeaderActions.ts index a8648ba4..8b486735 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDocumentWorkspaceHeaderActions.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/buildNonIntegratedDocumentWorkspaceHeaderActions.ts @@ -1,5 +1,6 @@ import type { ComponentProps } from "react"; +import { NON_INTEGRATED_PRODUCT_FACTORY_WORKBOOK_ACCEPT } from "@/lib/non-integrated-product-factory-workbook"; import { NON_INTEGRATED_PRODUCT_LIMIT_WORKBOOK_ACCEPT } from "@/lib/non-integrated-product-limit-workbook"; import { NON_INTEGRATED_PRODUCT_PRICING_WORKBOOK_ACCEPT } from "@/lib/non-integrated-product-pricing-workbook"; import { NON_INTEGRATED_SAMPLE_MEASUREMENT_WORKBOOK_ACCEPT } from "@/lib/non-integrated-sample-measurement-workbook"; @@ -50,11 +51,13 @@ type DirectStateName = | "installationWorkspaceUploadInputRef" | "isCurrentUserAdmin" | "isCurrentUserPlainReadOnlyViewer" + | "isExportingProductFactory" | "isExportingProductLimit" | "isExportingProductPricing" | "isExportingQualityInspection" | "isExportingReviewSheet" | "isExportingSampleMeasurement" + | "isImportingProductFactory" | "isImportingProductLimit" | "isImportingProductPricing" | "isImportingSampleMeasurement" @@ -63,6 +66,7 @@ type DirectStateName = | "isProductPricingNodeLocked" | "isReadOnly" | "productFactoryReopenField" + | "productFactoryWorkbookInputRef" | "productLimitWorkbookInputRef" | "productPricingWorkbookInputRef" | "sampleMeasurementWorkbookInputRef" @@ -156,6 +160,8 @@ export function buildNonIntegratedDocumentWorkspaceHeaderActions({ isProjectReviewWorkspace: presentation.isProjectReviewWorkspace, isReviewWorkspace: presentation.isReviewWorkspace, isSampleMeasurementWorkspace: presentation.isSampleMeasurementWorkspace, + productFactoryWorkbookAccept: + NON_INTEGRATED_PRODUCT_FACTORY_WORKBOOK_ACCEPT, productLimitWorkbookAccept: NON_INTEGRATED_PRODUCT_LIMIT_WORKBOOK_ACCEPT, productPricingWorkbookAccept: diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailDomainControllers.ts b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailDomainControllers.ts index fad875d7..4f145222 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailDomainControllers.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailDomainControllers.ts @@ -239,6 +239,7 @@ export function useNonIntegratedDetailDomainControllers( }); const skuActions = useNonIntegratedSkuReviewActionControllers({ artifactState, + warehouseOptions: directoryOptions.warehouseDirectoryOptions, workflowCompletion: workflow.workflowCompletion, basicInfoController: formActions.basicInfoController, basicInfoDerived: setup.basicInfoDerived, diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailInputRefs.ts b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailInputRefs.ts index bba521fc..e3286fa4 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailInputRefs.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedDetailInputRefs.ts @@ -10,6 +10,7 @@ export function useNonIntegratedDetailInputRefs() { const batchSkuImageInputRef = useRef(null); const skuWorkbookInputRef = useRef(null); const productLimitWorkbookInputRef = useRef(null); + const productFactoryWorkbookInputRef = useRef(null); const productPricingWorkbookInputRef = useRef(null); const sampleMeasurementWorkbookInputRef = useRef( null, @@ -47,6 +48,7 @@ export function useNonIntegratedDetailInputRefs() { pendingProductPhotoCurrentUploadTargetRef, pendingProductPhotoPanelUploadRef, previewFileInputRef, + productFactoryWorkbookInputRef, productLimitWorkbookInputRef, productPhotoCurrentUploadInputRef, productPhotoDetailUploadInputRef, diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.test.ts b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.test.ts index 8fe31696..0c256e9c 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.test.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.test.ts @@ -8,6 +8,7 @@ import { useSkuBatchLifecycleController } from "./workspaces/basic-info/useSkuBa import { useSkuCreationController } from "./workspaces/basic-info/useSkuCreationController"; import { useSkuDisplayState } from "./workspaces/basic-info/useSkuDisplayState"; import { useSkuWorkbookController } from "./workspaces/basic-info/useSkuWorkbookController"; +import { useProductFactoryWorkbookController } from "./workspaces/product-factory/useProductFactoryWorkbookController"; import { useProductLimitWorkbookController } from "./workspaces/product-workflow/useProductLimitWorkbookController"; import { useProductPricingWorkbookController } from "./workspaces/product-workflow/useProductPricingWorkbookController"; import { useSampleMeasurementWorkbookController } from "./workspaces/sample-workflow/useSampleMeasurementWorkbookController"; @@ -23,6 +24,9 @@ jest.mock("./workspaces/basic-info/useSkuBatchLifecycleController"); jest.mock("./workspaces/basic-info/useSkuCreationController"); jest.mock("./workspaces/basic-info/useSkuDisplayState"); jest.mock("./workspaces/basic-info/useSkuWorkbookController"); +jest.mock( + "./workspaces/product-factory/useProductFactoryWorkbookController", +); jest.mock("./workspaces/product-workflow/useProductLimitWorkbookController"); jest.mock( "./workspaces/product-workflow/useProductPricingWorkbookController", @@ -42,6 +46,9 @@ describe("useNonIntegratedSkuReviewActionControllers", () => { aiWhiteBackground: { canUseAiWhiteBackground: true }, skuBatchLifecycle: { handleAddSkuBatch: jest.fn() }, skuWorkbook: { handleExportSkuWorkbook: jest.fn() }, + productFactoryWorkbook: { + handleExportProductFactoryWorkbook: jest.fn(), + }, productLimitWorkbook: { handleExportProductLimitWorkbook: jest.fn() }, productPricingWorkbook: { handleExportProductPricingWorkbook: jest.fn(), @@ -65,6 +72,9 @@ describe("useNonIntegratedSkuReviewActionControllers", () => { .mocked(useSkuBatchLifecycleController) .mockReturnValue(returns.skuBatchLifecycle as never); jest.mocked(useSkuWorkbookController).mockReturnValue(returns.skuWorkbook as never); + jest + .mocked(useProductFactoryWorkbookController) + .mockReturnValue(returns.productFactoryWorkbook as never); jest .mocked(useProductLimitWorkbookController) .mockReturnValue(returns.productLimitWorkbook as never); @@ -150,6 +160,7 @@ describe("useNonIntegratedSkuReviewActionControllers", () => { expect(result.current).toEqual({ aiWhiteBackground: returns.aiWhiteBackground, bomWorkbook: returns.bomWorkbook, + productFactoryWorkbook: returns.productFactoryWorkbook, productLimitWorkbook: returns.productLimitWorkbook, productPricingWorkbook: returns.productPricingWorkbook, sampleMeasurementWorkbook: returns.sampleMeasurementWorkbook, diff --git a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.ts b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.ts index 9d450f8f..0534678c 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/useNonIntegratedSkuReviewActionControllers.ts @@ -8,6 +8,7 @@ import { useSkuBatchLifecycleController } from "./workspaces/basic-info/useSkuBa import { useSkuCreationController } from "./workspaces/basic-info/useSkuCreationController"; import { useSkuDisplayState } from "./workspaces/basic-info/useSkuDisplayState"; import { useSkuWorkbookController } from "./workspaces/basic-info/useSkuWorkbookController"; +import { useProductFactoryWorkbookController } from "./workspaces/product-factory/useProductFactoryWorkbookController"; import { useProductLimitWorkbookController } from "./workspaces/product-workflow/useProductLimitWorkbookController"; import { useProductPricingWorkbookController } from "./workspaces/product-workflow/useProductPricingWorkbookController"; import { useSampleMeasurementWorkbookController } from "./workspaces/sample-workflow/useSampleMeasurementWorkbookController"; @@ -91,6 +92,7 @@ type SkuReviewActionControllerProps = Pick< seriesDerived: ProductSetupState["seriesDerived"]; seriesDraft: ProductSetupState["seriesDraft"]; supplierCodeByName: ReadonlyMap; + warehouseOptions: readonly string[]; skuBatchAction: WorkflowDerivedState["skuBatchAction"]; skuState: ReturnType; spuEstimate: ReturnType; @@ -139,6 +141,7 @@ export function useNonIntegratedSkuReviewActionControllers({ seriesDerived, seriesDraft, supplierCodeByName, + warehouseOptions, skuBatchAction, skuState, spuEstimate, @@ -271,6 +274,32 @@ export function useNonIntegratedSkuReviewActionControllers({ transferProgress.updateGeneratedDownloadProgress, updateImportProgress: transferProgress.updateImportProgress, }); + const productFactoryWorkbook = useProductFactoryWorkbookController({ + allProductFactoryRows: workflowRows.productFactoryRows, + canDownloadWorkspaceFiles: permissionState.canDownloadWorkspaceFiles, + denyWorkspaceDownloadForCurrentUser: + documentDownloads.denyWorkspaceDownloadForCurrentUser, + factoryRows: workflowState.factoryRows, + finishDownloadProgress: transferProgress.finishDownloadProgress, + finishImportProgress: transferProgress.finishImportProgress, + isReadOnly: editability.isReadOnly, + skuItems: skuState.skuItems, + setDownloadProgress: transferProgress.setDownloadProgress, + setFactoryRows: workflowState.setFactoryRows, + setSkuItems: skuState.setSkuItems, + setSaveMessage: overlayState.setSaveMessage, + setSaveMessageType: overlayState.setSaveMessageType, + setUploadProgress: transferProgress.setUploadProgress, + spu: formState.formValues.spu || formState.formValues.productCode, + startGeneratedDownloadProgress: + transferProgress.startGeneratedDownloadProgress, + startImportProgress: transferProgress.startImportProgress, + supplierCodeByName, + updateGeneratedDownloadProgress: + transferProgress.updateGeneratedDownloadProgress, + updateImportProgress: transferProgress.updateImportProgress, + warehouseOptions, + }); const productLimitWorkbook = useProductLimitWorkbookController({ canDownloadWorkspaceFiles: permissionState.canDownloadWorkspaceFiles, denyWorkspaceDownloadForCurrentUser: @@ -493,6 +522,7 @@ export function useNonIntegratedSkuReviewActionControllers({ return { aiWhiteBackground, bomWorkbook, + productFactoryWorkbook, productLimitWorkbook, productPricingWorkbook, qualityInspectionActions, diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/ProductFactoryHeaderActions.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/ProductFactoryHeaderActions.tsx index 694fdde8..4e31d301 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/ProductFactoryHeaderActions.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/product-factory/ProductFactoryHeaderActions.tsx @@ -1,3 +1,4 @@ +import type { ChangeEvent, RefObject } from "react"; import Button from "@/components/ui/Button"; import { PRODUCT_FACTORY_REOPEN_FIELD_OPTIONS, @@ -7,8 +8,15 @@ import { type ProductFactoryHeaderActionsProps = { actionGroupClassName: string; basicInfoIsConfirmed: boolean; + canDownloadWorkspaceFiles: boolean; isAdmin: boolean; + isExportingWorkbook: boolean; + isImportingWorkbook: boolean; isReadOnly: boolean; + workbookAccept: string; + workbookInputRef: RefObject; + onExportWorkbook: () => void; + onImportWorkbookChange: (event: ChangeEvent) => void; rowCount: number; hasPendingRows: boolean; isConfirmed: boolean; @@ -25,8 +33,15 @@ type ProductFactoryHeaderActionsProps = { export function ProductFactoryHeaderActions({ actionGroupClassName, basicInfoIsConfirmed, + canDownloadWorkspaceFiles, isAdmin, + isExportingWorkbook, + isImportingWorkbook, isReadOnly, + workbookAccept, + workbookInputRef, + onExportWorkbook, + onImportWorkbookChange, rowCount, hasPendingRows, isConfirmed, @@ -51,6 +66,36 @@ export function ProductFactoryHeaderActions({ return (
+ {canDownloadWorkspaceFiles ? ( + + ) : null} + {isReadOnly ? null : ( + <> + + + + )} {isAdmin ? (