From c5a6f044c7e936c32324b66441d73ea9555e503e Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Wed, 20 May 2026 14:17:39 +0800 Subject: [PATCH] feat: refine non-integrated intake workflow --- .../app/api/intake/designer-options/route.ts | 20 +- .../app/api/intake/operation-options/route.ts | 5 +- .../app/api/intake/planning-options/route.ts | 5 +- .../api/intake/purchasing-options/route.ts | 5 +- .../src/app/api/intake/space-options/route.ts | 22 + .../src/app/intake/IntakeClient.tsx | 1 - .../NonIntegratedDetailClient.test.tsx | 289 +++++++++++- .../NonIntegratedDetailClient.tsx | 431 +++++++++++------- .../non-integrated/nonIntegratedStorage.ts | 2 - .../services/intake-owner-options.service.ts | 54 ++- 10 files changed, 626 insertions(+), 208 deletions(-) create mode 100644 furniture-3d-system/src/app/api/intake/space-options/route.ts diff --git a/furniture-3d-system/src/app/api/intake/designer-options/route.ts b/furniture-3d-system/src/app/api/intake/designer-options/route.ts index 88cd02e0..5c7342c9 100644 --- a/furniture-3d-system/src/app/api/intake/designer-options/route.ts +++ b/furniture-3d-system/src/app/api/intake/designer-options/route.ts @@ -1,29 +1,17 @@ import { NextRequest } from "next/server"; import { handleRequest, jsonSuccess } from "@/server/core/response"; -import { userService } from "@/server/services/user.service"; - -function getUniqueNames(values: Array) { - return Array.from( - new Set( - values - .map((value) => value?.trim() ?? "") - .filter((value) => value.length > 0), - ), - ); -} +import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service"; export async function GET(request: NextRequest) { return handleRequest( request, async (ctx) => { - const designers = await userService.listAll({ - businessRole: "DESIGN", - status: "ACTIVE", - }); + const names = + await listIntakeOwnerOptionNamesByDepartmentScope("productResearch"); return jsonSuccess( { - names: getUniqueNames(designers.map((designer) => designer.name)), + names, }, 200, ctx.requestId, diff --git a/furniture-3d-system/src/app/api/intake/operation-options/route.ts b/furniture-3d-system/src/app/api/intake/operation-options/route.ts index 6e68c06a..3fb44d5a 100644 --- a/furniture-3d-system/src/app/api/intake/operation-options/route.ts +++ b/furniture-3d-system/src/app/api/intake/operation-options/route.ts @@ -1,12 +1,13 @@ import { NextRequest } from "next/server"; import { handleRequest, jsonSuccess } from "@/server/core/response"; -import { listIntakeOwnerOptionNamesByCenter } from "@/server/services/intake-owner-options.service"; +import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service"; export async function GET(request: NextRequest) { return handleRequest( request, async (ctx) => { - const names = await listIntakeOwnerOptionNamesByCenter("operation"); + const names = + await listIntakeOwnerOptionNamesByDepartmentScope("operation"); return jsonSuccess( { diff --git a/furniture-3d-system/src/app/api/intake/planning-options/route.ts b/furniture-3d-system/src/app/api/intake/planning-options/route.ts index 16eca5c3..816e34ec 100644 --- a/furniture-3d-system/src/app/api/intake/planning-options/route.ts +++ b/furniture-3d-system/src/app/api/intake/planning-options/route.ts @@ -1,12 +1,13 @@ import { NextRequest } from "next/server"; import { handleRequest, jsonSuccess } from "@/server/core/response"; -import { listIntakeOwnerOptionNamesByCenter } from "@/server/services/intake-owner-options.service"; +import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service"; export async function GET(request: NextRequest) { return handleRequest( request, async (ctx) => { - const names = await listIntakeOwnerOptionNamesByCenter("brand"); + const names = + await listIntakeOwnerOptionNamesByDepartmentScope("brandPlanning"); return jsonSuccess( { diff --git a/furniture-3d-system/src/app/api/intake/purchasing-options/route.ts b/furniture-3d-system/src/app/api/intake/purchasing-options/route.ts index 37f4f2ab..24b2dcd5 100644 --- a/furniture-3d-system/src/app/api/intake/purchasing-options/route.ts +++ b/furniture-3d-system/src/app/api/intake/purchasing-options/route.ts @@ -1,12 +1,13 @@ import { NextRequest } from "next/server"; import { handleRequest, jsonSuccess } from "@/server/core/response"; -import { listIntakeOwnerOptionNamesByCenter } from "@/server/services/intake-owner-options.service"; +import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service"; export async function GET(request: NextRequest) { return handleRequest( request, async (ctx) => { - const names = await listIntakeOwnerOptionNamesByCenter("purchasing"); + const names = + await listIntakeOwnerOptionNamesByDepartmentScope("purchasing"); return jsonSuccess( { diff --git a/furniture-3d-system/src/app/api/intake/space-options/route.ts b/furniture-3d-system/src/app/api/intake/space-options/route.ts new file mode 100644 index 00000000..34ce3383 --- /dev/null +++ b/furniture-3d-system/src/app/api/intake/space-options/route.ts @@ -0,0 +1,22 @@ +import { NextRequest } from "next/server"; +import { handleRequest, jsonSuccess } from "@/server/core/response"; +import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service"; + +export async function GET(request: NextRequest) { + return handleRequest( + request, + async (ctx) => { + const names = + await listIntakeOwnerOptionNamesByDepartmentScope("spaceVisual"); + + return jsonSuccess( + { + names, + }, + 200, + ctx.requestId, + ); + }, + { requireAuth: true }, + ); +} diff --git a/furniture-3d-system/src/app/intake/IntakeClient.tsx b/furniture-3d-system/src/app/intake/IntakeClient.tsx index 2c00225e..72a142fc 100644 --- a/furniture-3d-system/src/app/intake/IntakeClient.tsx +++ b/furniture-3d-system/src/app/intake/IntakeClient.tsx @@ -1906,7 +1906,6 @@ function getPopulatedSampleMeasurementRowCountForList( row.widthCm, row.heightCm, row.weightKg, - row.productShape, ].some((value) => value.trim().length > 0), ).length; } 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 6dcd2afb..3dc2d88d 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 @@ -1935,6 +1935,8 @@ describe("NonIntegratedDetailClient", () => { data: { names: url.includes("planning-options") ? ["企划A", "企划B"] + : url.includes("space-options") + ? ["空间A", "空间B"] : url.includes("operation-options") ? ["运营A", "运营B"] : url.includes("purchasing-options") @@ -1977,6 +1979,7 @@ describe("NonIntegratedDetailClient", () => { await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith("/api/intake/designer-options"); expect(fetchMock).toHaveBeenCalledWith("/api/intake/planning-options"); + expect(fetchMock).toHaveBeenCalledWith("/api/intake/space-options"); expect(fetchMock).toHaveBeenCalledWith("/api/intake/operation-options"); expect(fetchMock).toHaveBeenCalledWith( "/api/intake/purchasing-options", @@ -1992,6 +1995,7 @@ describe("NonIntegratedDetailClient", () => { expect(designerOptions).toHaveTextContent("陆南亭"); expect(designerOptions).toHaveTextContent("结构A"); expect(designerOptions).toHaveTextContent("徐霞客"); + expect(designerOptions).not.toHaveTextContent("空间A"); }); await actor.click(structureDesignerInput); @@ -2021,6 +2025,17 @@ describe("NonIntegratedDetailClient", () => { expect(savedProduct?.openingReference.designer).toBe("手动设计师"); expect(savedProduct?.openingReference.planningOwner).toBe("手动企划"); }); + + await actor.click(screen.getByTestId("document-workspace-basic-info-trigger")); + const spaceDesignerInput = screen.getByLabelText("空间设计师"); + await actor.click(spaceDesignerInput); + await waitFor(() => { + const spaceDesignerOptions = screen.getByRole("listbox"); + expect(spaceDesignerOptions).toHaveTextContent("空间A"); + expect(spaceDesignerOptions).toHaveTextContent("空间B"); + expect(spaceDesignerOptions).not.toHaveTextContent("陆南亭"); + expect(spaceDesignerOptions).not.toHaveTextContent("企划A"); + }); } finally { Object.defineProperty(global, "fetch", { configurable: true, @@ -2644,6 +2659,38 @@ describe("NonIntegratedDetailClient", () => { target: { value: "2026-06-20" }, }); + const productionTypeSelects = within(factoryWorkspace).getAllByRole( + "combobox", + { name: "产品生产类型" }, + ); + expect(productionTypeSelects).toHaveLength(3); + await selectInlineOption(actor, productionTypeSelects[1]!, "ODM-Q"); + + const installationServiceSelects = within(factoryWorkspace).getAllByRole( + "combobox", + { name: "安装服务" }, + ); + expect(installationServiceSelects).toHaveLength(3); + await selectInlineOption(actor, installationServiceSelects[2]!, "安装"); + + const supplierCategorySelects = within(factoryWorkspace).getAllByRole( + "combobox", + { name: "供应商分类" }, + ); + expect(supplierCategorySelects).toHaveLength(3); + await selectInlineOption(actor, supplierCategorySelects[0]!, "辅力供应商"); + + const purchaseOrderMethodSelects = within(factoryWorkspace).getAllByRole( + "combobox", + { name: "采购下单方式" }, + ); + expect(purchaseOrderMethodSelects).toHaveLength(3); + await selectInlineOption( + actor, + purchaseOrderMethodSelects[1]!, + "特殊订单式", + ); + await waitFor(() => { const savedProduct = getNonIntegratedProductById( "ZWJ0218-factory-editable-fields", @@ -2677,6 +2724,22 @@ describe("NonIntegratedDetailClient", () => { "2026年06月20日", "2026年06月20日", ]); + expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ + "ODM-Q", + "ODM-Q", + "ODM-Q", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.installationService), + ).toEqual(["安装", "安装", "安装"]); + expect(savedProduct?.factoryRows.map((row) => row.supplierCategory)).toEqual([ + "辅力供应商", + "辅力供应商", + "辅力供应商", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.purchaseOrderMethod), + ).toEqual(["特殊订单式", "特殊订单式", "特殊订单式"]); }); const installMethodSelect = within(factoryWorkspace).getAllByRole("combobox", { @@ -2708,6 +2771,226 @@ describe("NonIntegratedDetailClient", () => { }); }); + it("shares product factory selection fields by model without changing SKU content", async () => { + const actor = userEvent.setup(); + + saveNonIntegratedProduct( + createProductRecord({ + id: "SZ0230-factory-model-shared-fields", + spu: "SZ0230", + skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT, + skuItems: [ + createSkuItem({ + id: "sku-factory-model-shared-1", + skuSequence: 1, + model: "SZ0230K01", + manufacturer: "曹县嘉欢", + manufacturerCode: "N396", + installMethod: "整装", + }), + createSkuItem({ + id: "sku-factory-model-shared-2", + skuSequence: 2, + model: "SZ0230K01", + manufacturer: "曹县嘉欢", + manufacturerCode: "N396", + installMethod: "整装", + }), + createSkuItem({ + id: "sku-factory-model-shared-3", + skuSequence: 3, + model: "SZ0230K02", + manufacturer: "曹县嘉欢", + manufacturerCode: "N396", + installMethod: "整装", + }), + ], + }), + ); + + render( + , + ); + + await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂")); + + const factoryWorkspace = screen.getByTestId("product-factory-workspace"); + const warehouseInputs = within(factoryWorkspace).getAllByLabelText("仓库"); + expect(warehouseInputs).toHaveLength(3); + fireEvent.change(warehouseInputs[0]!, { target: { value: "郑州仓" } }); + + const minimumOrderQuantityInputs = + within(factoryWorkspace).getAllByPlaceholderText("请输入最低起订量"); + expect(minimumOrderQuantityInputs).toHaveLength(3); + fireEvent.change(minimumOrderQuantityInputs[0]!, { + target: { value: "5" }, + }); + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-factory-model-shared-fields", + ); + + expect( + savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity), + ).toEqual(["5", "5", ""]); + }); + fireEvent.change(minimumOrderQuantityInputs[0]!, { + target: { value: "50" }, + }); + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-factory-model-shared-fields", + ); + + expect( + savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity), + ).toEqual(["50", "50", ""]); + }); + fireEvent.change(minimumOrderQuantityInputs[1]!, { + target: { value: "80" }, + }); + + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "产品生产类型", + })[0]!, + "OEM", + ); + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "安装服务", + })[1]!, + "上楼", + ); + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "供应商分类", + })[0]!, + "主力供应商", + ); + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "采购下单方式", + })[1]!, + "订单式", + ); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-factory-model-shared-fields", + ); + + expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([ + "郑州仓", + "郑州仓", + "", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity), + ).toEqual(["50", "80", ""]); + expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ + "OEM", + "OEM", + "", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.installationService), + ).toEqual(["上楼", "上楼", ""]); + expect(savedProduct?.factoryRows.map((row) => row.supplierCategory)).toEqual([ + "主力供应商", + "主力供应商", + "", + ]); + expect( + savedProduct?.factoryRows.map((row) => row.purchaseOrderMethod), + ).toEqual(["订单式", "订单式", ""]); + expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([ + "曹县嘉欢", + "曹县嘉欢", + "曹县嘉欢", + ]); + expect(savedProduct?.skuItems.map((item) => item.manufacturerCode)).toEqual([ + "N396", + "N396", + "N396", + ]); + expect(savedProduct?.skuItems.map((item) => item.installMethod)).toEqual([ + "整装", + "整装", + "整装", + ]); + }); + + fireEvent.change(warehouseInputs[1]!, { target: { value: "杭州仓" } }); + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "产品生产类型", + })[1]!, + "ODM", + ); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-factory-model-shared-fields", + ); + + expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([ + "郑州仓", + "杭州仓", + "", + ]); + expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ + "OEM", + "ODM", + "", + ]); + expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([ + "曹县嘉欢", + "曹县嘉欢", + "曹县嘉欢", + ]); + expect(savedProduct?.skuItems.map((item) => item.installMethod)).toEqual([ + "整装", + "整装", + "整装", + ]); + }); + + fireEvent.change(warehouseInputs[1]!, { target: { value: "" } }); + await selectInlineOption( + actor, + within(factoryWorkspace).getAllByRole("combobox", { + name: "产品生产类型", + })[0]!, + "", + ); + + await waitFor(() => { + const savedProduct = getNonIntegratedProductById( + "SZ0230-factory-model-shared-fields", + ); + + expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([ + "郑州仓", + "", + "", + ]); + expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ + "", + "ODM", + "", + ]); + }); + }); + it("persists the SKU national standard compliance selection", async () => { const actor = userEvent.setup(); @@ -3142,6 +3425,7 @@ describe("NonIntegratedDetailClient", () => { expect(panel).toHaveTextContent("条码规格名称"); expect(panel).toHaveTextContent("长(CM)"); expect(panel).toHaveTextContent("重量(KG)"); + expect(panel).not.toHaveTextContent("商品外形"); expect(screen.getByText("SZ0230K01")).toBeInTheDocument(); expect(screen.getByText("SZ023001")).toBeInTheDocument(); expect(screen.getByText("SZ023001N1280V01")).toBeInTheDocument(); @@ -3177,10 +3461,6 @@ describe("NonIntegratedDetailClient", () => { screen.getByTestId("sample-measurement-weightKg-sku-sample-1"), "18.5", ); - await actor.type( - screen.getByTestId("sample-measurement-productShape-sku-sample-1"), - "长方形", - ); await waitFor(() => { const savedProduct = getNonIntegratedProductById( @@ -3193,7 +3473,6 @@ describe("NonIntegratedDetailClient", () => { widthCm: "60", heightCm: "75", weightKg: "18.5", - productShape: "长方形", }, ]); }); 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 deb53835..093e8d95 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -286,6 +286,10 @@ type ProductFactoryEditableKey = Exclude< keyof NonIntegratedFactoryRow, "skuId" | "confirmed" >; +type ProductFactoryModelAutofillEntry = { + sourceSkuId: string; + value: string; +}; type FinishedSampleShipmentEditableKey = Exclude< keyof NonIntegratedFinishedSampleShipmentRow, "skuId" @@ -374,28 +378,24 @@ 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 MODEL_SHARED_PRODUCT_FACTORY_KEYS = new Set([ + "warehouse", + "minimumOrderQuantity", + "productionType", + "installationService", + "supplierCategory", + "purchaseOrderMethod", +]); const SKU_COLUMN_MAX_WIDTH = 520; function normalizeSkuColumnWidth( @@ -2579,25 +2579,6 @@ const BASIC_INFO_CORE_FIELDS: FieldDefinition[] = [ }, ]; -const BASIC_INFO_PROCESS_FIELDS: FieldDefinition[] = [ - { key: "demandFlowNo", label: "需求流程号", placeholder: "后续回填" }, - { key: "processTemplate", label: "流程模板", placeholder: "后续回填" }, - { key: "demandDepartment", label: "需求部门", placeholder: "后续回填" }, - { key: "visualDirector", label: "视觉主管", placeholder: "后续回填" }, - { key: "patentOwner", label: "专利所有方", placeholder: "后续回填" }, - { key: "salesChannel", label: "销售渠道", placeholder: "后续回填" }, - { key: "salesPlatform", label: "销售平台", placeholder: "后续回填" }, - { key: "productionType", label: "产品生产类型", placeholder: "后续回填" }, - { key: "shippingMode", label: "发货模式", placeholder: "后续回填" }, - { key: "manualProducer", label: "说明书制作方", placeholder: "后续回填" }, - { - key: "isSelfDevelopedPatentProduct", - label: "是否自研专利产品", - placeholder: "后续回填", - }, - { key: "needsClearanceHandling", label: "是否处理清货", placeholder: "后续回填" }, -]; - const EMPTY_FORM_VALUES: FormValues = { spu: "", year: "", @@ -3183,6 +3164,21 @@ function getDefaultProductPhotoStyleKey(knownStyleKeys: string[]) { } const INSTALL_METHOD_OPTIONS = ["整装", "拆装"] as const; +const PRODUCT_FACTORY_PRODUCTION_TYPE_OPTIONS = ["OEM", "ODM", "ODM-Q"] as const; +const PRODUCT_FACTORY_INSTALLATION_SERVICE_OPTIONS = [ + "送货", + "上楼", + "安装", +] as const; +const PRODUCT_FACTORY_SUPPLIER_CATEGORY_OPTIONS = [ + "主力供应商", + "辅力供应商", +] as const; +const PRODUCT_FACTORY_PURCHASE_ORDER_METHOD_OPTIONS = [ + "非订单式", + "订单式", + "特殊订单式", +] as const; function normalizeInstallationModeLabel(value: string) { const normalizedValue = value.trim(); @@ -5012,6 +5008,17 @@ function getSkuPackageGroupKey(sku: Pick) { + return sku.model.trim(); +} + +function getProductFactoryModelAutofillKey( + skuId: string, + key: ProductFactoryEditableKey, +) { + return `${skuId}:${key}`; +} + function hasPackageSharedFactoryRowContent(row: NonIntegratedFactoryRow) { return Array.from(PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS).some((key) => row[key].trim(), @@ -5030,14 +5037,8 @@ function mergePackageSharedFactoryRow( 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, }; } @@ -5097,18 +5098,19 @@ function buildProductFactoryRows( const sharedRowByPackageGroupKey = new Map(); 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); + if (sku.packageGroupId.trim()) { + const groupKey = getSkuPackageGroupKey(sku); + const currentSharedRow = sharedRowByPackageGroupKey.get(groupKey); + if ( + !currentSharedRow || + (!hasPackageSharedFactoryRowContent(currentSharedRow) && + hasPackageSharedFactoryRowContent(savedRow)) + ) { + sharedRowByPackageGroupKey.set(groupKey, savedRow); + } } }); @@ -5512,7 +5514,6 @@ function createSampleMeasurementRowFromSku( widthCm: existingRow?.widthCm?.trim() ?? "", heightCm: existingRow?.heightCm?.trim() ?? "", weightKg: existingRow?.weightKg?.trim() ?? "", - productShape: existingRow?.productShape?.trim() ?? "", }; } @@ -5542,7 +5543,6 @@ function normalizeSampleMeasurementRowsForPayload( widthCm: row.widthCm.trim(), heightCm: row.heightCm.trim(), weightKg: row.weightKg.trim(), - productShape: row.productShape.trim(), })) .filter( (row) => @@ -5552,7 +5552,6 @@ function normalizeSampleMeasurementRowsForPayload( row.widthCm, row.heightCm, row.weightKg, - row.productShape, ].some(Boolean), ); } @@ -5565,7 +5564,6 @@ function isSampleMeasurementRowComplete( row.widthCm, row.heightCm, row.weightKg, - row.productShape, ].every((value) => value.trim()); } @@ -9811,6 +9809,9 @@ export default function NonIntegratedDetailClient({ const [factoryRows, setFactoryRows] = useState( existingProduct?.factoryRows ?? [], ); + const productFactoryModelAutofillRef = useRef( + new Map(), + ); const [pricingRows, setPricingRows] = useState( existingProduct?.pricingRows ?? [], ); @@ -9868,6 +9869,9 @@ export default function NonIntegratedDetailClient({ const [planningOwnerNameOptions, setPlanningOwnerNameOptions] = useState< string[] >([]); + const [spaceDesignerNameOptions, setSpaceDesignerNameOptions] = useState< + string[] + >([]); const [operationOwnerNameOptions, setOperationOwnerNameOptions] = useState< string[] >([]); @@ -10116,6 +10120,25 @@ export default function NonIntegratedDetailClient({ } } + async function loadSpaceDesignerNameOptions() { + try { + const response = await fetch("/api/intake/space-options"); + const payload = (await response.json()) as { + success?: boolean; + data?: { names?: Array }; + }; + const names = Array.isArray(payload.data?.names) + ? payload.data.names + : []; + + if (!cancelled && response.ok && payload.success) { + setSpaceDesignerNameOptions(getUniqueNonEmptyStrings(names)); + } + } catch { + // 候选加载失败时仍允许手动输入。 + } + } + async function loadPurchasingOwnerNameOptions() { try { const response = await fetch("/api/intake/purchasing-options"); @@ -10138,6 +10161,7 @@ export default function NonIntegratedDetailClient({ void Promise.all([ loadDesignerNameOptions(), loadPlanningOwnerNameOptions(), + loadSpaceDesignerNameOptions(), loadOperationOwnerNameOptions(), loadPurchasingOwnerNameOptions(), ]); @@ -11202,6 +11226,11 @@ export default function NonIntegratedDetailClient({ formValues.operator, formValues.operationsOwner, ]); + const basicInfoSpaceDesignerOptions = getUniqueNonEmptyStrings([ + ...spaceDesignerNameOptions, + formValues.visualPlanning, + formValues.visualDirector, + ]); const basicInfoPurchasingOwnerOptions = getUniqueNonEmptyStrings([ ...purchasingOwnerNameOptions, formValues.purchaseOwner, @@ -11664,8 +11693,7 @@ export default function NonIntegratedDetailClient({ row.widthCm, row.heightCm, row.weightKg, - row.productShape, - ].some((value) => value.trim()), + ].some((value) => (value ?? "").trim()), ).length, [productSampleMeasurementRows], ); @@ -12709,7 +12737,7 @@ export default function NonIntegratedDetailClient({ getProductPhotoStyleLabel(defaultProductPhotoStyleKey, derivedSkuItems); const documentWorkspaceTitle = `${getDocumentWorkspaceCategoryLabel(activeDocumentWorkspaceCategory)}工作区`; const documentWorkspaceDescription = isSampleMeasurementWorkspace - ? "从 SKU 信息自动带出基础字段;长宽高、重量和商品外形由采购填写并自动保存。" + ? "从 SKU 信息自动带出基础字段;长宽高和重量由采购填写并自动保存。" : isFinishedSampleShipmentWorkspace ? "从 SKU 信息自动带出基础字段;寄样数量开放填写,是否寄出和快递单号由采购填写并确认。" : isProductPhotoWorkspace @@ -14573,6 +14601,52 @@ export default function NonIntegratedDetailClient({ ); } + function getProductFactoryTargetSkuIds( + items: Pick[], + targetId: string, + key: ProductFactoryEditableKey, + savedRows: NonIntegratedFactoryRow[] = [], + value = "", + autofillEntries: ReadonlyMap = + new Map(), + ) { + const targetItem = items.find((item) => item.id === targetId); + if (!targetItem) return new Set([targetId]); + + if (MODEL_SHARED_PRODUCT_FACTORY_KEYS.has(key)) { + const targetModelKey = getSkuFactoryModelKey(targetItem); + if (!targetModelKey || !value.trim()) return new Set([targetId]); + const savedRowBySkuId = new Map( + savedRows.map((row) => [row.skuId, row]), + ); + const previousTargetValue = + savedRowBySkuId.get(targetId)?.[key]?.trim() ?? ""; + + return new Set( + items + .filter((item) => { + if (item.id === targetId) return true; + if (getSkuFactoryModelKey(item) !== targetModelKey) return false; + + const savedValue = savedRowBySkuId.get(item.id)?.[key]?.trim() ?? ""; + const autofillEntry = autofillEntries.get( + getProductFactoryModelAutofillKey(item.id, key), + ); + + return ( + !savedValue || + (savedValue === previousTargetValue && + autofillEntry?.sourceSkuId === targetId && + autofillEntry.value === previousTargetValue) + ); + }) + .map((item) => item.id), + ); + } + + return getPackageFactoryTargetSkuIds(items, targetId, key); + } + function updateProductFactoryRow( skuId: string, key: ProductFactoryEditableKey, @@ -14589,7 +14663,7 @@ export default function NonIntegratedDetailClient({ const supplierName = supplier.name || value; setSkuItems((current) => { - const targetSkuIds = getPackageFactoryTargetSkuIds( + const targetSkuIds = getProductFactoryTargetSkuIds( current, skuId, key, @@ -14604,7 +14678,7 @@ export default function NonIntegratedDetailClient({ if (key === "setCount") { setSkuItems((current) => { - const targetSkuIds = getPackageFactoryTargetSkuIds( + const targetSkuIds = getProductFactoryTargetSkuIds( current, skuId, key, @@ -14653,12 +14727,15 @@ export default function NonIntegratedDetailClient({ current, supplierCodeByName, ); - const targetSkuIds = getPackageFactoryTargetSkuIds( + const targetSkuIds = getProductFactoryTargetSkuIds( derivedSkuItems, skuId, key, + current, + value, + productFactoryModelAutofillRef.current, ); - return currentRows.map((row) => { + const nextRows = currentRows.map((row) => { if (!targetSkuIds.has(row.skuId) || row.confirmed) return row; const derivedSkuItem = derivedSkuItems.find( (item) => item.id === row.skuId, @@ -14683,6 +14760,29 @@ export default function NonIntegratedDetailClient({ [key]: value, }; }); + + if (MODEL_SHARED_PRODUCT_FACTORY_KEYS.has(key)) { + const normalizedValue = value.trim(); + productFactoryModelAutofillRef.current.delete( + getProductFactoryModelAutofillKey(skuId, key), + ); + + nextRows.forEach((row) => { + const autofillKey = getProductFactoryModelAutofillKey(row.skuId, key); + if (row.skuId === skuId || !targetSkuIds.has(row.skuId)) return; + + if (normalizedValue && row[key].trim() === normalizedValue) { + productFactoryModelAutofillRef.current.set(autofillKey, { + sourceSkuId: skuId, + value: normalizedValue, + }); + } else { + productFactoryModelAutofillRef.current.delete(autofillKey); + } + }); + } + + return nextRows; }); setSaveMessage(""); setSaveMessageType("success"); @@ -14872,7 +14972,6 @@ export default function NonIntegratedDetailClient({ widthCm: "", heightCm: "", weightKg: "", - productShape: "", ...rowBySkuId.get(skuId), [key]: value, }; @@ -20134,32 +20233,72 @@ export default function NonIntegratedDetailClient({ /> - {renderFactoryInput( - row, - "productionType", - "请输入生产类型", - )} + + updateProductFactoryRow( + row.skuId, + "productionType", + value, + ) + } + buttonClassName="h-8 w-full min-w-[112px] rounded border border-apple-separator bg-apple-bg-secondary px-2 text-xs text-apple-label-primary outline-none disabled:bg-apple-bg-primary disabled:text-apple-label-secondary" + /> - {renderFactoryInput( - row, - "installationService", - "请输入安装服务", - )} + + updateProductFactoryRow( + row.skuId, + "installationService", + value, + ) + } + buttonClassName="h-8 w-full min-w-[96px] rounded border border-apple-separator bg-apple-bg-secondary px-2 text-xs text-apple-label-primary outline-none disabled:bg-apple-bg-primary disabled:text-apple-label-secondary" + /> - {renderFactoryInput( - row, - "supplierCategory", - "请输入供应商分类", - )} + + updateProductFactoryRow( + row.skuId, + "supplierCategory", + value, + ) + } + buttonClassName="h-8 w-full min-w-[120px] rounded border border-apple-separator bg-apple-bg-secondary px-2 text-xs text-apple-label-primary outline-none disabled:bg-apple-bg-primary disabled:text-apple-label-secondary" + /> - {renderFactoryInput( - row, - "purchaseOrderMethod", - "请输入采购下单方式", - )} + + updateProductFactoryRow( + row.skuId, + "purchaseOrderMethod", + value, + ) + } + buttonClassName="h-8 w-full min-w-[128px] rounded border border-apple-separator bg-apple-bg-secondary px-2 text-xs text-apple-label-primary outline-none disabled:bg-apple-bg-primary disabled:text-apple-label-secondary" + /> ( {renderMeasurementInput(row, "weightKg", "请输入重量")} - - {renderMeasurementInput( - row, - "productShape", - "请输入商品外形", - )} - ))} @@ -21644,7 +21775,7 @@ function renderFactoryQualityWorkspace() { ].map((label) => ( {label} @@ -21658,18 +21789,16 @@ function renderFactoryQualityWorkspace() { return ( - - - 批次{row.batchNo} - + + 批次{row.batchNo} - + {row.image ? ( ) : ( -
+
无图
)} - + {renderReadOnlyCell(row.model)} - + {renderReadOnlyCell(row.skuCode)} - + {renderReadOnlyCell(row.barcode, "132px")} - + {renderReadOnlyCell(row.setCount, "64px")} - + {renderReadOnlyCell(row.specName, "128px")} - + {renderReadOnlyCell(row.size, "120px")} - + {renderReadOnlyCell(row.color, "80px")} - + {renderReadOnlyCell(row.material, "112px")} - + {renderReadOnlyCell(row.supplierName, "104px")} - + - + - -