修复本月新品负责人审核口径

This commit is contained in:
江林铖 2026-08-26 12:23:05 +08:00
parent 8490b3b371
commit 484781f23c
8 changed files with 155 additions and 44 deletions

View File

@ -42,7 +42,12 @@ describe('CoreMetricDetail', () => {
assigneeName: '陈负责人',
assigneeDepartment: '品牌中心/创意设计部',
isBrandCenter: true,
isLeadership: false,
isActive: true,
reviewPassed: false,
result: null,
createdAt: '2026-08-14T02:00:00.000Z',
finishedAt: null,
}],
}],
total: 1,
@ -130,6 +135,8 @@ describe('CoreMetricDetail', () => {
id: 'completed-1',
launchStatus: 'completed',
sourceFinishedAt: '2026-08-12T03:00:00.000Z',
reviewPassedAt: '2026-08-12T02:30:00.000Z',
reviewerName: '清云',
},
{
...baseProject,
@ -139,6 +146,8 @@ describe('CoreMetricDetail', () => {
status: 'RUNNING',
launchStatus: 'ongoing',
sourceFinishedAt: null,
reviewPassedAt: null,
reviewerName: null,
activeTaskCount: 1,
activeTasks: [{
taskId: 'task-1',
@ -148,7 +157,12 @@ describe('CoreMetricDetail', () => {
assigneeName: '陈负责人',
assigneeDepartment: '品牌中心',
isBrandCenter: true,
isLeadership: false,
isActive: true,
reviewPassed: false,
result: null,
createdAt: '2026-08-14T02:00:00.000Z',
finishedAt: null,
}],
},
],

View File

@ -265,14 +265,14 @@ export default function CoreMetricDetail({ metric, query, projectInsights, month
<section aria-label="指标口径" className="mt-6 grid overflow-hidden rounded-lg border border-[#e2e8f0] bg-white sm:grid-cols-3">
<div className="border-b border-slate-200 p-5 sm:border-b-0 sm:border-r">
<p className="text-xs font-bold text-slate-500">{hasFirstPassSource ? '当前首轮通过率' : hasRealProjectSource ? '当前未完结流程' : hasMonthlyLaunchSource ? '本月完成 / 新品总数' : '总览示例值'}</p>
<p className="text-xs font-bold text-slate-500">{hasFirstPassSource ? '当前首轮通过率' : hasRealProjectSource ? '当前未完结流程' : hasMonthlyLaunchSource ? '本月审核通过 / 新品总数' : '总览示例值'}</p>
{isOnTimeDelivery && metric.exampleNumericValue !== undefined
? <OnTimeDeliveryRate rate={metric.exampleNumericValue} contextLabel="按时交付示例值" size="summary" />
: <p className="mt-2 text-2xl font-bold tabular-nums">{hasFirstPassSource ? formatRate(firstPassPage.summary.rate) : hasRealProjectSource ? projectInsights.total : hasMonthlyLaunchSource ? `${monthlyLaunches.summary.completed}/${monthlyLaunches.summary.total}` : metric.exampleValue}</p>}
</div>
<div className="border-b border-slate-200 p-5 sm:border-b-0 sm:border-r">
<p className="text-xs font-bold text-slate-500">明细数据</p>
<p className="mt-2 text-base font-bold">{hasFirstPassSource ? `V1通过 ${firstPassPage.summary.firstPassApproved} / 全部 ${firstPassPage.summary.totalUploads}` : hasRealProjectSource ? `${projectInsights.total} 条流程快照` : hasMonthlyLaunchSource ? `已完成 ${monthlyLaunches.summary.completed} / 进行中 ${monthlyLaunches.summary.ongoing}` : '等待业务数据源'}</p>
<p className="mt-2 text-base font-bold">{hasFirstPassSource ? `V1通过 ${firstPassPage.summary.firstPassApproved} / 全部 ${firstPassPage.summary.totalUploads}` : hasRealProjectSource ? `${projectInsights.total} 条流程快照` : hasMonthlyLaunchSource ? `已通过 ${monthlyLaunches.summary.completed} / 进行中 ${monthlyLaunches.summary.ongoing}` : '等待业务数据源'}</p>
</div>
<div className="p-5">
<p className="text-xs font-bold text-slate-500">同步状态</p>

View File

@ -68,7 +68,7 @@ describe('CoreMetricsDashboard', () => {
expect(screen.getByText(value)).toBeInTheDocument()
}
expect(screen.getByText('已完成 8 · 进行中 5')).toBeInTheDocument()
expect(screen.getByText('已通过 8 · 进行中 5')).toBeInTheDocument()
expect(screen.getAllByText('公式待接入')).toHaveLength(3)
expect(screen.getAllByText('钉钉流程快照').length).toBeGreaterThan(0)

View File

@ -209,7 +209,7 @@ export default function CoreMetricsDashboard() {
)}
{isMonthlyLaunch && monthlyLaunches.status === 'success' && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
已完成 {monthlyLaunches.data.completed} · 进行中 {monthlyLaunches.data.ongoing}
已通过 {monthlyLaunches.data.completed} · 进行中 {monthlyLaunches.data.ongoing}
</p>
)}
{isMonthlyLaunch && monthlyLaunches.status === 'loading' && (

View File

@ -35,8 +35,10 @@ function CurrentProgress({ item }: { item: MonthlyLaunchWorkflowItem }) {
if (item.launchStatus === 'completed') {
return (
<div>
<p className="font-bold text-emerald-700">流程已完成</p>
<p className="mt-1 text-xs tabular-nums text-slate-500">完成于 {formatDate(item.sourceFinishedAt)}</p>
<p className="font-bold text-emerald-700">负责人审核已通过</p>
<p className="mt-1 text-xs tabular-nums text-slate-500">
{item.reviewerName ? `${item.reviewerName} · ` : ''}通过于 {formatDate(item.reviewPassedAt)}
</p>
</div>
)
}
@ -75,7 +77,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl
<div>
<h2 id="monthly-launch-title" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">六品牌本月新品流程</h2>
<p className="mt-1 max-w-3xl text-sm leading-6 text-slate-600">
已完成按本月完成时间统计;进行中读取当前未完结流程,不限制发起月份。取消和终止流程不计入。
已完成按本月通过品牌中心负责人审核统计;进行中只读取当前节点处理人属于品牌中心的流程。流程自 2026 年 4 月 1 日起计入。
</p>
</div>
<p className="text-sm font-semibold tabular-nums text-slate-600">
@ -127,7 +129,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl
<h3 id="monthly-launch-detail-title" className="text-lg font-bold text-slate-950">
{selectedBrand} · {statusLabel(selectedStatus)}
</h3>
<p className="mt-1 text-sm text-slate-500">流程编码、发起人、所在部门、当前节点与业务时间</p>
<p className="mt-1 text-sm text-slate-500">流程编码、发起人、所在部门、负责人审核或当前节点与业务时间</p>
</div>
<p className="text-sm font-semibold tabular-nums text-slate-600">共 {selectedItems.length} 条</p>
</div>
@ -148,7 +150,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl
<th scope="col" className="w-[25%] px-5 py-3.5">流程</th>
<th scope="col" className="w-[15%] px-4 py-3.5">任务类型</th>
<th scope="col" className="w-[18%] px-4 py-3.5">发起人 / 所在部门</th>
<th scope="col" className="w-[24%] px-4 py-3.5">当前进度</th>
<th scope="col" className="w-[24%] px-4 py-3.5">审核 / 当前进度</th>
<th scope="col" className="w-[18%] px-5 py-3.5">发起 / 最近更新</th>
</tr>
</thead>

View File

@ -41,7 +41,7 @@ export const CORE_METRICS: CoreMetric[] = [
key: 'monthly-launches',
label: '本月上新',
exampleValue: '18/24',
description: '本月已完成新品流程与当前进行中新品流程的对照。',
description: '本月通过品牌中心负责人审核的新品流程与当前品牌中心在办流程的对照。',
icon: PackagePlus,
detailDescription: '查看 2026 年 4 月 1 日后发起的美术设计流程;已完成按本月完成时间统计,进行中按当前未完结状态统计。',
plannedFields: ['流程编码', '品牌', '发起人', '所在部门', '当前节点', '业务时间'],

View File

@ -4,6 +4,7 @@ import {
DING_WORKFLOW_STARTED_AT_CUTOFF_ISO,
buildBrandCenterOngoingWorkflowWhere,
buildMonthlyLaunchWorkflowWhere,
findBrandCenterReviewMilestone,
getShanghaiMonthRange,
getDingWorkflowProjectType,
isAllowedDingWorkflowProcessCode,
@ -79,10 +80,40 @@ describe('ding workflow snapshot sync semantics', () => {
isOngoing: true,
processCode: { in: [...DING_WORKFLOW_ALLOWED_PROCESS_CODES] },
sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO },
activeTasks: { array_contains: [{ isBrandCenter: true }] },
activeTasks: { array_contains: [{ isBrandCenter: true, isActive: true }] },
})
})
it('counts the first brand-center leadership approval completed in the selected month', () => {
const range = getShanghaiMonthRange(new Date('2026-08-26T03:00:00.000Z'))
expect(findBrandCenterReviewMilestone([
{
taskId: 'current',
isBrandCenter: true,
isLeadership: true,
isActive: true,
reviewPassed: false,
finishedAt: null,
},
{
taskId: 'later-review',
isBrandCenter: true,
isLeadership: true,
isActive: false,
reviewPassed: true,
finishedAt: '2026-08-25T11:00:00.000Z',
},
{
taskId: 'first-review',
isBrandCenter: true,
isLeadership: true,
isActive: false,
reviewPassed: true,
finishedAt: '2026-08-18T11:15:00.000Z',
},
], range)?.taskId).toBe('first-review')
})
it('only queries post-April art-design workflows for the six-brand monthly launch metric', () => {
expect(buildMonthlyLaunchWorkflowWhere()).toEqual({
processCode: 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838',

View File

@ -23,7 +23,7 @@ export const DING_WORKFLOW_ALLOWED_PROCESS_CODES = [
export const DING_WORKFLOW_BRANDS = ['亿家达', '卓禾', '欧意朗', '顾家', '索菲亚', '朴美'] as const
const ALLOWED_PROCESS_CODE_VALUES: string[] = [...DING_WORKFLOW_ALLOWED_PROCESS_CODES]
const ALLOWED_PROCESS_CODE_SET = new Set<string>(DING_WORKFLOW_ALLOWED_PROCESS_CODES)
const BRAND_CENTER_ACTIVE_TASK_JSON = [{ isBrandCenter: true }]
const BRAND_CENTER_ACTIVE_TASK_JSON = [{ isBrandCenter: true, isActive: true }]
const PROCESS_TYPE_LABELS: Record<string, string> = {
[DING_WORKFLOW_ALLOWED_PROCESS_CODES[0]]: '美术设计流程',
[DING_WORKFLOW_ALLOWED_PROCESS_CODES[1]]: '内容视频和图片拍摄',
@ -74,7 +74,9 @@ type SourceTaskRow = {
activityId: unknown
userId: unknown
status: unknown
result: unknown
createTime: unknown
finishTime: unknown
}
type SourceSchemeAggregate = {
@ -104,7 +106,12 @@ export type OngoingDingWorkflowActiveTask = {
assigneeName: string
assigneeDepartment: string | null
isBrandCenter: boolean
isLeadership: boolean
isActive: boolean
reviewPassed: boolean
result: string | null
createdAt: string | null
finishedAt: string | null
}
type WorkflowSnapshotPayload = {
@ -190,6 +197,8 @@ export type MonthlyLaunchWorkflowItem = OngoingDingWorkflowListItem & {
launchStatus: MonthlyLaunchWorkflowStatus
result: string | null
sourceFinishedAt: string | null
reviewPassedAt: string | null
reviewerName: string | null
}
export type MonthlyLaunchBrandSummary = {
@ -345,7 +354,7 @@ function titleOriginatorName(value: unknown): string | null {
return clean(value)?.match(/^(.+?)提交的/)?.[1]?.trim() || null
}
function normalizeActiveTasks(value: unknown): OngoingDingWorkflowActiveTask[] {
function normalizeWorkflowTasks(value: unknown): OngoingDingWorkflowActiveTask[] {
if (!Array.isArray(value)) return []
return value.flatMap((task) => {
if (!task || typeof task !== 'object') return []
@ -360,11 +369,30 @@ function normalizeActiveTasks(value: unknown): OngoingDingWorkflowActiveTask[] {
assigneeName: clean(row.assigneeName) || '待同步人员',
assigneeDepartment: clean(row.assigneeDepartment),
isBrandCenter: row.isBrandCenter === true,
isLeadership: row.isLeadership === true,
isActive: row.isActive !== false,
reviewPassed: row.reviewPassed === true,
result: clean(row.result)?.toUpperCase() || null,
createdAt: clean(row.createdAt),
finishedAt: clean(row.finishedAt),
}]
})
}
export function findBrandCenterReviewMilestone(
value: unknown,
range?: { start: Date; end: Date },
) {
return normalizeWorkflowTasks(value)
.filter((task) => {
if (!task.reviewPassed || !task.isBrandCenter || !task.isLeadership || !task.finishedAt) return false
if (!range) return true
const finishedAt = new Date(task.finishedAt).getTime()
return finishedAt >= range.start.getTime() && finishedAt < range.end.getTime()
})
.sort((a, b) => String(a.finishedAt).localeCompare(String(b.finishedAt)))[0] || null
}
function nonNegativeInteger(value: unknown): number {
const number = Number(value)
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0
@ -378,6 +406,10 @@ function tableRef(table: { schema: string; table: string }) {
return `${quoteIdentifier(table.schema)}.${quoteIdentifier(table.table)}`
}
function sourceTimestampSql(value: string) {
return `TRY_CONVERT(datetime2, REPLACE(REPLACE(REPLACE(NULLIF(CONVERT(nvarchar(64), ${value}), ''), 'T', ' '), 'Z', ''), 'z', ''))`
}
export function isDingWorkflowOngoing(status: unknown, finishedAt: unknown): boolean {
const normalizedStatus = String(status || '').trim().toUpperCase()
return Boolean(normalizedStatus)
@ -432,8 +464,8 @@ export function isDingWorkflowStartedOnOrAfterCutoff(value: unknown): boolean {
* 品牌中心“进行中项目”的统一查询口径。
*
* 一个流程可能同时存在多个当前任务;只要至少一个当前节点处理人属于品牌中心,
* 该流程就计入。同步快照中的 activeTasks 只保存未完结任务,因此这里不会误把
* 历史节点处理人当作当前负责人。
* 该流程就计入。快照 JSON 同时保存当前任务和审核历史,所以查询必须显式要求
* isActive=true,避免把历史品牌中心处理人误当作当前负责人。
*/
export function buildBrandCenterOngoingWorkflowWhere() {
return {
@ -539,18 +571,18 @@ async function fetchSourceWorkflows(pool: ConnectionPool, cursor: string | null)
@allowedProcessCode0,
@allowedProcessCode1
)
AND TRY_CONVERT(datetime2, NULLIF(CONVERT(nvarchar(64), createAt), '')) >=
AND ${sourceTimestampSql('createAt')} >=
TRY_CONVERT(datetime2, @startedAtCutoff)
AND (
@cursor IS NULL
OR UPPER(LTRIM(RTRIM(COALESCE(CONVERT(nvarchar(100), status), '')))) NOT IN ('COMPLETED', 'TERMINATED', 'CANCELED')
OR (
UPPER(LTRIM(RTRIM(COALESCE(CONVERT(nvarchar(100), status), '')))) = 'COMPLETED'
AND TRY_CONVERT(datetime2, NULLIF(CONVERT(nvarchar(64), finishTime), '')) >=
UPPER(LTRIM(RTRIM(COALESCE(CONVERT(nvarchar(100), status), '')))) IN ('COMPLETED', 'TERMINATED', 'CANCELED')
AND ${sourceTimestampSql('finishTime')} >=
TRY_CONVERT(datetime2, @currentMonthStart)
)
OR TRY_CONVERT(datetime2, NULLIF(CONVERT(nvarchar(64), updateAt), '')) >=
DATEADD(minute, -5, TRY_CONVERT(datetime2, @cursor))
OR ${sourceTimestampSql('updateAt')} >=
DATEADD(minute, -5, ${sourceTimestampSql('@cursor')})
)
`)
return result.recordset
@ -580,7 +612,9 @@ async function fetchWorkflowAggregates(pool: ConnectionPool, processInstanceIds:
CONVERT(nvarchar(255), task.activityId) AS activityId,
CONVERT(nvarchar(255), task.userId) AS userId,
CONVERT(nvarchar(100), task.status) AS status,
CONVERT(nvarchar(100), task.result) AS result,
CONVERT(nvarchar(64), task.createTime) AS createTime,
CONVERT(nvarchar(64), task.finishTime) AS finishTime,
ROW_NUMBER() OVER (
PARTITION BY task.processInstanceId, task.taskId
ORDER BY task.id DESC
@ -595,7 +629,7 @@ async function fetchWorkflowAggregates(pool: ConnectionPool, processInstanceIds:
;WITH ranked_tasks AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY processInstanceId
ORDER BY TRY_CONVERT(datetime2, NULLIF(createTime, '')) DESC, sourceRowId DESC
ORDER BY ${sourceTimestampSql('createTime')} DESC, sourceRowId DESC
) AS latestRow
FROM #workflow_task_rows
WHERE duplicateRow = 1
@ -609,7 +643,7 @@ async function fetchWorkflowAggregates(pool: ConnectionPool, processInstanceIds:
FROM ranked_tasks
GROUP BY processInstanceId;
SELECT processInstanceId, taskId, activityId, userId, status, createTime
SELECT processInstanceId, taskId, activityId, userId, status, result, createTime, finishTime
FROM #workflow_task_rows
WHERE duplicateRow = 1;
@ -673,6 +707,7 @@ type EmployeeDirectoryEntry = {
name: string
departmentName: string | null
isBrandCenter: boolean
isLeadership: boolean
}
async function readEmployeeDirectory(
@ -691,6 +726,9 @@ async function readEmployeeDirectory(
displayName: true,
departmentName: true,
departmentPath: true,
isLeadership: true,
levelType: true,
positionLevel: true,
employmentState: true,
departmentMemberships: { select: { departmentPath: true } },
},
@ -699,6 +737,9 @@ async function readEmployeeDirectory(
profile.dingtalkUserId ? [[profile.dingtalkUserId, {
name: profile.displayName,
departmentName: profile.departmentName,
isLeadership: profile.isLeadership
|| profile.levelType?.toUpperCase() === 'M'
|| profile.positionLevel?.toUpperCase().startsWith('M') === true,
isBrandCenter: profile.employmentState !== 'RESIGNED' && [
profile.departmentPath,
...profile.departmentMemberships.map((membership) => membership.departmentPath),
@ -773,16 +814,22 @@ function buildSnapshotPayload(
|| (originatorUserId ? employeeDirectory.get(originatorUserId)?.name : null)
|| titleOriginatorName(row.title)
|| '未记录'
const activeTasks = ongoing ? (taskRowsByWorkflow.get(processInstanceId) || []).flatMap((sourceTask) => {
if (!sourceTaskIsOngoing(sourceTask.status)) return []
const workflowTasks = (taskRowsByWorkflow.get(processInstanceId) || []).flatMap((sourceTask) => {
const taskId = clean(sourceTask.taskId)
if (!taskId) return []
const activityId = clean(sourceTask.activityId)
const assigneeUserId = clean(sourceTask.userId)
const taskStatus = clean(sourceTask.status)?.toUpperCase() || null
const taskResult = clean(sourceTask.result)?.toUpperCase() || null
const isActive = ongoing && sourceTaskIsOngoing(sourceTask.status)
const matchingAssignments = assigneeUserId
? assignments.filter((assignment) => assignment.userId === assigneeUserId)
: []
const employee = assigneeUserId ? employeeDirectory.get(assigneeUserId) : null
const reviewPassed = taskStatus === 'COMPLETED'
&& taskResult === 'AGREE'
&& employee?.isBrandCenter === true
&& employee.isLeadership
const uniqueAssignmentNodeNames = Array.from(new Set(matchingAssignments.map((assignment) => assignment.nodeName)))
const inferredNodeName = processCode && activityId
? activityNodeNames.get(`${processCode}:${activityId}`)
@ -790,18 +837,25 @@ function buildSnapshotPayload(
return [{
taskId,
activityId,
nodeName: inferredNodeName
nodeName: reviewPassed
? '品牌中心负责人审核'
: inferredNodeName
|| (uniqueAssignmentNodeNames.length === 1 ? uniqueAssignmentNodeNames[0] : null)
|| '当前处理节点',
|| (isActive ? '当前处理节点' : '历史处理节点'),
assigneeUserId,
assigneeName: matchingAssignments.find((assignment) => assignment.name)?.name
|| employee?.name
|| (assigneeUserId === 'bpms_system' ? '系统处理' : '待同步人员'),
assigneeDepartment: employee?.departmentName || null,
isBrandCenter: employee?.isBrandCenter === true,
isLeadership: employee?.isLeadership === true,
isActive,
reviewPassed,
result: taskResult,
createdAt: sourceWorkflowTimestampToIso(sourceTask.createTime),
finishedAt: sourceWorkflowTimestampToIso(sourceTask.finishTime),
}]
}) : []
})
return [{
processInstanceId,
@ -820,16 +874,16 @@ function buildSnapshotPayload(
sourceUpdatedAtRaw: clean(row.sourceUpdatedAtRaw),
sourceFinishedAtRaw: sourceWorkflowTimestampToIso(row.sourceFinishedAtRaw),
isOngoing: ongoing,
schemeFieldCount: ongoing ? nonNegativeInteger(scheme?.schemeFieldCount) : 0,
taskCount: ongoing ? nonNegativeInteger(task?.taskCount) : 0,
schemeFieldCount: nonNegativeInteger(scheme?.schemeFieldCount),
taskCount: nonNegativeInteger(task?.taskCount),
activeTaskCount: ongoing ? nonNegativeInteger(task?.activeTaskCount) : 0,
latestTaskStatus: ongoing ? clean(task?.latestTaskStatus)?.toUpperCase() || null : null,
latestTaskAtRaw: ongoing ? clean(task?.latestTaskAtRaw) : null,
latestTaskStatus: clean(task?.latestTaskStatus)?.toUpperCase() || null,
latestTaskAtRaw: clean(task?.latestTaskAtRaw),
brand,
requesterName,
requesterDepartment,
projectType: getDingWorkflowProjectType(processCode),
activeTasks,
activeTasks: workflowTasks,
}]
})
}
@ -1152,7 +1206,7 @@ function mapSnapshotListItem(row: {
|| '未记录部门',
initiatorName: initiator.name,
initiatorDepartment: initiator.department,
activeTasks: normalizeActiveTasks(row.activeTasks),
activeTasks: normalizeWorkflowTasks(row.activeTasks).filter((task) => task.isActive),
originatorUserId: row.originatorUserId,
originatorDeptName: row.originatorDeptName,
sourceCreatedAt: sourceWorkflowTimestampToIso(row.sourceCreatedAtRaw),
@ -1254,11 +1308,15 @@ async function readMonthlyLaunchInsightsWithoutAccessCheck(now = new Date()): Pr
const baseWhere = buildMonthlyLaunchWorkflowWhere()
const [completedCandidates, ongoingRows, state] = await Promise.all([
prisma.dingWorkflowSnapshot.findMany({
where: { ...baseWhere, status: 'COMPLETED' },
where: baseWhere,
select: MONTHLY_LAUNCH_WORKFLOW_SELECT,
}),
prisma.dingWorkflowSnapshot.findMany({
where: { ...baseWhere, isOngoing: true },
where: {
...baseWhere,
isOngoing: true,
activeTasks: { array_contains: BRAND_CENTER_ACTIVE_TASK_JSON },
},
select: MONTHLY_LAUNCH_WORKFLOW_SELECT,
}),
prisma.dingWorkflowSyncState.findUnique({
@ -1266,14 +1324,18 @@ async function readMonthlyLaunchInsightsWithoutAccessCheck(now = new Date()): Pr
select: { status: true, lastCompletedAt: true },
}),
])
const completedRows = completedCandidates.filter((row) => (
isDingWorkflowCompletedInRange(row.status, row.sourceFinishedAtRaw, range)
))
const allRows = [...completedRows, ...ongoingRows]
const completedRows = completedCandidates.flatMap((row) => {
const milestone = findBrandCenterReviewMilestone(row.activeTasks, range)
return milestone ? [{ row, milestone }] : []
})
const completedIds = new Set(completedRows.map(({ row }) => row.processInstanceId))
const uniqueOngoingRows = ongoingRows.filter((row) => !completedIds.has(row.processInstanceId))
const allRows = [...completedRows.map(({ row }) => row), ...uniqueOngoingRows]
const initiatorDirectory = await readInitiatorDirectory(allRows)
const mapMonthlyItem = (
row: typeof allRows[number],
launchStatus: MonthlyLaunchWorkflowStatus,
milestone: OngoingDingWorkflowActiveTask | null = null,
): MonthlyLaunchWorkflowItem => ({
...mapSnapshotListItem(
row,
@ -1282,13 +1344,15 @@ async function readMonthlyLaunchInsightsWithoutAccessCheck(now = new Date()): Pr
launchStatus,
result: row.result,
sourceFinishedAt: sourceWorkflowTimestampToIso(row.sourceFinishedAtRaw),
reviewPassedAt: milestone?.finishedAt || null,
reviewerName: milestone?.assigneeName || null,
})
const items = [
...completedRows.map((row) => mapMonthlyItem(row, 'completed')),
...ongoingRows.map((row) => mapMonthlyItem(row, 'ongoing')),
...completedRows.map(({ row, milestone }) => mapMonthlyItem(row, 'completed', milestone)),
...uniqueOngoingRows.map((row) => mapMonthlyItem(row, 'ongoing')),
].sort((a, b) => {
const aTime = a.launchStatus === 'completed' ? a.sourceFinishedAt : a.sourceUpdatedAt
const bTime = b.launchStatus === 'completed' ? b.sourceFinishedAt : b.sourceUpdatedAt
const aTime = a.launchStatus === 'completed' ? a.reviewPassedAt : a.sourceUpdatedAt
const bTime = b.launchStatus === 'completed' ? b.reviewPassedAt : b.sourceUpdatedAt
return String(bTime || '').localeCompare(String(aTime || '')) || b.id.localeCompare(a.id)
})
const brands = DING_WORKFLOW_BRANDS.map((brand) => {
@ -1298,7 +1362,7 @@ async function readMonthlyLaunchInsightsWithoutAccessCheck(now = new Date()): Pr
return { brand, completed, ongoing, total: completed + ongoing }
})
const completed = completedRows.length
const ongoing = ongoingRows.length
const ongoing = uniqueOngoingRows.length
return {
items,