feat: refine non-integrated intake workflow
This commit is contained in:
parent
b7c794ac2f
commit
c5a6f044c7
@ -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<string | null | undefined>) {
|
||||
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,
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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 },
|
||||
);
|
||||
}
|
||||
@ -1906,7 +1906,6 @@ function getPopulatedSampleMeasurementRowCountForList(
|
||||
row.widthCm,
|
||||
row.heightCm,
|
||||
row.weightKg,
|
||||
row.productShape,
|
||||
].some((value) => value.trim().length > 0),
|
||||
).length;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="SZ0230-factory-model-shared-fields"
|
||||
/>,
|
||||
);
|
||||
|
||||
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: "长方形",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@ -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<ProductFactoryEditableKey>([
|
||||
"setCount",
|
||||
"supplierName",
|
||||
"supplierCode",
|
||||
"warehouse",
|
||||
"minimumOrderQuantity",
|
||||
"leadTime",
|
||||
"assemblyMode",
|
||||
"productionType",
|
||||
"installationService",
|
||||
"supplierCategory",
|
||||
"purchaseOrderMethod",
|
||||
]);
|
||||
const PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS =
|
||||
new Set<ProductFactoryEditableKey>([
|
||||
"supplierName",
|
||||
"supplierCode",
|
||||
"warehouse",
|
||||
"minimumOrderQuantity",
|
||||
"leadTime",
|
||||
"assemblyMode",
|
||||
"productionType",
|
||||
"installationService",
|
||||
"supplierCategory",
|
||||
"purchaseOrderMethod",
|
||||
]);
|
||||
const MODEL_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([
|
||||
"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<NonIntegratedSkuItem, "id" | "packageGr
|
||||
return sku.packageGroupId.trim() || sku.id;
|
||||
}
|
||||
|
||||
function getSkuFactoryModelKey(sku: Pick<NonIntegratedSkuItem, "model">) {
|
||||
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<string, NonIntegratedFactoryRow>();
|
||||
|
||||
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<NonIntegratedFactoryRow[]>(
|
||||
existingProduct?.factoryRows ?? [],
|
||||
);
|
||||
const productFactoryModelAutofillRef = useRef(
|
||||
new Map<string, ProductFactoryModelAutofillEntry>(),
|
||||
);
|
||||
const [pricingRows, setPricingRows] = useState<NonIntegratedPricingRow[]>(
|
||||
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<string | null | undefined> };
|
||||
};
|
||||
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<NonIntegratedSkuItem, "id" | "packageGroupId" | "model">[],
|
||||
targetId: string,
|
||||
key: ProductFactoryEditableKey,
|
||||
savedRows: NonIntegratedFactoryRow[] = [],
|
||||
value = "",
|
||||
autofillEntries: ReadonlyMap<string, ProductFactoryModelAutofillEntry> =
|
||||
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({
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{renderFactoryInput(
|
||||
row,
|
||||
"productionType",
|
||||
"请输入生产类型",
|
||||
)}
|
||||
<InlineSelect
|
||||
ariaLabel="产品生产类型"
|
||||
value={row.productionType}
|
||||
options={PRODUCT_FACTORY_PRODUCTION_TYPE_OPTIONS}
|
||||
placeholder="请选择"
|
||||
readOnly={isReadOnly || row.confirmed}
|
||||
onChange={(value) =>
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{renderFactoryInput(
|
||||
row,
|
||||
"installationService",
|
||||
"请输入安装服务",
|
||||
)}
|
||||
<InlineSelect
|
||||
ariaLabel="安装服务"
|
||||
value={row.installationService}
|
||||
options={PRODUCT_FACTORY_INSTALLATION_SERVICE_OPTIONS}
|
||||
placeholder="请选择"
|
||||
readOnly={isReadOnly || row.confirmed}
|
||||
onChange={(value) =>
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{renderFactoryInput(
|
||||
row,
|
||||
"supplierCategory",
|
||||
"请输入供应商分类",
|
||||
)}
|
||||
<InlineSelect
|
||||
ariaLabel="供应商分类"
|
||||
value={row.supplierCategory}
|
||||
options={PRODUCT_FACTORY_SUPPLIER_CATEGORY_OPTIONS}
|
||||
placeholder="请选择"
|
||||
readOnly={isReadOnly || row.confirmed}
|
||||
onChange={(value) =>
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{renderFactoryInput(
|
||||
row,
|
||||
"purchaseOrderMethod",
|
||||
"请输入采购下单方式",
|
||||
)}
|
||||
<InlineSelect
|
||||
ariaLabel="采购下单方式"
|
||||
value={row.purchaseOrderMethod}
|
||||
options={PRODUCT_FACTORY_PURCHASE_ORDER_METHOD_OPTIONS}
|
||||
placeholder="请选择"
|
||||
readOnly={isReadOnly || row.confirmed}
|
||||
onChange={(value) =>
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<SkuExecutionStandardField
|
||||
@ -20306,7 +20445,6 @@ export default function NonIntegratedDetailClient({
|
||||
"宽(CM)",
|
||||
"高(CM)",
|
||||
"重量(KG)",
|
||||
"商品外形",
|
||||
].map((label) => (
|
||||
<th
|
||||
key={label}
|
||||
@ -20357,13 +20495,6 @@ export default function NonIntegratedDetailClient({
|
||||
<td className="px-3 py-2">
|
||||
{renderMeasurementInput(row, "weightKg", "请输入重量")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{renderMeasurementInput(
|
||||
row,
|
||||
"productShape",
|
||||
"请输入商品外形",
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -21644,7 +21775,7 @@ function renderFactoryQualityWorkspace() {
|
||||
].map((label) => (
|
||||
<th
|
||||
key={label}
|
||||
className="whitespace-nowrap border-b border-apple-separator px-3 py-2 text-left font-apple-medium"
|
||||
className="whitespace-nowrap border-b border-apple-separator px-3 py-2 text-center font-apple-medium"
|
||||
>
|
||||
{label}
|
||||
</th>
|
||||
@ -21658,18 +21789,16 @@ function renderFactoryQualityWorkspace() {
|
||||
return (
|
||||
<tr
|
||||
key={row.skuId}
|
||||
className="border-b border-apple-separator align-top last:border-b-0"
|
||||
className="border-b border-apple-separator align-middle last:border-b-0"
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<span className="rounded-apple-md bg-[#eef2f7] px-2 py-1 text-[11px] text-apple-label-primary">
|
||||
批次{row.batchNo}
|
||||
</span>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-center align-middle text-xs text-apple-label-secondary">
|
||||
批次{row.batchNo}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{row.image ? (
|
||||
<button
|
||||
type="button"
|
||||
className="block h-12 w-12 overflow-hidden rounded border border-apple-separator bg-apple-bg-secondary"
|
||||
className="mx-auto block h-8 w-12 overflow-hidden rounded border border-apple-separator bg-apple-bg-secondary"
|
||||
onClick={() =>
|
||||
setSkuImagePreview({
|
||||
src: row.image,
|
||||
@ -21685,39 +21814,39 @@ function renderFactoryQualityWorkspace() {
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded border border-dashed border-apple-separator bg-apple-bg-secondary text-[11px] text-apple-label-tertiary">
|
||||
<div className="mx-auto flex h-8 w-12 items-center justify-center rounded border border-dashed border-apple-separator bg-apple-bg-secondary text-[11px] text-apple-label-tertiary">
|
||||
无图
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.model)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.skuCode)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.barcode, "132px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.setCount, "64px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.specName, "128px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.size, "120px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.color, "80px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.material, "112px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
{renderReadOnlyCell(row.supplierName, "104px")}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
<SkuRowField
|
||||
value={row.qualityBatch}
|
||||
placeholder="填写质检批次"
|
||||
@ -21733,7 +21862,7 @@ function renderFactoryQualityWorkspace() {
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
<InlineSelect
|
||||
ariaLabel={`${row.model || row.skuCode}质检结论`}
|
||||
value={row.conclusion}
|
||||
@ -21750,8 +21879,9 @@ function renderFactoryQualityWorkspace() {
|
||||
panelClassName="absolute left-0 top-[calc(100%+4px)] z-50 max-h-48 min-w-full overflow-auto rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-1 shadow-apple-md"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<textarea
|
||||
<td className="px-3 py-2 align-middle">
|
||||
<input
|
||||
type="text"
|
||||
value={row.detail}
|
||||
readOnly={isReadOnly}
|
||||
onChange={(event) =>
|
||||
@ -21762,46 +21892,44 @@ function renderFactoryQualityWorkspace() {
|
||||
)
|
||||
}
|
||||
placeholder="填写首批质检详细说明"
|
||||
className={`h-16 min-h-16 w-[220px] resize-none rounded border border-apple-separator px-2 py-1.5 text-xs leading-5 outline-none ${
|
||||
className={`h-8 w-[220px] rounded border border-apple-separator px-2 text-xs leading-8 outline-none ${
|
||||
isReadOnly
|
||||
? "bg-apple-bg-primary text-apple-label-secondary"
|
||||
: "bg-[#eef1f4] text-apple-label-primary"
|
||||
}`}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex min-w-[148px] flex-col gap-2">
|
||||
<div className="rounded-apple-md bg-apple-bg-secondary px-2 py-1 text-[11px] text-apple-label-secondary">
|
||||
<td className="px-3 py-2 align-middle">
|
||||
<div className="flex min-w-[196px] items-center justify-center gap-1.5 whitespace-nowrap">
|
||||
<div className="inline-flex h-7 items-center rounded-apple-md bg-apple-bg-secondary px-2 text-[11px] text-apple-label-secondary">
|
||||
当前 {rowFiles.length} 个文件
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isReadOnly ? null : (
|
||||
<label className="inline-flex h-7 cursor-pointer items-center justify-center rounded-apple-md border border-apple-separator px-2 text-[11px] text-apple-label-secondary hover:bg-apple-bg-secondary/60">
|
||||
上传
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(event) =>
|
||||
void handleUploadFirstQualityFiles(
|
||||
event,
|
||||
row.skuId,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadWorkspaceFiles(rowFiles)
|
||||
}
|
||||
disabled={rowFiles.length === 0}
|
||||
className="inline-flex h-7 items-center justify-center rounded-apple-md border border-apple-separator px-2 text-[11px] text-apple-label-secondary disabled:opacity-40"
|
||||
>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
{isReadOnly ? null : (
|
||||
<label className="inline-flex h-7 cursor-pointer items-center justify-center rounded-apple-md border border-apple-separator px-2 text-[11px] text-apple-label-secondary hover:bg-apple-bg-secondary/60">
|
||||
上传
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(event) =>
|
||||
void handleUploadFirstQualityFiles(
|
||||
event,
|
||||
row.skuId,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadWorkspaceFiles(rowFiles)
|
||||
}
|
||||
disabled={rowFiles.length === 0}
|
||||
className="inline-flex h-7 items-center justify-center rounded-apple-md border border-apple-separator px-2 text-[11px] text-apple-label-secondary disabled:opacity-40"
|
||||
>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -24991,9 +25119,10 @@ function renderFactoryQualityWorkspace() {
|
||||
? basicInfoSecondaryCategoryOptions
|
||||
: field.key === "productCategory"
|
||||
? basicInfoProductCategoryOptions
|
||||
: field.key === "designer" ||
|
||||
field.key === "visualPlanning"
|
||||
: field.key === "designer"
|
||||
? basicInfoDesignerOptions
|
||||
: field.key === "visualPlanning"
|
||||
? basicInfoSpaceDesignerOptions
|
||||
: field.key === "productPlanning"
|
||||
? basicInfoPlanningOwnerOptions
|
||||
: field.key === "spuOwner" ||
|
||||
@ -25054,24 +25183,6 @@ function renderFactoryQualityWorkspace() {
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-apple-separator/70 pt-3">
|
||||
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
|
||||
{BASIC_INFO_PROCESS_FIELDS.map((field) => (
|
||||
<CreateFieldShell key={String(field.key)}>
|
||||
<BasicField
|
||||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
value={formValues[field.key] as string}
|
||||
readOnly={isReadOnly}
|
||||
onChange={(value) =>
|
||||
updateField(field.key, value)
|
||||
}
|
||||
compact
|
||||
/>
|
||||
</CreateFieldShell>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
@ -389,7 +389,6 @@ export type NonIntegratedSampleMeasurementRow = {
|
||||
widthCm: string;
|
||||
heightCm: string;
|
||||
weightKg: string;
|
||||
productShape: string;
|
||||
};
|
||||
|
||||
export type NonIntegratedFinishedSampleShipmentRow = {
|
||||
@ -1350,7 +1349,6 @@ function normalizeSampleMeasurementRow(
|
||||
: typeof item.weightKg === "number" && Number.isFinite(item.weightKg)
|
||||
? String(item.weightKg)
|
||||
: "",
|
||||
productShape: normalizeString(item.productShape),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type IntakeOwnerCenter = "operation" | "purchasing" | "brand";
|
||||
|
||||
const INTAKE_OWNER_CENTER_KEYWORDS: Record<IntakeOwnerCenter, string[]> = {
|
||||
operation: ["运营中心", "运营"],
|
||||
purchasing: ["供应链中心", "供应链"],
|
||||
brand: ["品牌中心", "品牌"],
|
||||
};
|
||||
type IntakeOwnerDepartmentScope =
|
||||
| "productResearch"
|
||||
| "brandPlanning"
|
||||
| "spaceVisual"
|
||||
| "operation"
|
||||
| "purchasing";
|
||||
|
||||
function getUniqueNames(values: Array<string | null | undefined>) {
|
||||
return Array.from(
|
||||
@ -18,22 +17,41 @@ function getUniqueNames(values: Array<string | null | undefined>) {
|
||||
);
|
||||
}
|
||||
|
||||
function textMatchesAnyKeyword(
|
||||
function normalizeDepartmentText(value: string | null | undefined) {
|
||||
return value?.replace(/\s+/g, "") ?? "";
|
||||
}
|
||||
|
||||
function departmentTextMatchesScope(
|
||||
values: Array<string | null | undefined>,
|
||||
keywords: string[],
|
||||
scope: IntakeOwnerDepartmentScope,
|
||||
) {
|
||||
return values.some((value) => {
|
||||
const normalized = value?.trim();
|
||||
return normalized
|
||||
? keywords.some((keyword) => normalized.includes(keyword))
|
||||
: false;
|
||||
const text = normalizeDepartmentText(value);
|
||||
if (!text) return false;
|
||||
|
||||
if (scope === "productResearch") {
|
||||
return text.includes("产品研发部");
|
||||
}
|
||||
|
||||
if (scope === "brandPlanning") {
|
||||
return text.includes("品牌中心") && text.includes("商品企划部");
|
||||
}
|
||||
|
||||
if (scope === "spaceVisual") {
|
||||
return text.includes("品牌中心") && text.includes("空间视觉部");
|
||||
}
|
||||
|
||||
if (scope === "operation") {
|
||||
return text.includes("运营中心");
|
||||
}
|
||||
|
||||
return text.includes("供应链中心");
|
||||
});
|
||||
}
|
||||
|
||||
export async function listIntakeOwnerOptionNamesByCenter(
|
||||
center: IntakeOwnerCenter,
|
||||
export async function listIntakeOwnerOptionNamesByDepartmentScope(
|
||||
scope: IntakeOwnerDepartmentScope,
|
||||
) {
|
||||
const keywords = INTAKE_OWNER_CENTER_KEYWORDS[center];
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
status: "ACTIVE",
|
||||
@ -66,7 +84,7 @@ export async function listIntakeOwnerOptionNamesByCenter(
|
||||
return getUniqueNames(
|
||||
users
|
||||
.filter((user) =>
|
||||
textMatchesAnyKeyword(
|
||||
departmentTextMatchesScope(
|
||||
[
|
||||
user.department?.name,
|
||||
user.department?.fullPath,
|
||||
@ -74,7 +92,7 @@ export async function listIntakeOwnerOptionNamesByCenter(
|
||||
user.team?.department?.name,
|
||||
user.team?.department?.fullPath,
|
||||
],
|
||||
keywords,
|
||||
scope,
|
||||
),
|
||||
)
|
||||
.map((user) => user.name),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user