diff --git a/components/department-dashboard/CoreMetricDetail.test.tsx b/components/department-dashboard/CoreMetricDetail.test.tsx index c686afe..0a20301 100644 --- a/components/department-dashboard/CoreMetricDetail.test.tsx +++ b/components/department-dashboard/CoreMetricDetail.test.tsx @@ -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, }], }, ], diff --git a/components/department-dashboard/CoreMetricDetail.tsx b/components/department-dashboard/CoreMetricDetail.tsx index 957404a..7d88aca 100644 --- a/components/department-dashboard/CoreMetricDetail.tsx +++ b/components/department-dashboard/CoreMetricDetail.tsx @@ -265,14 +265,14 @@ export default function CoreMetricDetail({ metric, query, projectInsights, month
-

{hasFirstPassSource ? '当前首轮通过率' : hasRealProjectSource ? '当前未完结流程' : hasMonthlyLaunchSource ? '本月完成 / 新品总数' : '总览示例值'}

+

{hasFirstPassSource ? '当前首轮通过率' : hasRealProjectSource ? '当前未完结流程' : hasMonthlyLaunchSource ? '本月审核通过 / 新品总数' : '总览示例值'}

{isOnTimeDelivery && metric.exampleNumericValue !== undefined ? :

{hasFirstPassSource ? formatRate(firstPassPage.summary.rate) : hasRealProjectSource ? projectInsights.total : hasMonthlyLaunchSource ? `${monthlyLaunches.summary.completed}/${monthlyLaunches.summary.total}` : metric.exampleValue}

}

明细数据

-

{hasFirstPassSource ? `V1通过 ${firstPassPage.summary.firstPassApproved} / 全部 ${firstPassPage.summary.totalUploads}` : hasRealProjectSource ? `${projectInsights.total} 条流程快照` : hasMonthlyLaunchSource ? `已完成 ${monthlyLaunches.summary.completed} / 进行中 ${monthlyLaunches.summary.ongoing}` : '等待业务数据源'}

+

{hasFirstPassSource ? `V1通过 ${firstPassPage.summary.firstPassApproved} / 全部 ${firstPassPage.summary.totalUploads}` : hasRealProjectSource ? `${projectInsights.total} 条流程快照` : hasMonthlyLaunchSource ? `已通过 ${monthlyLaunches.summary.completed} / 进行中 ${monthlyLaunches.summary.ongoing}` : '等待业务数据源'}

同步状态

diff --git a/components/department-dashboard/CoreMetricsDashboard.test.tsx b/components/department-dashboard/CoreMetricsDashboard.test.tsx index 4b036d4..2e4e1c4 100644 --- a/components/department-dashboard/CoreMetricsDashboard.test.tsx +++ b/components/department-dashboard/CoreMetricsDashboard.test.tsx @@ -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) diff --git a/components/department-dashboard/CoreMetricsDashboard.tsx b/components/department-dashboard/CoreMetricsDashboard.tsx index a8606dd..0e6742f 100644 --- a/components/department-dashboard/CoreMetricsDashboard.tsx +++ b/components/department-dashboard/CoreMetricsDashboard.tsx @@ -209,7 +209,7 @@ export default function CoreMetricsDashboard() { )} {isMonthlyLaunch && monthlyLaunches.status === 'success' && (

- 已完成 {monthlyLaunches.data.completed} · 进行中 {monthlyLaunches.data.ongoing} + 已通过 {monthlyLaunches.data.completed} · 进行中 {monthlyLaunches.data.ongoing}

)} {isMonthlyLaunch && monthlyLaunches.status === 'loading' && ( diff --git a/components/department-dashboard/MonthlyLaunchesInsights.tsx b/components/department-dashboard/MonthlyLaunchesInsights.tsx index 3c9072d..aa73167 100644 --- a/components/department-dashboard/MonthlyLaunchesInsights.tsx +++ b/components/department-dashboard/MonthlyLaunchesInsights.tsx @@ -35,8 +35,10 @@ function CurrentProgress({ item }: { item: MonthlyLaunchWorkflowItem }) { if (item.launchStatus === 'completed') { return (
-

流程已完成

-

完成于 {formatDate(item.sourceFinishedAt)}

+

负责人审核已通过

+

+ {item.reviewerName ? `${item.reviewerName} · ` : ''}通过于 {formatDate(item.reviewPassedAt)} +

) } @@ -75,7 +77,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl

六品牌本月新品流程

- 已完成按本月完成时间统计;进行中读取当前未完结流程,不限制发起月份。取消和终止流程不计入。 + 已完成按本月通过品牌中心负责人审核统计;进行中只读取当前节点处理人属于品牌中心的流程。流程自 2026 年 4 月 1 日起计入。

@@ -127,7 +129,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl

{selectedBrand} · {statusLabel(selectedStatus)}

-

流程编码、发起人、所在部门、当前节点与业务时间

+

流程编码、发起人、所在部门、负责人审核或当前节点与业务时间

共 {selectedItems.length} 条

@@ -148,7 +150,7 @@ export default function MonthlyLaunchesInsights({ insights }: { insights: Monthl 流程 任务类型 发起人 / 所在部门 - 当前进度 + 审核 / 当前进度 发起 / 最近更新 diff --git a/components/department-dashboard/core-metrics.ts b/components/department-dashboard/core-metrics.ts index 93113ae..d28466e 100644 --- a/components/department-dashboard/core-metrics.ts +++ b/components/department-dashboard/core-metrics.ts @@ -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: ['流程编码', '品牌', '发起人', '所在部门', '当前节点', '业务时间'], diff --git a/lib/__tests__/ding-workflow-sync.test.ts b/lib/__tests__/ding-workflow-sync.test.ts index e1ee5ed..19669f4 100644 --- a/lib/__tests__/ding-workflow-sync.test.ts +++ b/lib/__tests__/ding-workflow-sync.test.ts @@ -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', diff --git a/lib/ding-workflow-sync.ts b/lib/ding-workflow-sync.ts index f070744..0767073 100644 --- a/lib/ding-workflow-sync.ts +++ b/lib/ding-workflow-sync.ts @@ -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(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 = { [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,