"use client" import Link from "next/link" import { AlertTriangleIcon, CircleDashedIcon, RefreshCcwIcon, TimerResetIcon, WrenchIcon } from "lucide-react" import { useCallback, useEffect, useMemo, useState } from "react" import { toast } from "sonner" import { OptionCombobox } from "@/components/option-combobox" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { fetchAgentTeamsAll, type AdminAgentTeam, } from "@/lib/api/admin" import { fetchTicketRiskOverview, fetchTicketRiskList, type TicketItem, type TicketRiskOverview, } from "@/lib/api/ticket" import { formatDateTime } from "@/lib/utils" import { TicketPriorityBadge } from "../tickets/_components/ticket-priority-badge" import { TicketSLABadge } from "../tickets/_components/ticket-sla-badge" import { TicketStatusBadge } from "../tickets/_components/ticket-status-badge" type RiskTableProps = { title: string description: string items: TicketItem[] emptyText: string } function RiskTable({ title, description, items, emptyText }: RiskTableProps) { return ( {title} {description} 工单 分类 优先级 状态 SLA 处理人 更新时间 操作 {items.length > 0 ? ( items.map((item) => (
{item.title}
{item.ticketNo}
{item.tags && item.tags.length > 0 ? item.tags.map((tag) => tag.name).join(" / ") : "未打标签"} {item.currentAssigneeName || "未指派"} {item.updatedAt ? formatDateTime(item.updatedAt) : "—"}
)) ) : ( {emptyText} )}
) } export default function TicketRiskPage() { const [loading, setLoading] = useState(true) const [overview, setOverview] = useState(null) const [overdueTickets, setOverdueTickets] = useState([]) const [highRiskTickets, setHighRiskTickets] = useState([]) const [unassignedTickets, setUnassignedTickets] = useState([]) const [pendingInternalTickets, setPendingInternalTickets] = useState([]) const [teams, setTeams] = useState([]) const [teamFilter, setTeamFilter] = useState("all") const [riskWindow, setRiskWindow] = useState("240") useEffect(() => { void (async () => { try { const data = await fetchAgentTeamsAll() setTeams(Array.isArray(data) ? data : []) } catch { setTeams([]) } })() }, []) const loadData = useCallback(async () => { setLoading(true) try { const currentTeamId = teamFilter === "all" ? undefined : Number(teamFilter) const riskMinutes = Number(riskWindow) const [overviewData, overdueData, highRiskData, unassignedData, pendingInternalData] = await Promise.all([ fetchTicketRiskOverview({ currentTeamId, riskWindowMins: riskMinutes }), fetchTicketRiskList({ riskType: "overdue", currentTeamId, riskWindowMins: riskMinutes, page: 1, limit: 10 }), fetchTicketRiskList({ riskType: "high_risk", currentTeamId, riskWindowMins: riskMinutes, page: 1, limit: 10 }), fetchTicketRiskList({ riskType: "unassigned", currentTeamId, riskWindowMins: riskMinutes, page: 1, limit: 10 }), fetchTicketRiskList({ riskType: "pending_internal", currentTeamId, riskWindowMins: riskMinutes, page: 1, limit: 10 }), ]) setOverview(overviewData) setOverdueTickets(Array.isArray(overdueData.results) ? overdueData.results : []) setHighRiskTickets(Array.isArray(highRiskData.results) ? highRiskData.results : []) setUnassignedTickets(Array.isArray(unassignedData.results) ? unassignedData.results : []) setPendingInternalTickets( Array.isArray(pendingInternalData.results) ? pendingInternalData.results : [], ) } catch (error) { toast.error(error instanceof Error ? error.message : "加载 SLA 风险页失败") } finally { setLoading(false) } }, [riskWindow, teamFilter]) useEffect(() => { void loadData() }, [loadData]) const cards = useMemo( () => [ { title: "已超时", description: "解决 SLA 已经 breach 的工单", value: overview?.overdue ?? 0, icon: AlertTriangleIcon, tone: "text-red-700 bg-red-500/10", }, { title: `${Number(riskWindow) / 60} 小时内到期`, description: "建议组长优先盯防的风险队列", value: overview?.highRisk ?? 0, icon: TimerResetIcon, tone: "text-orange-700 bg-orange-500/10", }, { title: "待分配", description: "目前还没有明确负责人的工单", value: overview?.unassigned ?? 0, icon: CircleDashedIcon, tone: "text-amber-700 bg-amber-500/10", }, { title: "待内部处理", description: "等待内部团队协作处理的工单", value: overview?.pendingInternal ?? 0, icon: WrenchIcon, tone: "text-blue-700 bg-blue-500/10", }, ], [overview, riskWindow], ) return (

SLA 风险运营

给主管和组长使用的风险盯防页,优先查看超时、临近超时和待分配工单

({ value: String(team.id), label: team.name })), ]} />
{cards.map((item) => { const Icon = item.icon return (
{item.title} {item.description}
{loading ? "..." : item.value.toLocaleString()}
) })}
滞留原因 帮助主管快速判断风险是由分配、协作还是 SLA 配置问题造成
{(overview?.reasons?.length ?? 0) > 0 ? ( overview?.reasons?.map((item) => (
{item.title}
{item.count.toLocaleString()}
{item.description}
)) ) : (
暂无滞留原因数据
)}
) }