feat: 优化正式入库人员字段与首批质检
This commit is contained in:
parent
8d76d4ef9d
commit
0f7e647127
@ -1859,7 +1859,9 @@ describe('IntakeClient', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows view-only actions for non-matching designers', () => {
|
||||
it('hides unrelated products from non-matching designers in the created scope', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
window.localStorage.setItem(
|
||||
NON_INTEGRATED_STORAGE_KEY,
|
||||
JSON.stringify([createNonIntegratedProduct()]),
|
||||
@ -1871,17 +1873,10 @@ describe('IntakeClient', () => {
|
||||
userName: 'Designer B',
|
||||
})
|
||||
|
||||
const productRow = screen.getByText('DNZ0001').closest('tr')
|
||||
expect(productRow).not.toBeNull()
|
||||
expect(
|
||||
within(productRow as HTMLElement).getByRole('button', { name: '查看' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(productRow as HTMLElement).queryByRole('button', { name: '复制' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
within(productRow as HTMLElement).queryByRole('button', { name: '删除' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByText('DNZ0001')).toBeInTheDocument()
|
||||
await actor.click(screen.getByRole('button', { name: '我创建的' }))
|
||||
|
||||
expect(screen.queryByText('DNZ0001')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lets matching structure designers open products for editing', () => {
|
||||
@ -1941,6 +1936,39 @@ describe('IntakeClient', () => {
|
||||
expect(actionCell?.querySelectorAll('button')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('filters created products by the current DingTalk account name', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
window.localStorage.setItem(
|
||||
NON_INTEGRATED_STORAGE_KEY,
|
||||
JSON.stringify([
|
||||
createNonIntegratedProduct({
|
||||
id: 'caishen-created',
|
||||
spu: 'CS0001',
|
||||
createdBy: '财神',
|
||||
designer: '',
|
||||
}),
|
||||
createNonIntegratedProduct({
|
||||
id: 'xuxiake-created',
|
||||
spu: 'XXK0001',
|
||||
createdBy: '徐霞客',
|
||||
designer: '',
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
renderSelector({
|
||||
eligibleSkuList: [],
|
||||
userRole: 'DESIGNER',
|
||||
userName: '财神',
|
||||
})
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '我创建的' }))
|
||||
|
||||
expect(screen.getByText('CS0001')).toBeInTheDocument()
|
||||
expect(screen.queryByText('XXK0001')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lets planning users open products for opening-reference editing only', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
@ -2029,7 +2057,7 @@ describe('IntakeClient', () => {
|
||||
|
||||
const productRow = screen.getByText('DNZ0001').closest('tr')
|
||||
expect(productRow).not.toBeNull()
|
||||
expect(within(productRow as HTMLElement).getByText('已废弃')).toBeInTheDocument()
|
||||
expect(within(productRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
|
||||
|
||||
const editButton = (productRow as HTMLElement).querySelector(
|
||||
'button[aria-label="修改"]',
|
||||
@ -2078,7 +2106,7 @@ describe('IntakeClient', () => {
|
||||
|
||||
const abandonedRow = screen.getByText('DNZ0001').closest('tr')
|
||||
expect(abandonedRow).not.toBeNull()
|
||||
expect(within(abandonedRow as HTMLElement).getByText('已废弃')).toBeInTheDocument()
|
||||
expect(within(abandonedRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
|
||||
|
||||
await actor.click(
|
||||
within(abandonedRow as HTMLElement).getByRole('button', { name: '恢复' }),
|
||||
@ -2087,7 +2115,7 @@ describe('IntakeClient', () => {
|
||||
expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('重新审批'))
|
||||
const restoredRow = screen.getByText('DNZ0001').closest('tr')
|
||||
expect(restoredRow).not.toBeNull()
|
||||
expect(within(restoredRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
|
||||
expect(within(restoredRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
|
||||
expect(
|
||||
within(restoredRow as HTMLElement).queryByRole('button', { name: '恢复' }),
|
||||
).not.toBeInTheDocument()
|
||||
@ -2234,7 +2262,7 @@ describe('IntakeClient', () => {
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '修改' }))
|
||||
expect(screen.getByTestId('embedded-workspace-state')).toHaveTextContent(
|
||||
'detail-基础信息',
|
||||
'document-开品参考',
|
||||
)
|
||||
|
||||
await actor.click(screen.getByRole('button', { name: '模拟切到专利申请' }))
|
||||
@ -2399,13 +2427,20 @@ describe('IntakeClient', () => {
|
||||
|
||||
expect(checkedRow).not.toBeNull()
|
||||
expect(pendingRow).not.toBeNull()
|
||||
expect(within(checkedRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
|
||||
expect(within(pendingRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '产品实拍' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '产品资料' })).toBeInTheDocument()
|
||||
expect(within(checkedRow as HTMLElement).getByText('设计卖点')).toBeInTheDocument()
|
||||
expect(within(pendingRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: '产品实拍 结构设计师' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: '产品资料 外观/结构' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('columnheader', { name: '工厂质检' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: '首批质检 质检组' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('columnheader', { name: '仓库抽检' }),
|
||||
).not.toBeInTheDocument()
|
||||
@ -2732,7 +2767,7 @@ describe('IntakeClient', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('shows operation and purchaser fields and filters non-integrated products by query button', async () => {
|
||||
it('shows owner fields and filters non-integrated products by query button', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
window.localStorage.setItem(
|
||||
@ -2742,8 +2777,17 @@ describe('IntakeClient', () => {
|
||||
id: 'DNZ0002-1',
|
||||
spu: 'GBGJ-DNZ0002',
|
||||
designer: 'Designer B',
|
||||
openingReference: {
|
||||
...createEmptyNonIntegratedOpeningReference(),
|
||||
designer: 'Designer B',
|
||||
structureDesigner: 'Structure B',
|
||||
planningOwner: 'Planning B',
|
||||
},
|
||||
operator: 'Operator B',
|
||||
purchaser: 'Buyer B',
|
||||
productPlanning: 'Planning B',
|
||||
visualPlanning: 'Space B',
|
||||
purchaseOwner: 'Buyer B',
|
||||
previewImage: 'data:image/png;base64,test-image-2',
|
||||
previewImageName: 'preview-2.png',
|
||||
createdAt: '2026-04-14T10:00:00.000Z',
|
||||
@ -2760,30 +2804,46 @@ describe('IntakeClient', () => {
|
||||
|
||||
renderSelector({ eligibleSkuList: [] })
|
||||
|
||||
expect(screen.getByRole('button', { name: '全部' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: '我创建的' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '待处理' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '待处理的' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('DNZ0001')).toBeInTheDocument()
|
||||
expect(screen.getByText('Operator A')).toBeInTheDocument()
|
||||
expect(screen.getByText('Buyer A')).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '当前节点负责人' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '创建时间' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '外观设计师' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '结构设计师' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: 'SPU负责人' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '企划负责人' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '空间设计师' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '采购负责人' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('columnheader', { name: '当前节点' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('columnheader', { name: '当前节点负责人' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('columnheader', { name: '创建时间' })).not.toBeInTheDocument()
|
||||
const defaultProductRow = screen.getByText('DNZ0001').closest('tr')
|
||||
expect(defaultProductRow).not.toBeNull()
|
||||
expect(within(defaultProductRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
|
||||
expect(
|
||||
within(defaultProductRow as HTMLElement).getByText('2026-04-15 10:00:00'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('SPU')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('设计师')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('运营')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('采购')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('外观设计师')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('结构设计师')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('SPU负责人')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('企划负责人')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('空间设计师')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('采购负责人')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '查询' })).toBeInTheDocument()
|
||||
|
||||
await actor.type(screen.getByLabelText('运营'), 'Operator B')
|
||||
await actor.type(screen.getByLabelText('采购负责人'), 'Buyer B')
|
||||
await actor.click(screen.getByRole('button', { name: '查询' }))
|
||||
|
||||
expect(screen.getByText('GBGJ-DNZ0002')).toBeInTheDocument()
|
||||
expect(screen.queryByText('DNZ0001')).not.toBeInTheDocument()
|
||||
|
||||
await actor.clear(screen.getByLabelText('运营'))
|
||||
await actor.clear(screen.getByLabelText('采购负责人'))
|
||||
await actor.type(screen.getByLabelText('SPU'), 'DNZ0002')
|
||||
await actor.click(screen.getByRole('button', { name: '查询' }))
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
Package,
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
@ -366,13 +367,13 @@ function DocumentStatusMark({
|
||||
<span
|
||||
aria-label={`${label}${statusText}`}
|
||||
title={statusText}
|
||||
className={`inline-flex h-6 w-6 items-center justify-center rounded-md border transition-colors ${
|
||||
className={`inline-flex h-[17px] w-[17px] items-center justify-center rounded border transition-colors ${
|
||||
ready
|
||||
? "border-zinc-950 bg-zinc-950 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]"
|
||||
: "border-zinc-200 bg-zinc-50 text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" strokeWidth={2.4} aria-hidden="true" />
|
||||
<Icon className="h-2.5 w-2.5" strokeWidth={2.4} aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -382,7 +383,6 @@ type NonIntegratedDocumentStatusCategory = NonIntegratedWorkflowNodeCategory;
|
||||
const NON_INTEGRATED_DOCUMENT_STATUS_HIDDEN_CATEGORIES: ReadonlySet<NonIntegratedDocumentStatusCategory> =
|
||||
new Set<NonIntegratedDocumentStatusCategory>([
|
||||
"立项评审",
|
||||
"工厂质检",
|
||||
"检测报告",
|
||||
]);
|
||||
|
||||
@ -398,6 +398,109 @@ const NON_INTEGRATED_FLOW_COMPLETE_CATEGORY =
|
||||
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS[
|
||||
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length - 1
|
||||
];
|
||||
const NON_INTEGRATED_NODE_OWNER_LABELS: Partial<
|
||||
Record<NonIntegratedDocumentStatusCategory, string>
|
||||
> = {
|
||||
开品参考: "企划负责人",
|
||||
设计卖点: "外观设计师",
|
||||
产品模型: "外观/结构",
|
||||
评审资料: "设计/采购",
|
||||
基础信息: "结构设计师",
|
||||
企划布局: "空间设计师",
|
||||
专利申请: "外观设计师",
|
||||
产品实拍图: "结构设计师",
|
||||
安装方式: "结构设计师",
|
||||
包装细节: "结构设计师",
|
||||
产品定厂: "采购负责人",
|
||||
样包测量: "采购/核价组",
|
||||
产品核价: "采购/核价组",
|
||||
产品限价: "企划负责人",
|
||||
工厂质检: "质检组",
|
||||
};
|
||||
|
||||
function getNonIntegratedListNodeLabel(
|
||||
category: NonIntegratedDocumentStatusCategory,
|
||||
) {
|
||||
if (category === "企划布局") return "空间布局";
|
||||
return getNonIntegratedDocumentWorkspaceCategoryLabel(category);
|
||||
}
|
||||
|
||||
function getNonIntegratedListNodeOwnerLabel(
|
||||
category: NonIntegratedDocumentStatusCategory,
|
||||
) {
|
||||
return NON_INTEGRATED_NODE_OWNER_LABELS[category] ?? "";
|
||||
}
|
||||
|
||||
function safeTrim(value: unknown) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function getNonIntegratedListPeople(product: NonIntegratedProductRecord) {
|
||||
return {
|
||||
appearanceDesigner:
|
||||
safeTrim(product.openingReference?.designer) || safeTrim(product.designer),
|
||||
structureDesigner:
|
||||
safeTrim(product.openingReference?.structureDesigner),
|
||||
spuOwner:
|
||||
safeTrim(product.spuOwner) ||
|
||||
safeTrim(product.operator) ||
|
||||
safeTrim(product.operationsOwner),
|
||||
planningOwner:
|
||||
safeTrim(product.productPlanning) ||
|
||||
safeTrim(product.openingReference?.planningOwner),
|
||||
spaceDesigner:
|
||||
safeTrim(product.visualPlanning) || safeTrim(product.visualDirector),
|
||||
purchaseOwner: safeTrim(product.purchaseOwner) || safeTrim(product.purchaser),
|
||||
};
|
||||
}
|
||||
|
||||
function displayPerson(value: string) {
|
||||
return value.trim() || "-";
|
||||
}
|
||||
|
||||
function productMatchesText(value: string, keyword: string) {
|
||||
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||
return (
|
||||
normalizedKeyword.length === 0 ||
|
||||
value.toLowerCase().includes(normalizedKeyword)
|
||||
);
|
||||
}
|
||||
|
||||
function getLegacyNonIntegratedProductOwnerNames(
|
||||
product: NonIntegratedProductRecord,
|
||||
) {
|
||||
const people = getNonIntegratedListPeople(product);
|
||||
return getUniqueNonEmptyValues([
|
||||
people.appearanceDesigner,
|
||||
people.structureDesigner,
|
||||
people.spuOwner,
|
||||
people.planningOwner,
|
||||
people.spaceDesigner,
|
||||
people.purchaseOwner,
|
||||
]);
|
||||
}
|
||||
|
||||
function isProductCreatedByCurrentUser(
|
||||
product: NonIntegratedProductRecord,
|
||||
user: IntakeProductPermissionUser,
|
||||
) {
|
||||
const userId = user.id.trim();
|
||||
const userName = user.name.trim();
|
||||
const creator = product.createdBy?.trim() ?? "";
|
||||
|
||||
if (
|
||||
creator &&
|
||||
((userId && creator === userId) || (userName && creator === userName))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
!creator &&
|
||||
userName &&
|
||||
getLegacyNonIntegratedProductOwnerNames(product).includes(userName),
|
||||
);
|
||||
}
|
||||
|
||||
const PENDING_PAGE_SIZE = 25;
|
||||
const TABLE_ACTION_BUTTON_BASE =
|
||||
@ -406,6 +509,7 @@ const TABLE_ACTION_BUTTON_SECONDARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-
|
||||
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";
|
||||
type NonIntegratedQuickScope = "all" | "created" | "pending";
|
||||
|
||||
const BOM_LIBRARY_OPTIONS: {
|
||||
key: BomLibraryKey;
|
||||
@ -1020,6 +1124,50 @@ function getPopulatedProductPricingRowCountForList(
|
||||
).length;
|
||||
}
|
||||
|
||||
function getPopulatedPlanningLayoutRowCountForList(
|
||||
product: NonIntegratedProductRecord,
|
||||
) {
|
||||
return (product.planningLayoutRows ?? []).filter((row) =>
|
||||
[
|
||||
row.style,
|
||||
row.styleTone,
|
||||
row.audience,
|
||||
row.model,
|
||||
row.priceBand,
|
||||
row.brand ?? "",
|
||||
row.shop,
|
||||
...row.brands,
|
||||
...row.spaces,
|
||||
...row.categories,
|
||||
].some((value) => value.trim().length > 0),
|
||||
).length;
|
||||
}
|
||||
|
||||
function getPopulatedProductLimitRowCountForList(
|
||||
product: NonIntegratedProductRecord,
|
||||
) {
|
||||
return (product.productLimitRows ?? []).filter((row) =>
|
||||
[
|
||||
row.expectedDailyLimitPrice,
|
||||
row.expectedLimitGrossMarginRate,
|
||||
row.nonSpuOwnerLimitPrice,
|
||||
].some((value) => value.trim().length > 0),
|
||||
).length;
|
||||
}
|
||||
|
||||
function getPopulatedFirstQualityRowCountForList(
|
||||
product: NonIntegratedProductRecord,
|
||||
) {
|
||||
return (product.firstQualityRows ?? []).filter((row) => {
|
||||
const conclusion = row.conclusion.trim();
|
||||
return Boolean(
|
||||
row.qualityBatch.trim() ||
|
||||
row.detail.trim() ||
|
||||
(conclusion && conclusion !== "待检"),
|
||||
);
|
||||
}).length;
|
||||
}
|
||||
|
||||
function hasPatentApplicationContentForList(product: NonIntegratedProductRecord) {
|
||||
const application = normalizeNonIntegratedPatentApplication(
|
||||
product.patentApplication,
|
||||
@ -1087,6 +1235,9 @@ function getDocumentWorkspaceCountByCategory(
|
||||
检测报告:
|
||||
uploadFileCountByCategory["检测报告"] +
|
||||
(qualityInspectionHasContent ? 1 : 0),
|
||||
企划布局:
|
||||
uploadFileCountByCategory["企划布局"] +
|
||||
getPopulatedPlanningLayoutRowCountForList(product),
|
||||
安装方式:
|
||||
uploadFileCountByCategory["安装方式"] +
|
||||
(hasInstallationContentForList(product) ? 1 : 0),
|
||||
@ -1102,6 +1253,12 @@ function getDocumentWorkspaceCountByCategory(
|
||||
产品核价:
|
||||
uploadFileCountByCategory["产品核价"] +
|
||||
getPopulatedProductPricingRowCountForList(product),
|
||||
产品限价:
|
||||
uploadFileCountByCategory["产品限价"] +
|
||||
getPopulatedProductLimitRowCountForList(product),
|
||||
工厂质检:
|
||||
uploadFileCountByCategory["工厂质检"] +
|
||||
getPopulatedFirstQualityRowCountForList(product),
|
||||
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: hasBasicInfoContentForList(
|
||||
product,
|
||||
)
|
||||
@ -1242,14 +1399,9 @@ function getCurrentNodeSummary(
|
||||
number
|
||||
>,
|
||||
) {
|
||||
const currentCategory = NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.find(
|
||||
(category) => {
|
||||
return !isDocumentWorkspaceReadyForList(
|
||||
product,
|
||||
category,
|
||||
documentWorkspaceCountByCategory,
|
||||
);
|
||||
},
|
||||
const currentCategory = getCurrentNodeCategoryForList(
|
||||
product,
|
||||
documentWorkspaceCountByCategory,
|
||||
);
|
||||
|
||||
if (!currentCategory) {
|
||||
@ -1265,11 +1417,74 @@ function getCurrentNodeSummary(
|
||||
);
|
||||
|
||||
return {
|
||||
nodeLabel: getNonIntegratedDocumentWorkspaceCategoryLabel(currentCategory),
|
||||
nodeLabel: getNonIntegratedListNodeLabel(currentCategory),
|
||||
ownerText: ownerCandidates.length > 0 ? ownerCandidates.join("、") : "-",
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentNodeCategoryForList(
|
||||
product: NonIntegratedProductRecord,
|
||||
documentWorkspaceCountByCategory: Record<
|
||||
NonIntegratedDocumentStatusCategory,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
return (
|
||||
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.find((category) => {
|
||||
return !isDocumentWorkspaceReadyForList(
|
||||
product,
|
||||
category,
|
||||
documentWorkspaceCountByCategory,
|
||||
);
|
||||
}) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function isCurrentUserPendingNonIntegratedProduct(
|
||||
product: NonIntegratedProductRecord,
|
||||
user: IntakeProductPermissionUser,
|
||||
documentWorkspaceCountByCategory: Record<
|
||||
NonIntegratedDocumentStatusCategory,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
const currentCategory = getCurrentNodeCategoryForList(
|
||||
product,
|
||||
documentWorkspaceCountByCategory,
|
||||
);
|
||||
if (!currentCategory) return false;
|
||||
|
||||
const userName = user.name.trim();
|
||||
const ownerCandidates = getCurrentNodeOwnerCandidates(product, currentCategory);
|
||||
if (userName && ownerCandidates.includes(userName)) return true;
|
||||
|
||||
const people = getNonIntegratedListPeople(product);
|
||||
if (
|
||||
["开品参考", "产品限价"].includes(currentCategory) &&
|
||||
isPlanningPermissionUser(user)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
currentCategory === "企划布局" &&
|
||||
(user.department?.name.includes("空间视觉") ||
|
||||
user.department?.name.includes("品牌中心") ||
|
||||
people.spaceDesigner === userName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
["产品定厂", "样包测量", "产品核价", "工厂质检", "检测报告"].includes(
|
||||
currentCategory,
|
||||
) &&
|
||||
isPurchasingPermissionUser(user)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDocumentWorkspaceApprovedForList(
|
||||
product: NonIntegratedProductRecord,
|
||||
category: NonIntegratedDocumentStatusCategory,
|
||||
@ -1743,17 +1958,25 @@ function NonIntegratedPendingDetail({
|
||||
} | null>(null);
|
||||
const [copyErrorMessage, setCopyErrorMessage] = useState("");
|
||||
const [isCopySubmitting, setIsCopySubmitting] = useState(false);
|
||||
const [activeQuickScope, setActiveQuickScope] =
|
||||
useState<NonIntegratedQuickScope>("all");
|
||||
const [queryDraft, setQueryDraft] = useState({
|
||||
spu: "",
|
||||
designer: "",
|
||||
operator: "",
|
||||
purchaser: "",
|
||||
appearanceDesigner: "",
|
||||
structureDesigner: "",
|
||||
spuOwner: "",
|
||||
planningOwner: "",
|
||||
spaceDesigner: "",
|
||||
purchaseOwner: "",
|
||||
});
|
||||
const [appliedQuery, setAppliedQuery] = useState({
|
||||
spu: "",
|
||||
designer: "",
|
||||
operator: "",
|
||||
purchaser: "",
|
||||
appearanceDesigner: "",
|
||||
structureDesigner: "",
|
||||
spuOwner: "",
|
||||
planningOwner: "",
|
||||
spaceDesigner: "",
|
||||
purchaseOwner: "",
|
||||
});
|
||||
const shouldUseSharedNonIntegratedApi = process.env.NODE_ENV !== "test";
|
||||
|
||||
@ -1858,24 +2081,6 @@ function NonIntegratedPendingDetail({
|
||||
});
|
||||
}
|
||||
|
||||
function getProductStatus(
|
||||
product: NonIntegratedProductRecord,
|
||||
documentWorkspaceCountByCategory: Record<
|
||||
NonIntegratedDocumentStatusCategory,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
if (product.projectStatus === "rejected") {
|
||||
return getFlowStatusDisplay("scrapped");
|
||||
}
|
||||
|
||||
return getFlowStatusDisplay(
|
||||
isNonIntegratedFlowCompleted(product, documentWorkspaceCountByCategory)
|
||||
? "done"
|
||||
: "running",
|
||||
);
|
||||
}
|
||||
|
||||
async function handleConfirmCopy() {
|
||||
if (!copyDialogState) return;
|
||||
|
||||
@ -1930,103 +2135,137 @@ function NonIntegratedPendingDetail({
|
||||
const normalizedSpuKeyword = appliedQuery.spu.trim().toUpperCase();
|
||||
const productDisplayName =
|
||||
getNonIntegratedProductDisplayName(product).toUpperCase();
|
||||
const documentWorkspaceCountByCategory =
|
||||
getDocumentWorkspaceCountByCategory(product);
|
||||
const people = getNonIntegratedListPeople(product);
|
||||
const spuMatched =
|
||||
normalizedSpuKeyword.length === 0 ||
|
||||
productDisplayName.includes(normalizedSpuKeyword) ||
|
||||
getSearchableNonIntegratedSpuKeywords(product.spu).some((keyword) =>
|
||||
keyword.includes(normalizedSpuKeyword),
|
||||
);
|
||||
const designerMatched =
|
||||
appliedQuery.designer.trim().length === 0 ||
|
||||
product.designer
|
||||
.toLowerCase()
|
||||
.includes(appliedQuery.designer.trim().toLowerCase());
|
||||
const operatorMatched =
|
||||
appliedQuery.operator.trim().length === 0 ||
|
||||
product.operator
|
||||
.toLowerCase()
|
||||
.includes(appliedQuery.operator.trim().toLowerCase());
|
||||
const purchaserMatched =
|
||||
appliedQuery.purchaser.trim().length === 0 ||
|
||||
product.purchaser
|
||||
.toLowerCase()
|
||||
.includes(appliedQuery.purchaser.trim().toLowerCase());
|
||||
const peopleMatched =
|
||||
productMatchesText(
|
||||
people.appearanceDesigner,
|
||||
appliedQuery.appearanceDesigner,
|
||||
) &&
|
||||
productMatchesText(
|
||||
people.structureDesigner,
|
||||
appliedQuery.structureDesigner,
|
||||
) &&
|
||||
productMatchesText(people.spuOwner, appliedQuery.spuOwner) &&
|
||||
productMatchesText(people.planningOwner, appliedQuery.planningOwner) &&
|
||||
productMatchesText(people.spaceDesigner, appliedQuery.spaceDesigner) &&
|
||||
productMatchesText(people.purchaseOwner, appliedQuery.purchaseOwner);
|
||||
const quickScopeMatched =
|
||||
activeQuickScope === "all" ||
|
||||
(activeQuickScope === "created"
|
||||
? isProductCreatedByCurrentUser(product, currentUser)
|
||||
: isCurrentUserPendingNonIntegratedProduct(
|
||||
product,
|
||||
currentUser,
|
||||
documentWorkspaceCountByCategory,
|
||||
));
|
||||
|
||||
return (
|
||||
spuMatched && designerMatched && operatorMatched && purchaserMatched
|
||||
);
|
||||
return spuMatched && peopleMatched && quickScopeMatched;
|
||||
});
|
||||
}, [appliedQuery, products]);
|
||||
}, [activeQuickScope, appliedQuery, currentUser, products]);
|
||||
const filterFields: Array<{
|
||||
key: keyof typeof queryDraft;
|
||||
label: ReactNode;
|
||||
ariaLabel: string;
|
||||
placeholder: string;
|
||||
}> = [
|
||||
{ key: "spu", label: <SpuLabel />, ariaLabel: "SPU", placeholder: "搜索 SPU" },
|
||||
{
|
||||
key: "appearanceDesigner",
|
||||
label: "外观设计师",
|
||||
ariaLabel: "外观设计师",
|
||||
placeholder: "搜索外观设计师",
|
||||
},
|
||||
{
|
||||
key: "structureDesigner",
|
||||
label: "结构设计师",
|
||||
ariaLabel: "结构设计师",
|
||||
placeholder: "搜索结构设计师",
|
||||
},
|
||||
{
|
||||
key: "spuOwner",
|
||||
label: "SPU负责人",
|
||||
ariaLabel: "SPU负责人",
|
||||
placeholder: "搜索SPU负责人",
|
||||
},
|
||||
{
|
||||
key: "planningOwner",
|
||||
label: "企划负责人",
|
||||
ariaLabel: "企划负责人",
|
||||
placeholder: "搜索企划负责人",
|
||||
},
|
||||
{
|
||||
key: "spaceDesigner",
|
||||
label: "空间设计师",
|
||||
ariaLabel: "空间设计师",
|
||||
placeholder: "搜索空间设计师",
|
||||
},
|
||||
{
|
||||
key: "purchaseOwner",
|
||||
label: "采购负责人",
|
||||
ariaLabel: "采购负责人",
|
||||
placeholder: "搜索采购负责人",
|
||||
},
|
||||
];
|
||||
const quickScopes: Array<{ key: NonIntegratedQuickScope; label: string }> = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "created", label: "我创建的" },
|
||||
{ key: "pending", label: "待处理" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="border-b border-apple-separator px-4 py-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid gap-2.5 md:grid-cols-[1fr_1fr_1fr_1fr_88px_112px]">
|
||||
<label className="space-y-1.5 text-xs text-apple-label-secondary">
|
||||
<SpuLabel />
|
||||
<input
|
||||
aria-label="SPU"
|
||||
value={queryDraft.spu}
|
||||
onChange={(event) =>
|
||||
setQueryDraft((current) => ({
|
||||
...current,
|
||||
spu: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="搜索 SPU"
|
||||
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1.5 text-xs text-apple-label-secondary">
|
||||
<span>设计师</span>
|
||||
<input
|
||||
aria-label="设计师"
|
||||
value={queryDraft.designer}
|
||||
onChange={(event) =>
|
||||
setQueryDraft((current) => ({
|
||||
...current,
|
||||
designer: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="搜索设计师"
|
||||
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1.5 text-xs text-apple-label-secondary">
|
||||
<span>运营</span>
|
||||
<input
|
||||
aria-label="运营"
|
||||
value={queryDraft.operator}
|
||||
onChange={(event) =>
|
||||
setQueryDraft((current) => ({
|
||||
...current,
|
||||
operator: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="搜索运营"
|
||||
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1.5 text-xs text-apple-label-secondary">
|
||||
<span>采购</span>
|
||||
<input
|
||||
aria-label="采购"
|
||||
value={queryDraft.purchaser}
|
||||
onChange={(event) =>
|
||||
setQueryDraft((current) => ({
|
||||
...current,
|
||||
purchaser: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="搜索采购"
|
||||
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{quickScopes.map((scope) => (
|
||||
<button
|
||||
key={scope.key}
|
||||
type="button"
|
||||
aria-pressed={activeQuickScope === scope.key}
|
||||
onClick={() => setActiveQuickScope(scope.key)}
|
||||
className={`h-7 rounded-apple-md px-3 text-xs font-apple-medium ${
|
||||
activeQuickScope === scope.key
|
||||
? "bg-zinc-950 text-white"
|
||||
: "border border-apple-separator bg-apple-bg-primary text-apple-label-secondary"
|
||||
}`}
|
||||
>
|
||||
{scope.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid items-end gap-2 md:grid-cols-[repeat(7,minmax(0,1fr))_72px_104px]">
|
||||
{filterFields.map((filter) => (
|
||||
<label
|
||||
key={filter.key}
|
||||
className="space-y-1.5 text-xs text-apple-label-secondary"
|
||||
>
|
||||
<span>{filter.label}</span>
|
||||
<input
|
||||
aria-label={filter.ariaLabel}
|
||||
value={queryDraft[filter.key]}
|
||||
onChange={(event) =>
|
||||
setQueryDraft((current) => ({
|
||||
...current,
|
||||
[filter.key]: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={filter.placeholder}
|
||||
className="h-8 w-full rounded-apple-md border border-apple-separator bg-apple-bg-secondary px-2.5 text-xs text-apple-label-primary outline-none"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
size="small"
|
||||
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
|
||||
className={`h-8 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
|
||||
onClick={() => setAppliedQuery(queryDraft)}
|
||||
>
|
||||
查询
|
||||
@ -2035,9 +2274,10 @@ function NonIntegratedPendingDetail({
|
||||
<div className="flex items-end justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
className={`h-9 w-full px-4 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
|
||||
className={`h-8 w-full px-3 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
|
||||
onClick={onOpenNewNonIntegratedProductWorkspace}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
新增产品
|
||||
</Button>
|
||||
</div>
|
||||
@ -2048,7 +2288,7 @@ function NonIntegratedPendingDetail({
|
||||
<div className="min-h-0 flex-1 overflow-auto p-4">
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="overflow-x-auto overflow-y-hidden pb-3">
|
||||
<table className="w-full min-w-[1920px] text-left text-xs">
|
||||
<table className="w-full min-w-[1880px] text-left text-xs">
|
||||
<thead className="bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr className="border-b border-apple-separator">
|
||||
<th className="min-w-[56px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
@ -2061,32 +2301,40 @@ function NonIntegratedPendingDetail({
|
||||
<SpuLabel />
|
||||
</th>
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
设计师
|
||||
外观设计师
|
||||
</th>
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
运营
|
||||
结构设计师
|
||||
</th>
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
采购
|
||||
SPU负责人
|
||||
</th>
|
||||
<th className="min-w-[88px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
状态
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
企划负责人
|
||||
</th>
|
||||
<th className="min-w-[148px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
当前节点负责人
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
空间设计师
|
||||
</th>
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
采购负责人
|
||||
</th>
|
||||
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
当前节点
|
||||
</th>
|
||||
{NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.map((category) => (
|
||||
<th
|
||||
key={category}
|
||||
className="min-w-[88px] px-3 py-2.5 text-center font-apple-medium whitespace-nowrap"
|
||||
>
|
||||
{getNonIntegratedDocumentWorkspaceCategoryLabel(category)}
|
||||
<div className="mx-auto grid w-[64px] justify-items-center gap-1">
|
||||
<span>{getNonIntegratedListNodeLabel(category)}</span>
|
||||
<span className="text-[10px] font-normal leading-none text-apple-label-tertiary">
|
||||
{getNonIntegratedListNodeOwnerLabel(category)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
<th className="min-w-[144px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
|
||||
创建时间
|
||||
</th>
|
||||
<th className="sticky right-0 z-20 min-w-[184px] border-l border-apple-separator bg-apple-bg-primary px-2.5 py-2.5 text-center font-apple-medium whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.28)]">
|
||||
<th className="sticky right-0 z-20 min-w-[144px] border-l border-apple-separator bg-apple-bg-primary px-2.5 py-2.5 text-center font-apple-medium whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.28)]">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
@ -2096,7 +2344,7 @@ function NonIntegratedPendingDetail({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={
|
||||
10 + NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length
|
||||
11 + NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length
|
||||
}
|
||||
className="px-4 py-10 text-center text-xs text-apple-label-secondary"
|
||||
>
|
||||
@ -2113,10 +2361,7 @@ function NonIntegratedPendingDetail({
|
||||
getDocumentWorkspaceCountByCategory(product);
|
||||
const isFlowAbandoned =
|
||||
product.projectStatus === "rejected";
|
||||
const productStatus = getProductStatus(
|
||||
product,
|
||||
documentWorkspaceCountByCategory,
|
||||
);
|
||||
const people = getNonIntegratedListPeople(product);
|
||||
const canEditProduct = canEditNonIntegratedProduct(
|
||||
product,
|
||||
currentUser,
|
||||
@ -2181,29 +2426,27 @@ function NonIntegratedPendingDetail({
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{product.designer}
|
||||
{displayPerson(people.appearanceDesigner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{product.operator || "-"}
|
||||
{displayPerson(people.structureDesigner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{product.purchaser || "-"}
|
||||
</td>
|
||||
<td className="px-3 py-3 whitespace-nowrap">
|
||||
<Badge
|
||||
type={productStatus.badgeType}
|
||||
variant="subtle"
|
||||
>
|
||||
{productStatus.label}
|
||||
</Badge>
|
||||
{displayPerson(people.spuOwner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
<div className="max-w-[168px] truncate">
|
||||
{currentNodeSummary.ownerText}
|
||||
</div>
|
||||
<div className="mt-1 max-w-[168px] truncate text-[11px] text-apple-label-secondary">
|
||||
{displayPerson(people.planningOwner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{displayPerson(people.spaceDesigner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{displayPerson(people.purchaseOwner)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
<span className="rounded-apple-md bg-[#eef2f7] px-2 py-1 text-[11px] font-apple-medium text-apple-label-primary">
|
||||
{currentNodeSummary.nodeLabel}
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
{NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.map(
|
||||
(category) => {
|
||||
@ -2226,9 +2469,6 @@ function NonIntegratedPendingDetail({
|
||||
);
|
||||
},
|
||||
)}
|
||||
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
|
||||
{getFormattedListDateTime(product.createdAt)}
|
||||
</td>
|
||||
<td className="sticky right-0 z-10 border-l border-apple-separator bg-apple-bg-primary px-2 py-2 text-center whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.2)]">
|
||||
<div className="flex flex-nowrap items-center justify-center gap-1.5">
|
||||
<Button
|
||||
@ -7342,13 +7582,12 @@ export default function IntakeClient(props: IntakeClientProps) {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleActiveProductWorkspaceDetailStateChange = useCallback(
|
||||
(detailState: WorkspaceProductDetailState) => {
|
||||
if (!activeProductWorkspaceTabId) return;
|
||||
updateProductWorkspaceDetailState(activeProductWorkspaceTabId, detailState);
|
||||
},
|
||||
[activeProductWorkspaceTabId, updateProductWorkspaceDetailState],
|
||||
);
|
||||
function handleActiveProductWorkspaceDetailStateChange(
|
||||
detailState: WorkspaceProductDetailState,
|
||||
) {
|
||||
if (!activeProductWorkspaceTabId) return;
|
||||
updateProductWorkspaceDetailState(activeProductWorkspaceTabId, detailState);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setNonIntegratedProducts(props.initialNonIntegratedProducts);
|
||||
@ -7527,6 +7766,17 @@ export default function IntakeClient(props: IntakeClientProps) {
|
||||
"non-integrated-product",
|
||||
`${product.id}:${effectiveMode}`,
|
||||
);
|
||||
const currentCategory = getCurrentNodeCategoryForList(
|
||||
product,
|
||||
getDocumentWorkspaceCountByCategory(product),
|
||||
);
|
||||
const currentDocumentCategory =
|
||||
currentCategory &&
|
||||
NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES.includes(
|
||||
currentCategory as NonIntegratedDocumentWorkspaceCategory,
|
||||
)
|
||||
? (currentCategory as NonIntegratedDocumentWorkspaceCategory)
|
||||
: null;
|
||||
const nextTab: IntakeWorkspaceTab = {
|
||||
id: tabId,
|
||||
kind: "nonIntegratedProduct",
|
||||
@ -7534,6 +7784,9 @@ export default function IntakeClient(props: IntakeClientProps) {
|
||||
productId: product.id,
|
||||
product,
|
||||
mode: effectiveMode,
|
||||
detailState: currentDocumentCategory
|
||||
? { view: "document", category: currentDocumentCategory }
|
||||
: undefined,
|
||||
};
|
||||
openWorkspaceTab(nextTab);
|
||||
}
|
||||
|
||||
@ -457,7 +457,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
expect(screen.getByLabelText("产品图片")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("品牌")).toHaveValue("基本盘");
|
||||
await actor.type(screen.getByLabelText("名称"), "测试开品");
|
||||
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
|
||||
await actor.selectOptions(screen.getByLabelText("品牌"), "顾家");
|
||||
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
|
||||
|
||||
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
|
||||
const fileInputs = container.querySelectorAll('input[type="file"]');
|
||||
@ -473,13 +475,17 @@ describe("NonIntegratedDetailClient", () => {
|
||||
expect(savedProducts).toHaveLength(1);
|
||||
expect(savedProducts[0]?.spu).toBe("PD20260505175418001");
|
||||
expect(savedProducts[0]?.projectStatus).toBe("draft");
|
||||
expect(savedProducts[0]?.createdBy).toBe("creator-user-id");
|
||||
expect(savedProducts[0]?.updatedBy).toBe("creator-user-id");
|
||||
expect(savedProducts[0]?.createdBy).toBe("Creator");
|
||||
expect(savedProducts[0]?.updatedBy).toBe("Creator");
|
||||
expect(savedProducts[0]?.brand).toBe("顾家");
|
||||
expect(savedProducts[0]?.adaptableBrands).toEqual(["顾家"]);
|
||||
expect(savedProducts[0]?.openingReference.productName).toBe("测试开品");
|
||||
expect(savedProducts[0]?.openingReference.brand).toBe("顾家");
|
||||
expect(savedProducts[0]?.openingReference.adaptableBrands).toEqual(["顾家"]);
|
||||
expect(savedProducts[0]?.openingReference.designer).toBe("外观A");
|
||||
expect(savedProducts[0]?.openingReference.planningOwner).toBe("企划A");
|
||||
expect(savedProducts[0]?.designer).toBe("外观A");
|
||||
expect(savedProducts[0]?.productPlanning).toBe("企划A");
|
||||
expect(savedProducts[0]?.openingReference.temporaryCode).toBe(
|
||||
"PD20260505175418001",
|
||||
);
|
||||
@ -506,6 +512,8 @@ describe("NonIntegratedDetailClient", () => {
|
||||
const { container } = render(<NonIntegratedDetailClient mode="create" />);
|
||||
|
||||
await actor.type(screen.getByLabelText("名称"), "远程开品");
|
||||
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
|
||||
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
|
||||
|
||||
const imageFile = new File(["image"], "remote-preview.png", {
|
||||
type: "image/png",
|
||||
@ -903,6 +911,8 @@ describe("NonIntegratedDetailClient", () => {
|
||||
const { container } = render(<NonIntegratedDetailClient mode="create" />);
|
||||
|
||||
await actor.type(screen.getByLabelText("名称"), "仅填写名称的开品");
|
||||
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
|
||||
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
|
||||
|
||||
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
|
||||
const fileInputs = container.querySelectorAll('input[type="file"]');
|
||||
@ -1195,6 +1205,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
"document-workspace-trigger-样包测量",
|
||||
"document-workspace-trigger-产品核价",
|
||||
"document-workspace-trigger-产品限价",
|
||||
"document-workspace-trigger-工厂质检",
|
||||
]);
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-立项评审"),
|
||||
@ -1206,8 +1217,8 @@ describe("NonIntegratedDetailClient", () => {
|
||||
expect(reviewTrigger).toHaveAttribute("data-has-files", "true");
|
||||
expect(modelTrigger).toHaveAttribute("data-has-files", "true");
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-工厂质检"),
|
||||
).not.toBeInTheDocument();
|
||||
screen.getByTestId("document-workspace-trigger-工厂质检"),
|
||||
).toHaveTextContent("首批质检");
|
||||
expect(
|
||||
screen.queryByTestId("document-workspace-trigger-检测报告"),
|
||||
).not.toBeInTheDocument();
|
||||
@ -1275,6 +1286,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
"document-workspace-trigger-样包测量",
|
||||
"document-workspace-trigger-产品核价",
|
||||
"document-workspace-trigger-产品限价",
|
||||
"document-workspace-trigger-工厂质检",
|
||||
]);
|
||||
expect(
|
||||
sidebar.querySelector<HTMLButtonElement>(
|
||||
@ -1437,6 +1449,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
await actor.clear(screen.getByLabelText("结构设计师"));
|
||||
await actor.type(screen.getByLabelText("结构设计师"), "结构B");
|
||||
|
||||
expect(
|
||||
await screen.findByText("已自动保存", undefined, { timeout: 3000 }),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"SZ0230-opening-reference",
|
||||
@ -2155,6 +2170,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
operator: "徐霞客",
|
||||
profitCenter: "测试利润中心",
|
||||
goodsType: "测试货品类型",
|
||||
materialType: "橡胶木",
|
||||
priceBand: "测试价格段",
|
||||
outputRequirementType: "测试出图需求",
|
||||
visualPlanning: "测试视觉策划",
|
||||
@ -2173,6 +2189,9 @@ describe("NonIntegratedDetailClient", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("SPU负责人")).toHaveValue("徐霞客");
|
||||
expect(screen.getByLabelText("空间设计师")).toHaveValue("测试视觉策划");
|
||||
expect(screen.getByLabelText("材质类型")).toHaveValue("橡胶木");
|
||||
expect(screen.getByRole("option", { name: "岩板+钢架" })).toBeInTheDocument();
|
||||
|
||||
[
|
||||
"商品ID",
|
||||
@ -2195,6 +2214,113 @@ describe("NonIntegratedDetailClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists space designer and material type from basic info", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
id: "SZ0230-basic-material-save",
|
||||
spu: "SZ0230",
|
||||
openingReference: {
|
||||
...createEmptyNonIntegratedOpeningReference(),
|
||||
material: "18ENF级橡胶木",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="SZ0230-basic-material-save"
|
||||
currentUserRole="SUPER_ADMIN"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("材质类型")).toHaveValue("");
|
||||
|
||||
await actor.clear(screen.getByLabelText("空间设计师"));
|
||||
await actor.type(screen.getByLabelText("空间设计师"), "空间A");
|
||||
await actor.selectOptions(screen.getByLabelText("材质类型"), "岩板+钢架");
|
||||
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"SZ0230-basic-material-save",
|
||||
);
|
||||
expect(savedProduct?.visualPlanning).toBe("空间A");
|
||||
expect(savedProduct?.materialType).toBe("岩板+钢架");
|
||||
expect(savedProduct?.openingReference.material).toBe("18ENF级橡胶木");
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="SZ0230-basic-material-save"
|
||||
currentUserRole="SUPER_ADMIN"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("空间设计师")).toHaveValue("空间A");
|
||||
expect(screen.getByLabelText("材质类型")).toHaveValue("岩板+钢架");
|
||||
|
||||
await actor.clear(screen.getByLabelText("空间设计师"));
|
||||
await actor.selectOptions(screen.getByLabelText("材质类型"), "");
|
||||
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"SZ0230-basic-material-save",
|
||||
);
|
||||
expect(savedProduct?.visualPlanning).toBe("");
|
||||
expect(savedProduct?.materialType).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps basic info confirmed while auto-saving non-SKU basic fields", async () => {
|
||||
const actor = userEvent.setup();
|
||||
const approvedAt = "2026-05-13T08:00:00.000Z";
|
||||
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
id: "SZ0230-basic-material-reconfirm",
|
||||
spu: "SZ0230",
|
||||
materialType: "橡胶木",
|
||||
visualPlanning: "空间旧",
|
||||
documentWorkspaceConfirmations: {
|
||||
...createDefaultDocumentWorkspaceConfirmations(),
|
||||
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true,
|
||||
},
|
||||
documentWorkspaceApprovedAt: {
|
||||
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: approvedAt,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="SZ0230-basic-material-reconfirm"
|
||||
currentUserRole="SUPER_ADMIN"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("basic-info-confirmed")).toBeInTheDocument();
|
||||
|
||||
await actor.clear(screen.getByLabelText("空间设计师"));
|
||||
await actor.type(screen.getByLabelText("空间设计师"), "空间新");
|
||||
await actor.selectOptions(screen.getByLabelText("材质类型"), "亚克力");
|
||||
expect(screen.getByTestId("basic-info-confirmed")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("basic-info-confirm")).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"SZ0230-basic-material-reconfirm",
|
||||
);
|
||||
expect(savedProduct?.visualPlanning).toBe("空间新");
|
||||
expect(savedProduct?.materialType).toBe("亚克力");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders sample measurement rows from SKU data and persists purchaser fields", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
@ -2487,7 +2613,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
).toHaveAttribute("readonly");
|
||||
});
|
||||
|
||||
it("renders factory quality archive by style and asset type", async () => {
|
||||
it("renders first quality rows by SKU batch", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
window.localStorage.setItem(
|
||||
@ -2501,6 +2627,14 @@ describe("NonIntegratedDetailClient", () => {
|
||||
model: "SZ0230K01",
|
||||
}),
|
||||
],
|
||||
firstQualityRows: [
|
||||
{
|
||||
skuId: "sku-factory-quality-1",
|
||||
qualityBatch: "FQ-001",
|
||||
conclusion: "合格",
|
||||
detail: "首批通过",
|
||||
},
|
||||
],
|
||||
uploadFiles: [
|
||||
{
|
||||
id: "factory-quality-image-1",
|
||||
@ -2532,42 +2666,33 @@ describe("NonIntegratedDetailClient", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-包装细节"));
|
||||
await actor.click(screen.getByTestId("packaging-workspace-panel-factoryQuality"));
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-工厂质检"));
|
||||
|
||||
const panel = screen.getByTestId("document-workspace-panel");
|
||||
expect(screen.getByTestId("factory-quality-workspace")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("factory-quality-style-K01")).toHaveTextContent(
|
||||
"SZ0230K01",
|
||||
expect(screen.getByTestId("first-quality-workspace-panel-firstQuality")).toHaveTextContent(
|
||||
"首批质检",
|
||||
);
|
||||
expect(screen.getByTestId("first-quality-workspace-panel-qualityStandard")).toHaveTextContent(
|
||||
"质检标准",
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId("factory-quality-asset-type-drop-image"),
|
||||
).toHaveTextContent("摔包图片");
|
||||
screen.queryByTestId("packaging-workspace-panel-factoryQuality"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(panel).toHaveTextContent("批次1");
|
||||
expect(panel).toHaveTextContent("SZ0230K01");
|
||||
expect(panel).toHaveTextContent("质检批次");
|
||||
expect(panel).toHaveTextContent("质检结论");
|
||||
expect(panel).toHaveTextContent("质检详细说明");
|
||||
expect(panel).toHaveTextContent("质检附件");
|
||||
expect(screen.getByDisplayValue("FQ-001")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("SZ0230K01质检结论")).toHaveValue("合格");
|
||||
expect(screen.getByDisplayValue("首批通过")).toBeInTheDocument();
|
||||
expect(panel).toHaveTextContent("当前 2 个文件");
|
||||
expect(screen.queryByTestId("factory-quality-style-K01")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("factory-quality-asset-type-drop-video"),
|
||||
).toHaveTextContent("摔包视频");
|
||||
expect(
|
||||
screen.getByTestId("factory-quality-workspace-upload-input"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("archive-preview-panel")).toHaveClass("flex-1");
|
||||
expect(screen.getByTestId("archive-preview-panel")).not.toHaveClass(
|
||||
"self-start",
|
||||
);
|
||||
expect(screen.getByTestId("archive-preview-frame")).toHaveClass("flex-1");
|
||||
expect(
|
||||
screen.getByTestId("factory-quality-file-factory-quality-image-1"),
|
||||
).toHaveTextContent("K01-摔包图片.png");
|
||||
|
||||
await actor.click(
|
||||
screen.getByTestId("factory-quality-asset-type-drop-video"),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("factory-quality-file-factory-quality-video-1"),
|
||||
).toHaveTextContent("K01-摔包视频.mp4");
|
||||
expect(panel).toHaveTextContent("摔包图片");
|
||||
expect(panel).toHaveTextContent("摔包视频");
|
||||
expect(panel).toHaveTextContent("当前文件类型:视频");
|
||||
screen.queryByTestId("factory-quality-workspace-upload-input"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders installation style tabs from SKU models", async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -86,6 +86,7 @@ export function getNonIntegratedDocumentWorkspaceCategoryLabel(
|
||||
if (category === "产品实拍图") return "产品实拍";
|
||||
if (category === "产品模型") return "产品资料";
|
||||
if (category === "评审资料") return "样品评审";
|
||||
if (category === "工厂质检") return "首批质检";
|
||||
if (category === "检测报告") return "仓库抽检";
|
||||
return category;
|
||||
}
|
||||
@ -270,6 +271,7 @@ const NON_INTEGRATED_UPLOAD_CATEGORY_MAP: Record<string, string> = {
|
||||
产品核价: "产品核价",
|
||||
产品限价: "产品限价",
|
||||
工厂质检: "工厂质检",
|
||||
首批质检: "工厂质检",
|
||||
仓库质检: "工厂质检",
|
||||
仓库抽检: "检测报告",
|
||||
产品资料: "产品模型",
|
||||
@ -370,6 +372,13 @@ export type NonIntegratedSampleMeasurementRow = {
|
||||
productShape: string;
|
||||
};
|
||||
|
||||
export type NonIntegratedFirstQualityRow = {
|
||||
skuId: string;
|
||||
qualityBatch: string;
|
||||
conclusion: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
export type NonIntegratedDetectionAssetType = "attachment" | "quality-standard";
|
||||
|
||||
export type NonIntegratedUploadFile = {
|
||||
@ -570,6 +579,7 @@ export type NonIntegratedProductRecord = {
|
||||
planningLayoutRows: NonIntegratedPlanningLayoutRow[];
|
||||
productLimitRows: NonIntegratedProductLimitRow[];
|
||||
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
|
||||
firstQualityRows?: NonIntegratedFirstQualityRow[];
|
||||
uploadFiles: NonIntegratedUploadFile[];
|
||||
documentWorkspaceNotes?: Record<string, string>;
|
||||
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
|
||||
@ -1246,6 +1256,19 @@ function normalizeSampleMeasurementRow(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFirstQualityRow(
|
||||
item: Partial<NonIntegratedFirstQualityRow>,
|
||||
): NonIntegratedFirstQualityRow | null {
|
||||
if (typeof item.skuId !== "string") return null;
|
||||
|
||||
return {
|
||||
skuId: item.skuId,
|
||||
qualityBatch: normalizeString(item.qualityBatch),
|
||||
conclusion: normalizeString(item.conclusion) || "待检",
|
||||
detail: normalizeString(item.detail),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUploadFile(
|
||||
item: Partial<NonIntegratedUploadFile>,
|
||||
): NonIntegratedUploadFile | null {
|
||||
@ -1850,6 +1873,13 @@ function toProductRecord(
|
||||
entry !== null,
|
||||
)
|
||||
: [],
|
||||
firstQualityRows: Array.isArray(item.firstQualityRows)
|
||||
? item.firstQualityRows
|
||||
.map((entry) => normalizeFirstQualityRow(entry))
|
||||
.filter(
|
||||
(entry): entry is NonIntegratedFirstQualityRow => entry !== null,
|
||||
)
|
||||
: [],
|
||||
uploadFiles: Array.isArray(item.uploadFiles)
|
||||
? item.uploadFiles
|
||||
.map((entry) => normalizeUploadFile(entry))
|
||||
|
||||
@ -165,7 +165,7 @@ function ToolbarButton({
|
||||
className={cn(
|
||||
"flex h-10 w-10 flex-col items-center justify-center rounded-[6px] transition-all duration-200",
|
||||
active
|
||||
? "bg-[#f1f4f5] text-[#101719] shadow-none ring-1 ring-[#101719]/15"
|
||||
? "bg-[var(--obs-shell-action-bg)] text-[var(--obs-shell-action-text)] shadow-none ring-1 ring-[var(--obs-shell-action-border)]"
|
||||
: "text-[#5a6062] hover:bg-[#f1f4f5] hover:text-[#101719] disabled:opacity-30 disabled:hover:scale-100"
|
||||
)}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user