fix(oa-board): unify stage overtime followups
This commit is contained in:
parent
100d2d05fa
commit
5fffc3d184
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import ExcelJS from 'exceljs'
|
||||
import { handleRequest, jsonError } from '@/server/core/response'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { isOaBoardTimelineMetricOverdue } from '@/lib/oa-board-overtime'
|
||||
import {
|
||||
hasOaBoardProjectOvertime,
|
||||
oaBoardService,
|
||||
@ -226,7 +227,7 @@ function readMetricValue(
|
||||
if (!metric) return ''
|
||||
if (valueType === 'timeout') {
|
||||
if (metric.targetDays === null || metric.actualDays === null) return ''
|
||||
return metric.actualDays - metric.targetDays > 0 ? '是' : '否'
|
||||
return isOaBoardTimelineMetricOverdue(metric) ? '是' : '否'
|
||||
}
|
||||
return valueType === 'target'
|
||||
? formatDayValue(metric.targetDays)
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
getOaBoardFollowupDeadlineStatus,
|
||||
isOaBoardTimelineMetricOverdue,
|
||||
listHistoricalOverdueStageMetrics,
|
||||
OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS,
|
||||
pickCurrentOverdueStageMetric,
|
||||
@ -551,7 +552,7 @@ function readMetricValue(
|
||||
if (!metric) return '-'
|
||||
if (valueType === 'timeout') {
|
||||
if (metric.targetDays === null || metric.actualDays === null) return '-'
|
||||
return metric.actualDays - metric.targetDays > 0 ? '是' : '否'
|
||||
return isOaBoardTimelineMetricOverdue(metric) ? '是' : '否'
|
||||
}
|
||||
return valueType === 'target'
|
||||
? formatDayValue(metric.targetDays)
|
||||
@ -689,9 +690,7 @@ function buildTimingDetailFields(item: OaBoardProjectListItemDTO): TimingDetailF
|
||||
return TIMING_DETAIL_DEFINITIONS.map((field) => {
|
||||
const metric = metrics[field.metricKey]
|
||||
const timeoutState =
|
||||
field.valueType === 'timeout' && metric && metric.targetDays !== null && metric.actualDays !== null
|
||||
? metric.actualDays - metric.targetDays > 0
|
||||
: null
|
||||
field.valueType === 'timeout' ? isOaBoardTimelineMetricOverdue(metric) : null
|
||||
|
||||
return {
|
||||
id: field.id,
|
||||
@ -803,7 +802,6 @@ function buildHistoricalFollowupStage(followup: OaBoardTimeoutFollowup): Overtim
|
||||
function getFollowupDisplayStages(
|
||||
currentOvertimeStages: OvertimeStage[],
|
||||
historicalOvertimeStages: OvertimeStage[],
|
||||
followups: OaBoardTimeoutFollowup[],
|
||||
) {
|
||||
const stagesByKey = new Map(currentOvertimeStages.map((stage) => [stage.stageKey, stage]))
|
||||
|
||||
@ -811,12 +809,6 @@ function getFollowupDisplayStages(
|
||||
if (!stagesByKey.has(stage.stageKey)) stagesByKey.set(stage.stageKey, stage)
|
||||
})
|
||||
|
||||
followups.forEach((followup) => {
|
||||
if (!stagesByKey.has(followup.stageKey)) {
|
||||
stagesByKey.set(followup.stageKey, buildHistoricalFollowupStage(followup))
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(stagesByKey.values())
|
||||
}
|
||||
|
||||
@ -881,27 +873,15 @@ function getTimeoutFollowupSummary(
|
||||
overtimeStages: OvertimeStage[],
|
||||
followups: OaBoardTimeoutFollowup[],
|
||||
) {
|
||||
const relevantFollowups =
|
||||
overtimeStages.length === 0
|
||||
? followups
|
||||
: followups.filter((followup) =>
|
||||
if (overtimeStages.length === 0) return null
|
||||
|
||||
const relevantFollowups = followups.filter((followup) =>
|
||||
overtimeStages.some((stage) => stage.stageKey === followup.stageKey),
|
||||
)
|
||||
const allFollowupDeadlineStatuses = relevantFollowups
|
||||
.map((followup) => getFollowupDeadlineStatus(followup))
|
||||
.filter((status): status is NonNullable<typeof status> => Boolean(status))
|
||||
|
||||
if (overtimeStages.length === 0) {
|
||||
if (followups.length === 0) return null
|
||||
if (allFollowupDeadlineStatuses.length > 0) {
|
||||
return {
|
||||
label: `${allFollowupDeadlineStatuses.length} 项跟进到期`,
|
||||
tone: 'danger' as const,
|
||||
}
|
||||
}
|
||||
return { label: `${followups.length} 项历史跟进`, tone: 'default' as const }
|
||||
}
|
||||
|
||||
const overtimeStageKeys = new Set(overtimeStages.map((stage) => stage.stageKey))
|
||||
const records = followups.filter((followup) => overtimeStageKeys.has(followup.stageKey))
|
||||
const pending = records.filter((record) => record.status === 'PENDING').length
|
||||
@ -1590,7 +1570,6 @@ function TimeoutFollowupInspector({
|
||||
const overtimeStages = getFollowupDisplayStages(
|
||||
currentOvertimeStages,
|
||||
historicalOvertimeStages,
|
||||
followups,
|
||||
)
|
||||
const currentOvertimeStageKeys = new Set(currentOvertimeStages.map((stage) => stage.stageKey))
|
||||
const [selectedStageKey, setSelectedStageKey] = useState(overtimeStages[0]?.stageKey ?? '')
|
||||
@ -2206,7 +2185,6 @@ function DetailTableRow({
|
||||
const followupDisplayStages = getFollowupDisplayStages(
|
||||
currentOvertimeStages,
|
||||
historicalOvertimeStages,
|
||||
timeoutFollowups,
|
||||
)
|
||||
const isOvertime = item.overallTimeoutFlag
|
||||
const timeoutFollowupSummary = getTimeoutFollowupSummary(followupDisplayStages, timeoutFollowups)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import {
|
||||
getOaBoardFollowupDeadlineStatus,
|
||||
getOaBoardTimelineMetricOvertimeDays,
|
||||
isOaBoardTimelineMetricOverdue,
|
||||
listHistoricalOverdueStageMetrics,
|
||||
pickCurrentOverdueStageMetric,
|
||||
} from './oa-board-overtime'
|
||||
@ -42,6 +44,20 @@ describe('OA 看板时效归类', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('节点旧判定超时不能覆盖阶段时效详情', () => {
|
||||
const sampleMetric = {
|
||||
key: 'sample_prepare',
|
||||
label: '打样',
|
||||
targetDays: 3,
|
||||
actualDays: 1.14,
|
||||
}
|
||||
|
||||
expect(getOaBoardTimelineMetricOvertimeDays(sampleMetric)).toBeNull()
|
||||
expect(isOaBoardTimelineMetricOverdue(sampleMetric)).toBe(false)
|
||||
expect(pickCurrentOverdueStageMetric(['产品打样'], [sampleMetric])).toBeNull()
|
||||
expect(listHistoricalOverdueStageMetrics([], [sampleMetric])).toEqual([])
|
||||
})
|
||||
|
||||
it('仅标记计划日期已到且尚未完成的跟进', () => {
|
||||
const today = new Date(2026, 7, 4)
|
||||
|
||||
|
||||
@ -86,6 +86,33 @@ export const OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS: Record<string, string
|
||||
运营上架: 'listing',
|
||||
}
|
||||
|
||||
const OA_BOARD_TRACKED_TIMELINE_METRIC_KEYS = new Set(
|
||||
Object.values(OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS),
|
||||
)
|
||||
|
||||
export function getOaBoardTimelineMetricOvertimeDays(
|
||||
metric: OaBoardTimelineMetricLike | null | undefined,
|
||||
) {
|
||||
if (
|
||||
!metric ||
|
||||
metric.targetDays === null ||
|
||||
metric.actualDays === null ||
|
||||
!Number.isFinite(metric.targetDays) ||
|
||||
!Number.isFinite(metric.actualDays)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const overtimeDays = Number((metric.actualDays - metric.targetDays).toFixed(2))
|
||||
return overtimeDays > 0 ? overtimeDays : null
|
||||
}
|
||||
|
||||
export function isOaBoardTimelineMetricOverdue(
|
||||
metric: OaBoardTimelineMetricLike | null | undefined,
|
||||
) {
|
||||
return getOaBoardTimelineMetricOvertimeDays(metric) !== null
|
||||
}
|
||||
|
||||
export function listHistoricalOverdueStageMetrics(
|
||||
currentStageNames: readonly string[],
|
||||
timelineMetrics: readonly OaBoardTimelineMetricLike[],
|
||||
@ -95,11 +122,9 @@ export function listHistoricalOverdueStageMetrics(
|
||||
.map((stageName) => OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS[stageName])
|
||||
.filter((metricKey): metricKey is string => Boolean(metricKey)),
|
||||
)
|
||||
const trackedMetricKeys = new Set(Object.values(OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS))
|
||||
|
||||
return timelineMetrics.flatMap((metric) => {
|
||||
if (
|
||||
!trackedMetricKeys.has(metric.key) ||
|
||||
!OA_BOARD_TRACKED_TIMELINE_METRIC_KEYS.has(metric.key) ||
|
||||
currentMetricKeys.has(metric.key) ||
|
||||
metric.targetDays === null ||
|
||||
metric.actualDays === null
|
||||
@ -107,8 +132,8 @@ export function listHistoricalOverdueStageMetrics(
|
||||
return []
|
||||
}
|
||||
|
||||
const overtimeDays = Number((metric.actualDays - metric.targetDays).toFixed(2))
|
||||
if (overtimeDays <= 0) return []
|
||||
const overtimeDays = getOaBoardTimelineMetricOvertimeDays(metric)
|
||||
if (overtimeDays === null) return []
|
||||
|
||||
return [{
|
||||
stageName: metric.label,
|
||||
@ -131,8 +156,8 @@ export function pickCurrentOverdueStageMetric(
|
||||
const metric = metricKey ? metricsByKey.get(metricKey) : null
|
||||
if (!metric || metric.targetDays === null || metric.actualDays === null) return []
|
||||
|
||||
const overtimeDays = Number((metric.actualDays - metric.targetDays).toFixed(2))
|
||||
if (overtimeDays <= 0) return []
|
||||
const overtimeDays = getOaBoardTimelineMetricOvertimeDays(metric)
|
||||
if (overtimeDays === null) return []
|
||||
|
||||
return [{
|
||||
stageName,
|
||||
|
||||
@ -159,13 +159,6 @@ type OaBoardTimeoutFollowupAssessment = {
|
||||
ownerLabel: string | null
|
||||
}
|
||||
|
||||
type SavedTimeoutFollowupAssessment = RowDataPacket & {
|
||||
stageLabel: string
|
||||
targetDays: string | number
|
||||
actualDays: string | number
|
||||
overtimeDays: string | number
|
||||
}
|
||||
|
||||
function asNullableText(value: string | null | undefined) {
|
||||
const text = String(value ?? '').trim()
|
||||
return text || null
|
||||
@ -303,26 +296,6 @@ function readHistoricalOverdueAssessment(
|
||||
}
|
||||
}
|
||||
|
||||
function readSavedHistoricalAssessment(
|
||||
row: SavedTimeoutFollowupAssessment | undefined,
|
||||
): OaBoardTimeoutFollowupAssessment | null {
|
||||
if (!row) return null
|
||||
|
||||
const targetDays = toNumber(row.targetDays)
|
||||
const actualDays = toNumber(row.actualDays)
|
||||
const overtimeDays = toNumber(row.overtimeDays)
|
||||
if (!row.stageLabel.trim() || overtimeDays <= 0) return null
|
||||
|
||||
return {
|
||||
mode: 'HISTORICAL',
|
||||
stageLabel: row.stageLabel.trim(),
|
||||
targetDays,
|
||||
actualDays,
|
||||
overtimeDays,
|
||||
ownerLabel: null,
|
||||
}
|
||||
}
|
||||
|
||||
function isFullAccessEditor(user: AuthUser | undefined) {
|
||||
if (!user) return false
|
||||
if (user.role === 'SUPER_ADMIN' || user.role === 'ADMIN') return true
|
||||
@ -463,22 +436,7 @@ export class OaBoardTimeoutFollowupService {
|
||||
? null
|
||||
: readHistoricalOverdueAssessment(snapshot, timelineMetrics, input.stageKey)
|
||||
|
||||
await ensureTable(connection)
|
||||
const [savedRows] = await connection.query<SavedTimeoutFollowupAssessment[]>(
|
||||
`
|
||||
SELECT stage_label AS stageLabel, target_days AS targetDays, actual_days AS actualDays,
|
||||
overtime_days AS overtimeDays
|
||||
FROM ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE}
|
||||
WHERE board_key = ? AND stage_key = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[input.boardKey.trim(), input.stageKey.trim()],
|
||||
)
|
||||
const savedHistoricalStage =
|
||||
currentOverdueStage || historicalOverdueStage
|
||||
? null
|
||||
: readSavedHistoricalAssessment(savedRows[0])
|
||||
const overdueStage = currentOverdueStage ?? historicalOverdueStage ?? savedHistoricalStage
|
||||
const overdueStage = currentOverdueStage ?? historicalOverdueStage
|
||||
if (!overdueStage) {
|
||||
return { allowed: false, message: '该节点未超时,或流程数据已变化,无法保存超时跟进' }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user