Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-dynamic-20260807

This commit is contained in:
WuYongJie 2026-08-10 12:08:06 +08:00
commit 81b1453db5
2 changed files with 71 additions and 1 deletions

View File

@ -1,5 +1,6 @@
import {
calculateOaBoardOverallActualDays,
calculateOaBoardPricingFlowActualDays,
getRunningTasks,
type OaBoardLifecycleFlowRecord,
} from '../oa-board-sync.service'
@ -71,3 +72,39 @@ describe('OA 看板当前任务选择', () => {
expect(tasks[0]?.taskId).toBe('new-running-task')
})
})
describe('OA 看板限价时效', () => {
it('限价子流程尚未发起时,从首单和美术并行流程最后完成时间开始计算', () => {
const actualDays = calculateOaBoardPricingFlowActualDays(
[
{
flowType: 'review',
status: 'COMPLETED',
result: 'AGREE',
fieldSnapshots: [
{
fieldName: '是否需要视觉出图',
fieldValue: '视觉出图',
fieldExtValue: null,
normalizedValue: '视觉出图',
metadata: null,
},
],
},
{
flowType: 'order',
status: 'COMPLETED',
finishedAtSource: new Date('2026-08-01T05:00:00.000Z'),
},
{
flowType: 'art',
status: 'COMPLETED',
finishedAtSource: new Date('2026-08-02T05:00:00.000Z'),
},
] as never,
'限价-发起',
new Date('2026-08-04T05:00:00.000Z'),
)
expect(actualDays).toBe(2)
})
})

View File

@ -1984,6 +1984,39 @@ function pickFlowActualDays(records: NormalizedFlowRecord[], flowType: OaBoardFl
return Number(Math.max(...values).toFixed(2))
}
export function calculateOaBoardPricingFlowActualDays(
records: NormalizedFlowRecord[],
pendingStageName: string | null,
now = new Date(),
) {
const pricingActual = pickFlowActualDays(records, 'pricing')
if (pricingActual !== null) return pricingActual
// 限价子流程尚未发起时,待发起阶段已由前序流程完成触发。
// 该阶段从最后一个必经的并行流程完成时开始计时,直到真正发起限价。
if (pendingStageName !== '限价-发起') return null
const reviewRecord = records.find((record) => record.flowType === 'review') ?? null
const orderRecord = records.find((record) => record.flowType === 'order') ?? null
const artRecord = records.find((record) => record.flowType === 'art') ?? null
const orderFinishedAt =
getFlowLifecycleStatus(orderRecord) === 'COMPLETED' ? orderRecord?.finishedAtSource ?? null : null
if (!orderFinishedAt) return null
const artFinishedAt = isVisualDesignRequired(reviewRecord)
? getFlowLifecycleStatus(artRecord) === 'COMPLETED'
? artRecord?.finishedAtSource ?? null
: null
: null
if (isVisualDesignRequired(reviewRecord) && !artFinishedAt) return null
const startedAt = [orderFinishedAt, artFinishedAt]
.filter((value): value is Date => Boolean(value))
.sort((left, right) => right.getTime() - left.getTime())[0]
return startedAt ? calculateHours(startedAt, null, now) : null
}
function buildTimelineMetrics(
records: NormalizedFlowRecord[],
productType: string | null,
@ -2066,7 +2099,7 @@ function buildTimelineMetrics(
pricingParallelActual,
])
const artActual = pickFlowActualDays(records, 'art')
const pricingFlowActual = pickFlowActualDays(records, 'pricing')
const pricingFlowActual = calculateOaBoardPricingFlowActualDays(records, pendingStageName)
const listingActual = pickFlowActualDays(records, 'main')
const definitions: TimelineMetricDefinition[] = [
{