'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 ? (
) : ( <>
{items.map((item) => )}
{department.name}{week.label}团队产能流程明细
流程 本周动作 负责人 状态 发起时间 完结时间
)}
) } 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 (

    团队产能

    按二级部门查看团队近 8 周收到与完结的流程,点击周次展开任务明细。

    {state.status === 'success' &&

    }
    {state.status === 'loading' &&
    } {state.status === '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)}
    {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 }