diff --git a/components/department-dashboard/OnTimeDeliveryInsights.tsx b/components/department-dashboard/OnTimeDeliveryInsights.tsx
index 6c3617a..6bc9f83 100644
--- a/components/department-dashboard/OnTimeDeliveryInsights.tsx
+++ b/components/department-dashboard/OnTimeDeliveryInsights.tsx
@@ -158,7 +158,7 @@ export default function OnTimeDeliveryInsightsView({ insights }: { insights: OnT
本月审核节点完成时效明细
- 仅统计对应审核节点在本月完成、审批状态为已完结且未被拒绝的流程;待计算流程不计入分母。美术按“品牌策划主管分配任务”至“品牌策划主管审核”计算,内容按规则计至“编导审核”或“内容主管审核”。
+ 仅统计对应审核节点在本月完成、审批状态为已完结且未被拒绝的流程;待计算流程不计入分母。美术按“品牌策划主管分配任务”至“品牌策划主管审核”计算,内容按外部看板规则计至“编导审核”或“内容主管审核”,并应用排队顺延后的起算时间。
{activeLabel} · 共 {filteredSummary.total} 条
@@ -201,6 +201,11 @@ export default function OnTimeDeliveryInsightsView({ insights }: { insights: OnT
{formatDate(item.assessment.startedAt)}
{item.assessment.endNodeLabel}
{formatDate(item.assessment.finishedAt)}
+ {item.assessment.queueDeferred && (
+
+ 已按排队规则顺延 {item.assessment.queueShiftDays || 0} 天
+
+ )}
{formatDuration(item.assessment.standardDuration, item.assessment.durationUnit)}
diff --git a/components/department-dashboard/core-metrics.ts b/components/department-dashboard/core-metrics.ts
index bd60f2d..484998a 100644
--- a/components/department-dashboard/core-metrics.ts
+++ b/components/department-dashboard/core-metrics.ts
@@ -53,7 +53,7 @@ export const CORE_METRICS: CoreMetric[] = [
exampleNumericValue: 94.6,
description: '本月未超时审核节点完成流程占本月审核节点完成流程的比例,待计算流程不计入分母。',
icon: Clock3,
- detailDescription: '查看本月审核节点完成流程的标准时效、实际用时与超时判定;待计算流程不计入分母,美术和内容流程分别沿用对应外部看板的计算规则。',
+ detailDescription: '查看本月审核节点完成流程的标准时效、实际用时与超时判定;待计算流程不计入分母,美术沿用外部看板规则,内容视频沿用包含排队顺延的外部看板规则。',
plannedFields: ['流程', '发起人', '计时节点', '标准时效', '实际用时', '判定结果'],
},
{
diff --git a/lib/__tests__/ding-workflow-delivery.test.ts b/lib/__tests__/ding-workflow-delivery.test.ts
index 3fa8d64..1ad83ef 100644
--- a/lib/__tests__/ding-workflow-delivery.test.ts
+++ b/lib/__tests__/ding-workflow-delivery.test.ts
@@ -1,4 +1,4 @@
-import { assessDingWorkflowDelivery } from '@/lib/ding-workflow-delivery'
+import { assessDingWorkflowDelivery, assessDingWorkflowDeliveryBatch } from '@/lib/ding-workflow-delivery'
import type { DingWorkflowDetailFormFields, OngoingDingWorkflowActiveTask } from '@/lib/ding-workflow-sync'
const ART_PROCESS_CODE = 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838'
@@ -197,4 +197,88 @@ describe('Ding workflow delivery SLA', () => {
actualDuration: null,
})
})
+
+ it('defers content assessment start when the daily queue is full', () => {
+ const inputs = [
+ {
+ processCode: CONTENT_PROCESS_CODE,
+ sourceCreatedAt: '2026-08-01T00:00:00.000Z',
+ sourceFinishedAt: '2026-08-02T00:00:00.000Z',
+ formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }),
+ tasks: [
+ task('内容主管分配任务', '2026-08-01T01:00:00.000Z', '2026-08-01T01:00:00.000Z'),
+ task('编导审核', '2026-08-02T00:00:00.000Z', '2026-08-02T00:00:00.000Z'),
+ ],
+ workflowId: 'a',
+ workflowStatus: 'COMPLETED',
+ },
+ {
+ processCode: CONTENT_PROCESS_CODE,
+ sourceCreatedAt: '2026-08-01T00:00:00.000Z',
+ sourceFinishedAt: '2026-08-03T00:00:00.000Z',
+ formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }),
+ tasks: [
+ task('内容主管分配任务', '2026-08-01T02:00:00.000Z', '2026-08-01T02:00:00.000Z'),
+ task('编导审核', '2026-08-03T00:00:00.000Z', '2026-08-03T00:00:00.000Z'),
+ ],
+ workflowId: 'b',
+ workflowStatus: 'COMPLETED',
+ },
+ {
+ processCode: CONTENT_PROCESS_CODE,
+ sourceCreatedAt: '2026-08-01T00:00:00.000Z',
+ sourceFinishedAt: '2026-08-04T00:00:00.000Z',
+ formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }),
+ tasks: [
+ task('内容主管分配任务', '2026-08-01T03:00:00.000Z', '2026-08-01T03:00:00.000Z'),
+ task('编导审核', '2026-08-04T00:00:00.000Z', '2026-08-04T00:00:00.000Z'),
+ ],
+ workflowId: 'c',
+ workflowStatus: 'COMPLETED',
+ },
+ {
+ processCode: CONTENT_PROCESS_CODE,
+ sourceCreatedAt: '2026-08-01T00:00:00.000Z',
+ sourceFinishedAt: '2026-08-05T00:00:00.000Z',
+ formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }),
+ tasks: [
+ task('内容主管分配任务', '2026-08-01T04:00:00.000Z', '2026-08-01T04:00:00.000Z'),
+ task('编导审核', '2026-08-05T00:00:00.000Z', '2026-08-05T00:00:00.000Z'),
+ ],
+ workflowId: 'd',
+ workflowStatus: 'COMPLETED',
+ },
+ ]
+
+ const assessments = assessDingWorkflowDeliveryBatch(inputs)
+ expect(assessments.slice(0, 3).map((assessment) => assessment.queueShiftDays)).toEqual([0, 0, 0])
+ expect(assessments[3]).toMatchObject({
+ queueDeferred: true,
+ queueLimit: 3,
+ queueSequence: 1,
+ queueShiftDays: 1,
+ startedAt: '2026-08-02T01:00:00.000Z',
+ actualDuration: 71,
+ status: 'on_time',
+ })
+ })
+
+ it('uses a two-day interval for installation-video queue deferrals', () => {
+ const makeInput = (id: string, day: string) => ({
+ processCode: CONTENT_PROCESS_CODE,
+ sourceCreatedAt: '2026-08-01T00:00:00.000Z',
+ sourceFinishedAt: `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`,
+ formFields: formFields({ demandType: '安装视频', taskType: '布局拍摄', installVideo: 'A档' }),
+ tasks: [
+ task('内容主管分配任务', `2026-08-${day}T01:00:00.000Z`, `2026-08-${day}T01:00:00.000Z`),
+ task('内容主管审核', `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`, `2026-08-${day === '01' ? '03' : '05'}T00:00:00.000Z`),
+ ],
+ workflowId: id,
+ workflowStatus: 'COMPLETED',
+ })
+ const [first, second] = assessDingWorkflowDeliveryBatch([makeInput('a', '01'), makeInput('b', '01')])
+ expect(first.queueShiftDays).toBe(0)
+ expect(second.queueShiftDays).toBe(2)
+ expect(second.startedAt).toBe('2026-08-03T01:00:00.000Z')
+ })
})
diff --git a/lib/ding-workflow-delivery.ts b/lib/ding-workflow-delivery.ts
index f026cfc..b1fcf0a 100644
--- a/lib/ding-workflow-delivery.ts
+++ b/lib/ding-workflow-delivery.ts
@@ -39,8 +39,53 @@ export type DingWorkflowDeliveryAssessment = {
finishedAt: string | null
startNodeLabel: string
endNodeLabel: string
+ /** 内容视频外部看板的排队结果;美术或未命中排队规则时为默认值。 */
+ queueDeferred?: boolean
+ queueLimit?: number | null
+ queueSequence?: number | null
+ queueShiftDays?: number
}
+type DingWorkflowDeliveryInput = {
+ processCode: string
+ sourceCreatedAt: string | null
+ sourceFinishedAt: string | null
+ formFields: DingWorkflowDetailFormFields
+ tasks: OngoingDingWorkflowActiveTask[]
+ /** Used only by the batch queue calculation to produce a deterministic tie-breaker. */
+ workflowId?: string | null
+ /** Raw workflow status; processing rows occupy queue slots, rejected rows do not. */
+ workflowStatus?: string | null
+}
+
+type ContentQueuePolicy = {
+ key: string
+ displayLimit: number
+ dailyLimit: number
+ intervalDays: number
+ effectiveFrom: number
+}
+
+const CONTENT_QUEUE_SHIFT_START_HOUR = 9
+const CONTENT_QUEUE_LEGACY_EFFECTIVE_FROM = Date.parse('2026-06-01T00:00:00.000+08:00')
+const CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM = Date.parse('2026-07-20T00:00:00.000+08:00')
+const LEGACY_CONTENT_QUEUE_LIMITS: Record = {
+ '淘系-逛逛': 3,
+ '淘系主图': 3,
+ '实拍-策划专用': 2,
+}
+const CONTENT_QUEUE_GROUP_DOUYIN_LAYOUT = new Set([
+ '抖音-千川素材',
+ '抖音-账号维护(内容部专用)',
+ '抖音千川+抖音账户维护视频',
+])
+const CONTENT_QUEUE_GROUP_TAOXI_LAYOUT = new Set([
+ '淘系-逛逛',
+ '淘系主图',
+ '淘系-逛逛+淘系主图视频',
+])
+const CONTENT_QUEUE_GROUP_TAOXI_SECOND_EDIT = new Set(['淘系-逛逛', '淘系主图'])
+
const ART_RULES: ArtRule[] = [
{ type: '视觉 打板', detail: '主图+sku+详情+视频', days: 15, byQuantity: true },
{ type: '品牌/品类视觉制作规范', detail: '主图或详情视觉规范制作', days: 7, byQuantity: true },
@@ -296,6 +341,11 @@ function earliestTaskTime(tasks: OngoingDingWorkflowActiveTask[], names: string[
.sort((a, b) => a.timestamp - b.timestamp)[0] || null
}
+function latestTaskTimeByField(tasks: OngoingDingWorkflowActiveTask[], names: string[], field: 'createdAt' | 'finishedAt') {
+ return tasks.flatMap((task) => names.includes(task.nodeName) && taskTime(task, field) ? [taskTime(task, field)!] : [])
+ .sort((a, b) => b.timestamp - a.timestamp)[0] || null
+}
+
function latestTaskTime(tasks: OngoingDingWorkflowActiveTask[], names: string[], after: number | null) {
return tasks.flatMap((task) => names.includes(task.nodeName) && taskTime(task, 'finishedAt') ? [taskTime(task, 'finishedAt')!] : [])
.filter((entry) => after === null || entry.timestamp >= after)
@@ -348,6 +398,75 @@ function resolveContentRule(fields: DingWorkflowDetailFormFields, createdAt: num
)).sort((a, b) => a.totalHours - b.totalHours)[0] || null
}
+function resolveContentQueuePolicy(fields: DingWorkflowDetailFormFields, createdAt: number | null): ContentQueuePolicy | null {
+ if (createdAt === null) return null
+ const demandType = normalizeContentRuleValue(fields.demandType)
+ const taskType = normalizeContentRuleValue(fields.taskType) || '布局拍摄'
+
+ if (createdAt >= CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM) {
+ if (taskType === '布局拍摄' && CONTENT_QUEUE_GROUP_DOUYIN_LAYOUT.has(demandType)) {
+ return { key: 'douyin-layout-20260720', displayLimit: 3, dailyLimit: 3, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ if (taskType === '二次剪辑' && demandType === '抖音-千川素材') {
+ return { key: 'douyin-second-edit-20260720', displayLimit: 1, dailyLimit: 1, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ if (taskType === '布局拍摄' && CONTENT_QUEUE_GROUP_TAOXI_LAYOUT.has(demandType)) {
+ return { key: 'taoxi-layout-20260720', displayLimit: 3, dailyLimit: 3, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ if (taskType === '二次剪辑' && CONTENT_QUEUE_GROUP_TAOXI_SECOND_EDIT.has(demandType)) {
+ return { key: 'taoxi-second-edit-20260720', displayLimit: 1, dailyLimit: 1, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ if (demandType === '实拍-策划专用') {
+ return { key: 'realshot-20260720', displayLimit: 1, dailyLimit: 1, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ if (demandType === '安装视频') {
+ return { key: 'install-video-20260720', displayLimit: 0.5, dailyLimit: 1, intervalDays: 2, effectiveFrom: CONTENT_QUEUE_RULE_CHANGE_EFFECTIVE_FROM }
+ }
+ return null
+ }
+
+ if (createdAt < CONTENT_QUEUE_LEGACY_EFFECTIVE_FROM) return null
+ const legacyLimit = LEGACY_CONTENT_QUEUE_LIMITS[demandType] || 0
+ return legacyLimit
+ ? { key: `legacy-${demandType}`, displayLimit: legacyLimit, dailyLimit: legacyLimit, intervalDays: 1, effectiveFrom: CONTENT_QUEUE_LEGACY_EFFECTIVE_FROM }
+ : null
+}
+
+function formatShanghaiDateKey(timestamp: number) {
+ const parts = new Intl.DateTimeFormat('en-CA', {
+ timeZone: 'Asia/Shanghai',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ }).formatToParts(new Date(timestamp))
+ const year = parts.find((part) => part.type === 'year')?.value
+ const month = parts.find((part) => part.type === 'month')?.value
+ const day = parts.find((part) => part.type === 'day')?.value
+ return year && month && day ? `${year}-${month}-${day}` : ''
+}
+
+function dateAtShanghaiHour(dateKey: string, hour = CONTENT_QUEUE_SHIFT_START_HOUR) {
+ const match = dateKey.match(/^(\d{4})-(\d{2})-(\d{2})$/)
+ if (!match) return null
+ const [, year, month, day] = match
+ return Date.UTC(Number(year), Number(month) - 1, Number(day), hour, 0, 0) - 8 * 3_600_000
+}
+
+function addUtcDays(dateKey: string, days: number) {
+ const base = dateAtShanghaiHour(dateKey, 0)
+ if (base === null) return ''
+ return formatShanghaiDateKey(base + days * 86_400_000)
+}
+
+function isRejectedWorkflowStatus(value: string | null | undefined) {
+ const status = normalized(value).toUpperCase()
+ return status.includes('REJECT') || status.includes('TERMINAT') || status.includes('CANCEL')
+}
+
+function isoAtUtc(timestamp: number) {
+ return new Date(timestamp).toISOString()
+}
+
function finishAssessment(
standardDuration: number | null,
durationUnit: 'day' | 'hour',
@@ -371,16 +490,14 @@ function finishAssessment(
finishedAt: finishedAt?.value || null,
startNodeLabel,
endNodeLabel,
+ queueDeferred: false,
+ queueLimit: null,
+ queueSequence: null,
+ queueShiftDays: 0,
}
}
-export function assessDingWorkflowDelivery(input: {
- processCode: string
- sourceCreatedAt: string | null
- sourceFinishedAt: string | null
- formFields: DingWorkflowDetailFormFields
- tasks: OngoingDingWorkflowActiveTask[]
-}): DingWorkflowDeliveryAssessment {
+export function assessDingWorkflowDelivery(input: DingWorkflowDeliveryInput): DingWorkflowDeliveryAssessment {
const createdAt = parseIsoTime(input.sourceCreatedAt)
if (input.processCode === ART_PROCESS_CODE) {
const start = earliestTaskTime(input.tasks, ['品牌策划主管分配任务'], 'createdAt')
@@ -411,3 +528,98 @@ export function assessDingWorkflowDelivery(input: {
return finishAssessment(null, 'hour', null, null, '分配任务', '审核')
}
+
+/**
+ * Apply the content-video dashboard's queue deferral rules to a set of
+ * workflows. Queue capacity is shared by the same policy group and ordered by
+ * assignment time (then workflow id), matching the external dashboard. The
+ * shift changes the assessment start clock; the SLA standard itself is not
+ * inflated. A single-workflow assessment intentionally remains unshifted, so
+ * callers that do not have the surrounding queue still get deterministic data.
+ */
+export function assessDingWorkflowDeliveryBatch(inputs: DingWorkflowDeliveryInput[]): DingWorkflowDeliveryAssessment[] {
+ const assessments = inputs.map((input) => assessDingWorkflowDelivery(input))
+ const queueGroups = new Map>()
+
+ inputs.forEach((input, index) => {
+ if (input.processCode !== CONTENT_PROCESS_CODE || isRejectedWorkflowStatus(input.workflowStatus)) return
+ const policy = resolveContentQueuePolicy(input.formFields, parseIsoTime(input.sourceCreatedAt))
+ if (!policy) return
+ // The external dashboard schedules by the latest assignment timestamp,
+ // while the unqueued assessment still starts at the first assignment.
+ const assignmentNames = ['内容主管分配任务', '内容负责人分配任务', '内容部负责人']
+ const baseStart = latestTaskTimeByField(input.tasks, assignmentNames, 'createdAt')?.timestamp
+ ?? parseIsoTime(assessments[index].startedAt)
+ if (baseStart === null) return
+ const entries = queueGroups.get(policy.key) || []
+ entries.push({
+ index,
+ policy,
+ baseStart,
+ workflowId: normalized(input.workflowId) || String(index).padStart(8, '0'),
+ })
+ queueGroups.set(policy.key, entries)
+ })
+
+ queueGroups.forEach((entries: Array<{
+ index: number
+ policy: ContentQueuePolicy
+ baseStart: number
+ workflowId: string
+ }>) => {
+ entries.sort((a: typeof entries[number], b: typeof entries[number]) => a.baseStart - b.baseStart || a.workflowId.localeCompare(b.workflowId, 'zh-Hans-CN'))
+ const dayCounts = new Map()
+ let lastScheduledDateKey = ''
+
+ for (const entry of entries) {
+ const { index, policy, baseStart } = entry
+ const baseDateKey = formatShanghaiDateKey(baseStart)
+ let targetDateKey = baseDateKey
+ const nextAvailableDateKey = policy.intervalDays > 1 && lastScheduledDateKey
+ ? addUtcDays(lastScheduledDateKey, policy.intervalDays)
+ : ''
+ if (nextAvailableDateKey && targetDateKey < nextAvailableDateKey) {
+ targetDateKey = nextAvailableDateKey
+ }
+ while ((dayCounts.get(targetDateKey) || 0) >= policy.dailyLimit) {
+ targetDateKey = addUtcDays(targetDateKey, policy.intervalDays)
+ }
+
+ const queueSequence = (dayCounts.get(targetDateKey) || 0) + 1
+ dayCounts.set(targetDateKey, queueSequence)
+ if (!lastScheduledDateKey || targetDateKey > lastScheduledDateKey) {
+ lastScheduledDateKey = targetDateKey
+ }
+
+ const scheduledStart = targetDateKey === baseDateKey
+ ? baseStart
+ : dateAtShanghaiHour(targetDateKey)
+ if (scheduledStart === null) continue
+
+ const shiftDays = Math.max(0, Math.round(
+ ((dateAtShanghaiHour(targetDateKey, 0) || 0) - (dateAtShanghaiHour(baseDateKey, 0) || 0)) / 86_400_000,
+ ))
+ const assessment = assessments[index]
+ assessment.queueDeferred = shiftDays > 0
+ assessment.queueLimit = policy.displayLimit
+ assessment.queueSequence = queueSequence
+ assessment.queueShiftDays = shiftDays
+ assessment.startedAt = isoAtUtc(scheduledStart)
+
+ const finishedAt = parseIsoTime(assessment.finishedAt)
+ assessment.actualDuration = finishedAt === null
+ ? null
+ : roundTwo(Math.max(0, finishedAt - scheduledStart) / 3_600_000)
+ assessment.status = assessment.standardDuration !== null && assessment.actualDuration !== null
+ ? assessment.actualDuration <= assessment.standardDuration ? 'on_time' : 'overtime'
+ : 'pending'
+ }
+ })
+
+ return assessments
+}
diff --git a/lib/ding-workflow-sync.ts b/lib/ding-workflow-sync.ts
index 943e550..cb5178e 100644
--- a/lib/ding-workflow-sync.ts
+++ b/lib/ding-workflow-sync.ts
@@ -5,7 +5,7 @@ import mssql, { type ConnectionPool } from 'mssql'
import { resolveBrandCenterAccessForUser } from '@/lib/brand-center-access'
import { isBrandCenterOrganizationPath } from '@/lib/brand-center-access-policy'
import {
- assessDingWorkflowDelivery,
+ assessDingWorkflowDeliveryBatch,
type DingWorkflowDeliveryAssessment,
} from '@/lib/ding-workflow-delivery'
import { prisma } from '@/lib/prisma'
@@ -1913,7 +1913,6 @@ async function readOnTimeDeliveryInsightsWithoutAccessCheck(now = new Date()): P
where: {
processCode: { in: ALLOWED_PROCESS_CODE_VALUES },
sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO },
- status: 'COMPLETED',
},
select: MONTHLY_LAUNCH_WORKFLOW_SELECT,
orderBy: [
@@ -1926,12 +1925,31 @@ async function readOnTimeDeliveryInsightsWithoutAccessCheck(now = new Date()): P
select: { status: true, lastCompletedAt: true },
}),
])
- const rows = candidates.filter((row) => isDingWorkflowApprovedCompletion(row.status, row.result, row.bizAction))
+ // Keep in-flight rows while calculating the shared content queue: the
+ // external dashboard lets earlier in-flight assignments consume capacity,
+ // even though only approved COMPLETED rows enter this month's denominator.
+ const rows = candidates.filter((row) => (
+ String(row.status || '').trim().toUpperCase() !== 'COMPLETED'
+ || isDingWorkflowApprovedCompletion(row.status, row.result, row.bizAction)
+ ))
const initiatorDirectory = await readInitiatorDirectory(rows)
- const assessedItems = rows.map((row): OnTimeDeliveryWorkflowItem => {
+ const assessmentInputs = rows.map((row) => {
const processCode = normalizeDingWorkflowProcessCode(row.processCode) || 'UNKNOWN'
const allTasks = normalizeWorkflowTasks(row.activeTasks)
const sourceCreatedAt = storedWorkflowTimestampToIso(row.sourceCreatedAtRaw)
+ const sourceFinishedAt = storedWorkflowTimestampToIso(row.sourceFinishedAtRaw)
+ return {
+ processCode,
+ sourceCreatedAt,
+ sourceFinishedAt,
+ formFields: normalizeDetailFormFields(row.formFields),
+ tasks: allTasks,
+ workflowId: clean(row.processInstanceId),
+ workflowStatus: clean(row.status),
+ }
+ })
+ const assessments = assessDingWorkflowDeliveryBatch(assessmentInputs)
+ const assessedItems = rows.map((row, index): OnTimeDeliveryWorkflowItem => {
const sourceFinishedAt = storedWorkflowTimestampToIso(row.sourceFinishedAtRaw)
return {
...mapSnapshotListItem(
@@ -1940,17 +1958,12 @@ async function readOnTimeDeliveryInsightsWithoutAccessCheck(now = new Date()): P
),
result: row.result,
sourceFinishedAt,
- assessment: assessDingWorkflowDelivery({
- processCode,
- sourceCreatedAt,
- sourceFinishedAt,
- formFields: normalizeDetailFormFields(row.formFields),
- tasks: allTasks,
- }),
+ assessment: assessments[index],
}
})
const items = assessedItems.filter((item) => (
- isDingWorkflowDeliveryAssessmentInRange(item.assessment, range)
+ String(item.status || '').trim().toUpperCase() === 'COMPLETED'
+ && isDingWorkflowDeliveryAssessmentInRange(item.assessment, range)
))
const onTime = items.filter((item) => item.assessment.status === 'on_time').length
const overtime = items.filter((item) => item.assessment.status === 'overtime').length
|