feat(intake): default product planner by category
This commit is contained in:
parent
1c9bd11d20
commit
6b1991d953
@ -0,0 +1,81 @@
|
||||
import type { OaBoardProjectLookupItem } from "../../core/nonIntegratedApi";
|
||||
import { createEmptyNonIntegratedOpeningReference } from "../../core/nonIntegratedStorage";
|
||||
import { buildOaBoardOpeningCodeImportPayload } from "./oaBoardOpeningCodeImport";
|
||||
import { createFormValuesFromProduct } from "./productFormValueHelpers";
|
||||
|
||||
function createOaBoardItem(productPlanner: string): OaBoardProjectLookupItem {
|
||||
return {
|
||||
title: "茶几开款",
|
||||
productName: "测试茶几",
|
||||
brand: null,
|
||||
productCategory: "几类",
|
||||
categoryName: "茶几",
|
||||
openingCode: "KK-TEA-001",
|
||||
currentStageName: null,
|
||||
currentApprovalStageName: null,
|
||||
styleImages: [],
|
||||
formSummary: {
|
||||
openingCode: "KK-TEA-001",
|
||||
demandType: "",
|
||||
category: "几类",
|
||||
categoryName: "茶几",
|
||||
productPlanner,
|
||||
operationBrandOwner: "",
|
||||
spuOwner: "",
|
||||
appearanceDesigner: "",
|
||||
structureDesigner: "",
|
||||
purchaseOwner: "",
|
||||
merchandiserOwner: "",
|
||||
competitorLink: "",
|
||||
openingReason: "",
|
||||
salesPriceBand: "",
|
||||
platformPriceBand: "",
|
||||
openingBrand: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildOaBoardOpeningCodeImportPayload", () => {
|
||||
it("keeps the OA planner ahead of the category default", () => {
|
||||
const result = buildOaBoardOpeningCodeImportPayload(
|
||||
createOaBoardItem("OA指定企划"),
|
||||
{
|
||||
formValues: createFormValuesFromProduct(null),
|
||||
openingReference: createEmptyNonIntegratedOpeningReference(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.formValues.productPlanning).toBe("OA指定企划");
|
||||
expect(result.openingReference.planningOwner).toBe("OA指定企划");
|
||||
});
|
||||
|
||||
it("uses the category default only when OA and existing values are empty", () => {
|
||||
const defaulted = buildOaBoardOpeningCodeImportPayload(
|
||||
createOaBoardItem(""),
|
||||
{
|
||||
formValues: createFormValuesFromProduct(null),
|
||||
openingReference: createEmptyNonIntegratedOpeningReference(),
|
||||
},
|
||||
);
|
||||
const retainedManualValue = buildOaBoardOpeningCodeImportPayload(
|
||||
createOaBoardItem(""),
|
||||
{
|
||||
formValues: {
|
||||
...createFormValuesFromProduct(null),
|
||||
productPlanning: "人工指定企划",
|
||||
},
|
||||
openingReference: {
|
||||
...createEmptyNonIntegratedOpeningReference(),
|
||||
planningOwner: "人工指定企划",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(defaulted.formValues.productPlanning).toBe("沈璃");
|
||||
expect(defaulted.openingReference.planningOwner).toBe("沈璃");
|
||||
expect(retainedManualValue.formValues.productPlanning).toBe("人工指定企划");
|
||||
expect(retainedManualValue.openingReference.planningOwner).toBe(
|
||||
"人工指定企划",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import type { OaBoardProjectLookupItem } from "../../core/nonIntegratedApi";
|
||||
import type { FormValues } from "./productFormValueHelpers";
|
||||
import { BRAND_OPTIONS } from "./basicInfoConfig";
|
||||
import { getNonIntegratedProductPlanningOwner } from "@/lib/non-integrated-category-planning";
|
||||
|
||||
type OaBoardImportPayload = {
|
||||
formValues: FormValues;
|
||||
@ -67,6 +68,9 @@ export function buildOaBoardOpeningCodeImportPayload(
|
||||
item.formSummary.categoryName,
|
||||
item.categoryName,
|
||||
);
|
||||
const importedPlanningOwner = pickFirstNonEmpty(
|
||||
item.formSummary.productPlanner,
|
||||
);
|
||||
const nextProductName = pickFirstNonEmpty(item.productName, item.title);
|
||||
const nextTargetPriceBand = pickFirstNonEmpty(
|
||||
item.formSummary.salesPriceBand,
|
||||
@ -100,8 +104,11 @@ export function buildOaBoardOpeningCodeImportPayload(
|
||||
pickFirstNonEmpty(item.formSummary.appearanceDesigner) ||
|
||||
current.openingReference.designer,
|
||||
planningOwner:
|
||||
pickFirstNonEmpty(item.formSummary.productPlanner) ||
|
||||
current.openingReference.planningOwner,
|
||||
importedPlanningOwner ||
|
||||
current.openingReference.planningOwner ||
|
||||
getNonIntegratedProductPlanningOwner(
|
||||
nextProductCategory || current.openingReference.productCategory,
|
||||
),
|
||||
structureDesigner:
|
||||
pickFirstNonEmpty(item.formSummary.structureDesigner) ||
|
||||
current.openingReference.structureDesigner,
|
||||
@ -132,8 +139,11 @@ export function buildOaBoardOpeningCodeImportPayload(
|
||||
pickFirstNonEmpty(item.formSummary.appearanceDesigner) ||
|
||||
current.formValues.designer,
|
||||
productPlanning:
|
||||
pickFirstNonEmpty(item.formSummary.productPlanner) ||
|
||||
current.formValues.productPlanning,
|
||||
importedPlanningOwner ||
|
||||
current.formValues.productPlanning ||
|
||||
getNonIntegratedProductPlanningOwner(
|
||||
nextProductCategory || current.formValues.productCategory,
|
||||
),
|
||||
spuOwner:
|
||||
pickFirstNonEmpty(item.formSummary.spuOwner) || current.formValues.spuOwner,
|
||||
operator:
|
||||
|
||||
@ -62,4 +62,53 @@ describe("useBasicInfoFieldController", () => {
|
||||
expect(openingReferenceRef.current.productCategory).toBe("");
|
||||
expect(scheduleAutoSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("defaults planning owner from a category without overwriting a manual choice", () => {
|
||||
let formValues = createFormValuesFromProduct(null);
|
||||
let openingReference = createEmptyNonIntegratedOpeningReference();
|
||||
const formValuesRef = {
|
||||
current: formValues,
|
||||
} as MutableRefObject<typeof formValues>;
|
||||
const openingReferenceRef = {
|
||||
current: openingReference,
|
||||
} as MutableRefObject<typeof openingReference>;
|
||||
|
||||
const controller = useBasicInfoFieldController({
|
||||
canUseExistingSpuCreateMode: false,
|
||||
formValues,
|
||||
formValuesRef,
|
||||
isLockedCategoryFormField: () => false,
|
||||
isManualSeriesProductSpu: false,
|
||||
isReadOnly: false,
|
||||
isStockProductCreate: false,
|
||||
mode: "create",
|
||||
openingReferenceRef,
|
||||
scheduleAutoSave: jest.fn(),
|
||||
setFormValuesAndRef: (updater) => {
|
||||
formValues = updater(formValues);
|
||||
formValuesRef.current = formValues;
|
||||
},
|
||||
setOpeningReferenceAndRef: (updater) => {
|
||||
openingReference = updater(openingReference);
|
||||
openingReferenceRef.current = openingReference;
|
||||
},
|
||||
setSaveMessage: jest.fn(),
|
||||
setSaveMessageType: jest.fn(),
|
||||
setSelectedExistingSpu: jest.fn(),
|
||||
setSkuItems: jest.fn(),
|
||||
setSpuCreateMode: jest.fn(),
|
||||
setStockProductSpu: jest.fn(),
|
||||
});
|
||||
|
||||
controller.updateField("productCategory", "茶几");
|
||||
|
||||
expect(formValuesRef.current.productPlanning).toBe("沈璃");
|
||||
expect(openingReferenceRef.current.planningOwner).toBe("沈璃");
|
||||
|
||||
controller.updateField("productPlanning", "人工指定企划");
|
||||
controller.updateField("productCategory", "书桌");
|
||||
|
||||
expect(formValuesRef.current.productPlanning).toBe("人工指定企划");
|
||||
expect(openingReferenceRef.current.planningOwner).toBe("人工指定企划");
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
type NonIntegratedSkuItem,
|
||||
} from "../../nonIntegratedStorage";
|
||||
import { normalizeNonIntegratedProductCategoryForSecondaryCategory } from "@/lib/non-integrated-category-options";
|
||||
import { getNonIntegratedProductPlanningOwner } from "@/lib/non-integrated-category-planning";
|
||||
import type {
|
||||
SpuCreateMode,
|
||||
} from "./existingSpuCreateOptions";
|
||||
@ -184,6 +185,28 @@ export function useBasicInfoFieldController({
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "productCategory") {
|
||||
const planningOwner =
|
||||
formValuesRef.current.productPlanning.trim() ||
|
||||
openingReferenceRef.current.planningOwner.trim() ||
|
||||
getNonIntegratedProductPlanningOwner(value);
|
||||
|
||||
setFormValuesAndRef((current) => ({
|
||||
...current,
|
||||
productCategory: value,
|
||||
productPlanning: current.productPlanning.trim() || planningOwner,
|
||||
}));
|
||||
setOpeningReferenceAndRef((current) => ({
|
||||
...current,
|
||||
productCategory: value,
|
||||
planningOwner: current.planningOwner.trim() || planningOwner,
|
||||
}));
|
||||
setSaveMessage("");
|
||||
setSaveMessageType("success");
|
||||
scheduleAutoSave();
|
||||
return;
|
||||
}
|
||||
|
||||
setFormValuesAndRef((current) => ({ ...current, [key]: value }));
|
||||
syncBasicInfoFieldToOpeningReference(key, value);
|
||||
setSaveMessage("");
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
import type { MutableRefObject } from "react";
|
||||
|
||||
import { createEmptyNonIntegratedOpeningReference } from "../../core/nonIntegratedStorage";
|
||||
import { createFormValuesFromProduct } from "../basic-info/productFormValueHelpers";
|
||||
import { useOpeningReferenceController } from "./useOpeningReferenceController";
|
||||
|
||||
describe("useOpeningReferenceController", () => {
|
||||
it("defaults planning owner from a category and retains a later manual change", () => {
|
||||
let formValues = createFormValuesFromProduct(null);
|
||||
let openingReference = createEmptyNonIntegratedOpeningReference();
|
||||
const formValuesRef = {
|
||||
current: formValues,
|
||||
} as MutableRefObject<typeof formValues>;
|
||||
const openingReferenceRef = {
|
||||
current: openingReference,
|
||||
} as MutableRefObject<typeof openingReference>;
|
||||
|
||||
const controller = useOpeningReferenceController({
|
||||
createSingleUploadProgressHandler: jest.fn(),
|
||||
finishUploadProgress: jest.fn(),
|
||||
formValues,
|
||||
isLockedCategoryOpeningReferenceField: () => false,
|
||||
isReadOnly: false,
|
||||
isStockProductCreate: false,
|
||||
openingReferenceRef,
|
||||
scheduleAutoSave: jest.fn(),
|
||||
setFormValuesAndRef: (updater) => {
|
||||
formValues = updater(formValues);
|
||||
formValuesRef.current = formValues;
|
||||
},
|
||||
setOpeningReferenceAndRef: (updater) => {
|
||||
openingReference = updater(openingReference);
|
||||
openingReferenceRef.current = openingReference;
|
||||
},
|
||||
setSaveMessage: jest.fn(),
|
||||
setSaveMessageType: jest.fn(),
|
||||
setSkuItems: jest.fn(),
|
||||
setStockProductSpu: jest.fn(),
|
||||
setUploadProgress: jest.fn(),
|
||||
startUploadProgress: jest.fn(),
|
||||
});
|
||||
|
||||
controller.handleOpeningReferenceFieldChange("productCategory", "茶几");
|
||||
|
||||
expect(formValuesRef.current.productPlanning).toBe("沈璃");
|
||||
expect(openingReferenceRef.current.planningOwner).toBe("沈璃");
|
||||
|
||||
controller.handleOpeningReferenceFieldChange("planningOwner", "人工指定企划");
|
||||
controller.handleOpeningReferenceFieldChange("productCategory", "书桌");
|
||||
|
||||
expect(formValuesRef.current.productPlanning).toBe("人工指定企划");
|
||||
expect(openingReferenceRef.current.planningOwner).toBe("人工指定企划");
|
||||
});
|
||||
});
|
||||
@ -14,6 +14,7 @@ import {
|
||||
type NonIntegratedSkuItem,
|
||||
} from "../../nonIntegratedStorage";
|
||||
import { normalizeNonIntegratedProductCategoryForSecondaryCategory } from "@/lib/non-integrated-category-options";
|
||||
import { getNonIntegratedProductPlanningOwner } from "@/lib/non-integrated-category-planning";
|
||||
import type { NonIntegratedUploadProgressHandler } from "../../nonIntegratedApi";
|
||||
import { BRAND_OPTIONS } from "../basic-info/basicInfoConfig";
|
||||
import type { FormValues } from "../basic-info/productFormValueHelpers";
|
||||
@ -82,6 +83,7 @@ export function useOpeningReferenceController({
|
||||
function syncOpeningReferenceFieldToBasicInfo(
|
||||
field: OpeningReferenceFieldKey,
|
||||
value: string,
|
||||
planningOwnerFallback = "",
|
||||
) {
|
||||
if (field === "salesCategory") {
|
||||
setFormValuesAndRef((current) => ({
|
||||
@ -95,6 +97,15 @@ export function useOpeningReferenceController({
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === "productCategory") {
|
||||
setFormValuesAndRef((current) => ({
|
||||
...current,
|
||||
productCategory: value,
|
||||
productPlanning: current.productPlanning.trim() || planningOwnerFallback,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedField = getOpeningReferenceBasicInfoFieldKey(field);
|
||||
|
||||
if (field === "brand") {
|
||||
@ -142,15 +153,33 @@ export function useOpeningReferenceController({
|
||||
return;
|
||||
}
|
||||
|
||||
setOpeningReferenceAndRef((current) =>
|
||||
updateOpeningReferenceFieldValue({
|
||||
const planningOwnerFallback =
|
||||
field === "productCategory"
|
||||
? formValues.productPlanning.trim() ||
|
||||
openingReferenceRef.current.planningOwner.trim() ||
|
||||
getNonIntegratedProductPlanningOwner(value)
|
||||
: "";
|
||||
|
||||
setOpeningReferenceAndRef((current) => {
|
||||
const nextReference = updateOpeningReferenceFieldValue({
|
||||
brandOptions: BRAND_OPTIONS,
|
||||
current,
|
||||
field,
|
||||
value,
|
||||
}),
|
||||
});
|
||||
|
||||
return field === "productCategory" && !nextReference.planningOwner.trim()
|
||||
? {
|
||||
...nextReference,
|
||||
planningOwner: planningOwnerFallback,
|
||||
}
|
||||
: nextReference;
|
||||
});
|
||||
syncOpeningReferenceFieldToBasicInfo(
|
||||
field,
|
||||
value,
|
||||
planningOwnerFallback,
|
||||
);
|
||||
syncOpeningReferenceFieldToBasicInfo(field, value);
|
||||
setSaveMessage("");
|
||||
setSaveMessageType("success");
|
||||
scheduleAutoSave();
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import {
|
||||
getNonIntegratedProductPlanningOwner,
|
||||
NON_INTEGRATED_PRODUCT_CATEGORY_PLANNING_OWNER,
|
||||
} from "./non-integrated-category-planning";
|
||||
|
||||
describe("non-integrated category planning owners", () => {
|
||||
it("returns the configured owner for an exact category after trimming", () => {
|
||||
expect(getNonIntegratedProductPlanningOwner(" 茶几 ")).toBe("沈璃");
|
||||
expect(getNonIntegratedProductPlanningOwner("书桌")).toBe("灵犀");
|
||||
});
|
||||
|
||||
it("keeps the imported category mapping available without guessing unmatched categories", () => {
|
||||
expect(
|
||||
NON_INTEGRATED_PRODUCT_CATEGORY_PLANNING_OWNER[
|
||||
"餐桌(新) > 不锈钢/PVC餐桌"
|
||||
],
|
||||
).toBe("敬亭");
|
||||
expect(getNonIntegratedProductPlanningOwner("未配置类目")).toBe("");
|
||||
});
|
||||
});
|
||||
111
furniture-3d-system/src/lib/non-integrated-category-planning.ts
Normal file
111
furniture-3d-system/src/lib/non-integrated-category-planning.ts
Normal file
@ -0,0 +1,111 @@
|
||||
export const NON_INTEGRATED_PRODUCT_CATEGORY_PLANNING_OWNER: Readonly<
|
||||
Record<string, string>
|
||||
> = {
|
||||
"茶几": "沈璃",
|
||||
"餐桌(新) > 岩板餐桌": "灵犀",
|
||||
"餐桌(新) > 实木餐桌": "灵犀",
|
||||
"衣柜(新) > 独立衣柜": "妙玉",
|
||||
"餐边柜": "昭君",
|
||||
"梳妆台/桌": "黄庭坚",
|
||||
"电视柜": "妙玉",
|
||||
"电脑桌": "灵犀",
|
||||
"椅类 > 餐椅": "黄庭坚",
|
||||
"斗柜": "妙玉",
|
||||
"书架": "敬亭",
|
||||
"床头柜": "昭君",
|
||||
"书桌": "灵犀",
|
||||
"鞋柜": "妙玉",
|
||||
"椅类 > 沙发椅": "敬亭",
|
||||
"衣帽架": "涂涂",
|
||||
"书柜 > 单个书柜": "昭君",
|
||||
"家用茶台/茶桌": "灵犀",
|
||||
"角几/边几": "沈璃",
|
||||
"搁板/置物架/家用陈列架": "涂涂",
|
||||
"穿衣镜": "沈璃",
|
||||
"餐桌(新) > 大理石餐桌": "灵犀",
|
||||
"餐桌(新) > 岛台餐桌": "灵犀",
|
||||
"折叠桌": "敬亭",
|
||||
"懒人沙发": "妙玉",
|
||||
"其它凳子": "敬亭",
|
||||
"电动升降桌": "黄庭坚",
|
||||
"鞋架": "黄庭坚",
|
||||
"变形柜类 > 组合模块柜/书柜": "昭君",
|
||||
"矮凳": "敬亭",
|
||||
"椅类 > 折叠椅/躺椅": "敬亭",
|
||||
"椅类 > 摇椅": "黄庭坚",
|
||||
"门厅/玄关柜/玄关架": "昭君",
|
||||
"壁炉架/壁炉柜": "昭君",
|
||||
"变形柜类 > 自由组合衣柜": "妙玉",
|
||||
"换鞋凳": "敬亭",
|
||||
"多宝格/博古架": "涂涂",
|
||||
"其它柜类": "昭君",
|
||||
"花架/花几": "涂涂",
|
||||
"凳类 > 梳妆凳": "黄庭坚",
|
||||
"酒柜": "妙玉",
|
||||
"变形几类 > 移动边几/推车": "沈璃",
|
||||
"餐桌(新) > 板木餐桌": "灵犀",
|
||||
"凳类 > 床尾凳/长凳": "敬亭",
|
||||
"角柜": "妙玉",
|
||||
"坐具 > 公园椅": "灵犀",
|
||||
"供桌": "灵犀",
|
||||
"家用陈列柜": "妙玉",
|
||||
"椅类 > 其它椅子": "敬亭",
|
||||
"飘窗柜": "妙玉",
|
||||
"化妆镜": "沈璃",
|
||||
"阳台柜": "妙玉",
|
||||
"吊柜/壁柜": "昭君",
|
||||
"沙发凳/脚踏": "敬亭",
|
||||
"餐桌(新) > 玻璃餐桌": "灵犀",
|
||||
"餐桌(新) > 不锈钢/PVC餐桌": "敬亭",
|
||||
"炕几/飘窗几": "沈璃",
|
||||
"简易衣柜": "妙玉",
|
||||
"变形几类 > 升降茶几": "沈璃",
|
||||
"变形柜类 > 可移动边柜": "昭君",
|
||||
"组合衣架": "涂涂",
|
||||
"药柜": "妙玉",
|
||||
"客厅成套家具 > 茶几+电视柜": "沈璃",
|
||||
"转角桌": "灵犀",
|
||||
"变形桌类 > 床边桌": "灵犀",
|
||||
"旋转书架": "敬亭",
|
||||
"书报架": "涂涂",
|
||||
"连体书桌柜": "昭君",
|
||||
"格架": "涂涂",
|
||||
"桌子配件 > 桌腿/桌架": "黄庭坚",
|
||||
"智能床头柜": "昭君",
|
||||
"家政柜": "妙玉",
|
||||
"桌 > 阳台桌": "黄庭坚",
|
||||
"衣挂一体镜": "沈璃",
|
||||
"椅类 > 普通餐椅": "黄庭坚",
|
||||
"其它几类": "沈璃",
|
||||
"落地首饰柜": "妙玉",
|
||||
"窄缝架": "涂涂",
|
||||
"折叠/组合鞋架": "黄庭坚",
|
||||
"卧室成套家具 > 双人床+床头柜": "昭君",
|
||||
"装饰镜": "沈璃",
|
||||
"棋桌": "灵犀",
|
||||
"桌 > 户外烧烤桌": "灵犀",
|
||||
"桌 > 公园桌": "灵犀",
|
||||
"桌 > 沙滩桌": "灵犀",
|
||||
"书房成套家具 > 书桌椅+书柜": "昭君",
|
||||
"CD架": "涂涂",
|
||||
"餐厅成套家具 > 餐桌椅+餐边柜": "昭君",
|
||||
"变形桌类 > 可伸缩/折叠餐桌": "敬亭",
|
||||
"门厅成套家具 > 隔断+ 鞋柜": "妙玉",
|
||||
"套几": "沈璃",
|
||||
"扫地机器人柜": "昭君",
|
||||
"伸缩衣/裤架": "涂涂",
|
||||
"变形休闲家具 > 壁挂折叠桌": "敬亭",
|
||||
"餐厅成套家具 > 餐边柜+酒架": "昭君",
|
||||
"餐厅成套家具 > 餐桌椅+餐边柜+酒柜+餐车": "妙玉",
|
||||
"卧室成套家具 > 单人床+床头柜+梳妆台+衣柜": "昭君",
|
||||
"卧室成套家具 > 双人床+床头柜+梳妆台": "昭君",
|
||||
"门厅成套家具 > 门厅柜+鞋柜": "妙玉",
|
||||
"适老餐桌": "灵犀",
|
||||
"悬挂/壁挂组合陈列架": "涂涂",
|
||||
};
|
||||
|
||||
export function getNonIntegratedProductPlanningOwner(productCategory: string) {
|
||||
return NON_INTEGRATED_PRODUCT_CATEGORY_PLANNING_OWNER[
|
||||
productCategory.trim()
|
||||
] ?? "";
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user