Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-assisted-assembly-next
This commit is contained in:
commit
edfe43d15f
@ -13,11 +13,25 @@ import {
|
||||
} from './non-integrated/nonIntegratedStorage'
|
||||
|
||||
const push = jest.fn()
|
||||
const mockAoaToSheet = jest.fn((rows: unknown[][]) => ({ rows }))
|
||||
const mockBookNew = jest.fn(() => ({}))
|
||||
const mockBookAppendSheet = jest.fn()
|
||||
const mockXlsxWrite = jest.fn(() => new ArrayBuffer(8))
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push }),
|
||||
}))
|
||||
|
||||
jest.mock('xlsx', () => ({
|
||||
__esModule: true,
|
||||
utils: {
|
||||
aoa_to_sheet: mockAoaToSheet,
|
||||
book_new: mockBookNew,
|
||||
book_append_sheet: mockBookAppendSheet,
|
||||
},
|
||||
write: mockXlsxWrite,
|
||||
}))
|
||||
|
||||
jest.mock('@/components/platform/PlatformSplitLayout', () => ({
|
||||
__esModule: true,
|
||||
PlatformSplitLayout: ({
|
||||
@ -262,8 +276,13 @@ function createNonIntegratedProduct(overrides: Record<string, unknown> = {}) {
|
||||
|
||||
describe('IntakeClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers()
|
||||
window.localStorage.clear()
|
||||
jest.restoreAllMocks()
|
||||
mockAoaToSheet.mockClear()
|
||||
mockBookNew.mockClear()
|
||||
mockBookAppendSheet.mockClear()
|
||||
mockXlsxWrite.mockClear()
|
||||
push.mockReset()
|
||||
})
|
||||
|
||||
@ -311,12 +330,121 @@ describe('IntakeClient', () => {
|
||||
const leftPanel = screen.getByTestId('left-panel')
|
||||
expect(screen.getByRole('tab', { name: '产品开发进度' })).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByText('产品开发进度总览')).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByRole('button', { name: /总进度/ })).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByRole('button', { name: /状态总览/ })).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByText('全部产品明细')).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByText('SZ0230')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters product progress rows by clicking a ring dimension item', async () => {
|
||||
it('shows running total days and node elapsed days in the product progress table', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2026-04-29T00:00:00.000Z') })
|
||||
const actor = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })
|
||||
|
||||
try {
|
||||
renderSelector({
|
||||
eligibleSkuList: [],
|
||||
initialNonIntegratedProducts: [
|
||||
createNonIntegratedProduct({
|
||||
id: 'duration-product',
|
||||
spu: 'Duration SKU',
|
||||
createdAt: '2026-04-28T00:00:00.000Z',
|
||||
updatedAt: '2026-04-28T00:00:00.000Z',
|
||||
documentWorkspaceApprovedAt: {
|
||||
[NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[0]]:
|
||||
'2026-04-28T12:00:00.000Z',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const rightPanel = screen.getByTestId('right-panel')
|
||||
await actor.click(
|
||||
within(rightPanel).getByRole('button', { name: /产品开发进度/ }),
|
||||
)
|
||||
|
||||
const row = within(screen.getByTestId('left-panel'))
|
||||
.getByText('Duration SKU')
|
||||
.closest('tr')
|
||||
|
||||
expect(row).not.toBeNull()
|
||||
expect(
|
||||
within(row as HTMLElement).getByText('1.00 天'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(row as HTMLElement).getByText('0.50 天'),
|
||||
).toBeInTheDocument()
|
||||
} finally {
|
||||
jest.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('exports current product progress rows as an Excel workbook', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2026-04-29T00:00:00.000Z') })
|
||||
const actor = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })
|
||||
const originalCreateObjectURL = URL.createObjectURL
|
||||
const originalRevokeObjectURL = URL.revokeObjectURL
|
||||
const createObjectURL = jest.fn(() => 'blob:product-progress')
|
||||
const revokeObjectURL = jest.fn()
|
||||
const clickSpy = jest
|
||||
.spyOn(HTMLAnchorElement.prototype, 'click')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: createObjectURL,
|
||||
})
|
||||
Object.defineProperty(URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
value: revokeObjectURL,
|
||||
})
|
||||
|
||||
try {
|
||||
renderSelector({
|
||||
eligibleSkuList: [],
|
||||
initialNonIntegratedProducts: [
|
||||
createNonIntegratedProduct({
|
||||
id: 'export-product',
|
||||
spu: 'Export SKU',
|
||||
createdAt: '2026-04-28T00:00:00.000Z',
|
||||
updatedAt: '2026-04-28T00:00:00.000Z',
|
||||
documentWorkspaceApprovedAt: {
|
||||
[NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES[0]]:
|
||||
'2026-04-28T12:00:00.000Z',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const rightPanel = screen.getByTestId('right-panel')
|
||||
await actor.click(
|
||||
within(rightPanel).getByRole('button', { name: /产品开发进度/ }),
|
||||
)
|
||||
await actor.click(screen.getByRole('button', { name: '导出明细' }))
|
||||
|
||||
await waitFor(() => expect(mockXlsxWrite).toHaveBeenCalled())
|
||||
|
||||
const exportedRows = mockAoaToSheet.mock.calls[0]?.[0] as unknown[][]
|
||||
expect(exportedRows[0]).toContain('总耗时')
|
||||
expect(exportedRows.flat()).toContain('Export SKU')
|
||||
expect(exportedRows.flat()).toContain('1.00 天')
|
||||
expect(exportedRows.flat()).toContain('0.50 天')
|
||||
expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
||||
expect(clickSpy).toHaveBeenCalled()
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith('blob:product-progress')
|
||||
} finally {
|
||||
clickSpy.mockRestore()
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: originalCreateObjectURL,
|
||||
})
|
||||
Object.defineProperty(URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
value: originalRevokeObjectURL,
|
||||
})
|
||||
jest.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('filters product progress rows by clicking a status overview dimension item', async () => {
|
||||
const actor = userEvent.setup()
|
||||
|
||||
renderSelector({
|
||||
@ -341,11 +469,10 @@ describe('IntakeClient', () => {
|
||||
)
|
||||
|
||||
const leftPanel = screen.getByTestId('left-panel')
|
||||
await actor.click(within(leftPanel).getByRole('button', { name: /环形视图/ }))
|
||||
await actor.click(within(leftPanel).getByRole('button', { name: /基本盘 1 个/ }))
|
||||
|
||||
expect(
|
||||
within(leftPanel).getByText('按品牌查看明细:基本盘'),
|
||||
within(leftPanel).getByText('全部产品明细:基本盘'),
|
||||
).toBeInTheDocument()
|
||||
expect(within(leftPanel).getByText('SZ0230')).toBeInTheDocument()
|
||||
expect(within(leftPanel).queryByText('GBGJ-SZ0230')).not.toBeInTheDocument()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -324,6 +324,7 @@ type FieldDefinition = {
|
||||
type?: BasicFieldType;
|
||||
placeholder?: string;
|
||||
options?: string[];
|
||||
auto?: boolean;
|
||||
};
|
||||
|
||||
type StyleDetailForm = {
|
||||
@ -834,16 +835,18 @@ const PROJECT_STATUS_COPY: Record<
|
||||
saveMessage: "立项已通过,已展开产品资料、样品评审、基础信息和后续节点",
|
||||
},
|
||||
};
|
||||
const OPENING_REFERENCE_STAGE_OPTIONS = [
|
||||
"立项前",
|
||||
"资料补充中",
|
||||
"待立项评审",
|
||||
"已立项",
|
||||
"退回修改",
|
||||
"废弃不开",
|
||||
];
|
||||
const WORKFLOW_DATE_TIME_ZONE = "Asia/Shanghai";
|
||||
|
||||
function getAutoProductYear(createdAt?: string) {
|
||||
const parsedDate = createdAt ? new Date(createdAt) : null;
|
||||
const sourceDate =
|
||||
parsedDate && !Number.isNaN(parsedDate.getTime())
|
||||
? parsedDate
|
||||
: new Date();
|
||||
|
||||
return String(sourceDate.getFullYear());
|
||||
}
|
||||
|
||||
function formatWorkflowDateTime(value: string) {
|
||||
const parsedDate = new Date(value);
|
||||
if (Number.isNaN(parsedDate.getTime())) return value;
|
||||
@ -1433,7 +1436,7 @@ const BRAND_OPTIONS = ["基本盘", "顾家", "索菲亚", "朴美"];
|
||||
const CREATE_TOP_FIELD_COLUMNS: FieldDefinition[][] = [
|
||||
[
|
||||
{ key: "spu", label: "SPU", required: true, placeholder: "请输入 SPU" },
|
||||
{ key: "year", label: "年份", placeholder: "请输入年份" },
|
||||
{ key: "year", label: "年份", placeholder: "系统自动带出", auto: true },
|
||||
{
|
||||
key: "salesCategory",
|
||||
label: "销售类目",
|
||||
@ -1562,7 +1565,8 @@ const BASIC_INFO_CORE_FIELDS: FieldDefinition[] = [
|
||||
{
|
||||
key: "year",
|
||||
label: "年份",
|
||||
placeholder: "开品参考带出,可调整",
|
||||
placeholder: "系统自动带出",
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
key: "materialType",
|
||||
@ -3283,7 +3287,7 @@ function createFormValuesFromProduct(
|
||||
product: NonIntegratedProductRecord | null,
|
||||
): FormValues {
|
||||
if (!product) {
|
||||
return { ...EMPTY_FORM_VALUES };
|
||||
return { ...EMPTY_FORM_VALUES, year: getAutoProductYear() };
|
||||
}
|
||||
|
||||
const normalizedBrand = product.brand || "基本盘";
|
||||
@ -3298,7 +3302,7 @@ function createFormValuesFromProduct(
|
||||
|
||||
return {
|
||||
spu: product.spu ?? "",
|
||||
year: product.year ?? "",
|
||||
year: product.year || getAutoProductYear(product.createdAt),
|
||||
demandFlowNo: product.demandFlowNo ?? "",
|
||||
processTemplate: product.processTemplate ?? "",
|
||||
productCategory: product.productCategory ?? "",
|
||||
@ -3361,6 +3365,26 @@ function getOpeningReferenceStageFromStatus(
|
||||
return "立项前";
|
||||
}
|
||||
|
||||
const OPENING_REFERENCE_DRAFT_STAGES = new Set([
|
||||
"立项前",
|
||||
"资料补充中",
|
||||
"待立项评审",
|
||||
]);
|
||||
|
||||
function getAutoOpeningReferenceStage(
|
||||
status: NonIntegratedProjectStatus,
|
||||
currentStage?: string,
|
||||
) {
|
||||
if (status !== "draft") {
|
||||
return getOpeningReferenceStageFromStatus(status);
|
||||
}
|
||||
|
||||
const normalizedStage = currentStage?.trim() ?? "";
|
||||
return OPENING_REFERENCE_DRAFT_STAGES.has(normalizedStage)
|
||||
? normalizedStage
|
||||
: getOpeningReferenceStageFromStatus(status);
|
||||
}
|
||||
|
||||
function createInitialOpeningReference(
|
||||
product: NonIntegratedProductRecord | null,
|
||||
): NonIntegratedOpeningReference {
|
||||
@ -3370,6 +3394,7 @@ function createInitialOpeningReference(
|
||||
brand: "基本盘",
|
||||
adaptableBrands: ["基本盘"],
|
||||
currentStage: "立项前",
|
||||
year: getAutoProductYear(),
|
||||
urgent: "否",
|
||||
};
|
||||
}
|
||||
@ -3414,9 +3439,11 @@ function createInitialOpeningReference(
|
||||
salesCategory: normalized.salesCategory || product.salesCategory || "",
|
||||
designer: normalized.designer || product.designer || "",
|
||||
planningOwner: normalized.planningOwner || product.productPlanning || "",
|
||||
currentStage:
|
||||
normalized.currentStage || getOpeningReferenceStageFromStatus(projectStatus),
|
||||
year: normalized.year || product.year || "",
|
||||
currentStage: getAutoOpeningReferenceStage(
|
||||
projectStatus,
|
||||
normalized.currentStage,
|
||||
),
|
||||
year: product.year || normalized.year || getAutoProductYear(product.createdAt),
|
||||
targetPriceBand: normalized.targetPriceBand || product.priceBand || "",
|
||||
size: normalized.size || product.size || "",
|
||||
material:
|
||||
@ -3479,7 +3506,7 @@ function buildOpeningReferenceFormValues(
|
||||
previewImage: reference.productImage.trim() || baseValues.previewImage,
|
||||
previewImageName:
|
||||
reference.productImageName.trim() || baseValues.previewImageName,
|
||||
year: reference.year.trim() || baseValues.year,
|
||||
year: baseValues.year || reference.year.trim() || getAutoProductYear(),
|
||||
brand: normalizedBrand,
|
||||
adaptableBrands: normalizedAdaptableBrands,
|
||||
productCategory:
|
||||
@ -5702,14 +5729,23 @@ function buildProductPayload(
|
||||
}));
|
||||
const primaryReviewSheet =
|
||||
normalizedReviewRounds[0]?.reviewSheet ?? createEmptyReviewSheet();
|
||||
const autoYear = formValues.year.trim() || getAutoProductYear(createdAt);
|
||||
const normalizedOpeningReference = normalizeNonIntegratedOpeningReference({
|
||||
...openingReference,
|
||||
currentStage: getAutoOpeningReferenceStage(
|
||||
projectStatus,
|
||||
openingReference.currentStage,
|
||||
),
|
||||
year: autoYear,
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
projectStatus,
|
||||
projectReview: normalizeNonIntegratedProjectReview(projectReview),
|
||||
openingReference: normalizeNonIntegratedOpeningReference(openingReference),
|
||||
openingReference: normalizedOpeningReference,
|
||||
spu: formValues.spu.trim(),
|
||||
year: formValues.year.trim(),
|
||||
year: autoYear,
|
||||
demandFlowNo: formValues.demandFlowNo.trim(),
|
||||
processTemplate: formValues.processTemplate.trim(),
|
||||
productCategory: formValues.productCategory.trim(),
|
||||
@ -8619,6 +8655,7 @@ export default function NonIntegratedDetailClient({
|
||||
|
||||
function updateField(key: keyof FormValues, value: string) {
|
||||
if (isReadOnly && key !== "brand") return;
|
||||
if (key === "year") return;
|
||||
|
||||
if (key === "spu") {
|
||||
setFormValues((current) => ({
|
||||
@ -9766,6 +9803,7 @@ export default function NonIntegratedDetailClient({
|
||||
value: string,
|
||||
) {
|
||||
if (isReadOnly) return;
|
||||
if (field === "currentStage" || field === "year") return;
|
||||
|
||||
setOpeningReference((current) => {
|
||||
if (field !== "brand") {
|
||||
@ -13117,17 +13155,6 @@ export default function NonIntegratedDetailClient({
|
||||
label: "企划负责人",
|
||||
placeholder: "请输入企划负责人",
|
||||
},
|
||||
{
|
||||
key: "currentStage",
|
||||
label: "当前阶段",
|
||||
type: "select",
|
||||
options: OPENING_REFERENCE_STAGE_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "year",
|
||||
label: "年份",
|
||||
placeholder: "如:2026",
|
||||
},
|
||||
];
|
||||
const demandFields: {
|
||||
key: OpeningReferenceFieldKey;
|
||||
@ -13174,6 +13201,31 @@ export default function NonIntegratedDetailClient({
|
||||
},
|
||||
];
|
||||
const openingReferenceFiles = activeDocumentWorkspaceFiles;
|
||||
const autoOpeningReferenceStage =
|
||||
getAutoOpeningReferenceStage(
|
||||
projectStatus,
|
||||
openingReference.currentStage,
|
||||
);
|
||||
const autoOpeningReferenceYear =
|
||||
openingReference.year ||
|
||||
formValues.year ||
|
||||
getAutoProductYear(existingProductCreatedAt);
|
||||
const renderAutoOpeningReferenceField = ({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) => (
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/35 p-1.5">
|
||||
<p className="mb-1 block text-[11px] leading-4 text-apple-label-secondary">
|
||||
{label}
|
||||
</p>
|
||||
<div className="flex h-8 items-center rounded-apple-md border border-apple-separator bg-apple-bg-primary px-2 text-xs font-apple-medium text-apple-label-primary">
|
||||
{value || "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const renderCompactOpeningReferenceField = (field: {
|
||||
key: OpeningReferenceFieldKey;
|
||||
label: string;
|
||||
@ -13287,6 +13339,14 @@ export default function NonIntegratedDetailClient({
|
||||
{topFields.map((field) =>
|
||||
renderCompactOpeningReferenceField(field),
|
||||
)}
|
||||
{renderAutoOpeningReferenceField({
|
||||
label: "当前阶段",
|
||||
value: autoOpeningReferenceStage,
|
||||
})}
|
||||
{renderAutoOpeningReferenceField({
|
||||
label: "年份",
|
||||
value: autoOpeningReferenceYear,
|
||||
})}
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-1.5">
|
||||
<AdaptableBrandField
|
||||
value={openingReference.adaptableBrands}
|
||||
@ -15120,7 +15180,7 @@ export default function NonIntegratedDetailClient({
|
||||
placeholder={field.placeholder}
|
||||
options={field.options}
|
||||
value={formValues[field.key] as string}
|
||||
readOnly={isReadOnly}
|
||||
readOnly={isReadOnly || field.auto === true}
|
||||
onChange={(value) =>
|
||||
updateField(field.key, value)
|
||||
}
|
||||
@ -15174,6 +15234,7 @@ export default function NonIntegratedDetailClient({
|
||||
value={formValues[field.key] as string}
|
||||
readOnly={
|
||||
isReadOnly ||
|
||||
field.auto === true ||
|
||||
(field.key === "brand" &&
|
||||
(isCopiedBrandProduct ||
|
||||
availableBrandOptions.length === 0))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user