diff --git a/furniture-3d-system/src/app/intake/IntakeClient.tsx b/furniture-3d-system/src/app/intake/IntakeClient.tsx index 802ebf35..2c00225e 100644 --- a/furniture-3d-system/src/app/intake/IntakeClient.tsx +++ b/furniture-3d-system/src/app/intake/IntakeClient.tsx @@ -501,6 +501,7 @@ const NON_INTEGRATED_NODE_OWNER_LABELS: Partial< 包装细节: "结构设计师", 产品定厂: "采购负责人", 样包测量: "采购/核价组", + 成品寄样: "品牌/采购", 产品核价: "采购/核价组", 产品限价: "企划负责人", 工厂质检: "质检组", @@ -1910,6 +1911,16 @@ function getPopulatedSampleMeasurementRowCountForList( ).length; } +function getPopulatedFinishedSampleShipmentRowCountForList( + product: NonIntegratedProductRecord, +) { + return (product.finishedSampleShipmentRows ?? []).filter((row) => + [row.sampleQuantity, row.shipped, row.trackingNumber].some( + (value) => value.trim().length > 0, + ), + ).length; +} + function getPopulatedProductPricingRowCountForList( product: NonIntegratedProductRecord, ) { @@ -2046,6 +2057,9 @@ function getDocumentWorkspaceCountByCategory( 样包测量: uploadFileCountByCategory["样包测量"] + getPopulatedSampleMeasurementRowCountForList(product), + 成品寄样: + uploadFileCountByCategory["成品寄样"] + + getPopulatedFinishedSampleShipmentRowCountForList(product), 产品核价: uploadFileCountByCategory["产品核价"] + getPopulatedProductPricingRowCountForList(product), @@ -2170,9 +2184,14 @@ function getCurrentNodeOwnerCandidates( } if ( - ["产品定厂", "样包测量", "产品核价", "工厂质检", "检测报告"].includes( - category, - ) + [ + "产品定厂", + "样包测量", + "成品寄样", + "产品核价", + "工厂质检", + "检测报告", + ].includes(category) ) { return getUniqueNonEmptyValues([ product.purchaser, 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 810ad9a3..6dcd2afb 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 @@ -328,6 +328,7 @@ function createProductRecord( planningLayoutRows: [], productLimitRows: [], sampleMeasurementRows: [], + finishedSampleShipmentRows: [], uploadFiles: [], documentWorkspaceNotes: {}, documentWorkspaceConfirmations: @@ -1554,6 +1555,7 @@ describe("NonIntegratedDetailClient", () => { "document-workspace-trigger-包装细节", "document-workspace-trigger-产品定厂", "document-workspace-trigger-样包测量", + "document-workspace-trigger-成品寄样", "document-workspace-trigger-产品核价", "document-workspace-trigger-产品限价", "document-workspace-trigger-工厂质检", @@ -1635,6 +1637,7 @@ describe("NonIntegratedDetailClient", () => { "document-workspace-trigger-包装细节", "document-workspace-trigger-产品定厂", "document-workspace-trigger-样包测量", + "document-workspace-trigger-成品寄样", "document-workspace-trigger-产品核价", "document-workspace-trigger-产品限价", "document-workspace-trigger-工厂质检", @@ -1771,13 +1774,9 @@ describe("NonIntegratedDetailClient", () => { expect(screen.getByRole("button", { name: "删除" })).toBeInTheDocument(); await actor.click(screen.getByLabelText("二级类目")); - expect( - within(openingReferenceWorkspace).getByRole("listbox"), - ).toHaveTextContent("桌类"); + expect(screen.getByRole("listbox")).toHaveTextContent("桌类"); await actor.click(screen.getByLabelText("商品类目")); - const productCategoryOptions = within( - openingReferenceWorkspace, - ).getByRole("listbox"); + const productCategoryOptions = screen.getByRole("listbox"); expect(productCategoryOptions).toHaveTextContent("书桌"); expect(productCategoryOptions).not.toHaveTextContent("鞋柜"); @@ -2467,6 +2466,248 @@ describe("NonIntegratedDetailClient", () => { expect(screen.queryByDisplayValue("ZWJ021801N396V01")).not.toBeInTheDocument(); }); + it("recalculates imported SKU barcodes when product factory supplier changes", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "ZWJ0218-imported-factory-supplier", + spu: "ZWJ0218", + skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT, + skuItems: [ + createSkuItem({ + id: "sku-imported-factory-supplier-1", + skuSequence: 1, + model: "ZWJ0218K01", + skuCode: "ZWJ021801", + barcode: "ZWJ021801N396V01", + manufacturer: "曹县嘉欢", + manufacturerCode: "N396", + source: "sku_workbook", + sourceTable: "sku_workbook", + contentLocked: true, + }), + ], + factoryRows: [ + { + skuId: "sku-factory-editable-fields-1", + confirmed: false, + model: "ZWJ0218K01", + skuCode: "ZWJ021801", + setCount: "9", + barcode: "ZWJ021801N396V01", + specName: "旧定厂暂存规格", + size: "", + color: "", + material: "", + installMethod: "拆装", + supplierName: "旧定厂暂存供应商", + supplierCode: "OLD001", + warehouse: "", + minimumOrderQuantity: "", + leadTime: "", + assemblyMode: "", + productionType: "", + installationService: "", + supplierCategory: "", + purchaseOrderMethod: "", + executeStandard: "", + }, + ], + }), + ); + + render( + , + ); + + await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂")); + + const factoryWorkspace = screen.getByTestId("product-factory-workspace"); + const supplierInput = within(factoryWorkspace).getByRole("combobox", { + name: "供应商名称", + }); + await actor.clear(supplierInput); + await actor.type(supplierInput, "兰考华兰家具"); + await actor.tab(); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "ZWJ0218-imported-factory-supplier", + ); + + expect(savedProduct?.skuItems[0]?.manufacturer).toBe("兰考华兰家具"); + expect(savedProduct?.skuItems[0]?.manufacturerCode).toBe("N1280"); + expect(savedProduct?.skuItems[0]?.barcode).toBe("ZWJ021801N1280V01"); + expect(savedProduct?.factoryRows[0]?.supplierName).toBe("兰考华兰家具"); + expect(savedProduct?.factoryRows[0]?.supplierCode).toBe("N1280"); + expect(savedProduct?.factoryRows[0]?.barcode).toBe("ZWJ021801N1280V01"); + }); + }); + + it("lets product factory maintain set count and install method after SKU confirmation", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "ZWJ0218-factory-editable-fields", + spu: "ZWJ0218", + skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT, + skuItems: [ + createSkuItem({ + id: "sku-factory-editable-fields-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("请输入套件量"); + expect(setCountInput).not.toHaveAttribute("readonly"); + expect(setCountInput).toHaveValue("1"); + fireEvent.change(setCountInput, { target: { value: "3" } }); + fireEvent.blur(setCountInput); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "ZWJ0218-factory-editable-fields", + ); + + expect(savedProduct?.skuItems.map((item) => item.setCount)).toEqual([ + "3", + "3", + "3", + ]); + expect(savedProduct?.skuItems.map((item) => item.skuCode)).toEqual([ + "ZWJ021801A", + "ZWJ021801B", + "ZWJ021801C", + ]); + expect(savedProduct?.factoryRows.map((row) => row.skuCode)).toEqual([ + "ZWJ021801A", + "ZWJ021801B", + "ZWJ021801C", + ]); + expect(savedProduct?.factoryRows[0]?.specName).toContain("A包"); + }); + + const supplierInputs = within(factoryWorkspace).getAllByRole("combobox", { + name: "供应商名称", + }); + expect(supplierInputs).toHaveLength(3); + expect(supplierInputs[1]).not.toHaveAttribute("readonly"); + fireEvent.change(supplierInputs[1]!, { + target: { value: "兰考华兰家具" }, + }); + + const warehouseInputs = within(factoryWorkspace).getAllByLabelText("仓库"); + expect(warehouseInputs).toHaveLength(3); + expect(warehouseInputs[2]).not.toHaveAttribute("readonly"); + fireEvent.change(warehouseInputs[2]!, { target: { value: "郑州仓" } }); + + const minimumOrderQuantityInputs = + within(factoryWorkspace).getAllByPlaceholderText("请输入最低起订量"); + expect(minimumOrderQuantityInputs).toHaveLength(3); + expect(minimumOrderQuantityInputs[1]).not.toHaveAttribute("readonly"); + fireEvent.change(minimumOrderQuantityInputs[1]!, { + target: { value: "50" }, + }); + + const leadTimeInputs = within(factoryWorkspace).getAllByLabelText("交期"); + expect(leadTimeInputs).toHaveLength(3); + expect(leadTimeInputs[2]).not.toHaveAttribute("readonly"); + fireEvent.change(leadTimeInputs[2]!, { + target: { value: "2026-06-20" }, + }); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "ZWJ0218-factory-editable-fields", + ); + + expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([ + "兰考华兰家具", + "兰考华兰家具", + "兰考华兰家具", + ]); + expect(savedProduct?.skuItems.map((item) => item.manufacturerCode)).toEqual([ + "N1280", + "N1280", + "N1280", + ]); + expect(savedProduct?.factoryRows.map((row) => row.supplierName)).toEqual([ + "兰考华兰家具", + "兰考华兰家具", + "兰考华兰家具", + ]); + expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([ + "郑州仓", + "郑州仓", + "郑州仓", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity), + ).toEqual(["50", "50", "50"]); + expect(savedProduct?.factoryRows.map((row) => row.leadTime)).toEqual([ + "2026年06月20日", + "2026年06月20日", + "2026年06月20日", + ]); + }); + + const installMethodSelect = within(factoryWorkspace).getAllByRole("combobox", { + name: "安装方式", + })[0]; + expect(installMethodSelect).not.toBeDisabled(); + expect(installMethodSelect).toHaveTextContent("整装"); + await actor.click(installMethodSelect); + const installMethodOptions = screen.getByRole("listbox"); + expect(installMethodOptions.parentElement).toBe(document.body); + expect(installMethodOptions).toHaveStyle({ position: "fixed" }); + await actor.click(screen.getByRole("option", { name: "拆装" })); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "ZWJ0218-factory-editable-fields", + ); + + expect(savedProduct?.skuItems.map((item) => item.installMethod)).toEqual([ + "拆装", + "拆装", + "拆装", + ]); + expect(savedProduct?.factoryRows.map((row) => row.installMethod)).toEqual([ + "拆装", + "拆装", + "拆装", + ]); + }); + }); + it("persists the SKU national standard compliance selection", async () => { const actor = userEvent.setup(); @@ -2549,14 +2790,14 @@ describe("NonIntegratedDetailClient", () => { }); }); - it("requires complete SKU info before showing the SKU confirmation action", async () => { + it("removes SKU content confirmation actions and keeps SKU fields editable before factory confirmation", async () => { const actor = userEvent.setup(); window.localStorage.setItem( NON_INTEGRATED_STORAGE_KEY, JSON.stringify([ createProductRecord({ - id: "DNZ0001-sku-confirmation-incomplete", + id: "DNZ0001-sku-confirmation-removed", skuItems: [ createSkuItem({ boardMaterialType: "", @@ -2577,7 +2818,7 @@ describe("NonIntegratedDetailClient", () => { render( , @@ -2588,17 +2829,23 @@ describe("NonIntegratedDetailClient", () => { expect( screen.queryByTestId("non-integrated-confirm-sku-content"), ).not.toBeInTheDocument(); - - await actor.click(screen.getByTestId("non-integrated-check-sku-content")); - expect( - screen.getByText(/SKU信息未填写完整.*第1行缺少:板材类型/), - ).toBeInTheDocument(); + screen.queryByTestId("non-integrated-check-sku-content"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("non-integrated-reopen-sku-content"), + ).not.toBeInTheDocument(); + + const skuRow = screen.getByDisplayValue("SZ0230K01").closest("tr"); + expect(skuRow).not.toBeNull(); + expect( + within(skuRow!).getByRole("combobox", { name: "SZ0230K01 板材类型" }), + ).toBeEnabled(); }); - it("locks only the confirmed SKU batch after a second confirmation", async () => { + it("locks SKU content after product factory confirmation and unlocks after reopening factory", async () => { const actor = userEvent.setup(); - const productId = "DNZ0001-sku-confirmation-complete"; + const productId = "DNZ0001-sku-lock-by-factory"; saveNonIntegratedProduct( createProductRecord({ @@ -2606,6 +2853,7 @@ describe("NonIntegratedDetailClient", () => { spu: "DNZ0001", skuItems: [ createSkuItem({ + id: "sku-lock-by-factory-1", boardMaterialType: "ENF级", executeStandard: "GB/T 3324", nationalStandardCompliant: "是", @@ -2617,6 +2865,32 @@ describe("NonIntegratedDetailClient", () => { skuBatchCount: 1, skuBatchChangeTypes: { 1: "新品" }, skuBatchFlowNos: { 1: "DD20260519001" }, + factoryRows: [ + { + skuId: "sku-lock-by-factory-1", + confirmed: true, + model: "SZ0230K01", + skuCode: "DNZ000101", + setCount: "1", + barcode: "DNZ000101GYS001V01", + specName: "款式一 140/60/75", + size: "140/60/75", + color: "胡桃色", + material: "橡胶木", + installMethod: "拆装", + supplierName: "测试供应商", + supplierCode: "GYS001", + warehouse: "郑州仓", + minimumOrderQuantity: "50", + leadTime: "30天", + assemblyMode: "", + productionType: "常规生产", + installationService: "无", + supplierCategory: "主供", + purchaseOrderMethod: "系统下单", + executeStandard: "GB/T 3324", + }, + ], }), ); @@ -2631,197 +2905,29 @@ describe("NonIntegratedDetailClient", () => { await actor.click(screen.getByTestId("document-workspace-basic-info-trigger")); - const confirmButton = screen.getByTestId( - "non-integrated-confirm-sku-content", - ); - expect(confirmButton).toHaveTextContent("确认SKU信息"); - - await actor.click(confirmButton); - - expect( - screen.getByText( - /批次1 SKU 信息已检查完整.*再次点击“确认SKU信息”后/, - ), - ).toBeInTheDocument(); - await waitFor(() => { - expect( - screen.getByTestId("non-integrated-confirm-sku-content"), - ).toHaveTextContent(/^再次确认SKU信息$/); - }); - const secondConfirmButton = screen.getByTestId( - "non-integrated-confirm-sku-content", - ); - - await actor.click(secondConfirmButton); - - await waitFor(() => { - const savedProduct = getNonIntegratedProductById(productId); - expect(savedProduct?.skuBatchConfirmedAt?.["1"]).toEqual( - expect.any(String), - ); - }); - - expect(await screen.findByText("已确认SKU信息")).toBeInTheDocument(); - expect( - screen.queryByTestId("non-integrated-confirm-sku-content"), - ).not.toBeInTheDocument(); + expect(screen.getByDisplayValue("SZ0230K01")).toHaveAttribute("readonly"); expect( screen.queryByTestId("non-integrated-add-sku"), ).not.toBeInTheDocument(); expect(screen.queryByText("上传资料")).not.toBeInTheDocument(); - expect(screen.getByText("下载资料")).toBeInTheDocument(); - expect(screen.getByText("上传图片")).toBeInTheDocument(); - expect(screen.getByText("新增批次")).toBeInTheDocument(); - }); + expect(screen.queryByText("上传图片")).not.toBeInTheDocument(); - it("hides the SKU confirmation prompt after five seconds", async () => { - jest.useFakeTimers(); - const actor = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); - - try { - saveNonIntegratedProduct( - createProductRecord({ - id: "DNZ0001-sku-confirmation-prompt-timeout", - skuItems: [ - createSkuItem({ - boardMaterialType: "", - executeStandard: "GB/T 3324", - nationalStandardCompliant: "是", - manufacturer: "测试供应商", - manufacturerCode: "GYS001", - salesStatus: "销售", - }), - ], - skuBatchCount: 1, - skuBatchChangeTypes: { 1: "新品" }, - skuBatchFlowNos: { 1: "DD20260519001" }, - }), - ); - - render( - , - ); - - await actor.click( - screen.getByTestId("document-workspace-basic-info-trigger"), - ); - await actor.click(screen.getByTestId("non-integrated-check-sku-content")); - - expect( - screen.getByText(/SKU信息未填写完整.*第1行缺少:板材类型/), - ).toBeInTheDocument(); - - act(() => { - jest.advanceTimersByTime(5000); - }); - - expect( - screen.queryByText(/SKU信息未填写完整.*第1行缺少:板材类型/), - ).not.toBeInTheDocument(); - } finally { - jest.useRealTimers(); - } - }); - - it("lets admins reopen confirmed SKU content before sample measurement is approved", async () => { - const actor = userEvent.setup(); - const productId = "DNZ0001-sku-reopen-before-measurement"; - - saveNonIntegratedProduct( - createProductRecord({ - id: productId, - spu: "DNZ0001", - skuItems: [ - createSkuItem({ - boardMaterialType: "ENF级", - executeStandard: "GB/T 3324", - nationalStandardCompliant: "是", - manufacturer: "测试供应商", - manufacturerCode: "GYS001", - salesStatus: "销售", - }), - ], - skuBatchCount: 1, - skuBatchChangeTypes: { 1: "新品" }, - skuBatchFlowNos: { 1: "DD20260519001" }, - skuBatchConfirmedAt: { 1: "2026-05-19T10:00:00.000Z" }, - }), - ); - - render( - , - ); - - await actor.click(screen.getByTestId("document-workspace-basic-info-trigger")); - - expect(screen.getByTestId("non-integrated-reopen-sku-content")).toBeEnabled(); - - await actor.click(screen.getByTestId("non-integrated-reopen-sku-content")); + await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂")); + await actor.click(screen.getByTestId("product-factory-reopen-confirmation")); await waitFor(() => { const savedProduct = getNonIntegratedProductById(productId); - expect(savedProduct?.skuBatchConfirmedAt?.["1"]).toBeUndefined(); - expect(savedProduct?.skuItems[0]?.model).toBe("SZ0230K01"); + expect(savedProduct?.factoryRows?.[0]?.confirmed).toBe(false); }); - expect( - screen.getByTestId("non-integrated-confirm-sku-content"), - ).toHaveTextContent("确认SKU信息"); - }); - - it("prevents admins from reopening SKU content after sample measurement is approved", async () => { - const actor = userEvent.setup(); - - saveNonIntegratedProduct( - createProductRecord({ - id: "DNZ0001-sku-reopen-after-measurement", - spu: "DNZ0001", - skuItems: [ - createSkuItem({ - boardMaterialType: "ENF级", - executeStandard: "GB/T 3324", - nationalStandardCompliant: "是", - manufacturer: "测试供应商", - manufacturerCode: "GYS001", - salesStatus: "销售", - }), - ], - skuBatchCount: 1, - skuBatchChangeTypes: { 1: "新品" }, - skuBatchFlowNos: { 1: "DD20260519001" }, - skuBatchConfirmedAt: { 1: "2026-05-19T10:00:00.000Z" }, - documentWorkspaceConfirmations: { - ...createDefaultDocumentWorkspaceConfirmations(), - 样包测量: true, - }, - documentWorkspaceApprovedAt: { - 样包测量: "2026-05-19T11:00:00.000Z", - }, - }), - ); - - render( - , - ); - await actor.click(screen.getByTestId("document-workspace-basic-info-trigger")); - expect(screen.getByTestId("non-integrated-reopen-sku-content")).toBeDisabled(); + expect(screen.getByDisplayValue("SZ0230K01")).not.toHaveAttribute( + "readonly", + ); + expect(screen.getByTestId("non-integrated-add-sku")).toBeInTheDocument(); + expect(screen.getByText("上传资料")).toBeInTheDocument(); + expect(screen.getByText("上传图片")).toBeInTheDocument(); }); it("hides redundant fields from the product detail summary", async () => { @@ -3040,6 +3146,20 @@ describe("NonIntegratedDetailClient", () => { expect(screen.getByText("SZ023001")).toBeInTheDocument(); expect(screen.getByText("SZ023001N1280V01")).toBeInTheDocument(); expect(screen.getByText("兰考华兰家具")).toBeInTheDocument(); + const specNameCell = screen.getByTestId( + "sample-measurement-readonly-specName-sku-sample-1", + ); + expect(specNameCell).toHaveClass( + "whitespace-nowrap", + "resize-x", + "overflow-hidden", + "text-ellipsis", + ); + expect(specNameCell).toHaveStyle({ + minWidth: "180px", + width: "180px", + }); + expect(specNameCell).toHaveAttribute("title"); await actor.type( screen.getByTestId("sample-measurement-lengthCm-sku-sample-1"), @@ -3100,6 +3220,224 @@ describe("NonIntegratedDetailClient", () => { ).toHaveAttribute("readonly"); }); + it("lets link users edit finished sample quantities only", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "SZ0230-finished-sample-link-user", + spu: "SZ0230", + skuItems: [ + createSkuItem({ + id: "sku-finished-link-1", + model: "SZ0230K01", + skuCode: "SZ023001", + barcode: "SZ023001N1280V01", + setCount: "1", + specName: "款式一 140/60/75", + manufacturer: "兰考华兰家具", + installMethod: "拆装", + }), + ], + }), + ); + + render( + , + ); + + const panel = screen.getByTestId("document-workspace-panel"); + expect(screen.getByTestId("finished-sample-shipment-workspace")).toBeInTheDocument(); + expect(panel).toHaveTextContent("图片"); + expect(panel).toHaveTextContent("规格码"); + expect(panel).toHaveTextContent("供应商名称"); + expect(panel).toHaveTextContent("安装方式"); + + const quantityInput = screen.getByTestId( + "finished-sample-shipment-sampleQuantity-sku-finished-link-1", + ); + expect(quantityInput).not.toHaveAttribute("readonly"); + expect( + screen.getByTestId("finished-sample-shipment-shipped-sku-finished-link-1"), + ).toBeDisabled(); + expect( + screen.getByTestId( + "finished-sample-shipment-trackingNumber-sku-finished-link-1", + ), + ).toHaveAttribute("readonly"); + expect(screen.getByTestId("finished-sample-shipment-confirm")).toBeDisabled(); + + await actor.type(quantityInput, "2"); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-finished-sample-link-user", + ); + expect(savedProduct?.finishedSampleShipmentRows).toEqual([ + { + skuId: "sku-finished-link-1", + sampleQuantity: "2", + shipped: "", + trackingNumber: "", + }, + ]); + }); + }); + + it("lets purchasing users fill and confirm finished sample shipments", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "SZ0230-finished-sample-purchase", + spu: "SZ0230", + purchaser: "采购同事", + purchaseOwner: "采购同事", + skuItems: [ + createSkuItem({ + id: "sku-finished-purchase-1", + model: "SZ0230K01", + skuCode: "SZ023001", + barcode: "SZ023001N1280V01", + setCount: "1", + specName: "款式一 140/60/75", + manufacturer: "兰考华兰家具", + installMethod: "拆装", + }), + ], + }), + ); + + render( + 采购部"]} + />, + ); + + await actor.type( + screen.getByTestId( + "finished-sample-shipment-sampleQuantity-sku-finished-purchase-1", + ), + "3", + ); + await selectInlineOption( + actor, + screen.getByTestId( + "finished-sample-shipment-shipped-sku-finished-purchase-1", + ), + "是", + ); + await actor.type( + screen.getByTestId( + "finished-sample-shipment-trackingNumber-sku-finished-purchase-1", + ), + "SF123456789", + ); + + const confirmButton = screen.getByTestId("finished-sample-shipment-confirm"); + expect(confirmButton).toBeEnabled(); + + await actor.click(confirmButton); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-finished-sample-purchase", + ); + expect(savedProduct?.finishedSampleShipmentRows).toEqual([ + { + skuId: "sku-finished-purchase-1", + sampleQuantity: "3", + shipped: "是", + trackingNumber: "SF123456789", + }, + ]); + expect(savedProduct?.documentWorkspaceConfirmations?.成品寄样).toBe(true); + expect(savedProduct?.documentWorkspaceApprovedAt?.成品寄样).toEqual( + expect.any(String), + ); + }); + }); + + it("lets admins fill finished sample shipment shipping fields", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "SZ0230-finished-sample-admin", + spu: "SZ0230", + skuItems: [ + createSkuItem({ + id: "sku-finished-admin-1", + model: "SZ0230K01", + skuCode: "SZ023001", + barcode: "SZ023001N1280V01", + manufacturer: "兰考华兰家具", + installMethod: "拆装", + }), + ], + }), + ); + + render( + , + ); + + expect( + screen.getByTestId("finished-sample-shipment-shipped-sku-finished-admin-1"), + ).toBeEnabled(); + expect( + screen.getByTestId( + "finished-sample-shipment-trackingNumber-sku-finished-admin-1", + ), + ).not.toHaveAttribute("readonly"); + + await actor.type( + screen.getByTestId( + "finished-sample-shipment-sampleQuantity-sku-finished-admin-1", + ), + "1", + ); + await selectInlineOption( + actor, + screen.getByTestId("finished-sample-shipment-shipped-sku-finished-admin-1"), + "是", + ); + await actor.type( + screen.getByTestId( + "finished-sample-shipment-trackingNumber-sku-finished-admin-1", + ), + "SFADMIN001", + ); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-finished-sample-admin", + ); + expect(savedProduct?.finishedSampleShipmentRows?.[0]).toMatchObject({ + sampleQuantity: "1", + shipped: "是", + trackingNumber: "SFADMIN001", + }); + }); + }); + it("lets purchasing users edit only purchasing document nodes", async () => { const actor = userEvent.setup(); const openingReferenceCategory = "开品参考"; @@ -3624,6 +3962,41 @@ describe("NonIntegratedDetailClient", () => { }); }); + it("shows the product factory reopen action disabled for admins while factory rows are pending", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "DNZ0001-product-factory-reopen-pending", + skuItems: [ + createSkuItem({ + id: "sku-factory-reopen-pending-1", + barcode: "SZ023001N1280V01", + manufacturer: "兰考华兰家具", + manufacturerCode: "N1280", + installMethod: "拆装", + executeStandard: "GB/T 3324", + }), + ], + }), + ); + + render( + , + ); + + await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂")); + + expect( + screen.getByTestId("product-factory-reopen-confirmation"), + ).toBeDisabled(); + }); + it("prevents admins from reopening product factory after sample measurement is approved", async () => { const actor = userEvent.setup(); @@ -3693,13 +4066,12 @@ describe("NonIntegratedDetailClient", () => { ).toBeDisabled(); }); - it("renders planning layout rows from SKU data without SKU edit actions", async () => { + it("renders planning layout rows from SKU data without SKU confirmation or sales-status hiding", async () => { const actor = userEvent.setup(); saveNonIntegratedProduct( createProductRecord({ id: "DNZ0001-planning-layout", - skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT, skuItems: [ createSkuItem({ id: "sku-planning-layout-1", @@ -3741,8 +4113,8 @@ describe("NonIntegratedDetailClient", () => { const planningWorkspace = screen.getByTestId("planning-layout-workspace"); expect(planningWorkspace).toHaveTextContent("SZ0230K01"); expect(planningWorkspace).toHaveTextContent("SZ023001N1280V01"); - expect(planningWorkspace).not.toHaveTextContent("SZ0230K02"); - expect(planningWorkspace).not.toHaveTextContent("SZ023002N1280V01"); + expect(planningWorkspace).toHaveTextContent("SZ0230K02"); + expect(planningWorkspace).toHaveTextContent("SZ023002N1280V01"); expect(planningWorkspace).toHaveTextContent("GB/T3324-2024"); expect(planningWorkspace).toHaveTextContent("是"); expect( 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 fbb75a92..deb53835 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -21,6 +21,7 @@ import { useRef, useState, } from "react"; +import { createPortal } from "react-dom"; import { ArrowLeft, Ban, ChevronDown, Plus, Search, X } from "lucide-react"; import CameraScannerModal from "@/components/mobile/CameraScannerModal"; import { ModelPreviewPanel } from "@/components/platform/ModelPreviewPanel"; @@ -186,6 +187,7 @@ import { type NonIntegratedDocumentWorkspaceConfirmationMap, type NonIntegratedDocumentWorkspaceApprovalMap, type NonIntegratedFactoryRow, + type NonIntegratedFinishedSampleShipmentRow, type NonIntegratedFirstQualityRow, type NonIntegratedOpeningReference, type NonIntegratedPatentApplication, @@ -240,6 +242,7 @@ import { canCreateNonIntegratedProductFromIntake, canEditNonIntegratedProductIdentity, canEditNonIntegratedWorkspace, + isCurrentUserNonIntegratedPurchaseOwner, isIntakeNonIntegratedFullEditor, type IntakeNonIntegratedPermissionUser, } from "@/lib/non-integrated-permissions"; @@ -279,6 +282,25 @@ type SkuColumnWidthKey = | "salesStatus" | "batchNo" | "dingTalkFlowNo"; +type ProductFactoryEditableKey = Exclude< + keyof NonIntegratedFactoryRow, + "skuId" | "confirmed" +>; +type FinishedSampleShipmentEditableKey = Exclude< + keyof NonIntegratedFinishedSampleShipmentRow, + "skuId" +>; +type FinishedSampleShipmentColumnWidthKey = + | "model" + | "skuCode" + | "setCount" + | "supplierName" + | "barcode" + | "specName" + | "installMethod" + | "sampleQuantity" + | "shipped" + | "trackingNumber"; type ProductFactoryTabKey = "factoryInfo" | "skuInfo"; type UploadCategoryKey = | "未分类" @@ -295,6 +317,7 @@ type UploadCategoryKey = | "包装细节" | "产品定厂" | "样包测量" + | "成品寄样" | "产品核价" | "产品限价" | "工厂质检"; @@ -332,6 +355,47 @@ const SKU_COLUMN_DEFAULT_WIDTHS: Record = { batchNo: 64, dingTalkFlowNo: 120, }; +const FINISHED_SAMPLE_SHIPMENT_COLUMN_DEFAULT_WIDTHS: Record< + FinishedSampleShipmentColumnWidthKey, + number +> = { + model: SKU_COLUMN_DEFAULT_WIDTHS.model, + skuCode: SKU_COLUMN_DEFAULT_WIDTHS.skuCode, + setCount: SKU_COLUMN_DEFAULT_WIDTHS.setCount, + supplierName: SKU_COLUMN_DEFAULT_WIDTHS.supplierName, + barcode: SKU_COLUMN_DEFAULT_WIDTHS.barcode, + specName: 180, + installMethod: SKU_COLUMN_DEFAULT_WIDTHS.installMethod, + sampleQuantity: 120, + shipped: 96, + trackingNumber: 150, +}; +const PACKAGE_SHARED_PRODUCT_FACTORY_KEYS = new Set([ + "setCount", + "supplierName", + "supplierCode", + "warehouse", + "minimumOrderQuantity", + "leadTime", + "assemblyMode", + "productionType", + "installationService", + "supplierCategory", + "purchaseOrderMethod", +]); +const PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS = + new Set([ + "supplierName", + "supplierCode", + "warehouse", + "minimumOrderQuantity", + "leadTime", + "assemblyMode", + "productionType", + "installationService", + "supplierCategory", + "purchaseOrderMethod", + ]); const SKU_COLUMN_MAX_WIDTH = 520; function normalizeSkuColumnWidth( @@ -345,6 +409,17 @@ function normalizeSkuColumnWidth( ); } +function normalizeFinishedSampleShipmentColumnWidth( + column: FinishedSampleShipmentColumnWidthKey, + width: number, +) { + const minWidth = FINISHED_SAMPLE_SHIPMENT_COLUMN_DEFAULT_WIDTHS[column]; + return Math.min( + SKU_COLUMN_MAX_WIDTH, + Math.max(minWidth, Math.round(width)), + ); +} + type DocumentWorkspaceView = "detail" | "document"; type DocumentWorkspaceCategory = NonIntegratedDocumentWorkspaceCategory; type WorkflowNodeCategory = NonIntegratedWorkflowNodeCategory; @@ -786,6 +861,8 @@ const PRODUCT_FACTORY_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = "产品定厂"; const SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = "样包测量"; +const FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = + "成品寄样"; const PRODUCT_PRICING_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = "产品核价"; const PRODUCT_LIMIT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = @@ -1562,7 +1639,6 @@ const DEFAULT_DOCUMENT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory = const PRODUCT_PHOTO_CATEGORY: DocumentWorkspaceCategory = "产品实拍图"; const AUTO_SAVE_DELAY_MS = 500; -const SKU_CONFIRMATION_PROMPT_VISIBLE_MS = 5000; const STOCK_PRODUCT_LABEL = "\u5b58\u91cf\u5546\u54c1"; function getChangedWorkflowNodeCategories( @@ -1831,6 +1907,7 @@ const EMPTY_DOCUMENT_WORKSPACE_NOTES: Record< 检测报告: "", 产品定厂: "", 样包测量: "", + 成品寄样: "", 产品核价: "", 产品限价: "", 工厂质检: "", @@ -1945,6 +2022,13 @@ const DOCUMENT_WORKSPACE_COPY: Record< emptyTitle: "当前还没有样包测量资料", emptyDescription: "该入口已预留,后续可配置样包测量资料内容。", }, + 成品寄样: { + description: "成品寄样资料入口已预留,详情内容后续配置。", + noteLabel: "寄样备注", + notePlaceholder: "可填写成品寄样相关说明。", + emptyTitle: "当前还没有成品寄样资料", + emptyDescription: "该入口已预留,后续可配置成品寄样资料内容。", + }, 产品核价: { description: "产品核价资料入口已预留,详情内容后续配置。", noteLabel: "核价备注", @@ -2940,6 +3024,51 @@ function normalizeSetCountValue(value: string) { return Math.min(numericValue, 26); } +function normalizeDateInputValue(value: string) { + const normalizedValue = value.trim(); + if (!normalizedValue) return ""; + + const isoMatch = normalizedValue.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); + const zhMatch = normalizedValue.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/); + const slashMatch = normalizedValue.match(/^(\d{4})[/.](\d{1,2})[/.](\d{1,2})$/); + const match = isoMatch ?? zhMatch ?? slashMatch; + if (!match) return ""; + + const year = Number.parseInt(match[1] ?? "", 10); + const month = Number.parseInt(match[2] ?? "", 10); + const day = Number.parseInt(match[3] ?? "", 10); + if ( + !Number.isFinite(year) || + !Number.isFinite(month) || + !Number.isFinite(day) || + month < 1 || + month > 12 || + day < 1 || + day > 31 + ) { + return ""; + } + + const parsedDate = new Date(Date.UTC(year, month - 1, day)); + if ( + parsedDate.getUTCFullYear() !== year || + parsedDate.getUTCMonth() !== month - 1 || + parsedDate.getUTCDate() !== day + ) { + return ""; + } + + return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +} + +function formatDateWithChineseYearMonthDay(value: string) { + const normalizedValue = normalizeDateInputValue(value); + if (!normalizedValue) return ""; + + const [year = "", month = "", day = ""] = normalizedValue.split("-"); + return `${year}年${month}月${day}日`; +} + function normalizeNationalStandardCompliance(value: string | undefined) { const normalizedValue = value?.trim() ?? ""; return NATIONAL_STANDARD_COMPLIANCE_OPTIONS.includes( @@ -4879,6 +5008,39 @@ function resolveAutoAwareBarcode( return fallbackBarcode; } +function getSkuPackageGroupKey(sku: Pick) { + return sku.packageGroupId.trim() || sku.id; +} + +function hasPackageSharedFactoryRowContent(row: NonIntegratedFactoryRow) { + return Array.from(PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS).some((key) => + row[key].trim(), + ); +} + +function mergePackageSharedFactoryRow( + exactRow: NonIntegratedFactoryRow | undefined, + sharedRow: NonIntegratedFactoryRow | undefined, +) { + if (!sharedRow) return exactRow; + if (!exactRow) return sharedRow; + + return { + ...exactRow, + confirmed: exactRow.confirmed || sharedRow.confirmed, + supplierName: sharedRow.supplierName, + supplierCode: sharedRow.supplierCode, + warehouse: sharedRow.warehouse, + minimumOrderQuantity: sharedRow.minimumOrderQuantity, + leadTime: sharedRow.leadTime, + assemblyMode: sharedRow.assemblyMode, + productionType: sharedRow.productionType, + installationService: sharedRow.installationService, + supplierCategory: sharedRow.supplierCategory, + purchaseOrderMethod: sharedRow.purchaseOrderMethod, + }; +} + function createProductFactoryRowFromSku( sku: NonIntegratedSkuItem, existingRow?: NonIntegratedFactoryRow, @@ -4886,25 +5048,14 @@ function createProductFactoryRowFromSku( ): NonIntegratedFactoryRow { const confirmed = existingRow?.confirmed ?? false; const savedSupplierName = existingRow?.supplierName?.trim() ?? ""; - const importedContentLocked = isImportedSkuContentLocked(sku); - const supplierSource = - importedContentLocked && !confirmed - ? sku.manufacturer || savedSupplierName - : confirmed - ? sku.manufacturer || savedSupplierName - : savedSupplierName || sku.manufacturer; + const savedSupplierCode = + existingRow?.supplierCode?.trim().toUpperCase() ?? ""; + const supplierSource = sku.manufacturer || savedSupplierName; const supplier = normalizeFactorySupplier(supplierSource, supplierCodeByName); const supplierCode = - importedContentLocked && !confirmed - ? supplier.code || - sku.manufacturerCode.trim().toUpperCase() || - existingRow?.supplierCode?.trim().toUpperCase() || - "" - : confirmed - ? sku.manufacturerCode.trim().toUpperCase() || supplier.code - : existingRow?.supplierCode?.trim().toUpperCase() || - supplier.code || - sku.manufacturerCode.trim().toUpperCase(); + supplier.code || + sku.manufacturerCode.trim().toUpperCase() || + savedSupplierCode; const supplierName = supplierSource.trim() || supplier.name || sku.manufacturer; const skuCode = sku.skuCode.trim(); @@ -4943,14 +5094,36 @@ function buildProductFactoryRows( supplierCodeByName: ReadonlyMap = SUPPLIER_CODE_BY_NAME, ) { const rowBySkuId = new Map(savedRows.map((row) => [row.skuId, row])); + const sharedRowByPackageGroupKey = new Map(); - return skuItems.map((sku) => - createProductFactoryRowFromSku( + skuItems.forEach((sku) => { + if (!sku.packageGroupId.trim()) return; + const savedRow = rowBySkuId.get(sku.id); + if (!savedRow) return; + + const groupKey = getSkuPackageGroupKey(sku); + const currentSharedRow = sharedRowByPackageGroupKey.get(groupKey); + if ( + !currentSharedRow || + (!hasPackageSharedFactoryRowContent(currentSharedRow) && + hasPackageSharedFactoryRowContent(savedRow)) + ) { + sharedRowByPackageGroupKey.set(groupKey, savedRow); + } + }); + + return skuItems.map((sku) => { + const exactRow = rowBySkuId.get(sku.id); + const sharedRow = sku.packageGroupId.trim() + ? sharedRowByPackageGroupKey.get(getSkuPackageGroupKey(sku)) + : undefined; + + return createProductFactoryRowFromSku( sku, - rowBySkuId.get(sku.id), + mergePackageSharedFactoryRow(exactRow, sharedRow), supplierCodeByName, - ), - ); + ); + }); } function normalizeFactoryRowsForPayload( @@ -5263,10 +5436,6 @@ function isProductLimitRowComplete(row: NonIntegratedProductLimitRow) { ].every((value) => value.trim()); } -function isPlanningLayoutVisibleSkuItem(sku: NonIntegratedSkuItem) { - return sku.salesStatus.trim() === "销售"; -} - function buildPlanningLayoutSkuRows( skuItems: NonIntegratedSkuItem[], savedRows: NonIntegratedPlanningLayoutRow[], @@ -5275,7 +5444,7 @@ function buildPlanningLayoutSkuRows( const rowBySkuId = new Map(savedRows.map((row) => [row.skuId, row])); const limitRowBySkuId = new Map(productLimitRows.map((row) => [row.skuId, row])); - return skuItems.filter(isPlanningLayoutVisibleSkuItem).map((item) => ({ + return skuItems.map((item) => ({ ...(() => { const row = rowBySkuId.get(item.id); const savedBrands = getUniqueNonEmptyStrings([ @@ -5400,6 +5569,84 @@ function isSampleMeasurementRowComplete( ].every((value) => value.trim()); } +type ProductFinishedSampleShipmentDisplayRow = + NonIntegratedFinishedSampleShipmentRow & { + image: string; + model: string; + skuCode: string; + setCount: string; + supplierName: string; + barcode: string; + specName: string; + installMethod: string; + batchNo: number; + }; + +function createFinishedSampleShipmentRowFromSku( + sku: NonIntegratedSkuItem, + existingRow?: NonIntegratedFinishedSampleShipmentRow, +): ProductFinishedSampleShipmentDisplayRow { + return { + skuId: sku.id, + image: sku.image.trim(), + model: sku.model.trim(), + skuCode: sku.skuCode.trim(), + setCount: sku.setCount.trim() || "1", + supplierName: sku.manufacturer.trim(), + barcode: sku.barcode.trim(), + specName: sku.specName.trim(), + installMethod: sku.installMethod.trim(), + batchNo: sku.batchNo || 1, + sampleQuantity: existingRow?.sampleQuantity?.trim() ?? "", + shipped: existingRow?.shipped?.trim() ?? "", + trackingNumber: existingRow?.trackingNumber?.trim() ?? "", + }; +} + +function buildFinishedSampleShipmentRows( + skuItems: NonIntegratedSkuItem[], + savedRows: NonIntegratedFinishedSampleShipmentRow[], +): ProductFinishedSampleShipmentDisplayRow[] { + const rowBySkuId = new Map(savedRows.map((row) => [row.skuId, row])); + + return skuItems.map((sku) => + createFinishedSampleShipmentRowFromSku(sku, rowBySkuId.get(sku.id)), + ); +} + +function normalizeFinishedSampleShipmentRowsForPayload( + rows: NonIntegratedFinishedSampleShipmentRow[], +): NonIntegratedFinishedSampleShipmentRow[] { + return rows + .map((row) => ({ + skuId: row.skuId, + sampleQuantity: row.sampleQuantity.trim(), + shipped: row.shipped.trim(), + trackingNumber: row.trackingNumber.trim(), + })) + .filter( + (row) => + row.skuId && + [row.sampleQuantity, row.shipped, row.trackingNumber].some(Boolean), + ); +} + +function hasFinishedSampleShipmentRowContent( + row: NonIntegratedFinishedSampleShipmentRow, +) { + return [row.sampleQuantity, row.shipped, row.trackingNumber].some((value) => + value.trim(), + ); +} + +function isFinishedSampleShipmentRowComplete( + row: NonIntegratedFinishedSampleShipmentRow, +) { + return [row.sampleQuantity, row.shipped, row.trackingNumber].every((value) => + value.trim(), + ); +} + type ProductFirstQualityDisplayRow = NonIntegratedFirstQualityRow & { image: string; model: string; @@ -6020,6 +6267,61 @@ function CreateFieldShell({ children }: { children: ReactNode }) { ); } +type FloatingPanelPosition = { + left: number; + top: number; + width: number; + maxWidth: number; + maxHeight: number; +}; + +function useFloatingPanelPosition( + isOpen: boolean, + anchorRef: RefObject, + gap = 4, +) { + const [position, setPosition] = useState(null); + + const updatePosition = useCallback(() => { + const anchor = anchorRef.current; + if (!anchor || typeof window === "undefined") return; + + const rect = anchor.getBoundingClientRect(); + const viewportPadding = 12; + const left = Math.min( + Math.max(viewportPadding, rect.left), + Math.max(viewportPadding, window.innerWidth - rect.width - viewportPadding), + ); + const availableBelow = window.innerHeight - rect.bottom - gap - viewportPadding; + + setPosition({ + left, + top: rect.bottom + gap, + width: rect.width, + maxWidth: Math.max(rect.width, window.innerWidth - left - viewportPadding), + maxHeight: Math.max(64, availableBelow), + }); + }, [anchorRef, gap]); + + useLayoutEffect(() => { + if (!isOpen) { + setPosition(null); + return; + } + + updatePosition(); + window.addEventListener("resize", updatePosition); + window.addEventListener("scroll", updatePosition, true); + + return () => { + window.removeEventListener("resize", updatePosition); + window.removeEventListener("scroll", updatePosition, true); + }; + }, [isOpen, updatePosition]); + + return position; +} + function InlineSuggestionInput({ ariaLabel, dataTestId, @@ -6056,6 +6358,8 @@ function InlineSuggestionInput({ onCommit?: () => void; }) { const suggestionListId = useId(); + const wrapperRef = useRef(null); + const panelRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const previousValueRef = useRef(value); const normalizedOptions = getUniqueNonEmptyStrings(options ?? []); @@ -6064,6 +6368,7 @@ function InlineSuggestionInput({ filterOptions && keyword ? option.toLowerCase().includes(keyword) : true, ); const hasOptions = !readOnly && normalizedOptions.length > 0; + const floatingPosition = useFloatingPanelPosition(isOpen && hasOptions, wrapperRef); useEffect(() => { if (previousValueRef.current && !value) { @@ -6072,6 +6377,25 @@ function InlineSuggestionInput({ previousValueRef.current = value; }, [value]); + useEffect(() => { + if (!isOpen) return; + + function handleDocumentMouseDown(event: MouseEvent) { + const target = event.target as Node; + if ( + wrapperRef.current?.contains(target) || + panelRef.current?.contains(target) + ) { + return; + } + setIsOpen(false); + } + + document.addEventListener("mousedown", handleDocumentMouseDown); + return () => + document.removeEventListener("mousedown", handleDocumentMouseDown); + }, [isOpen]); + const closeLater = useCallback(() => { setIsOpen(false); }, []); @@ -6086,7 +6410,7 @@ function InlineSuggestionInput({ ); return ( -
+
) : null} - {hasOptions && isOpen ? ( -
- {visibleOptions.length > 0 ? ( - visibleOptions.map((option) => ( - - )) - ) : ( -
- 没有匹配项 -
- )} -
- ) : null} + {hasOptions && isOpen && floatingPosition + ? createPortal( +
+ {visibleOptions.length > 0 ? ( + visibleOptions.map((option) => ( + + )) + ) : ( +
+ 没有匹配项 +
+ )} +
, + document.body, + ) + : null}
); } @@ -6203,6 +6545,7 @@ function InlineSelect({ optionClassName = "flex w-full items-center rounded-apple-md px-2.5 py-1.5 text-left text-xs transition", includeEmptyOption, emptyOptionDisabled = false, + showIndicatorWhenReadOnly = false, onChange, }: { ariaLabel: string; @@ -6217,10 +6560,12 @@ function InlineSelect({ optionClassName?: string; includeEmptyOption?: boolean; emptyOptionDisabled?: boolean; + showIndicatorWhenReadOnly?: boolean; onChange: (value: string) => void; }) { const listId = useId(); const containerRef = useRef(null); + const panelRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const normalizedOptions = normalizeInlineSelectOptions(options); const shouldIncludeEmptyOption = includeEmptyOption ?? Boolean(placeholder); @@ -6237,14 +6582,22 @@ function InlineSelect({ const selectedOption = selectOptions.find((option) => option.value === value); const displayValue = selectedOption?.label ?? (value || placeholder || ""); const hasOptions = !readOnly && selectOptions.length > 0; + const shouldShowIndicator = + hasOptions || (readOnly && showIndicatorWhenReadOnly && selectOptions.length > 0); + const floatingPosition = useFloatingPanelPosition(isOpen && hasOptions, containerRef); useEffect(() => { if (!isOpen) return; function handleDocumentMouseDown(event: MouseEvent) { - if (!containerRef.current?.contains(event.target as Node)) { - setIsOpen(false); + const target = event.target as Node; + if ( + containerRef.current?.contains(target) || + panelRef.current?.contains(target) + ) { + return; } + setIsOpen(false); } document.addEventListener("mousedown", handleDocumentMouseDown); @@ -6282,40 +6635,58 @@ function InlineSelect({ {displayValue || placeholder} - {hasOptions ? ( + {shouldShowIndicator ? (
, + document.body, + ) + : null} ); } @@ -8091,17 +8462,21 @@ function FactoryWarehouseField({ value, options, readOnly = false, + columnWidth = SKU_COLUMN_DEFAULT_WIDTHS.supplierName, onChange, }: { value: string; options: readonly string[]; readOnly?: boolean; + columnWidth?: number; onChange: (value: string) => void; }) { return ( -
-
+ + ); +} + +function FactoryLeadTimeField({ + value, + readOnly = false, + onChange, +}: { + value: string; + readOnly?: boolean; + onChange: (value: string) => void; +}) { + const normalizedDate = normalizeDateInputValue(value); + + return ( + + onChange(formatDateWithChineseYearMonthDay(event.target.value)) + } + className={`h-8 w-full min-w-[128px] rounded border border-apple-separator px-2 text-xs text-apple-label-primary outline-none ${ + readOnly + ? "bg-apple-bg-primary text-apple-label-secondary" + : "bg-apple-bg-secondary" + }`} + /> ); } @@ -8808,6 +9213,7 @@ function buildProductPayload( planningLayoutRows: NonIntegratedPlanningLayoutRow[], productLimitRows: NonIntegratedProductLimitRow[], sampleMeasurementRows: NonIntegratedSampleMeasurementRow[], + finishedSampleShipmentRows: NonIntegratedFinishedSampleShipmentRow[], firstQualityRows: NonIntegratedFirstQualityRow[], uploadFiles: NonIntegratedUploadFile[], documentWorkspaceNotes: Record, @@ -9178,6 +9584,9 @@ function buildProductPayload( productLimitRows: normalizeProductLimitRowsForPayload(productLimitRows), sampleMeasurementRows: normalizeSampleMeasurementRowsForPayload(sampleMeasurementRows), + finishedSampleShipmentRows: normalizeFinishedSampleShipmentRowsForPayload( + finishedSampleShipmentRows, + ), firstQualityRows: normalizeFirstQualityRowsForPayload(firstQualityRows), uploadFiles, documentWorkspaceNotes: Object.fromEntries( @@ -9378,12 +9787,6 @@ export default function NonIntegratedDetailClient({ const [skuBatchConfirmedAt, setSkuBatchConfirmedAt] = useState< Record >(existingProduct?.skuBatchConfirmedAt ?? {}); - const [pendingSkuConfirmationBatch, setPendingSkuConfirmationBatch] = - useState(null); - const [skuConfirmationPrompt, setSkuConfirmationPrompt] = useState<{ - type: "success" | "error"; - message: string; - } | null>(null); const [pendingSkuBatchDraft, setPendingSkuBatchDraft] = useState<{ batchNo: number; changeType: string; @@ -9393,6 +9796,12 @@ export default function NonIntegratedDetailClient({ const [skuColumnWidths, setSkuColumnWidths] = useState< Record >(() => ({ ...SKU_COLUMN_DEFAULT_WIDTHS })); + const [ + finishedSampleShipmentColumnWidths, + setFinishedSampleShipmentColumnWidths, + ] = useState>(() => ({ + ...FINISHED_SAMPLE_SHIPMENT_COLUMN_DEFAULT_WIDTHS, + })); const [barcodeScannerSkuId, setBarcodeScannerSkuId] = useState( null, ); @@ -9419,6 +9828,9 @@ export default function NonIntegratedDetailClient({ const [sampleMeasurementRows, setSampleMeasurementRows] = useState< NonIntegratedSampleMeasurementRow[] >(existingProduct?.sampleMeasurementRows ?? []); + const [finishedSampleShipmentRows, setFinishedSampleShipmentRows] = useState< + NonIntegratedFinishedSampleShipmentRow[] + >(existingProduct?.finishedSampleShipmentRows ?? []); const [firstQualityRows, setFirstQualityRows] = useState< NonIntegratedFirstQualityRow[] >(existingProduct?.firstQualityRows ?? []); @@ -10076,7 +10488,6 @@ export default function NonIntegratedDetailClient({ } | null>(null); const autoSaveTimeoutRef = useRef(null); const autoSaveNoticeTimeoutRef = useRef(null); - const skuConfirmationPromptTimeoutRef = useRef(null); const assetUploadCacheRef = useRef(new Map>()); const localStorageCompactionFinishedRef = useRef(false); const documentWorkspaceRevisionSignatureRef = useRef - isStockProductRecord || Boolean(skuBatchConfirmedAt[String(batchNo)]), - [isStockProductRecord, skuBatchConfirmedAt], - ); const activeSkuBatchNo = activeSkuBatch === "all" ? null : activeSkuBatch; - const isActiveSkuBatchConfirmed = - activeSkuBatchNo !== null && isSkuBatchConfirmed(activeSkuBatchNo); - const canEditActiveSkuBatch = - !isSkuContentReadOnly && !isActiveSkuBatchConfirmed; const formalSpuValue = formValues.spu.trim(); const hasFormalSpu = formalSpuValue.length > 0; const spuRuleProductCategory = @@ -10469,91 +10871,6 @@ export default function NonIntegratedDetailClient({ ), [formValues.tagMaterial, skuCodePreviewSpu, skuItems], ); - const getSkuBatchValidationIssues = useCallback( - (batchNo: number) => { - const batchKey = String(batchNo); - const batchItems = skuItems - .map((item, index) => ({ - item, - derivedItem: derivedSkuItems[index] ?? item, - rowNumber: index + 1, - })) - .filter( - ({ item, derivedItem }) => - (derivedItem.batchNo || item.batchNo || 1) === batchNo, - ); - const issues: string[] = []; - - if (batchItems.length === 0) { - issues.push(`批次${batchNo}还没有 SKU`); - } - - batchItems.forEach(({ item, derivedItem, rowNumber }) => { - const missingFields = [ - ["型号", item.model], - ["规格码", derivedItem.skuCode], - ["条码", derivedItem.barcode], - ["套件量", item.setCount], - ["规格名称", derivedItem.specName], - ["尺寸", item.size], - ["颜色", item.color], - ["材质", item.material], - ["板材类型", item.boardMaterialType], - ["版本号", derivedItem.versionNo], - ["属性代码", item.attributeCode], - ["图纸ID", derivedItem.drawingId], - ["安装方式", item.installMethod], - ["执行标准", item.executeStandard], - ["符合国标", item.nationalStandardCompliant], - ["供应商名称", item.manufacturer], - ["供应商代码", item.manufacturerCode], - ["销售状态", item.salesStatus], - ] - .filter(([, value]) => !String(value ?? "").trim()) - .map(([label]) => label); - - if (missingFields.length > 0) { - issues.push(`第${rowNumber}行缺少:${missingFields.join("、")}`); - } - }); - - const missingBatchFields = [ - ["新增类型", skuBatchChangeTypes[batchKey]], - ["钉钉流程号", skuBatchFlowNos[batchKey]], - ] - .filter(([, value]) => !String(value ?? "").trim()) - .map(([label]) => label); - - if (missingBatchFields.length > 0) { - issues.push(`批次信息缺少:${missingBatchFields.join("、")}`); - } - - return issues; - }, - [derivedSkuItems, skuBatchChangeTypes, skuBatchFlowNos, skuItems], - ); - const activeSkuBatchValidationIssues = useMemo(() => { - if (activeSkuBatchNo === null) return ["请先切换到具体批次"]; - return getSkuBatchValidationIssues(activeSkuBatchNo); - }, [activeSkuBatchNo, getSkuBatchValidationIssues]); - const canConfirmActiveSkuBatch = - Boolean(activeSkuBatchNo) && - canEditActiveSkuBatch && - activeSkuBatchValidationIssues.length === 0; - const isActiveSkuConfirmationPending = - activeSkuBatchNo !== null && - (pendingSkuConfirmationBatch === activeSkuBatchNo || - skuConfirmationPrompt?.message.startsWith( - `批次${activeSkuBatchNo} SKU 信息已检查完整`, - )); - useEffect(() => { - setPendingSkuConfirmationBatch(null); - if (skuConfirmationPromptTimeoutRef.current) { - window.clearTimeout(skuConfirmationPromptTimeoutRef.current); - skuConfirmationPromptTimeoutRef.current = null; - } - setSkuConfirmationPrompt(null); - }, [activeSkuBatch]); const getSeriesSkuComponentDetails = useCallback( (item: NonIntegratedSkuItem): NonIntegratedSkuComponentDetail[] => { const storedDetails = normalizeNonIntegratedSkuComponentDetails( @@ -10751,6 +11068,14 @@ export default function NonIntegratedDetailClient({ ), [derivedSkuItems, sampleMeasurementRows], ); + const productFinishedSampleShipmentRows = useMemo( + () => + buildFinishedSampleShipmentRows( + derivedSkuItems, + finishedSampleShipmentRows, + ), + [derivedSkuItems, finishedSampleShipmentRows], + ); const productFirstQualityRows = useMemo( () => buildFirstQualityRows(derivedSkuItems, firstQualityRows), [derivedSkuItems, firstQualityRows], @@ -10759,57 +11084,55 @@ export default function NonIntegratedDetailClient({ () => new Map(derivedSkuItems.map((item) => [item.id, item.batchNo || 1])), [derivedSkuItems], ); - const confirmedSkuItemIdSet = useMemo( + const factoryConfirmedSkuIdSet = useMemo( () => new Set( - derivedSkuItems - .filter((item) => isSkuBatchConfirmed(item.batchNo || 1)) - .map((item) => item.id), + productFactoryRows + .filter((row) => row.confirmed) + .map((row) => row.skuId), ), - [derivedSkuItems, isSkuBatchConfirmed], + [productFactoryRows], ); - const confirmedProductFactoryRows = useMemo( - () => productFactoryRows.filter((row) => confirmedSkuItemIdSet.has(row.skuId)), - [confirmedSkuItemIdSet, productFactoryRows], - ); - const confirmedProductPricingRows = useMemo( - () => productPricingRows.filter((row) => confirmedSkuItemIdSet.has(row.skuId)), - [confirmedSkuItemIdSet, productPricingRows], - ); - const confirmedPlanningLayoutSkuRows = useMemo( - () => - planningLayoutSkuRows.filter((row) => - confirmedSkuItemIdSet.has(row.skuId), + const isSkuItemFactoryConfirmed = useCallback( + (itemOrId: NonIntegratedSkuItem | string) => + factoryConfirmedSkuIdSet.has( + typeof itemOrId === "string" ? itemOrId : itemOrId.id, ), - [confirmedSkuItemIdSet, planningLayoutSkuRows], + [factoryConfirmedSkuIdSet], ); - const confirmedProductSampleMeasurementRows = useMemo( - () => - productSampleMeasurementRows.filter((row) => - confirmedSkuItemIdSet.has(row.skuId), + const isSkuBatchFactoryConfirmed = useCallback( + (batchNo: number | string) => + derivedSkuItems.some( + (item) => + String(item.batchNo || 1) === String(batchNo) && + factoryConfirmedSkuIdSet.has(item.id), ), - [confirmedSkuItemIdSet, productSampleMeasurementRows], + [derivedSkuItems, factoryConfirmedSkuIdSet], ); + const isActiveSkuBatchFactoryConfirmed = + activeSkuBatchNo !== null && isSkuBatchFactoryConfirmed(activeSkuBatchNo); + const canEditActiveSkuBatch = + !isSkuContentReadOnly && !isActiveSkuBatchFactoryConfirmed; const displayedProductFactoryRows = useMemo(() => { - if (activeDownstreamSkuBatch === "all") return confirmedProductFactoryRows; - return confirmedProductFactoryRows.filter( + if (activeDownstreamSkuBatch === "all") return productFactoryRows; + return productFactoryRows.filter( (row) => (skuBatchNoById.get(row.skuId) ?? 1) === activeDownstreamSkuBatch, ); - }, [activeDownstreamSkuBatch, confirmedProductFactoryRows, skuBatchNoById]); + }, [activeDownstreamSkuBatch, productFactoryRows, skuBatchNoById]); const displayedProductPricingRows = useMemo(() => { - if (activeDownstreamSkuBatch === "all") return confirmedProductPricingRows; - return confirmedProductPricingRows.filter( + if (activeDownstreamSkuBatch === "all") return productPricingRows; + return productPricingRows.filter( (row) => (skuBatchNoById.get(row.skuId) ?? 1) === activeDownstreamSkuBatch, ); - }, [activeDownstreamSkuBatch, confirmedProductPricingRows, skuBatchNoById]); + }, [activeDownstreamSkuBatch, productPricingRows, skuBatchNoById]); const displayedPlanningLayoutSkuRows = useMemo(() => { - if (activeDownstreamSkuBatch === "all") return confirmedPlanningLayoutSkuRows; - return confirmedPlanningLayoutSkuRows.filter( + if (activeDownstreamSkuBatch === "all") return planningLayoutSkuRows; + return planningLayoutSkuRows.filter( (row) => row.batchNo === activeDownstreamSkuBatch, ); - }, [activeDownstreamSkuBatch, confirmedPlanningLayoutSkuRows]); + }, [activeDownstreamSkuBatch, planningLayoutSkuRows]); const displayedProductLimitRows = useMemo(() => { if (activeDownstreamSkuBatch === "all") return productLimitDisplayRows; return productLimitDisplayRows.filter( @@ -10819,17 +11142,26 @@ export default function NonIntegratedDetailClient({ }, [activeDownstreamSkuBatch, productLimitDisplayRows, skuBatchNoById]); const displayedProductSampleMeasurementRows = useMemo(() => { if (activeDownstreamSkuBatch === "all") { - return confirmedProductSampleMeasurementRows; + return productSampleMeasurementRows; } - return confirmedProductSampleMeasurementRows.filter( + return productSampleMeasurementRows.filter( (row) => (skuBatchNoById.get(row.skuId) ?? 1) === activeDownstreamSkuBatch, ); }, [ activeDownstreamSkuBatch, - confirmedProductSampleMeasurementRows, + productSampleMeasurementRows, skuBatchNoById, ]); + const displayedFinishedSampleShipmentRows = useMemo(() => { + if (activeDownstreamSkuBatch === "all") { + return productFinishedSampleShipmentRows; + } + + return productFinishedSampleShipmentRows.filter( + (row) => row.batchNo === activeDownstreamSkuBatch, + ); + }, [activeDownstreamSkuBatch, productFinishedSampleShipmentRows]); const displayedProductFirstQualityRows = useMemo(() => { if (activeDownstreamSkuBatch === "all") { return productFirstQualityRows; @@ -11302,15 +11634,15 @@ export default function NonIntegratedDetailClient({ [groupedUploadFiles], ); const confirmedProductFactoryRowCount = useMemo( - () => confirmedProductFactoryRows.filter((row) => row.confirmed).length, - [confirmedProductFactoryRows], + () => productFactoryRows.filter((row) => row.confirmed).length, + [productFactoryRows], ); const populatedProductPricingRowCount = useMemo( () => - confirmedProductPricingRows.filter( + productPricingRows.filter( (row) => row.costPrice.trim(), ).length, - [confirmedProductPricingRows], + [productPricingRows], ); const populatedProductLimitRowCount = useMemo( () => @@ -11323,10 +11655,10 @@ export default function NonIntegratedDetailClient({ ).length, [productLimitDisplayRows], ); - const planningLayoutHasContent = confirmedPlanningLayoutSkuRows.length > 0; + const planningLayoutHasContent = planningLayoutSkuRows.length > 0; const populatedSampleMeasurementRowCount = useMemo( () => - confirmedProductSampleMeasurementRows.filter((row) => + productSampleMeasurementRows.filter((row) => [ row.lengthCm, row.widthCm, @@ -11335,7 +11667,14 @@ export default function NonIntegratedDetailClient({ row.productShape, ].some((value) => value.trim()), ).length, - [confirmedProductSampleMeasurementRows], + [productSampleMeasurementRows], + ); + const populatedFinishedSampleShipmentRowCount = useMemo( + () => + productFinishedSampleShipmentRows.filter( + hasFinishedSampleShipmentRowContent, + ).length, + [productFinishedSampleShipmentRows], ); const populatedFirstQualityRowCount = useMemo( () => @@ -11356,6 +11695,11 @@ export default function NonIntegratedDetailClient({ displayedProductSampleMeasurementRows.every( isSampleMeasurementRowComplete, ); + const displayedFinishedSampleShipmentRowsComplete = + displayedFinishedSampleShipmentRows.length > 0 && + displayedFinishedSampleShipmentRows.every( + isFinishedSampleShipmentRowComplete, + ); const displayedProductPricingRowsComplete = displayedProductPricingRows.length > 0 && displayedProductPricingRows.every(isProductPricingRowComplete); @@ -11401,6 +11745,8 @@ export default function NonIntegratedDetailClient({ [PRODUCT_FACTORY_WORKSPACE_CATEGORY]: confirmedProductFactoryRowCount, [SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: populatedSampleMeasurementRowCount, + [FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY]: + populatedFinishedSampleShipmentRowCount, [PRODUCT_PRICING_WORKSPACE_CATEGORY]: populatedProductPricingRowCount, [PRODUCT_LIMIT_WORKSPACE_CATEGORY]: populatedProductLimitRowCount, [PLANNING_LAYOUT_WORKSPACE_CATEGORY]: planningLayoutHasContent ? 1 : 0, @@ -11414,6 +11760,7 @@ export default function NonIntegratedDetailClient({ populatedProductLimitRowCount, populatedProductPricingRowCount, populatedFirstQualityRowCount, + populatedFinishedSampleShipmentRowCount, populatedSampleMeasurementRowCount, qualityInspectionHasContent, styleDetails, @@ -11504,6 +11851,9 @@ export default function NonIntegratedDetailClient({ signatures[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY] = JSON.stringify( sampleMeasurementRows, ); + signatures[FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY] = JSON.stringify( + finishedSampleShipmentRows, + ); signatures[PRODUCT_PRICING_WORKSPACE_CATEGORY] = JSON.stringify(pricingRows); signatures[PRODUCT_LIMIT_WORKSPACE_CATEGORY] = JSON.stringify({ @@ -11532,6 +11882,7 @@ export default function NonIntegratedDetailClient({ }, [ documentWorkspaceNotes, factoryRows, + finishedSampleShipmentRows, formValues, openingReference, patentApplication, @@ -11573,6 +11924,9 @@ export default function NonIntegratedDetailClient({ const isSampleMeasurementNodeLocked = isDocumentWorkspaceNodeLocked( SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY, ); + const isFinishedSampleShipmentNodeLocked = isDocumentWorkspaceNodeLocked( + FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY, + ); const isProductPricingNodeLocked = isDocumentWorkspaceNodeLocked( PRODUCT_PRICING_WORKSPACE_CATEGORY, ); @@ -11583,12 +11937,10 @@ export default function NonIntegratedDetailClient({ const isSampleMeasurementNodeApproved = isSampleMeasurementNodeLocked && !dirtyWorkflowNodeCategories.has(SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY); - const canShowSkuReconfirmationAction = - activeSkuTab === "skuInfo" && - isCurrentUserAdmin && - activeSkuBatchNo !== null && - isActiveSkuBatchConfirmed && - !isSkuContentReadOnly; + const displayedFinishedSampleShipmentIsConfirmed = isWorkflowNodeConfirmed( + FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY, + displayedFinishedSampleShipmentRowsComplete, + ) || isFinishedSampleShipmentNodeLocked; const displayedProductPricingIsConfirmed = isWorkflowNodeConfirmed( PRODUCT_PRICING_WORKSPACE_CATEGORY, displayedProductPricingRowsComplete, @@ -11740,6 +12092,9 @@ export default function NonIntegratedDetailClient({ activeDocumentWorkspaceCategory === PRODUCT_FACTORY_WORKSPACE_CATEGORY; const isSampleMeasurementWorkspace = activeDocumentWorkspaceCategory === SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY; + const isFinishedSampleShipmentWorkspace = + activeDocumentWorkspaceCategory === + FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY; const isProductPricingWorkspace = activeDocumentWorkspaceCategory === PRODUCT_PRICING_WORKSPACE_CATEGORY; const isProductLimitWorkspace = @@ -11762,6 +12117,17 @@ export default function NonIntegratedDetailClient({ activeDocumentWorkspaceCategory === INSTALLATION_WORKSPACE_CATEGORY; const isPackagingWorkspace = activeDocumentWorkspaceCategory === PACKAGING_WORKSPACE_CATEGORY; + const canEditFinishedSampleShipmentQuantity = + !isReadOnly && !isFinishedSampleShipmentNodeLocked; + const canEditFinishedSampleShipmentShipping = + !isReadOnly && + !isFinishedSampleShipmentNodeLocked && + (!hasCurrentUserContext || + isCurrentUserFullDetailEditor || + isCurrentUserNonIntegratedPurchaseOwner( + currentPermissionProduct, + currentPermissionUser, + )); const skuStyleKeys = useMemo(() => { const nextKeys = new Set(); @@ -12344,6 +12710,8 @@ export default function NonIntegratedDetailClient({ const documentWorkspaceTitle = `${getDocumentWorkspaceCategoryLabel(activeDocumentWorkspaceCategory)}工作区`; const documentWorkspaceDescription = isSampleMeasurementWorkspace ? "从 SKU 信息自动带出基础字段;长宽高、重量和商品外形由采购填写并自动保存。" + : isFinishedSampleShipmentWorkspace + ? "从 SKU 信息自动带出基础字段;寄样数量开放填写,是否寄出和快递单号由采购填写并确认。" : isProductPhotoWorkspace ? `按款式分栏维护产品实拍。当前款式:${currentProductPhotoStyleLabel};九视图支持按文件名自动匹配,其他照片分组支持批量上传和替换。` : isProductDataWorkspace @@ -12745,20 +13113,6 @@ export default function NonIntegratedDetailClient({ }, 2000); }, []); - const showSkuConfirmationPrompt = useCallback( - (prompt: { type: "success" | "error"; message: string }) => { - if (skuConfirmationPromptTimeoutRef.current) { - window.clearTimeout(skuConfirmationPromptTimeoutRef.current); - } - setSkuConfirmationPrompt(prompt); - skuConfirmationPromptTimeoutRef.current = window.setTimeout(() => { - setSkuConfirmationPrompt(null); - skuConfirmationPromptTimeoutRef.current = null; - }, SKU_CONFIRMATION_PROMPT_VISIBLE_MS); - }, - [], - ); - const persistEditedProduct = useCallback( async ({ successMessage, @@ -12823,6 +13177,7 @@ export default function NonIntegratedDetailClient({ planningLayoutRows, productLimitRows, productSampleMeasurementRows, + productFinishedSampleShipmentRows, productFirstQualityRows, uploadFiles, documentWorkspaceNotes, @@ -12896,6 +13251,7 @@ export default function NonIntegratedDetailClient({ productFirstQualityRows, productLimitRows, productPricingRows, + productFinishedSampleShipmentRows, productSampleMeasurementRows, projectReview, projectStatus, @@ -12916,10 +13272,6 @@ export default function NonIntegratedDetailClient({ window.clearTimeout(autoSaveNoticeTimeoutRef.current); autoSaveNoticeTimeoutRef.current = null; } - if (skuConfirmationPromptTimeoutRef.current) { - window.clearTimeout(skuConfirmationPromptTimeoutRef.current); - skuConfirmationPromptTimeoutRef.current = null; - } }; }, []); @@ -13287,6 +13639,7 @@ export default function NonIntegratedDetailClient({ persistEditedProduct, planningLayoutRows, productFactoryRows, + productFinishedSampleShipmentRows, productFirstQualityRows, productId, productLimitRows, @@ -14018,7 +14371,7 @@ export default function NonIntegratedDetailClient({ if (!targetItem) return current; if ( isStockProductRecord || - isSkuBatchConfirmed(targetItem.batchNo || 1) || + isSkuItemFactoryConfirmed(targetItem) || isImportedSkuContentLocked(targetItem) ) { return current; @@ -14103,7 +14456,11 @@ export default function NonIntegratedDetailClient({ return { ...nextItem, - barcode: nextDerivedItem.barcode, + barcode: buildAutoBarcode( + nextDerivedItem.skuCode, + nextItem.manufacturerCode, + nextDerivedItem.versionNo, + ), }; } @@ -14116,7 +14473,7 @@ export default function NonIntegratedDetailClient({ if (item.id !== id) return item; if ( isStockProductRecord || - isSkuBatchConfirmed(item.batchNo || 1) || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item) ) return item; @@ -14136,7 +14493,7 @@ export default function NonIntegratedDetailClient({ if (item.id !== id) return item; if ( isStockProductRecord || - isSkuBatchConfirmed(item.batchNo || 1) || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item) ) return item; @@ -14157,7 +14514,7 @@ export default function NonIntegratedDetailClient({ if (item.id !== id) return item; if ( isStockProductRecord || - isSkuBatchConfirmed(item.batchNo || 1) || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item) ) return item; @@ -14177,7 +14534,7 @@ export default function NonIntegratedDetailClient({ if (item.id !== id) return item; if ( isStockProductRecord || - isSkuBatchConfirmed(item.batchNo || 1) || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item) ) return item; @@ -14195,9 +14552,30 @@ export default function NonIntegratedDetailClient({ setSaveMessageType("success"); } + function getPackageFactoryTargetSkuIds( + items: Pick[], + targetId: string, + key: ProductFactoryEditableKey, + ) { + const targetItem = items.find((item) => item.id === targetId); + if ( + !targetItem?.packageGroupId.trim() || + !PACKAGE_SHARED_PRODUCT_FACTORY_KEYS.has(key) + ) { + return new Set([targetId]); + } + + const targetGroupKey = getSkuPackageGroupKey(targetItem); + return new Set( + items + .filter((item) => getSkuPackageGroupKey(item) === targetGroupKey) + .map((item) => item.id), + ); + } + function updateProductFactoryRow( skuId: string, - key: Exclude, + key: ProductFactoryEditableKey, value: string, ) { if (isReadOnly) return; @@ -14210,16 +14588,63 @@ export default function NonIntegratedDetailClient({ const supplier = normalizeFactorySupplier(value, supplierCodeByName); const supplierName = supplier.name || value; - setSkuItems((current) => - current.map((item, index) => - item.id === skuId && - !isStockProductRecord && - !isSkuBatchConfirmed(item.batchNo || 1) && - !isImportedSkuContentLocked(item) + setSkuItems((current) => { + const targetSkuIds = getPackageFactoryTargetSkuIds( + current, + skuId, + key, + ); + return current.map((item, index) => + targetSkuIds.has(item.id) && !isStockProductRecord ? applySkuSupplierName(item, index, supplierName) : item, - ), - ); + ); + }); + } + + if (key === "setCount") { + setSkuItems((current) => { + const targetSkuIds = getPackageFactoryTargetSkuIds( + current, + skuId, + key, + ); + return current.map((item) => + targetSkuIds.has(item.id) && !isStockProductRecord + ? { ...item, setCount: value } + : item, + ); + }); + } + + if (key === "installMethod") { + setSkuItems((current) => { + if (isStockProductRecord) return current; + + const targetItem = current.find((item) => item.id === skuId); + if (!targetItem) return current; + + const targetStyleKey = getStyleKeyFromModel(targetItem.model); + if (targetStyleKey) { + setStyleDetails((currentDetails) => ({ + ...currentDetails, + [targetStyleKey]: { + ...(currentDetails[targetStyleKey] ?? EMPTY_STYLE_DETAIL), + installMethod: value, + }, + })); + + return current.map((item) => + getStyleKeyFromModel(item.model) === targetStyleKey + ? { ...item, installMethod: value } + : item, + ); + } + + return current.map((item) => + item.id === skuId ? { ...item, installMethod: value } : item, + ); + }); } setFactoryRows((current) => { @@ -14228,8 +14653,13 @@ export default function NonIntegratedDetailClient({ current, supplierCodeByName, ); + const targetSkuIds = getPackageFactoryTargetSkuIds( + derivedSkuItems, + skuId, + key, + ); return currentRows.map((row) => { - if (row.skuId !== skuId || row.confirmed) return row; + if (!targetSkuIds.has(row.skuId) || row.confirmed) return row; const derivedSkuItem = derivedSkuItems.find( (item) => item.id === row.skuId, ); @@ -14239,17 +14669,12 @@ export default function NonIntegratedDetailClient({ const supplier = normalizeFactorySupplier(value, supplierCodeByName); const supplierName = supplier.name || value; const supplierCode = supplier.code; - const importedContentLocked = - derivedSkuItem !== undefined && - isImportedSkuContentLocked(derivedSkuItem); return { ...row, supplierName, supplierCode, - barcode: importedContentLocked - ? row.barcode.trim() || derivedSkuItem?.barcode.trim() || "" - : buildAutoBarcode(row.skuCode, supplierCode, versionNo), + barcode: buildAutoBarcode(row.skuCode, supplierCode, versionNo), }; } @@ -14464,6 +14889,40 @@ export default function NonIntegratedDetailClient({ setSaveMessageType("success"); } + function updateFinishedSampleShipmentRow( + skuId: string, + key: FinishedSampleShipmentEditableKey, + value: string, + ) { + const canEditField = + key === "sampleQuantity" + ? canEditFinishedSampleShipmentQuantity + : canEditFinishedSampleShipmentShipping; + if (!canEditField) return; + + setFinishedSampleShipmentRows((current) => { + const rowBySkuId = new Map(current.map((row) => [row.skuId, row])); + const nextRow = { + skuId, + sampleQuantity: "", + shipped: "", + trackingNumber: "", + ...rowBySkuId.get(skuId), + [key]: value, + }; + + rowBySkuId.set(skuId, nextRow); + + return derivedSkuItems + .map((sku) => rowBySkuId.get(sku.id)) + .filter( + (row): row is NonIntegratedFinishedSampleShipmentRow => Boolean(row), + ); + }); + setSaveMessage(""); + setSaveMessageType("success"); + } + function updateFirstQualityRow( skuId: string, key: Exclude, @@ -14600,7 +15059,8 @@ export default function NonIntegratedDetailClient({ if ( isReadOnly || !isCurrentUserAdmin || - displayedProductFactoryRows.length === 0 + displayedProductFactoryRows.length === 0 || + displayedProductFactoryHasPendingRows ) { return; } @@ -15137,57 +15597,84 @@ export default function NonIntegratedDetailClient({ function handleSetCountCommit(targetId: string) { if (isReadOnly) return; - setSkuItems((current) => { - const targetIndex = current.findIndex((item) => item.id === targetId); - if (targetIndex < 0) return current; - - const targetItem = current[targetIndex]; - if ( - isStockProductRecord || - isSkuBatchConfirmed(targetItem.batchNo || 1) || - isImportedSkuContentLocked(targetItem) - ) { - return current; - } - const currentGroupId = targetItem.packageGroupId || targetItem.id; - const groupStartIndex = targetItem.packageGroupId - ? current.findIndex((item) => item.packageGroupId === currentGroupId) - : targetIndex; - const groupItems = current.filter((item) => - targetItem.packageGroupId - ? item.packageGroupId === currentGroupId - : item.id === targetId, - ); - const anchorItem = groupItems[0] ?? targetItem; - const nextRows = createPackageRowsFromItem( - { - ...anchorItem, - ...targetItem, - id: anchorItem.id, - packageGroupId: targetItem.packageGroupId, - packageSuffix: targetItem.packageSuffix, - }, - normalizeSetCountValue(targetItem.setCount), - ); - - const beforeGroup = current.filter((item, index) => { - if (targetItem.packageGroupId) { - return item.packageGroupId !== currentGroupId; - } - - return index !== targetIndex; - }); - - return [ - ...beforeGroup.slice(0, groupStartIndex), - ...nextRows, - ...beforeGroup.slice(groupStartIndex), - ]; - }); + setSkuItems((current) => + buildSkuItemsAfterSetCountCommit(current, targetId, { + allowImportedContentLocked: false, + }), + ); setSaveMessage(""); setSaveMessageType("success"); } + function handleProductFactorySetCountCommit(targetId: string) { + if (isReadOnly) return; + const currentFactoryRow = productFactoryRows.find( + (row) => row.skuId === targetId, + ); + if (currentFactoryRow?.confirmed) return; + + setSkuItems((current) => + buildSkuItemsAfterSetCountCommit(current, targetId, { + allowImportedContentLocked: true, + }), + ); + setSaveMessage(""); + setSaveMessageType("success"); + } + + function buildSkuItemsAfterSetCountCommit( + current: NonIntegratedSkuItem[], + targetId: string, + options: { allowImportedContentLocked: boolean }, + ) { + const targetIndex = current.findIndex((item) => item.id === targetId); + if (targetIndex < 0) return current; + + const targetItem = current[targetIndex]; + if ( + isStockProductRecord || + isSkuItemFactoryConfirmed(targetItem) || + (!options.allowImportedContentLocked && + isImportedSkuContentLocked(targetItem)) + ) { + return current; + } + const currentGroupId = targetItem.packageGroupId || targetItem.id; + const groupStartIndex = targetItem.packageGroupId + ? current.findIndex((item) => item.packageGroupId === currentGroupId) + : targetIndex; + const groupItems = current.filter((item) => + targetItem.packageGroupId + ? item.packageGroupId === currentGroupId + : item.id === targetId, + ); + const anchorItem = groupItems[0] ?? targetItem; + const nextRows = createPackageRowsFromItem( + { + ...anchorItem, + ...targetItem, + id: anchorItem.id, + packageGroupId: targetItem.packageGroupId, + packageSuffix: targetItem.packageSuffix, + }, + normalizeSetCountValue(targetItem.setCount), + ); + + const beforeGroup = current.filter((item, index) => { + if (targetItem.packageGroupId) { + return item.packageGroupId !== currentGroupId; + } + + return index !== targetIndex; + }); + + return [ + ...beforeGroup.slice(0, groupStartIndex), + ...nextRows, + ...beforeGroup.slice(groupStartIndex), + ]; + } + async function handlePreviewImageChange( event: ChangeEvent, ) { @@ -15240,7 +15727,9 @@ export default function NonIntegratedDetailClient({ skuItems.some( (item) => item.id === targetId && - (isStockProductRecord || isImportedSkuContentLocked(item)), + (isStockProductRecord || + isSkuItemFactoryConfirmed(item) || + isImportedSkuContentLocked(item)), ) ) { event.target.value = ""; @@ -15317,7 +15806,11 @@ export default function NonIntegratedDetailClient({ setSkuItems((current) => current.map((item, index) => { - if (isStockProductRecord || isImportedSkuContentLocked(item)) { + if ( + isStockProductRecord || + isSkuItemFactoryConfirmed(item) || + isImportedSkuContentLocked(item) + ) { return item; } const derivedItem = deriveSkuItem( @@ -15381,7 +15874,12 @@ export default function NonIntegratedDetailClient({ return; } - if (isReadOnly || isStockProductRecord || isImportedSkuContentLocked(item)) { + if ( + isReadOnly || + isStockProductRecord || + isSkuItemFactoryConfirmed(item) || + isImportedSkuContentLocked(item) + ) { return; } @@ -15871,6 +16369,24 @@ export default function NonIntegratedDetailClient({ ); } + async function handleConfirmFinishedSampleShipmentRows() { + if ( + !canEditFinishedSampleShipmentShipping || + displayedFinishedSampleShipmentRows.length === 0 + ) + return; + if (!displayedFinishedSampleShipmentRowsComplete) { + setSaveMessage("请先填完整成品寄样数据"); + setSaveMessageType("error"); + return; + } + if (displayedFinishedSampleShipmentIsConfirmed) return; + + await handleConfirmDocumentWorkspaceCategory( + FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY, + ); + } + async function handleConfirmProductPricingRows() { if (isReadOnly || displayedProductPricingRows.length === 0) return; if (!displayedProductPricingRowsComplete) { @@ -17224,8 +17740,10 @@ export default function NonIntegratedDetailClient({ function handleAddSku(targetBatchNo = skuBatchCount) { if (isSkuContentReadOnly) return; - if (isSkuBatchConfirmed(targetBatchNo)) { - setSaveMessage(`批次${targetBatchNo}的 SKU 信息已确认,不能继续新增 SKU。`); + if (isSkuBatchFactoryConfirmed(targetBatchNo)) { + setSaveMessage( + `批次${targetBatchNo}已定厂,SKU内容不能继续新增;如需修改请先由管理员重新打开产品定厂。`, + ); setSaveMessageType("error"); return; } @@ -17309,112 +17827,6 @@ export default function NonIntegratedDetailClient({ setSaveMessageType("success"); } - async function handleConfirmSkuContent() { - if (!activeSkuBatchNo || isSkuContentReadOnly) return; - - if (isActiveSkuBatchConfirmed) { - showSkuConfirmationPrompt({ - type: "success", - message: `批次${activeSkuBatchNo}的 SKU 信息已确认。`, - }); - return; - } - - if (isActiveSkuConfirmationPending) { - const confirmedAt = new Date().toISOString(); - const nextSkuBatchConfirmedAt = { - ...skuBatchConfirmedAt, - [String(activeSkuBatchNo)]: confirmedAt, - }; - - setSkuBatchConfirmedAt(nextSkuBatchConfirmedAt); - setPendingSkuConfirmationBatch(null); - - const saved = await persistEditedProduct({ - successMessage: `批次${activeSkuBatchNo} SKU信息已确认`, - errorFallbackMessage: "SKU信息确认保存失败,请稍后再试", - overrides: { - skuBatchConfirmedAt: nextSkuBatchConfirmedAt, - }, - }); - - if (!saved) { - setSkuBatchConfirmedAt(skuBatchConfirmedAt); - } else { - showSkuConfirmationPrompt({ - type: "success", - message: `批次${activeSkuBatchNo} SKU信息已确认,当前批次已锁定。`, - }); - } - return; - } - - const issues = getSkuBatchValidationIssues(activeSkuBatchNo); - if (issues.length > 0) { - const message = `SKU信息未填写完整,暂不能确认:${issues.slice(0, 3).join(";")}${ - issues.length > 3 ? `;还有 ${issues.length - 3} 项` : "" - }`; - setPendingSkuConfirmationBatch(null); - showSkuConfirmationPrompt({ type: "error", message }); - setSaveMessage(message); - setSaveMessageType("error"); - return; - } - - if (pendingSkuConfirmationBatch !== activeSkuBatchNo) { - const message = `批次${activeSkuBatchNo} SKU 信息已检查完整。再次点击“确认SKU信息”后,本批次 SKU 表和批次信息会锁定。`; - setPendingSkuConfirmationBatch(activeSkuBatchNo); - showSkuConfirmationPrompt({ type: "success", message }); - setSaveMessage(message); - setSaveMessageType("success"); - return; - } - } - - async function handleReopenSkuContentConfirmation() { - if ( - !activeSkuBatchNo || - !isCurrentUserAdmin || - !isActiveSkuBatchConfirmed || - isSkuContentReadOnly - ) { - return; - } - - if (isSampleMeasurementNodeApproved) { - const message = "样包测量节点已确认,不能重新确认 SKU 信息。"; - showSkuConfirmationPrompt({ type: "error", message }); - setSaveMessage(message); - setSaveMessageType("error"); - return; - } - - const batchKey = String(activeSkuBatchNo); - const nextSkuBatchConfirmedAt = { ...skuBatchConfirmedAt }; - delete nextSkuBatchConfirmedAt[batchKey]; - - setSkuBatchConfirmedAt(nextSkuBatchConfirmedAt); - setPendingSkuConfirmationBatch(null); - - const saved = await persistEditedProduct({ - successMessage: `批次${activeSkuBatchNo} SKU信息已重新打开`, - errorFallbackMessage: "SKU信息重新打开保存失败,请稍后再试", - overrides: { - skuBatchConfirmedAt: nextSkuBatchConfirmedAt, - }, - }); - - if (!saved) { - setSkuBatchConfirmedAt(skuBatchConfirmedAt); - return; - } - - showSkuConfirmationPrompt({ - type: "success", - message: `批次${activeSkuBatchNo} SKU信息已重新打开,可修改后再次确认。`, - }); - } - const displayedSkuItems = useMemo( () => skuItems.filter((item) => @@ -17436,14 +17848,14 @@ export default function NonIntegratedDetailClient({ } return ( - !isSkuBatchConfirmed(rowBatchNo) && + !isSkuBatchFactoryConfirmed(rowBatchNo) && !isImportedSkuContentLocked(item) ); }), [ activeSkuBatch, derivedSkuItems, - isSkuBatchConfirmed, + isSkuBatchFactoryConfirmed, isSkuContentReadOnly, skuItems, ], @@ -17451,7 +17863,7 @@ export default function NonIntegratedDetailClient({ const canDeleteActiveSkuBatch = isCurrentUserAdmin && !isSkuContentReadOnly && - !isActiveSkuBatchConfirmed && + !isActiveSkuBatchFactoryConfirmed && !pendingSkuBatchDraft && activeSkuBatch !== "all" && activeSkuBatch > 1 && @@ -17459,6 +17871,13 @@ export default function NonIntegratedDetailClient({ function handleAddSkuBatch() { if (isSkuContentReadOnly) return; + if (isSkuBatchFactoryConfirmed(skuBatchCount)) { + setSaveMessage( + `批次${skuBatchCount}已定厂,SKU内容不能继续新增批次;如需修改请先由管理员重新打开产品定厂。`, + ); + setSaveMessageType("error"); + return; + } const latestBatchHasSku = skuItems.some( (item) => (item.batchNo || 1) === skuBatchCount, ); @@ -17556,8 +17975,8 @@ export default function NonIntegratedDetailClient({ } const targetBatchNo = activeSkuBatch; - if (isSkuBatchConfirmed(targetBatchNo)) { - setSaveMessage(`批次${targetBatchNo}的 SKU 信息已确认,不能删除批次。`); + if (isSkuBatchFactoryConfirmed(targetBatchNo)) { + setSaveMessage(`批次${targetBatchNo}已定厂,不能删除批次。`); setSaveMessageType("error"); return; } @@ -17614,7 +18033,7 @@ export default function NonIntegratedDetailClient({ function handleUpdateSkuBatchChangeType(batchNo: number, value: string) { if (isReadOnly) return; - if (isSkuBatchConfirmed(batchNo)) return; + if (isSkuBatchFactoryConfirmed(batchNo)) return; setSkuBatchChangeTypes((current) => ({ ...current, [String(batchNo)]: value, @@ -17625,7 +18044,7 @@ export default function NonIntegratedDetailClient({ function handleUpdateSkuBatchFlowNo(batchNo: number, value: string) { if (isReadOnly) return; - if (isSkuBatchConfirmed(batchNo)) return; + if (isSkuBatchFactoryConfirmed(batchNo)) return; setSkuBatchFlowNos((current) => ({ ...current, [String(batchNo)]: value, @@ -17644,13 +18063,23 @@ export default function NonIntegratedDetailClient({ })); } + function handleFinishedSampleShipmentColumnResize( + column: FinishedSampleShipmentColumnWidthKey, + width: number, + ) { + setFinishedSampleShipmentColumnWidths((current) => ({ + ...current, + [column]: normalizeFinishedSampleShipmentColumnWidth(column, width), + })); + } + function handleDeleteSku(id: string) { if (isReadOnly) return; const targetItem = skuItems.find((item) => item.id === id); if (!targetItem) return; if ( isStockProductRecord || - isSkuBatchConfirmed(targetItem.batchNo || 1) || + isSkuItemFactoryConfirmed(targetItem) || isImportedSkuContentLocked(targetItem) ) return; @@ -17758,8 +18187,8 @@ export default function NonIntegratedDetailClient({ event: ChangeEvent, ) { if (isReadOnly) return; - if (activeSkuBatchNo && isSkuBatchConfirmed(activeSkuBatchNo)) { - setSaveMessage(`批次${activeSkuBatchNo}的 SKU 信息已确认,不能上传资料。`); + if (activeSkuBatchNo && isSkuBatchFactoryConfirmed(activeSkuBatchNo)) { + setSaveMessage(`批次${activeSkuBatchNo}已定厂,不能上传SKU资料。`); setSaveMessageType("error"); event.target.value = ""; return; @@ -18551,6 +18980,7 @@ export default function NonIntegratedDetailClient({ planningLayoutRows, productLimitRows, productSampleMeasurementRows, + productFinishedSampleShipmentRows, productFirstQualityRows, uploadFiles, documentWorkspaceNotes, @@ -19497,17 +19927,16 @@ export default function NonIntegratedDetailClient({ >([ "model", "skuCode", - "setCount", "barcode", "specName", "material", - "installMethod", "executeStandard", ]); const renderFactoryInput = ( row: NonIntegratedFactoryRow, key: Exclude, placeholder = "", + options: { onBlur?: () => void } = {}, ) => { const isSkuBackedField = skuBackedFactoryKeys.has(key); @@ -19519,6 +19948,7 @@ export default function NonIntegratedDetailClient({ mode="singleLineResizable" tone={isSkuBackedField ? "default" : "manual"} onChange={(value) => updateProductFactoryRow(row.skuId, key, value)} + onBlur={options.onBlur} /> ); }; @@ -19621,7 +20051,10 @@ export default function NonIntegratedDetailClient({ {renderFactoryInput(row, "skuCode", "请输入规格码")} - {renderFactoryInput(row, "setCount", "请输入套件量")} + {renderFactoryInput(row, "setCount", "请输入套件量", { + onBlur: () => + handleProductFactorySetCountCommit(row.skuId), + })} {renderFactoryInput(row, "barcode", "请输入条码")} @@ -19671,7 +20104,17 @@ export default function NonIntegratedDetailClient({ )} - {renderFactoryInput(row, "leadTime", "请输入交期")} + + updateProductFactoryRow( + row.skuId, + "leadTime", + value, + ) + } + /> updateProductFactoryRow( row.skuId, @@ -19797,8 +20240,17 @@ export default function NonIntegratedDetailClient({ } function renderSampleMeasurementWorkspace() { - const renderReadOnlyCell = (value: string) => ( -
+ const renderReadOnlyCell = ( + value: string, + minWidth = "112px", + testId?: string, + ) => ( +
{value || "-"}
); @@ -19884,10 +20336,14 @@ export default function NonIntegratedDetailClient({ {renderReadOnlyCell(row.supplierName)} - {renderReadOnlyCell(row.barcode)} + {renderReadOnlyCell(row.barcode, "160px")} - {renderReadOnlyCell(row.specName)} + {renderReadOnlyCell( + row.specName, + "180px", + `sample-measurement-readonly-specName-${row.skuId}`, + )} {renderMeasurementInput(row, "lengthCm", "请输入长")} @@ -19919,6 +20375,238 @@ export default function NonIntegratedDetailClient({ ); } + function renderFinishedSampleShipmentWorkspace() { + const renderReadOnlyCell = ( + value: string, + column: FinishedSampleShipmentColumnWidthKey, + ) => ( + + handleFinishedSampleShipmentColumnResize(column, width) + } + onChange={() => undefined} + /> + ); + const renderImageCell = ( + row: ProductFinishedSampleShipmentDisplayRow, + index: number, + ) => { + if (!row.image) { + return ( +
+ 无图 +
+ ); + } + + return ( + + ); + }; + const renderShipmentInput = ( + row: ProductFinishedSampleShipmentDisplayRow, + key: Extract< + FinishedSampleShipmentEditableKey, + "sampleQuantity" | "trackingNumber" + >, + placeholder: string, + readOnly: boolean, + column: Extract< + FinishedSampleShipmentColumnWidthKey, + "sampleQuantity" | "trackingNumber" + >, + ) => ( + + handleFinishedSampleShipmentColumnResize(column, width) + } + testId={`finished-sample-shipment-${key}-${row.skuId}`} + onChange={(value) => + updateFinishedSampleShipmentRow(row.skuId, key, value) + } + /> + ); + + return ( +
+
+
+ {renderInheritedSkuBatchTabs()} +
+
+ +
+ {productFinishedSampleShipmentRows.length === 0 ? ( +
+ 当前还没有 SKU 信息,先维护 SKU 后再进行成品寄样。 +
+ ) : displayedFinishedSampleShipmentRows.length === 0 ? ( +
+ 当前批次还没有 SKU 信息。 +
+ ) : ( +
+ + + + {[ + "寄样", + "序号", + "图片", + "型号", + "规格码", + "套件量", + "供应商名称", + "条码", + "条码规格名称", + "安装方式", + "寄样数量", + "是否寄出", + "快递单号", + ].map((label) => ( + + ))} + + + + {displayedFinishedSampleShipmentRows.map((row, index) => ( + + + + + + + + + + + + + + + + ))} + +
+ {label} +
+ + {displayedFinishedSampleShipmentIsConfirmed + ? "已确认" + : "待确认"} + + + {index + 1} + {renderImageCell(row, index)} + {renderReadOnlyCell(row.model, "model")} + + {renderReadOnlyCell(row.skuCode, "skuCode")} + + {renderReadOnlyCell(row.setCount, "setCount")} + + {renderReadOnlyCell(row.supplierName, "supplierName")} + + {renderReadOnlyCell(row.barcode, "barcode")} + + {renderReadOnlyCell(row.specName, "specName")} + + {renderReadOnlyCell(row.installMethod, "installMethod")} + + {renderShipmentInput( + row, + "sampleQuantity", + "请输入数量", + !canEditFinishedSampleShipmentQuantity, + "sampleQuantity", + )} + + + handleFinishedSampleShipmentColumnResize( + "shipped", + width, + ) + } + > + + updateFinishedSampleShipmentRow( + row.skuId, + "shipped", + value, + ) + } + buttonClassName="h-8 w-full min-w-0 rounded border border-apple-separator bg-apple-bg-secondary px-2 text-xs text-apple-label-primary outline-none disabled:cursor-not-allowed disabled:bg-apple-bg-primary disabled:text-apple-label-secondary" + /> + + + {renderShipmentInput( + row, + "trackingNumber", + "请输入快递单号", + !canEditFinishedSampleShipmentShipping, + "trackingNumber", + )} +
+
+ )} +
+
+ ); + } + function renderPlanningLayoutWorkspace() { const renderReadOnlyCell = (value: string, minWidth = "112px") => (
{derivedSkuItems.length === 0 ? "当前还没有 SKU 信息,先维护 SKU 后再查看空间布局。" - : "当前没有销售状态为“销售”的 SKU。"} + : "当前还没有可带出的 SKU 信息。"}
) : displayedPlanningLayoutSkuRows.length === 0 ? (
@@ -24449,7 +25137,7 @@ function renderFactoryQualityWorkspace() { {!isSkuContentReadOnly && activeSkuBatchNo && - !isActiveSkuBatchConfirmed ? ( + !isActiveSkuBatchFactoryConfirmed ? ( <> - ) : null} - {activeSkuTab === "skuInfo" ? ( - isActiveSkuBatchConfirmed ? ( - - 已确认SKU信息 - - ) : !isSkuContentReadOnly && - activeSkuBatchNo ? ( - - ) : !isSkuContentReadOnly ? ( - - ) : null - ) : null} -
+ {activeSkuTab === "skuInfo" && + isStockProductRecord ? ( +
+ + 已确认SKU信息 + +
+ ) : null}
- {activeSkuTab === "skuInfo" && - skuConfirmationPrompt ? ( -
- {skuConfirmationPrompt.message} -
- ) : null} {activeSkuTab === "componentSkuInfo" ? (
@@ -24944,7 +25560,9 @@ function renderFactoryQualityWorkspace() { placeholder="请选择" readOnly={ isSkuContentReadOnly || - isSkuBatchConfirmed(activeSkuBatch) + isSkuBatchFactoryConfirmed( + activeSkuBatch, + ) } onChange={(value) => handleUpdateSkuBatchChangeType( @@ -24966,7 +25584,9 @@ function renderFactoryQualityWorkspace() { } readOnly={ isSkuContentReadOnly || - isSkuBatchConfirmed(activeSkuBatch) + isSkuBatchFactoryConfirmed( + activeSkuBatch, + ) } onChange={(event) => handleUpdateSkuBatchFlowNo( @@ -25068,13 +25688,12 @@ function renderFactoryQualityWorkspace() { : derivedItem.drawingId; const skuRowReadOnly = isSkuContentReadOnly || - isSkuBatchConfirmed( - derivedItem.batchNo || 1, - ) || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item); const skuImageReadOnly = isReadOnly || isStockProductRecord || + isSkuItemFactoryConfirmed(item) || isImportedSkuContentLocked(item); return ( @@ -26796,6 +27415,7 @@ function renderFactoryQualityWorkspace() { isProductPhotoWorkspace || isProductFactoryWorkspace || isSampleMeasurementWorkspace || + isFinishedSampleShipmentWorkspace || isProductPricingWorkspace || isProductLimitWorkspace || isPlanningLayoutWorkspace @@ -26814,17 +27434,25 @@ function renderFactoryQualityWorkspace() {
) : isProductFactoryWorkspace ? (
- {isCurrentUserAdmin && - displayedProductFactoryRows.length > 0 && - !displayedProductFactoryHasPendingRows ? ( + {isCurrentUserAdmin ? (
+ ) : isFinishedSampleShipmentWorkspace ? ( +
+ +
) : isProductPricingWorkspace ? (