fix(oa-board): enforce brand planning field sources

This commit is contained in:
jingchuan 2026-07-24 14:48:52 +08:00
parent b1c8ef2e48
commit 2869c9a27c

View File

@ -124,17 +124,12 @@ type ProductSkuSource = {
type ProductSource = {
id: string
spu: string
brand: string
purchaser: string
payload: unknown
createdAt: Date
updatedAt: Date
profile: {
productCategory: string
productPlanning: string
spuOwner: string
purchaseOwner: string
visualPlanning: string
structureDesigner: string
} | null
skuItems: ProductSkuSource[]
@ -325,13 +320,6 @@ function findTaskDate(
return kind === 'started' ? task.startedAtSource : task.finishedAtSource
}
function findTaskOwner(flow: OaBoardProjectFlowInstanceInput | null, names: readonly string[]) {
const task = flow?.taskRecords.find((item) =>
names.some((name) => (item.nodeName ?? '').includes(name)),
)
return firstText(task?.ownerLabel)
}
function calculateDurationDays(start: Date | null, end: Date | null) {
if (!start || !end) return null
const difference = end.getTime() - start.getTime()
@ -340,7 +328,6 @@ function calculateDurationDays(start: Date | null, end: Date | null) {
}
function getGraphSpu(graph: OaBoardProjectGraphInput) {
if (graph.spu) return graph.spu.trim().toUpperCase()
const reviewFlow = findFlow(graph, 'review')
return readFlowField(reviewFlow, ['SPU'])?.toUpperCase() ?? null
}
@ -355,12 +342,43 @@ function getFactoryLeadTime(payload: ProductPayload, sku: ProductSkuSource | nul
return toText(asRecord(row).leadTime)
}
function getProductImage(payload: ProductPayload, sku: ProductSkuSource | null) {
return firstText(
sku?.image,
readPathText(payload, 'openingReference', 'productImage'),
payload.previewImage,
function readFlowTableField(
flow: OaBoardProjectFlowInstanceInput | null,
tableFieldName: string,
columnFieldName: string,
) {
const tableField = flow?.fieldSnapshots.find((field) => field.fieldName === tableFieldName)
const rawValue = firstText(
tableField?.normalizedValue,
tableField?.fieldValue,
tableField?.fieldExtValue,
)
if (!rawValue) return null
try {
const rows = JSON.parse(rawValue)
if (!Array.isArray(rows)) return null
const values = rows
.flatMap((row) => {
const rowValue = asRecord(row)
const cells = rowValue.ROWVALUE ?? rowValue.rowValue
return Array.isArray(cells) ? cells : []
})
.filter((cell) => {
const cellValue = asRecord(cell)
return toText(cellValue.LABEL ?? cellValue.label) === columnFieldName
})
.map((cell) => {
const cellValue = asRecord(cell)
return toDisplayFieldValue(cellValue.VALUE ?? cellValue.value)
})
.filter((value): value is string => Boolean(value))
return values.length > 0 ? Array.from(new Set(values)).join(';') : null
} catch {
return null
}
}
function buildSnapshotRows(
@ -377,7 +395,7 @@ function buildSnapshotRows(
return products.flatMap<BrandPlanningSnapshotInput>((product) => {
const payload = asRecord(product.payload)
const profile = product.profile
const productSkus = product.skuItems.length > 0 ? product.skuItems : [null]
const productSkus = product.skuItems
const graph = graphBySpu.get(product.spu.trim().toUpperCase())
const reviewFlow = findFlow(graph, 'review')
const artFlow = findFlow(graph, 'art')
@ -388,88 +406,63 @@ function buildSnapshotRows(
const taskAssignedAt = dateFromUnknown(
readFlowField(artFlow, ['品牌策划主管分配任务结束时间']),
) ?? findTaskDate(artFlow, ['品牌策划主管分配任务'], 'finished')
const planCompletedAt = dateFromUnknown(
readFlowField(artFlow, ['出策划案结束时间']),
) ?? findTaskDate(artFlow, ['出策划案'], 'finished')
const visualCompletedAt = dateFromUnknown(
readFlowField(artFlow, ['出图结束时间']),
) ?? findTaskDate(artFlow, ['出图'], 'finished')
const detailCompletedAt = dateFromUnknown(
readFlowField(artFlow, ['平面结束时间']),
) ?? findTaskDate(artFlow, ['平面'], 'finished')
const planningReviewAt =
findTaskDate(artFlow, ['品牌策划主管审核'], 'finished') ??
findTaskDate(artFlow, ['品牌策划主管审核'], 'started') ??
artFlow?.finishedAtSource ??
null
)
const planCompletedAt = dateFromUnknown(readFlowField(artFlow, ['出策划案结束时间']))
const visualCompletedAt = dateFromUnknown(readFlowField(artFlow, ['出图结束时间']))
const detailCompletedAt = dateFromUnknown(readFlowField(artFlow, ['平面结束时间']))
const planningTaskAssignedAt = findTaskDate(
artFlow,
['品牌策划主管分配任务'],
'finished',
)
const planningReviewAt = findTaskDate(artFlow, ['品牌策划主管审核'], 'finished')
return productSkus.map((sku) => {
const rowKey = `${product.id}:${sku?.skuItemId || sku?.id || 'PRODUCT'}`
const adaptableBrands = toTextList(payload.adaptableBrands)
const fullPlanPath = firstText(
readFlowField(artFlow, ['全案路径']),
payload.flatOutputPath,
payload.output3dPath,
)
return {
row_key: rowKey,
product_id: product.id,
sku_id: sku?.skuItemId || sku?.id || null,
image_url: getProductImage(payload, sku),
demand_type: firstText(
readPathText(payload, 'openingReference', 'demandType'),
payload.demandType,
readFlowField(reviewFlow, ['需求类型']),
),
image_url: sku?.image ?? null,
demand_type: readPathText(payload, 'openingReference', 'demandType'),
spu: product.spu || null,
sku: sku?.skuCode || null,
spec_name: sku?.specName || null,
size: sku?.size || firstText(payload.size, readPathText(payload, 'openingReference', 'size')),
color: sku?.color || toText(payload.color),
adaptable_brands: adaptableBrands.length > 0 ? adaptableBrands.join(';') : product.brand || null,
size: sku?.size || null,
color: sku?.color || null,
adaptable_brands: adaptableBrands.length > 0 ? adaptableBrands.join(';') : null,
barcode: sku?.barcode || null,
category_name: firstText(
profile?.productCategory,
payload.productCategory,
readPathText(payload, 'openingReference', 'productCategory'),
),
created_date: toMysqlDateTime(reviewFlow?.createdAtSource ?? product.createdAt),
structure_designer: firstText(
profile?.structureDesigner,
readPathText(payload, 'openingReference', 'structureDesigner'),
payload.structureDesigner,
),
product_planner: firstText(profile?.productPlanning, payload.productPlanning),
spu_owner: firstText(profile?.spuOwner, payload.spuOwner),
purchase_owner: firstText(profile?.purchaseOwner, payload.purchaseOwner, product.purchaser),
category_name: toText(profile?.productCategory),
created_date: toMysqlDateTime(reviewFlow?.createdAtSource),
structure_designer: toText(profile?.structureDesigner),
product_planner: toText(profile?.productPlanning),
spu_owner: toText(profile?.spuOwner),
purchase_owner: toText(profile?.purchaseOwner),
estimated_shipment_date: getFactoryLeadTime(payload, sku),
product_detail_link: `/intake/non-integrated/${encodeURIComponent(product.id)}`,
approval_stage: graph?.currentApprovalStageName ?? graph?.currentStageName ?? null,
opening_code: firstText(graph?.openingCode, payload.demandFlowNo),
approval_stage: graph?.currentApprovalStageName ?? null,
opening_code: readFlowField(reviewFlow, ['开款编号']),
market_insight_link: readFlowField(reviewFlow, ['市场洞察分析企划表链接']),
planning_owner: firstText(
readFlowField(artFlow, ['策划案负责人']),
findTaskOwner(artFlow, ['出策划案']),
profile?.visualPlanning,
payload.visualPlanning,
),
planning_owner: readFlowField(artFlow, ['策划案负责人']),
planning_received_at: toMysqlDateTime(taskAssignedAt),
planning_completed_at: toMysqlDateTime(planCompletedAt),
visual_output_owner: firstText(
readFlowField(artFlow, ['出图任务统计-全案策划']),
findTaskOwner(artFlow, ['出图']),
visual_output_owner: readFlowTableField(
artFlow,
'出图任务统计',
'全案策划',
),
visual_output_completed_at: toMysqlDateTime(visualCompletedAt),
detail_owner: firstText(
readFlowField(artFlow, ['平面任务统计-全案策划']),
findTaskOwner(artFlow, ['平面']),
detail_owner: readFlowTableField(
artFlow,
'平面任务统计',
'全案策划',
),
detail_completed_at: toMysqlDateTime(detailCompletedAt),
full_plan_path: fullPlanPath,
total_duration_days: calculateDurationDays(taskAssignedAt, planningReviewAt),
last_source_updated_at: toMysqlDateTime(graph?.lastSourceUpdatedAt ?? product.updatedAt),
full_plan_path: readFlowField(artFlow, ['全案路径']),
total_duration_days: calculateDurationDays(planningTaskAssignedAt, planningReviewAt),
last_source_updated_at: toMysqlDateTime(graph?.lastSourceUpdatedAt),
sync_version: syncVersion,
}
})
@ -493,18 +486,13 @@ async function loadProducts(): Promise<ProductSource[]> {
select: {
id: true,
spu: true,
brand: true,
payload: true,
createdAt: true,
updatedAt: true,
purchaser: true,
profile: {
select: {
productCategory: true,
productPlanning: true,
spuOwner: true,
purchaseOwner: true,
visualPlanning: true,
structureDesigner: true,
},
},