'use client'
import Link from 'next/link'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
ArrowLeft,
CalendarRange,
CheckCircle2,
CircleDot,
Database,
RefreshCw,
UsersRound,
} from 'lucide-react'
import DepartmentShell from './DepartmentShell'
import { TEAM_CAPACITY_DEPARTMENT_ORDER } from './team-capacity'
import type {
TeamCapacityDepartment,
TeamCapacityInsights,
TeamCapacityWeek,
TeamCapacityWorkflowItem,
} from './team-capacity'
type ViewState =
| { status: 'loading'; data: null; error: null }
| { status: 'success'; data: TeamCapacityInsights; error: null }
| { status: 'error'; data: null; error: string }
function formatDate(value: string | null, includeTime = false) {
if (!value) return '—'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '—'
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit', day: '2-digit',
...(includeTime ? { hour: '2-digit', minute: '2-digit', hour12: false } : {}),
timeZone: 'Asia/Shanghai',
}).format(date)
}
function personnelCountLabel(value: number | null) {
return value === null ? '暂无历史快照' : `${value}人`
}
function rosterHistoryLabel(insights: TeamCapacityInsights) {
const history = insights.summary.rosterHistory
if (history.source !== 'historical_snapshot') {
return '尚未采集到员工组织历史快照;历史周人数暂不展示,避免用当前花名册冒充过去。'
}
return `已覆盖 ${history.coveredWeeks}/${history.totalWeeks} 周;每周人数取该周结束前最近一次完整快照。`
}
function statusLabel(value: string) {
const status = value.trim().toUpperCase()
if (status === 'COMPLETED') return '已完结'
if (status === 'TERMINATED' || status === 'CANCELED') return '已终止'
return '进行中'
}
function syncLabel(insights: TeamCapacityInsights) {
if (insights.summary.lastSyncedAt) return `每小时增量同步 · ${formatDate(insights.summary.lastSyncedAt, true)}`
if (insights.summary.syncStatus === 'error') return '同步异常,当前展示本地已有快照'
if (insights.summary.syncStatus === 'syncing') return '正在同步'
return '等待首轮同步'
}
function WeekDetail({ week, department }: { week: TeamCapacityWeek; department: TeamCapacityDepartment }) {
const items = week.items
return (
{week.label} · {department.name}明细
本周收到和完结的流程,同一流程只列一次。
共 {items.length} 条
{items.length === 0 ? (
) : (
<>
{department.name}{week.label}团队产能流程明细
| 流程 |
本周动作 |
负责人 |
状态 |
发起时间 |
完结时间 |
{items.map((item) => )}
>
)}
)
}
function WorkflowRow({ item }: { item: TeamCapacityWorkflowItem }) {
return (
{item.title} 审批编号 {item.businessId || item.id} |
{item.receivedInWeek ? '收到' : ''}{item.receivedInWeek && item.completedInWeek ? ' · ' : ''}{item.completedInWeek ? '完结' : ''} |
{item.assigneeNames.join('、') || '—'} |
{statusLabel(item.status)} |
{formatDate(item.sourceCreatedAt, true)} |
{formatDate(item.sourceFinishedAt, true)} |
)
}
function WorkflowListItem({ item }: { item: TeamCapacityWorkflowItem }) {
return (
{item.title}
{statusLabel(item.status)}
审批编号 {item.businessId || item.id}
{item.receivedInWeek ? '本周收到' : ''}{item.receivedInWeek && item.completedInWeek ? ' · ' : ''}{item.completedInWeek ? '本周完结' : ''}
- 负责人
- {item.assigneeNames.join('、') || '—'}
- 发起时间
- {formatDate(item.sourceCreatedAt, true)}
- 完结时间
- {formatDate(item.sourceFinishedAt, true)}
)
}
function LoadingView() {
return
}
export default function TeamCapacityDashboard() {
const [selectedDepartmentKey, setSelectedDepartmentKey] = useState('')
const [selectedWeekKey, setSelectedWeekKey] = useState('')
const [state, setState] = useState({ status: 'loading', data: null, error: null })
const load = useCallback(async () => {
setState({ status: 'loading', data: null, error: null })
try {
const response = await fetch('/api/department-dashboard/team-capacity', { cache: 'no-store' })
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success || !payload.data) throw new Error(payload?.error || '团队产能读取失败')
setState({ status: 'success', data: payload.data, error: null })
} catch (error) {
setState({ status: 'error', data: null, error: (error as Error).message || '团队产能读取失败' })
}
}, [])
useEffect(() => { void load() }, [load])
const capacityData = state.status === 'success' ? state.data : null
const departments = useMemo(() => {
if (!capacityData) return []
return TEAM_CAPACITY_DEPARTMENT_ORDER.flatMap((name) => {
const department = capacityData.departments.find((candidate) => candidate.name === name)
return department ? [department] : []
})
}, [capacityData])
const selectedDepartment = useMemo(() => {
return departments.find((department) => department.name === selectedDepartmentKey) || departments[0]
}, [departments, selectedDepartmentKey])
const weeks = selectedDepartment?.weeks || []
const selectedWeek = weeks.find((week) => week.key === selectedWeekKey) || weeks.at(-1)
const weekMaxValue = Math.max(1, ...weeks.flatMap((week) => [week.receivedCount, week.completedCount]))
useEffect(() => {
if (state.status === 'success') {
if (!selectedDepartmentKey && departments[0]) setSelectedDepartmentKey(departments[0].name)
if (!selectedWeekKey && departments[0]?.weeks.at(-1)) setSelectedWeekKey(departments[0].weeks.at(-1)!.key)
}
}, [departments, selectedDepartmentKey, selectedWeekKey, state.status])
return (
{state.status === 'loading' &&
}
{state.status === 'error' && 团队产能暂时无法读取
{state.error}
}
{state.status === 'success' && departments.length === 0 && }
{state.status === 'success' && departments.length > 0 && selectedDepartment && (
当前部门
{selectedDepartment.name}
按部门架构归属统计 · 当前 {selectedDepartment.people.length} 名在职人员
- 当前在职人数
- {selectedDepartment.personnelCount}
- 近8周收到
- {weeks.reduce((sum, week) => sum + week.receivedCount, 0)}
- 近8周完结
- {weeks.reduce((sum, week) => sum + week.completedCount, 0)}
历史人员数:{rosterHistoryLabel(state.data)} 历史快照从首次成功采集后开始累计。
{weeks.map((week) => setSelectedWeekKey(week.key)} />)}
{selectedWeek && }
)}
)
}
function WeekButton({ week, maxValue, selected, onSelect }: { week: TeamCapacityWeek; maxValue: number; selected: boolean; onSelect: () => void }) {
const receivedHeight = week.receivedCount ? Math.max(10, Math.round((week.receivedCount / maxValue) * 88)) : 0
const completedHeight = week.completedCount ? Math.max(10, Math.round((week.completedCount / maxValue) * 88)) : 0
return
}