feat: add planning layout and product limit fields

This commit is contained in:
WuYongJie 2026-05-11 19:42:43 +08:00
parent 4415ef8b33
commit cf4b370f4a
8 changed files with 1106 additions and 79 deletions

View File

@ -11,6 +11,8 @@ const ALPHABET_LIBRARY_IMAGE_MAX_SIZE_BYTES = 1.5 * 1024 * 1024
const ALPHABET_LIBRARY_LETTERS = Array.from({ length: 26 }, (_, index) =>
String.fromCharCode('A'.charCodeAt(0) + index),
)
const LIBRARY_PRIMARY_BUTTON_CLASS =
'!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400'
type AlphabetLibraryLetter = (typeof ALPHABET_LIBRARY_LETTERS)[number]
@ -363,11 +365,22 @@ export default function AlphabetLibraryPanel({
<div className="space-y-2">
<p className="text-sm font-apple-medium text-apple-label-secondary">查询动作</p>
<div className="flex flex-wrap items-center gap-2">
<Button type="button" size="small" onClick={applySearch}>
<Button
type="button"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={applySearch}
>
<Search className="h-4 w-4" />
查询
</Button>
<Button type="button" variant="tinted" size="small" onClick={() => openCreateModal(activeLetter)}>
<Button
type="button"
variant="tinted"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={() => openCreateModal(activeLetter)}
>
<Plus className="h-4 w-4" />
新增当前字母
</Button>
@ -421,7 +434,7 @@ export default function AlphabetLibraryPanel({
<button
type="button"
onClick={() => openCreateModal(letterKey)}
className="rounded-full border border-apple-separator px-2 py-1 text-[11px] text-apple-blue transition hover:bg-apple-blue/10"
className="rounded-full border border-zinc-950 bg-zinc-950 px-2 py-1 text-[11px] text-white transition hover:border-zinc-800 hover:bg-zinc-800"
>
新增
</button>
@ -442,7 +455,13 @@ export default function AlphabetLibraryPanel({
当前显示 {currentLetterItems.length} 条数据,字段固定为编号、名称、图片、价格。
</p>
</div>
<Button type="button" variant="gray" size="small" onClick={() => openCreateModal(activeLetter)}>
<Button
type="button"
variant="gray"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={() => openCreateModal(activeLetter)}
>
<Plus className="h-4 w-4" />
新增 {activeLetter} 组
</Button>
@ -516,7 +535,7 @@ export default function AlphabetLibraryPanel({
<button
type="button"
onClick={() => openEditModal(item)}
className="rounded-full border border-apple-separator px-2 py-1 text-apple-blue transition hover:bg-apple-blue/10"
className="rounded-full border border-zinc-950 bg-zinc-950 px-2 py-1 text-white transition hover:border-zinc-800 hover:bg-zinc-800"
>
编辑
</button>
@ -569,7 +588,11 @@ export default function AlphabetLibraryPanel({
>
取消
</Button>
<Button type="button" onClick={saveDraft}>
<Button
type="button"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={saveDraft}
>
保存
</Button>
</div>

View File

@ -30,6 +30,8 @@ type EdgeBandingLibraryCategory = {
const EDGE_BANDING_CATEGORY_STORAGE_KEY =
'formal-intake-edge-banding-library-categories'
const LIBRARY_PRIMARY_BUTTON_CLASS =
'!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400'
function normalizeText(value: string) {
return value.trim().toLowerCase()
@ -576,11 +578,22 @@ export default function EdgeBandingLibraryPanel() {
placeholder="例如:12MM"
/>
<div className="flex flex-wrap items-end justify-end gap-2">
<Button type="button" size="small" onClick={applySearch}>
<Button
type="button"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={applySearch}
>
<Search className="h-4 w-4" />
查询
</Button>
<Button type="button" variant="tinted" size="small" onClick={startCreate}>
<Button
type="button"
variant="tinted"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={startCreate}
>
<Plus className="h-4 w-4" />
新建封边
</Button>
@ -606,7 +619,7 @@ export default function EdgeBandingLibraryPanel() {
type="button"
variant="tinted"
size="small"
className="!min-h-8 !px-2 !py-1 text-xs"
className={`!min-h-8 !px-2 !py-1 text-xs ${LIBRARY_PRIMARY_BUTTON_CLASS}`}
onClick={startCreateCategory}
>
<Plus className="h-3.5 w-3.5" />
@ -766,7 +779,7 @@ export default function EdgeBandingLibraryPanel() {
<Button
type="button"
size="small"
className="!min-h-8 !px-3 !py-1 text-xs"
className={`!min-h-8 !px-3 !py-1 text-xs ${LIBRARY_PRIMARY_BUTTON_CLASS}`}
onClick={isCreatingCategory ? saveCategoryDraft : saveDraft}
disabled={isCreatingCategory ? !categoryCanSave : !draftCanSave}
>

View File

@ -394,6 +394,8 @@ const TABLE_ACTION_BUTTON_BASE =
"!h-8 !min-h-8 !w-8 !rounded-md !px-0 !py-0 !shadow-none";
const TABLE_ACTION_BUTTON_SECONDARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-200 !bg-white !text-zinc-500 hover:!border-zinc-300 hover:!bg-zinc-50 hover:!text-zinc-950 focus:!border-zinc-400 disabled:!border-zinc-100 disabled:!bg-zinc-50 disabled:!text-zinc-300`;
const TABLE_ACTION_BUTTON_PRIMARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-950 !bg-zinc-950 !text-white hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400`;
const INTAKE_PRIMARY_BUTTON_CLASS =
"!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400";
const BOM_LIBRARY_OPTIONS: {
key: BomLibraryKey;
@ -1391,6 +1393,7 @@ function CopyProductDialog({
<Button
type="button"
size="small"
className={INTAKE_PRIMARY_BUTTON_CLASS}
onClick={onConfirm}
disabled={isSubmitting || brandOptions.length === 0}
>
@ -1553,7 +1556,7 @@ function IntegratedPendingDetail({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => {
setAppliedQuery(queryDraft);
setCurrentPage(1);
@ -1564,7 +1567,10 @@ function IntegratedPendingDetail({
</div>
<div className="flex items-end justify-end">
<Link href="/intake/integrated/new" className="w-full">
<Button size="small" className="h-9 w-full px-4 text-xs">
<Button
size="small"
className={`h-9 w-full px-4 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
>
新增产品
</Button>
</Link>
@ -1644,7 +1650,12 @@ function IntegratedPendingDetail({
</td>
<td className="px-3 py-3">
<div className="flex gap-2">
<Button size="small">修改</Button>
<Button
size="small"
className={INTAKE_PRIMARY_BUTTON_CLASS}
>
修改
</Button>
<Button variant="plain" size="small">
暂缓
</Button>
@ -2005,7 +2016,7 @@ function NonIntegratedPendingDetail({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => setAppliedQuery(queryDraft)}
>
查询
@ -2014,7 +2025,7 @@ function NonIntegratedPendingDetail({
<div className="flex items-end justify-end">
<Button
size="small"
className="h-9 w-full px-4 text-xs"
className={`h-9 w-full px-4 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={onOpenNewNonIntegratedProductWorkspace}
>
新增产品
@ -3544,7 +3555,7 @@ function ProductDevelopmentProgressDashboard({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => setAppliedQuery(queryDraft)}
>
查询
@ -3553,7 +3564,7 @@ function ProductDevelopmentProgressDashboard({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => setAppliedQuery(queryDraft)}
>
刷新
@ -5160,7 +5171,7 @@ function ProductPatentStatusDashboard({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => setAppliedQuery(queryDraft)}
>
查询
@ -5169,7 +5180,7 @@ function ProductPatentStatusDashboard({
<div className="flex items-end">
<Button
size="small"
className="h-9 w-full text-xs"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={refreshRecords}
>
刷新

View File

@ -32,6 +32,8 @@ const STEEL_FRAME_CREATE_PRESETS = [
{ key: 'flat', label: '新增扁管', name: '扁管' },
{ key: 'round', label: '新增圆管', name: '圆管' },
] as const
const LIBRARY_PRIMARY_BUTTON_CLASS =
'!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400'
export default function SteelFrameLibraryPanel() {
const initialState = useMemo(() => readSteelFrameLibraryState(), [])
@ -273,13 +275,19 @@ export default function SteelFrameLibraryPanel() {
type="button"
size="small"
variant="gray"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={() => handleStartCreate(preset.name)}
>
<Plus className="h-4 w-4" />
{preset.label}
</Button>
))}
<Button type="button" size="small" onClick={applySearch}>
<Button
type="button"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={applySearch}
>
<Search className="h-4 w-4" />
查询
</Button>
@ -399,7 +407,13 @@ export default function SteelFrameLibraryPanel() {
删除
</Button>
) : null}
<Button type="button" size="small" onClick={handleSaveDraft} disabled={!draftCanSave}>
<Button
type="button"
size="small"
className={LIBRARY_PRIMARY_BUTTON_CLASS}
onClick={handleSaveDraft}
disabled={!draftCanSave}
>
<Save className="h-4 w-4" />
保存
</Button>

View File

@ -274,6 +274,7 @@ function createProductRecord(
skuItems: [],
factoryRows: [],
pricingRows: [],
productLimitRows: [],
sampleMeasurementRows: [],
uploadFiles: [],
documentWorkspaceNotes: {},
@ -308,6 +309,7 @@ function createSkuItem(
drawingId: "SZ0230K01AV0",
installMethod: "拆装",
executeStandard: "",
nationalStandardCompliant: "",
manufacturer: "",
manufacturerCode: "",
tagMaterial: "",
@ -476,6 +478,7 @@ describe("NonIntegratedDetailClient", () => {
expect(mockedUploadDetachedNonIntegratedFile).toHaveBeenCalledWith(
undefined,
expect.objectContaining({ name: "remote-preview.png" }),
expect.any(Function),
);
expect(mockedSaveSharedNonIntegratedProduct.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
@ -1127,6 +1130,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-设计卖点",
"document-workspace-trigger-产品模型",
"document-workspace-trigger-评审资料",
"document-workspace-trigger-企划布局",
"document-workspace-trigger-专利申请",
"document-workspace-trigger-产品实拍图",
"document-workspace-trigger-安装方式",
@ -1134,6 +1138,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-产品定厂",
"document-workspace-trigger-样包测量",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
]);
expect(
screen.queryByTestId("document-workspace-trigger-立项评审"),
@ -1205,6 +1210,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-设计卖点",
"document-workspace-trigger-产品模型",
"document-workspace-trigger-评审资料",
"document-workspace-trigger-企划布局",
"document-workspace-trigger-专利申请",
"document-workspace-trigger-产品实拍图",
"document-workspace-trigger-安装方式",
@ -1212,6 +1218,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-产品定厂",
"document-workspace-trigger-样包测量",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
]);
expect(
sidebar.querySelector<HTMLButtonElement>(
@ -1747,11 +1754,7 @@ describe("NonIntegratedDetailClient", () => {
screen.queryByRole("button", { name: "删除当前" }),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("document-workspace-confirm-专利申请"),
).not.toBeInTheDocument();
await actor.click(screen.getByTestId("workflow-approve-current-node"));
await actor.click(screen.getByTestId("document-workspace-confirm-专利申请"));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
@ -1833,7 +1836,7 @@ describe("NonIntegratedDetailClient", () => {
expect(screen.queryByLabelText("申请号")).not.toBeInTheDocument();
expect(screen.queryByLabelText("申请缴费日")).not.toBeInTheDocument();
await actor.click(screen.getByTestId("workflow-approve-current-node"));
await actor.click(screen.getByTestId("document-workspace-confirm-专利申请"));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
@ -1918,6 +1921,55 @@ describe("NonIntegratedDetailClient", () => {
).not.toBeInTheDocument();
});
it("persists the SKU national standard compliance selection", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-national-standard",
spu: "SZ0230",
skuItems: [
createSkuItem({
id: "sku-national-standard-1",
model: "SZ0230K01",
}),
createSkuItem({
id: "sku-national-standard-2",
model: "SZ0230K01",
}),
createSkuItem({
id: "sku-national-standard-3",
model: "SZ0230K02",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-national-standard"
/>,
);
const skuRow = screen.getAllByDisplayValue("SZ0230K01")[0]?.closest("tr");
expect(skuRow).not.toBeNull();
await actor.selectOptions(
within(skuRow!).getByRole("combobox", { name: "符合国标" }),
"是",
);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-national-standard",
);
expect(savedProduct?.skuItems[0]?.nationalStandardCompliant).toBe("是");
expect(savedProduct?.skuItems[1]?.nationalStandardCompliant).toBe("是");
expect(savedProduct?.skuItems[2]?.nationalStandardCompliant).toBe("");
});
});
it("hides redundant fields from the product detail summary", () => {
saveNonIntegratedProduct(
createProductRecord({
@ -1981,6 +2033,7 @@ describe("NonIntegratedDetailClient", () => {
id: "sku-sample-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
barcode: "SZ023001N1280V01",
@ -2055,8 +2108,9 @@ describe("NonIntegratedDetailClient", () => {
it("lets purchasing users edit only purchasing document nodes", async () => {
const actor = userEvent.setup();
const openingReferenceCategory = NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[0];
const sampleMeasurementCategory = NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[10];
const openingReferenceCategory = "开品参考";
const sampleMeasurementCategory = "样包测量";
const productLimitCategory = "产品限价";
saveNonIntegratedProduct(
createProductRecord({
@ -2106,12 +2160,24 @@ describe("NonIntegratedDetailClient", () => {
);
expect(savedProduct?.sampleMeasurementRows?.[0]?.lengthCm).toBe("120");
});
await actor.click(
screen.getByTestId(`document-workspace-trigger-${productLimitCategory}`),
);
expect(
screen.getByTestId(
"product-limit-expectedDailyLimitPrice-sku-purchasing-permission-1",
),
).toHaveAttribute("readonly");
});
it("lets planning users edit only the opening reference node", async () => {
it("lets planning users edit opening reference, planning layout, and product limit nodes", async () => {
const actor = userEvent.setup();
const openingReferenceCategory = NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[0];
const sampleMeasurementCategory = NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[10];
const openingReferenceCategory = "开品参考";
const planningLayoutCategory = "企划布局";
const productLimitCategory = "产品限价";
const sampleMeasurementCategory = "样包测量";
saveNonIntegratedProduct(
createProductRecord({
@ -2137,7 +2203,7 @@ describe("NonIntegratedDetailClient", () => {
initialDocumentWorkspaceCategory={openingReferenceCategory}
currentUserRole="USER"
currentUserName="企划同事"
currentUserBusinessRoles={["PLANNING"]}
currentUserDepartmentName="品牌中心"
/>,
);
@ -2153,6 +2219,32 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.openingReference.reason).toBe("企划补充开品理由");
});
await actor.click(
screen.getByTestId(`document-workspace-trigger-${planningLayoutCategory}`),
);
expect(
screen.getByTestId(`document-workspace-confirm-${planningLayoutCategory}`),
).not.toBeDisabled();
await actor.click(
screen.getByTestId(`document-workspace-trigger-${productLimitCategory}`),
);
await actor.type(
screen.getByTestId(
"product-limit-expectedDailyLimitPrice-sku-planning-permission-1",
),
"1999",
);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-planning-permission",
);
expect(savedProduct?.productLimitRows?.[0]?.expectedDailyLimitPrice).toBe(
"1999",
);
});
await actor.click(
screen.getByTestId(`document-workspace-trigger-${sampleMeasurementCategory}`),
);
@ -2191,7 +2283,7 @@ describe("NonIntegratedDetailClient", () => {
});
it("keeps regular users read-only in purchasing document nodes", async () => {
const sampleMeasurementCategory = NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[10];
const sampleMeasurementCategory = "样包测量";
saveNonIntegratedProduct(
createProductRecord({
@ -2267,11 +2359,9 @@ describe("NonIntegratedDetailClient", () => {
);
await actor.click(screen.getByTestId("document-workspace-trigger-包装细节"));
await actor.click(screen.getByTestId("packaging-workspace-panel-factoryQuality"));
const panel = screen.getByTestId("document-workspace-panel");
expect(
screen.getByTestId("packaging-factory-quality-section"),
).toBeInTheDocument();
expect(screen.getByTestId("factory-quality-workspace")).toBeInTheDocument();
expect(screen.getByTestId("factory-quality-style-K01")).toHaveTextContent(
"SZ0230K01",
@ -2349,6 +2439,7 @@ describe("NonIntegratedDetailClient", () => {
id: "sku-pricing-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
manufacturer: "兰考华兰家具",
@ -2414,6 +2505,119 @@ describe("NonIntegratedDetailClient", () => {
});
});
it("renders planning layout rows from SKU data without SKU edit actions", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-planning-layout",
skuItems: [
createSkuItem({
id: "sku-planning-layout-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
size: "140/60/75",
color: "胡桃色",
material: "橡胶木",
installMethod: "整装",
manufacturer: "兰考华兰家具",
executeStandard: "GB/T3324-2024",
nationalStandardCompliant: "是",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-planning-layout"
/>,
);
await actor.click(screen.getByTestId("document-workspace-trigger-企划布局"));
const planningWorkspace = screen.getByTestId("planning-layout-workspace");
expect(planningWorkspace).toHaveTextContent("SZ0230K01");
expect(planningWorkspace).toHaveTextContent("SZ023001N1280V01");
expect(planningWorkspace).toHaveTextContent("GB/T3324-2024");
expect(planningWorkspace).toHaveTextContent("是");
expect(
screen.getByTestId("document-workspace-confirm-企划布局"),
).toBeInTheDocument();
expect(
within(planningWorkspace).queryByRole("button", { name: "新增SKU" }),
).not.toBeInTheDocument();
expect(
within(planningWorkspace).queryByText("上传图片"),
).not.toBeInTheDocument();
expect(
within(planningWorkspace).queryByText("删除"),
).not.toBeInTheDocument();
});
it("renders product limit rows and persists editable limit fields", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-product-limit",
spu: "SZ0230",
previewImage: "data:image/png;base64,product-limit-image",
skuItems: [
createSkuItem({
id: "sku-product-limit-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
}),
],
}),
);
render(
<NonIntegratedDetailClient mode="edit" productId="DNZ0001-product-limit" />,
);
await actor.click(screen.getByTestId("document-workspace-trigger-产品限价"));
const productLimitWorkspace = screen.getByTestId("product-limit-workspace");
expect(productLimitWorkspace).toHaveTextContent("SPU");
expect(productLimitWorkspace).toHaveTextContent("SZ0230");
expect(productLimitWorkspace).toHaveTextContent("期望日常限价");
expect(productLimitWorkspace).toHaveTextContent("期望限价毛利率(%)");
expect(productLimitWorkspace).toHaveTextContent("非SPU负责人限价");
await actor.type(screen.getByTestId("product-limit-expectedDailyLimitPrice-sku-product-limit-1"), "1999");
await actor.type(screen.getByTestId("product-limit-expectedLimitGrossMarginRate-sku-product-limit-1"), "45");
await actor.type(screen.getByTestId("product-limit-nonSpuOwnerLimitPrice-sku-product-limit-1"), "1899");
await waitFor(() => {
const savedProduct = getNonIntegratedProductById("DNZ0001-product-limit");
expect(savedProduct?.productLimitRows).toEqual([
{
skuId: "sku-product-limit-1",
expectedDailyLimitPrice: "1999",
expectedLimitGrossMarginRate: "45",
nonSpuOwnerLimitPrice: "1899",
},
]);
});
await actor.click(screen.getByRole("button", { name: "确认限价" }));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById("DNZ0001-product-limit");
expect(savedProduct?.documentWorkspaceConfirmations["产品限价"]).toBe(true);
expect(savedProduct?.documentWorkspaceApprovedAt?.["产品限价"]).toBeTruthy();
});
});
it.skip("persists distinct board material rows after submit", async () => {
const actor = userEvent.setup();
@ -2817,7 +3021,7 @@ describe("NonIntegratedDetailClient", () => {
expect(
screen.getByTestId("document-workspace-basic-info-trigger"),
).toHaveAttribute("data-pending-confirmation", "true");
expect(screen.getByTestId("workflow-approve-current-node")).toBeEnabled();
expect(screen.getByTestId("basic-info-confirm")).toBeEnabled();
});
});

View File

@ -37,6 +37,7 @@ export const NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES = [
"立项评审",
"产品模型",
"评审资料",
"企划布局",
"专利申请",
"产品实拍图",
"安装方式",
@ -44,11 +45,14 @@ export const NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES = [
"产品定厂",
"样包测量",
"产品核价",
"产品限价",
"工厂质检",
"检测报告",
] as const;
export const NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY = "基础信息" as const;
export const NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY =
"企划布局" as const;
export const NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES = [
"开品参考",
@ -57,6 +61,7 @@ export const NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES = [
"产品模型",
"评审资料",
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY,
"专利申请",
"产品实拍图",
"安装方式",
@ -64,6 +69,7 @@ export const NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES = [
"产品定厂",
"样包测量",
"产品核价",
"产品限价",
"工厂质检",
"检测报告",
] as const;
@ -250,6 +256,7 @@ const NON_INTEGRATED_UPLOAD_CATEGORY_MAP: Record<string, string> = {
立项评审: "立项评审",
评审资料: "评审资料",
样品评审: "评审资料",
企划布局: "企划布局",
专利申请: "专利申请",
资料信息: "评审资料",
产品模型: "产品模型",
@ -261,6 +268,7 @@ const NON_INTEGRATED_UPLOAD_CATEGORY_MAP: Record<string, string> = {
产品定厂: "产品定厂",
样包测量: "样包测量",
产品核价: "产品核价",
产品限价: "产品限价",
工厂质检: "工厂质检",
仓库质检: "工厂质检",
仓库抽检: "检测报告",
@ -291,6 +299,7 @@ export type NonIntegratedSkuItem = {
drawingId: string;
installMethod: string;
executeStandard: string;
nationalStandardCompliant: string;
manufacturer: string;
manufacturerCode: string;
tagMaterial: string;
@ -331,6 +340,13 @@ export type NonIntegratedPricingRow = {
productTag: string;
};
export type NonIntegratedProductLimitRow = {
skuId: string;
expectedDailyLimitPrice: string;
expectedLimitGrossMarginRate: string;
nonSpuOwnerLimitPrice: string;
};
export type NonIntegratedSampleMeasurementRow = {
skuId: string;
lengthCm: string;
@ -528,6 +544,7 @@ export type NonIntegratedProductRecord = {
skuBatchChangeTypes?: Record<string, string>;
factoryRows: NonIntegratedFactoryRow[];
pricingRows: NonIntegratedPricingRow[];
productLimitRows: NonIntegratedProductLimitRow[];
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
uploadFiles: NonIntegratedUploadFile[];
documentWorkspaceNotes?: Record<string, string>;
@ -1039,6 +1056,7 @@ function normalizeSkuItem(
drawingId: normalizeString(item.drawingId),
installMethod: normalizeString(item.installMethod),
executeStandard: normalizeString(item.executeStandard),
nationalStandardCompliant: normalizeString(item.nationalStandardCompliant),
manufacturer: normalizeString(item.manufacturer),
manufacturerCode: normalizeString(item.manufacturerCode),
tagMaterial: normalizeString(item.tagMaterial),
@ -1104,6 +1122,37 @@ function normalizePricingRow(
};
}
function normalizeProductLimitRow(
item: Partial<NonIntegratedProductLimitRow>,
): NonIntegratedProductLimitRow | null {
if (typeof item.skuId !== "string") return null;
return {
skuId: item.skuId,
expectedDailyLimitPrice:
typeof item.expectedDailyLimitPrice === "string"
? item.expectedDailyLimitPrice
: typeof item.expectedDailyLimitPrice === "number" &&
Number.isFinite(item.expectedDailyLimitPrice)
? String(item.expectedDailyLimitPrice)
: "",
expectedLimitGrossMarginRate:
typeof item.expectedLimitGrossMarginRate === "string"
? item.expectedLimitGrossMarginRate
: typeof item.expectedLimitGrossMarginRate === "number" &&
Number.isFinite(item.expectedLimitGrossMarginRate)
? String(item.expectedLimitGrossMarginRate)
: "",
nonSpuOwnerLimitPrice:
typeof item.nonSpuOwnerLimitPrice === "string"
? item.nonSpuOwnerLimitPrice
: typeof item.nonSpuOwnerLimitPrice === "number" &&
Number.isFinite(item.nonSpuOwnerLimitPrice)
? String(item.nonSpuOwnerLimitPrice)
: "",
};
}
function normalizeSampleMeasurementRow(
item: Partial<NonIntegratedSampleMeasurementRow>,
): NonIntegratedSampleMeasurementRow | null {
@ -1650,6 +1699,13 @@ function toProductRecord(
.map((entry) => normalizePricingRow(entry))
.filter((entry): entry is NonIntegratedPricingRow => entry !== null)
: [],
productLimitRows: Array.isArray(item.productLimitRows)
? item.productLimitRows
.map((entry) => normalizeProductLimitRow(entry))
.filter(
(entry): entry is NonIntegratedProductLimitRow => entry !== null,
)
: [],
sampleMeasurementRows: Array.isArray(item.sampleMeasurementRows)
? item.sampleMeasurementRows
.map((entry) => normalizeSampleMeasurementRow(entry))
@ -1976,6 +2032,7 @@ export function createCopiedNonIntegratedProduct(
})),
factoryRows: [],
pricingRows: [],
productLimitRows: [],
sampleMeasurementRows: [],
uploadFiles: sourceProduct.uploadFiles.map((file) => ({ ...file })),
documentWorkspaceNotes: {

View File

@ -15,6 +15,7 @@ export type NonIntegratedSkuExportRow = {
drawingId: string
installMethod: string
executeStandard: string
nationalStandardCompliant: string
manufacturer: string
manufacturerCode: string
batchNo: string
@ -47,6 +48,7 @@ export const NON_INTEGRATED_SKU_TEMPLATE_COLUMNS: {
{ key: 'drawingId', label: '图纸ID', width: 22 },
{ key: 'installMethod', label: '安装方式', width: 14 },
{ key: 'executeStandard', label: '执行标准', width: 18 },
{ key: 'nationalStandardCompliant', label: '符合国标', width: 12 },
{ key: 'manufacturer', label: '供应商名称', width: 18 },
{ key: 'manufacturerCode', label: '供应商代码', width: 14 },
{ key: 'batchNo', label: '创建批次', width: 12 },