修复本月新品负责人审核口径
This commit is contained in:
parent
8490b3b371
commit
484781f23c
@ -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,
|
||||
}],
|
||||
},
|
||||
],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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' && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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: ['流程编码', '品牌', '发起人', '所在部门', '当前节点', '业务时间'],
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user