301 lines
16 KiB
TypeScript
301 lines
16 KiB
TypeScript
'use client'
|
||
|
||
import Link from 'next/link'
|
||
import { useEffect, useState } from 'react'
|
||
import {
|
||
ArrowRight,
|
||
CalendarClock,
|
||
} from 'lucide-react'
|
||
import type { FirstPassReviewSummary } from '@/lib/work-review-metrics'
|
||
import type {
|
||
MonthlyLaunchSummary,
|
||
OnTimeDeliverySummary,
|
||
OngoingDingWorkflowSummary,
|
||
} from '@/lib/ding-workflow-sync'
|
||
import DepartmentShell from './DepartmentShell'
|
||
import { CORE_METRICS, getCoreMetricHref } from './core-metrics'
|
||
import OnTimeDeliveryRate from './OnTimeDeliveryRate'
|
||
|
||
type FirstPassState =
|
||
| { status: 'loading'; data: null }
|
||
| { status: 'success'; data: FirstPassReviewSummary }
|
||
| { status: 'error'; data: null }
|
||
|
||
type OngoingWorkflowState =
|
||
| { status: 'loading'; data: null }
|
||
| { status: 'success'; data: OngoingDingWorkflowSummary }
|
||
| { status: 'error'; data: null }
|
||
|
||
type MonthlyLaunchState =
|
||
| { status: 'loading'; data: null }
|
||
| { status: 'success'; data: MonthlyLaunchSummary }
|
||
| { status: 'error'; data: null }
|
||
|
||
type OnTimeDeliveryState =
|
||
| { status: 'loading'; data: null }
|
||
| { status: 'success'; data: OnTimeDeliverySummary }
|
||
| { status: 'error'; data: null }
|
||
|
||
function formatRate(rate: number | null) {
|
||
if (rate === null) return '—'
|
||
return `${Number.isInteger(rate) ? rate.toFixed(0) : rate.toFixed(1)}%`
|
||
}
|
||
|
||
function formatSyncTime(value: string | null) {
|
||
if (!value) return '等待首轮同步'
|
||
return new Intl.DateTimeFormat('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false,
|
||
timeZone: 'Asia/Shanghai',
|
||
}).format(new Date(value))
|
||
}
|
||
|
||
export default function CoreMetricsDashboard() {
|
||
const [firstPass, setFirstPass] = useState<FirstPassState>({ status: 'loading', data: null })
|
||
const [ongoingWorkflows, setOngoingWorkflows] = useState<OngoingWorkflowState>({ status: 'loading', data: null })
|
||
const [monthlyLaunches, setMonthlyLaunches] = useState<MonthlyLaunchState>({ status: 'loading', data: null })
|
||
const [onTimeDelivery, setOnTimeDelivery] = useState<OnTimeDeliveryState>({ status: 'loading', data: null })
|
||
|
||
useEffect(() => {
|
||
const controller = new AbortController()
|
||
|
||
async function loadFirstPassSummary() {
|
||
try {
|
||
const response = await fetch('/api/department-dashboard/core-metrics/first-pass', {
|
||
cache: 'no-store',
|
||
signal: controller.signal,
|
||
})
|
||
const payload = await response.json().catch(() => null)
|
||
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
|
||
setFirstPass({ status: 'success', data: payload.data })
|
||
} catch (error) {
|
||
if ((error as Error).name !== 'AbortError') setFirstPass({ status: 'error', data: null })
|
||
}
|
||
}
|
||
|
||
async function loadOngoingWorkflowSummary() {
|
||
try {
|
||
const response = await fetch('/api/department-dashboard/core-metrics/ongoing-projects', {
|
||
cache: 'no-store',
|
||
signal: controller.signal,
|
||
})
|
||
const payload = await response.json().catch(() => null)
|
||
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
|
||
setOngoingWorkflows({ status: 'success', data: payload.data })
|
||
} catch (error) {
|
||
if ((error as Error).name !== 'AbortError') setOngoingWorkflows({ status: 'error', data: null })
|
||
}
|
||
}
|
||
|
||
async function loadMonthlyLaunchSummary() {
|
||
try {
|
||
const response = await fetch('/api/department-dashboard/core-metrics/monthly-launches', {
|
||
cache: 'no-store',
|
||
signal: controller.signal,
|
||
})
|
||
const payload = await response.json().catch(() => null)
|
||
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
|
||
setMonthlyLaunches({ status: 'success', data: payload.data })
|
||
} catch (error) {
|
||
if ((error as Error).name !== 'AbortError') setMonthlyLaunches({ status: 'error', data: null })
|
||
}
|
||
}
|
||
|
||
async function loadOnTimeDeliverySummary() {
|
||
try {
|
||
const response = await fetch('/api/department-dashboard/core-metrics/on-time-delivery', {
|
||
cache: 'no-store',
|
||
signal: controller.signal,
|
||
})
|
||
const payload = await response.json().catch(() => null)
|
||
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
|
||
setOnTimeDelivery({ status: 'success', data: payload.data })
|
||
} catch (error) {
|
||
if ((error as Error).name !== 'AbortError') setOnTimeDelivery({ status: 'error', data: null })
|
||
}
|
||
}
|
||
|
||
void loadFirstPassSummary()
|
||
void loadOngoingWorkflowSummary()
|
||
void loadMonthlyLaunchSummary()
|
||
void loadOnTimeDeliverySummary()
|
||
return () => controller.abort()
|
||
}, [])
|
||
|
||
return (
|
||
<DepartmentShell>
|
||
<main className="mx-auto max-w-[1600px] px-4 pb-16 pt-7 sm:px-6 lg:px-8">
|
||
<header className="border-b border-[#dfe4ea] pb-6">
|
||
<h1 id="core-metrics-title" className="max-w-4xl text-balance text-3xl font-bold tracking-[-0.03em] sm:text-4xl">
|
||
核心指标
|
||
</h1>
|
||
<p className="mt-3 max-w-3xl text-base leading-7 text-[#66758b] sm:text-lg">
|
||
集中查看项目进度、交付质量、团队负荷与风险状态,为部门日常经营提供统一入口。
|
||
</p>
|
||
</header>
|
||
|
||
<section aria-labelledby="metrics-heading" className="mt-6">
|
||
<div className="mb-0 flex flex-wrap items-end justify-between gap-3 rounded-t-xl border border-b-0 border-[#e2e8f0] bg-white px-6 py-5">
|
||
<div>
|
||
<h2 id="metrics-heading" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">六项核心经营指标</h2>
|
||
<p className="mt-1 text-sm text-[#66758b]">每个分区已预留独立公式与同步状态。</p>
|
||
</div>
|
||
<p className="rounded-full bg-[#f1f6fc] px-4 py-2 text-sm font-semibold text-[#5f7189]">6 个指标分区</p>
|
||
</div>
|
||
|
||
<div
|
||
aria-describedby="metrics-disclaimer"
|
||
className="relative grid gap-px overflow-hidden rounded-b-xl border border-[#e2e8f0] bg-[#e2e9f2] pt-1 shadow-[0_6px_18px_rgba(15,23,42,0.05)] sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6"
|
||
>
|
||
<span className="absolute inset-x-0 top-0 h-1 bg-[#5a9ce6]" aria-hidden="true" />
|
||
{CORE_METRICS.map((metric) => {
|
||
const Icon = metric.icon
|
||
const isOngoingWorkflow = metric.key === 'ongoing-projects'
|
||
const isMonthlyLaunch = metric.key === 'monthly-launches'
|
||
const isFirstPass = metric.key === 'first-pass-rate'
|
||
const isOnTimeDelivery = metric.key === 'on-time-delivery'
|
||
const badge = isOngoingWorkflow
|
||
? ongoingWorkflows.status === 'success'
|
||
? ongoingWorkflows.data.syncStatus === 'ready'
|
||
? '每小时同步'
|
||
: ongoingWorkflows.data.syncStatus === 'syncing'
|
||
? '正在同步'
|
||
: ongoingWorkflows.data.syncStatus === 'error'
|
||
? '同步异常'
|
||
: '等待首轮同步'
|
||
: ongoingWorkflows.status === 'error' ? '读取失败' : '正在读取'
|
||
: isMonthlyLaunch
|
||
? monthlyLaunches.status === 'success'
|
||
? monthlyLaunches.data.syncStatus === 'ready'
|
||
? '每小时同步'
|
||
: monthlyLaunches.data.syncStatus === 'syncing'
|
||
? '正在同步'
|
||
: monthlyLaunches.data.syncStatus === 'error'
|
||
? '同步异常'
|
||
: '等待首轮同步'
|
||
: monthlyLaunches.status === 'error' ? '读取失败' : '正在读取'
|
||
: isOnTimeDelivery
|
||
? onTimeDelivery.status === 'success'
|
||
? onTimeDelivery.data.syncStatus === 'ready'
|
||
? '每小时同步'
|
||
: onTimeDelivery.data.syncStatus === 'syncing'
|
||
? '正在同步'
|
||
: onTimeDelivery.data.syncStatus === 'error'
|
||
? '同步异常'
|
||
: '等待首轮同步'
|
||
: onTimeDelivery.status === 'error' ? '读取失败' : '正在读取'
|
||
: isFirstPass
|
||
? firstPass.status === 'success'
|
||
? firstPass.data.syncStatus === 'ready' ? '每小时同步' : firstPass.data.syncStatus === 'error' ? '同步异常' : '正在同步'
|
||
: firstPass.status === 'error' ? '读取失败' : '正在读取'
|
||
: '示例值'
|
||
const value = isOngoingWorkflow
|
||
? ongoingWorkflows.status === 'success' ? String(ongoingWorkflows.data.total) : '—'
|
||
: isMonthlyLaunch
|
||
? monthlyLaunches.status === 'success' ? `${monthlyLaunches.data.completed}/${monthlyLaunches.data.total}` : '—'
|
||
: isOnTimeDelivery
|
||
? onTimeDelivery.status === 'success' ? formatRate(onTimeDelivery.data.rate) : '—'
|
||
: isFirstPass
|
||
? firstPass.status === 'success' ? formatRate(firstPass.data.rate) : '—'
|
||
: metric.exampleValue
|
||
|
||
return (
|
||
<Link
|
||
key={metric.key}
|
||
href={getCoreMetricHref(metric.key)}
|
||
aria-label={isFirstPass ? '查看所有作品明细' : `查看${metric.label}明细`}
|
||
className="group flex min-h-48 flex-col bg-[#f8fafd] p-5 transition-colors hover:bg-white focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#5a9ce6] sm:min-h-56 sm:p-6"
|
||
>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#edf5ff] text-[#176fc1]">
|
||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||
</span>
|
||
<span className="rounded-lg bg-white px-2.5 py-1 text-xs font-bold text-[#5f7189] shadow-[0_4px_12px_rgba(67,93,126,0.06)]">
|
||
{badge}
|
||
</span>
|
||
</div>
|
||
|
||
<h3 className="mt-6 text-base font-bold text-[#4f6077]">{metric.label}</h3>
|
||
<div aria-live={isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? 'polite' : undefined} aria-atomic={isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? 'true' : undefined}>
|
||
{isOnTimeDelivery && onTimeDelivery.status === 'success' && onTimeDelivery.data.rate !== null
|
||
? <OnTimeDeliveryRate rate={onTimeDelivery.data.rate} contextLabel={`${metric.label}当前值`} />
|
||
: (
|
||
<p className="mt-2 text-3xl font-bold tracking-[-0.03em] text-[#101828] sm:text-4xl" aria-label={`${metric.label}${isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? '当前值' : '示例值'}${value}`}>
|
||
{value}
|
||
</p>
|
||
)}
|
||
{isFirstPass && firstPass.status === 'success' && (
|
||
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
|
||
V1通过 {firstPass.data.firstPassApproved} / 已通过 {firstPass.data.totalApproved}
|
||
</p>
|
||
)}
|
||
{isFirstPass && firstPass.status === 'success' && firstPass.data.lastSyncedAt && (
|
||
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
|
||
最近同步 {formatSyncTime(firstPass.data.lastSyncedAt)}
|
||
</p>
|
||
)}
|
||
{isFirstPass && firstPass.status === 'loading' && (
|
||
<p className="mt-2 text-xs font-semibold text-slate-500">正在连接作品过稿</p>
|
||
)}
|
||
{isOngoingWorkflow && ongoingWorkflows.status === 'success' && (
|
||
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
|
||
最近同步 {formatSyncTime(ongoingWorkflows.data.lastSyncedAt)}
|
||
</p>
|
||
)}
|
||
{isOngoingWorkflow && ongoingWorkflows.status === 'loading' && (
|
||
<p className="mt-2 text-xs font-semibold text-slate-500">正在读取本地流程快照</p>
|
||
)}
|
||
{isMonthlyLaunch && monthlyLaunches.status === 'success' && (
|
||
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
|
||
本月已完结 {monthlyLaunches.data.completed} · 当前进行中 {monthlyLaunches.data.ongoing}
|
||
</p>
|
||
)}
|
||
{isMonthlyLaunch && monthlyLaunches.status === 'loading' && (
|
||
<p className="mt-2 text-xs font-semibold text-slate-500">正在读取本地流程快照</p>
|
||
)}
|
||
{isOnTimeDelivery && onTimeDelivery.status === 'success' && (
|
||
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
|
||
未超时 {onTimeDelivery.data.onTime} / 本月完结 {onTimeDelivery.data.total}
|
||
</p>
|
||
)}
|
||
{isOnTimeDelivery && onTimeDelivery.status === 'loading' && (
|
||
<p className="mt-2 text-xs font-semibold text-slate-500">正在计算本月交付时效</p>
|
||
)}
|
||
</div>
|
||
<p className="mt-3 text-sm leading-6 text-[#66758b]">{metric.description}</p>
|
||
|
||
<div className="mt-auto flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pt-6 text-xs font-semibold">
|
||
<span className="flex flex-wrap items-center gap-x-3 gap-y-1 text-slate-500">
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<CalendarClock className="h-3.5 w-3.5" aria-hidden="true" />
|
||
{isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery
|
||
? (isOngoingWorkflow ? ongoingWorkflows.status : isMonthlyLaunch ? monthlyLaunches.status : onTimeDelivery.status) === 'error' ? '刷新页面重试' : '每小时增量同步'
|
||
: isFirstPass ? (firstPass.status === 'error' ? '刷新页面重试' : '每小时增量同步') : '每日同步'}
|
||
</span>
|
||
<span aria-hidden="true">·</span>
|
||
<span>{isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery
|
||
? (isOngoingWorkflow ? ongoingWorkflows.status : isMonthlyLaunch ? monthlyLaunches.status : onTimeDelivery.status) === 'error' ? '数据未加载' : '钉钉流程快照'
|
||
: isFirstPass ? (firstPass.status === 'error' ? '数据未加载' : '作品过稿后台') : '公式待接入'}</span>
|
||
</span>
|
||
<span className="inline-flex items-center gap-1 text-[#176fc1]">
|
||
{isFirstPass ? '查看所有作品明细' : '查看明细'}
|
||
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
|
||
</span>
|
||
</div>
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
<p id="metrics-disclaimer" className="mt-3 text-xs leading-5 text-[#66758b]">
|
||
“进行中项目”和“本月上新”每小时读取钉钉审批流程三表快照,“首轮通过”读取作品过稿后台 V1 审核结果并每小时增量同步;其余示例值将在对应数据源与公式确认后替换。
|
||
</p>
|
||
</section>
|
||
|
||
</main>
|
||
</DepartmentShell>
|
||
)
|
||
}
|