diff --git a/furniture-3d-system/src/app/api/intake/oa-board/export/route.ts b/furniture-3d-system/src/app/api/intake/oa-board/export/route.ts index 88991db1..c94d2547 100644 --- a/furniture-3d-system/src/app/api/intake/oa-board/export/route.ts +++ b/furniture-3d-system/src/app/api/intake/oa-board/export/route.ts @@ -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) diff --git a/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx b/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx index 1d6b85ce..12169cbb 100644 --- a/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx +++ b/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx @@ -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) => - overtimeStages.some((stage) => stage.stageKey === followup.stageKey), - ) + 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 => 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) diff --git a/furniture-3d-system/src/lib/oa-board-overtime.test.ts b/furniture-3d-system/src/lib/oa-board-overtime.test.ts index 822df0ff..1fe14154 100644 --- a/furniture-3d-system/src/lib/oa-board-overtime.test.ts +++ b/furniture-3d-system/src/lib/oa-board-overtime.test.ts @@ -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) diff --git a/furniture-3d-system/src/lib/oa-board-overtime.ts b/furniture-3d-system/src/lib/oa-board-overtime.ts index f9b1b5b5..012d69eb 100644 --- a/furniture-3d-system/src/lib/oa-board-overtime.ts +++ b/furniture-3d-system/src/lib/oa-board-overtime.ts @@ -86,6 +86,33 @@ export const OA_BOARD_APPROVAL_STAGE_TIMELINE_METRIC_KEYS: Record 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, diff --git a/furniture-3d-system/src/server/services/oa-board-timeout-followup.service.ts b/furniture-3d-system/src/server/services/oa-board-timeout-followup.service.ts index 20586836..4277cf53 100644 --- a/furniture-3d-system/src/server/services/oa-board-timeout-followup.service.ts +++ b/furniture-3d-system/src/server/services/oa-board-timeout-followup.service.ts @@ -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( - ` - 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: '该节点未超时,或流程数据已变化,无法保存超时跟进' } }