"use client"; import { Suspense, useState, useEffect } from "react"; import { useSearchParams } from "next/navigation"; import { motion } from "motion/react"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertTriangle, CheckCircle, Clock, Eye, Search, XCircle, } from "lucide-react"; import { api } from "@/lib/api/client"; import { Pagination } from "@/components/ui/pagination"; interface ExceptionItem { id: number; task_id: number; exception_type: string; severity: "low" | "medium" | "high" | "critical"; status: "pending" | "processing" | "resolved" | "ignored"; employee_id: string; employee_name: string; description: string; detail: string | null; salary_amount: number | null; social_security_amount: number | null; tax_amount: number | null; bank_amount: number | null; difference_amount: number | null; resolution: string | null; handler: string | null; note: string | null; suggested_action: string | null; created_at: string; updated_at: string | null; } interface Summary { by_status: Record; by_severity: Record; by_type: Record; } const severityConfig = { low: { label: "低", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300", icon: "bg-blue-500" }, medium: { label: "中", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300", icon: "bg-yellow-500" }, high: { label: "高", color: "bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300", icon: "bg-orange-500" }, critical: { label: "严重", color: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300", icon: "bg-red-500" }, }; const statusConfig = { pending: { label: "待处理", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" }, processing: { label: "处理中", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300" }, confirmed: { label: "已确认", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" }, resolved: { label: "已解决", color: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300" }, ignored: { label: "已忽略", color: "bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500" }, }; const typeLabels: Record = { amount_mismatch: "金额不一致", missing_record: "记录缺失", duplicate_record: "重复记录", format_error: "格式错误", logic_error: "逻辑错误", rule_violation: "规则违规", threshold_exceeded: "阈值超限", missing_employee: "员工缺失", extra_employee: "多余员工", amount_difference: "金额差异", zero_amount: "金额为零", negative_amount: "负数金额", unusual_amount: "金额异常", duplicate_employee: "重复员工", bank_mismatch: "银行不匹配", bank_missing: "银行记录缺失", bank_extra: "银行多余记录", AMOUNT_MISMATCH: "金额不一致", MISSING_RECORD: "记录缺失", DUPLICATE_RECORD: "重复记录", FORMAT_ERROR: "格式错误", LOGIC_ERROR: "逻辑错误", RULE_VIOLATION: "规则违规", THRESHOLD_EXCEEDED: "阈值超限", MISSING_EMPLOYEE: "员工缺失", EXTRA_EMPLOYEE: "多余员工", AMOUNT_DIFFERENCE: "金额差异", ZERO_AMOUNT: "金额为零", NEGATIVE_AMOUNT: "负数金额", UNUSUAL_AMOUNT: "金额异常", DUPLICATE_EMPLOYEE: "重复员工", BANK_MISMATCH: "银行不匹配", BANK_MISSING: "银行记录缺失", BANK_EXTRA: "银行多余记录", }; const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.06 }, }, }; const item = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.16, 1, 0.3, 1] as const } }, }; function ExceptionsContent() { const searchParams = useSearchParams(); const [exceptions, setExceptions] = useState([]); const [summary, setSummary] = useState(null); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [taskId, setTaskId] = useState(searchParams.get("task_id") || ""); const [status, setStatus] = useState(searchParams.get("status") || ""); const [severity, setSeverity] = useState(searchParams.get("severity") || ""); const [exceptionType, setExceptionType] = useState(searchParams.get("type") || ""); const [page, setPage] = useState(Number(searchParams.get("page")) || 1); const [pageSize, setPageSize] = useState(10); const [selectedException, setSelectedException] = useState(null); const [detailOpen, setDetailOpen] = useState(false); useEffect(() => { fetchExceptions(); fetchSummary(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [taskId, status, severity, exceptionType]); function onPageChange(newPage: number, newPageSize: number) { setPage(newPage); if (newPageSize !== pageSize) { setPageSize(newPageSize); } fetchExceptions(); fetchSummary(); } async function fetchExceptions() { setLoading(true); try { const res = await api.get<{ items: ExceptionItem[]; total: number }>("/api/exceptions/", { params: { task_id: taskId || undefined, status: status || undefined, severity: severity || undefined, exception_type: exceptionType || undefined, page, page_size: pageSize, }, }); setExceptions(res.data.items || []); setTotal(res.data.total || 0); } catch (error) { console.error("加载异常列表失败", error); } finally { setLoading(false); } } async function fetchSummary() { try { const res = await api.get("/api/exceptions/summary", { params: { task_id: taskId || undefined }, }); setSummary(res.data); } catch (error) { console.error("加载汇总失败", error); } } function handleSearch() { setPage(1); fetchExceptions(); fetchSummary(); } function viewDetail(exception: ExceptionItem) { setSelectedException(exception); setDetailOpen(true); } async function updateStatus(id: number, newStatus: string) { try { await api.patch(`/api/exceptions/${id}`, { status: newStatus }); fetchExceptions(); fetchSummary(); } catch (error) { console.error("更新状态失败", error); } } const totalPages = Math.ceil(total / pageSize); return (
{/* Header */}

异常处理

管理和处理对账过程中发现的异常

{/* Summary Cards */} {summary && (

待处理

{summary.by_status?.pending || 0}

处理中

{summary.by_status?.processing || 0}

已解决

{summary.by_status?.resolved || 0}

严重异常

{summary.by_severity?.critical || 0}

)} {/* Filter Card */}
setTaskId(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} className="h-9" />
{/* Exceptions Table */}
异常列表 {total} 条记录
{total > 0 && ( )} 状态 员工 类型 严重程度 描述 差异金额 操作 {loading ? (
加载中...
) : exceptions.length === 0 ? (

暂无异常记录

所有记录都匹配成功

) : ( exceptions.map((exception, index) => ( {statusConfig[exception.status]?.label || exception.status}
{exception.employee_name}
{exception.employee_id}
{typeLabels[exception.exception_type] || typeLabels[exception.exception_type.toLowerCase()] || typeLabels[exception.exception_type.toUpperCase()] || exception.exception_type}
{severityConfig[exception.severity]?.label || exception.severity}
{exception.description} {exception.difference_amount != null && ( 0 ? "text-red-600" : "text-emerald-600"}> {exception.difference_amount > 0 ? "+" : ""} {exception.difference_amount.toFixed(2)} )}
{exception.status === "pending" && ( )} {exception.status === "processing" && ( )}
)) )}
{/* Detail Dialog */} 异常详情 {selectedException && (

员工姓名

{selectedException.employee_name}

员工ID

{selectedException.employee_id}

异常类型

{typeLabels[selectedException.exception_type] || typeLabels[selectedException.exception_type.toLowerCase()] || typeLabels[selectedException.exception_type.toUpperCase()] || selectedException.exception_type}

严重程度

{severityConfig[selectedException.severity]?.label || selectedException.severity}

金额详情

{selectedException.salary_amount != null && (

工资

{selectedException.salary_amount.toFixed(2)}

)} {selectedException.social_security_amount != null && (

社保

{selectedException.social_security_amount.toFixed(2)}

)} {selectedException.tax_amount != null && (

个税

{selectedException.tax_amount.toFixed(2)}

)} {selectedException.bank_amount != null && (

银行

{selectedException.bank_amount.toFixed(2)}

)}
{selectedException.difference_amount != null && (

差异金额

0 ? "text-red-600" : "text-emerald-600"}`}> {selectedException.difference_amount > 0 ? "+" : ""} {selectedException.difference_amount.toFixed(2)}

)}

描述

{selectedException.description}

{selectedException.detail && (

详细信息

                        {JSON.stringify(JSON.parse(selectedException.detail), null, 2)}
                      
)} {selectedException.suggested_action && (

建议操作

{selectedException.suggested_action}

)}
)}
); } function LoadingFallback() { return (
加载中...
); } export default function ExceptionsPage() { return ( }> ); }