From 3289809a9546c89c35ad33bb2fe1f1fb6eca33bf Mon Sep 17 00:00:00 2001 From: jingchuan Date: Sat, 1 Aug 2026 09:22:22 +0800 Subject: [PATCH] feat(oa-board): split current and historical timeout analysis --- .../src/app/intake/oa-board/OaBoardClient.tsx | 214 +++++++++++++++++- 1 file changed, 205 insertions(+), 9 deletions(-) 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 e8758066..ad7e25f2 100644 --- a/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx +++ b/furniture-3d-system/src/app/intake/oa-board/OaBoardClient.tsx @@ -98,6 +98,8 @@ type CurrentOwnerInsight = { type TimeoutFollowupStatus = 'PENDING' | 'IN_PROGRESS' | 'RESOLVED' | 'NOT_REQUIRED' +type TimeoutAnalysisView = 'current' | 'history' + type OaBoardTimeoutFollowup = { id: number boardKey: string @@ -849,7 +851,7 @@ function buildTimeoutAnalysis(items: OaBoardProjectListItemDTO[]) { let totalOvertimeNodes = 0 let maxOvertimeDays = 0 - items.forEach((item) => { + items.filter((item) => item.overallStatus === 'RUNNING').forEach((item) => { getOvertimeStages(item).forEach((stage) => { overdueProcessKeys.add(item.boardKey) totalOvertimeNodes += 1 @@ -1118,7 +1120,7 @@ function StatusBadge({ ) } -function TimeoutAnalysis({ items }: { items: OaBoardProjectListItemDTO[] }) { +function CurrentTimeoutAnalysis({ items }: { items: OaBoardProjectListItemDTO[] }) { const analysis = buildTimeoutAnalysis(items) const maxStageCount = analysis.stages[0]?.count ?? 0 @@ -1127,8 +1129,8 @@ function TimeoutAnalysis({ items }: { items: OaBoardProjectListItemDTO[] }) {
-

超时概览

-

基于当前流程快照与已应用筛选条件

+

当前超时概览

+

仅统计进行中流程的当前节点或当前审批阶段

@@ -1247,6 +1249,196 @@ function TimeoutAnalysis({ items }: { items: OaBoardProjectListItemDTO[] }) { ) } +function HistoricalTimeoutAnalysis({ + items, + followups, + onOpenFollowup, +}: { + items: OaBoardProjectListItemDTO[] + followups: OaBoardTimeoutFollowup[] + onOpenFollowup: (item: OaBoardProjectListItemDTO) => void +}) { + const [statusFilter, setStatusFilter] = useState('') + const [stageFilter, setStageFilter] = useState('') + const [ownerFilter, setOwnerFilter] = useState('') + const [updatedDateStart, setUpdatedDateStart] = useState('') + const [updatedDateEnd, setUpdatedDateEnd] = useState('') + const itemByBoardKey = useMemo( + () => new Map(items.map((item) => [item.boardKey, item])), + [items], + ) + const stageOptions = useMemo( + () => buildOptionList(followups.map((followup) => followup.stageLabel), '全部历史阶段'), + [followups], + ) + const ownerOptions = useMemo( + () => buildOptionList(followups.map((followup) => followup.followUpOwner), '全部跟进人'), + [followups], + ) + const statusOptions = useMemo( + () => [{ value: '', label: '全部跟进状态' }, ...TIMEOUT_FOLLOWUP_STATUS_OPTIONS], + [], + ) + const filteredFollowups = useMemo( + () => + followups.filter((followup) => { + const updatedDate = String(followup.updatedAt ?? '').slice(0, 10) + if (statusFilter && followup.status !== statusFilter) return false + if (stageFilter && followup.stageLabel !== stageFilter) return false + if (ownerFilter && followup.followUpOwner !== ownerFilter) return false + if (updatedDateStart && updatedDate < updatedDateStart) return false + if (updatedDateEnd && updatedDate > updatedDateEnd) return false + return true + }), + [followups, ownerFilter, stageFilter, statusFilter, updatedDateEnd, updatedDateStart], + ) + const historySummary = useMemo(() => { + const totalOvertimeDays = filteredFollowups.reduce( + (total, followup) => total + followup.overtimeDays, + 0, + ) + const pending = filteredFollowups.filter( + (followup) => followup.status === 'PENDING' || followup.status === 'IN_PROGRESS', + ).length + const resolved = filteredFollowups.filter( + (followup) => followup.status === 'RESOLVED' || followup.status === 'NOT_REQUIRED', + ).length + + return { + total: filteredFollowups.length, + pending, + resolved, + averageOvertimeDays: + filteredFollowups.length > 0 + ? Number((totalOvertimeDays / filteredFollowups.length).toFixed(1)) + : 0, + } + }, [filteredFollowups]) + + return ( +
+
+
+
+ + + + + +
+
+
+
+

历史跟进项

+

{historySummary.total}

+
+
+

待处理

+

{historySummary.pending}

+
+
+

已完成

+

{historySummary.resolved}

+
+
+

平均超时

+

+ {historySummary.averageOvertimeDays} 天 +

+
+
+
+ +
+
+
+

历史超时跟进

+

记录来源于已保存的超时跟进,不参与当前超时统计和钉钉待办发送。

+
+ {filteredFollowups.length} 条记录 +
+ {filteredFollowups.length === 0 ? ( +
当前筛选范围内没有历史跟进记录
+ ) : ( +
+ + + + + + + + + + + + + + + + + {filteredFollowups.map((followup) => { + const item = itemByBoardKey.get(followup.boardKey) + return ( + + + + + + + + + + + + + ) + })} + +
开款编号历史阶段跟进状态跟进人标准时效实际用时超时用时预计完成最近更新查看
{formatCellText(followup.openingCode || item?.formSummary.openingCode || item?.openingCode)}{followup.stageLabel}{formatCellText(followup.followUpOwner)}{followup.targetDays} 天{followup.actualDays} 天{followup.overtimeDays} 天{formatCellText(followup.expectedResolveDate)}{formatCellText(followup.updatedAt?.replace('T', ' ').slice(0, 16))} + {item ? ( + + ) : '-'} +
+
+ )} +
+
+ ) +} + +function TimeoutAnalysis({ + items, + followups, + onOpenFollowup, +}: { + items: OaBoardProjectListItemDTO[] + followups: OaBoardTimeoutFollowup[] + onOpenFollowup: (item: OaBoardProjectListItemDTO) => void +}) { + const [view, setView] = useState('current') + + return ( +
+
+ + +
+ {view === 'current' ? ( + + ) : ( + + )} +
+ ) +} + function TimeoutFollowupInspector({ item, followups, @@ -2486,7 +2678,7 @@ export default function OaBoardClient({ : activeTab === 'brand-planning' ? '新品品牌策划看板' : activeTab === 'timeout-analysis' - ? '超时分析' + ? '当前超时分析' : activeTab === 'shipment' ? '新品出货进度表' : '新品上架数据' @@ -2531,7 +2723,7 @@ export default function OaBoardClient({ }, { key: 'timeout-analysis', - label: '超时分析', + label: '当前超时分析', active: activeTab === 'timeout-analysis', disabled: false, onClick: () => switchTab('timeout-analysis'), @@ -2935,7 +3127,7 @@ export default function OaBoardClient({ ) useEffect(() => { - const boardKeys = Array.from(new Set(pagedItems.map((item) => item.boardKey).filter(Boolean))) + const boardKeys = Array.from(new Set(items.map((item) => item.boardKey).filter(Boolean))).slice(0, 500) if (boardKeys.length === 0) { setTimeoutFollowups([]) return @@ -2974,7 +3166,7 @@ export default function OaBoardClient({ return () => { cancelled = true } - }, [pagedItems]) + }, [items]) const metricCardClass = (isActive: boolean) => [ 'group flex h-[72px] items-center justify-between rounded-md border px-3.5 text-left transition', @@ -3488,7 +3680,11 @@ export default function OaBoardClient({ ) : activeTab === 'brand-planning' ? ( ) : activeTab === 'timeout-analysis' ? ( - + ) : (