"use client"; import { useState, useEffect, use, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { motion } from "motion/react"; import { api } from "@/lib/api/client"; import { Pagination } from "@/components/ui/pagination"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { CheckCircle, FileText, Download, RefreshCw, ArrowLeft, TrendingUp, Users, AlertOctagon, Eye, } from "lucide-react"; interface TaskDetail { id: number; name: string; period: string; status: string; total_employees: number; matched_count: number; exception_count: number; created_at: string; completed_at: string | null; } interface ReconciliationResult { task_id: number; period: string; total_employees: number; matched_employees: number; exception_count: number; exceptions_by_type: Record; exceptions_by_severity: Record; execution_time_ms: number; completed_at: string; } interface ExceptionItem { id: number; exception_type: string; severity: "low" | "medium" | "high" | "critical"; status: string; employee_id: string; employee_name: string; description: string; salary_amount: number | null; social_security_amount: number | null; tax_amount: number | null; bank_amount: number | null; difference_amount: number | null; } interface MatchedItem { employee_id: string; employee_name: string; salary_amount: number | null; social_security_amount: number | null; tax_amount: number | null; net_salary: number | null; } const severityConfig = { low: { label: "低", bg: "bg-blue-100 dark:bg-blue-900/40", text: "text-blue-700 dark:text-blue-300" }, medium: { label: "中", bg: "bg-yellow-100 dark:bg-yellow-900/40", text: "text-yellow-700 dark:text-yellow-300" }, high: { label: "高", bg: "bg-orange-100 dark:bg-orange-900/40", text: "text-orange-700 dark:text-orange-300" }, critical: { label: "严重", bg: "bg-red-100 dark:bg-red-900/40", text: "text-red-700 dark:text-red-300" }, }; 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: "银行多余记录", }; export default function TaskResultPage({ params }: { params: Promise<{ id: string }> }) { const { id: taskId } = use(params); const router = useRouter(); const [task, setTask] = useState(null); const [result, setResult] = useState(null); const [exceptions, setExceptions] = useState([]); const [matchedRecords, setMatchedRecords] = useState([]); const [matchedTotal, setMatchedTotal] = useState(0); const [matchedPage, setMatchedPage] = useState(1); const [matchedPageSize, setMatchedPageSize] = useState(10); const [loading, setLoading] = useState(true); const [matchedLoading, setMatchedLoading] = useState(false); const [exceptionDetailOpen, setExceptionDetailOpen] = useState(false); const [selectedException, setSelectedException] = useState(null); const [matchedDetailOpen, setMatchedDetailOpen] = useState(false); const [selectedMatched, setSelectedMatched] = useState(null); const fetchResultRef = useRef<(() => Promise) | undefined>(undefined); const fetchTaskDetail = useCallback(async () => { try { const res = await api.get(`/api/tasks/${taskId}`); setTask(res.data); if (res.data.status === "COMPLETED" && fetchResultRef.current) { fetchResultRef.current(); } } catch (error) { console.error("获取任务详情失败", error); } finally { setLoading(false); } }, [taskId]); const fetchResult = useCallback(async () => { try { const res = await api.get(`/api/reconciliation/result/${taskId}`); setResult(res.data); } catch (error) { console.error("获取对账结果失败", error); } }, [taskId]); // Store fetchResult in ref to avoid circular dependency useEffect(() => { fetchResultRef.current = fetchResult; }, [fetchResult]); const fetchExceptions = useCallback(async () => { try { const res = await api.get<{ items: ExceptionItem[] }>(`/api/exceptions/`, { params: { task_id: taskId, page_size: 100 }, }); setExceptions(res.data.items || []); } catch (error) { console.error("获取异常列表失败", error); } }, [taskId]); const fetchMatchedRecords = useCallback(async () => { setMatchedLoading(true); try { const res = await api.get<{ items: MatchedItem[]; total: number }>(`/api/reconciliation/matched/${taskId}`, { params: { page: matchedPage, page_size: matchedPageSize }, }); setMatchedRecords(res.data.items || []); setMatchedTotal(res.data.total || 0); } catch (error) { console.error("获取已匹配列表失败", error); } finally { setMatchedLoading(false); } }, [taskId, matchedPage, matchedPageSize]); function onMatchedPageChange(newPage: number, newPageSize: number) { setMatchedPage(newPage); if (newPageSize !== matchedPageSize) { setMatchedPageSize(newPageSize); } fetchMatchedRecords(); } useEffect(() => { if (taskId) { fetchTaskDetail(); fetchExceptions(); fetchMatchedRecords(); } }, [taskId, fetchTaskDetail, fetchExceptions, fetchMatchedRecords]); async function handleExport() { try { const res = await api.get(`/api/exports/task/${taskId}`, { responseType: "blob", }); const url = window.URL.createObjectURL(res.data); const a = document.createElement("a"); a.href = url; a.download = `对账结果_${task?.period || taskId}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error("导出失败", error); } } async function handleExportKingdee() { try { const res = await api.get(`/api/exports/kingdee/${taskId}`, { responseType: "blob", }); const url = window.URL.createObjectURL(res.data); const a = document.createElement("a"); a.href = url; a.download = `金蝶凭证_${task?.period || taskId}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error("导出金蝶凭证失败", error); } } if (loading) { return (
加载中...
); } if (!task) { return (

任务不存在

); } const matchRate = task.total_employees > 0 ? ((task.matched_count / task.total_employees) * 100).toFixed(1) : "0.0"; 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 } }, }; return (
{/* Header */}

{task.name}

对账期间: {task.period}

{/* Stats Cards */}

总人数

{task.total_employees}

匹配人数

{task.matched_count}

异常数量

{task.exception_count}

匹配率

{matchRate}%

{/* Progress bar */}
{/* Tabs */} 概览统计 异常列表 {task.exception_count > 0 && ( {task.exception_count} )} 已匹配列表 {/* Overview Tab */}
{/* Exception Types */} 异常类型分布 {result?.exceptions_by_type && Object.keys(result.exceptions_by_type).length > 0 ? (
{Object.entries(result.exceptions_by_type).map(([type, count]) => (
{typeLabels[type] || typeLabels[type.toLowerCase()] || typeLabels[type.toUpperCase()] || type} {count}
))}
) : (

暂无数据

)}
{/* Severity Distribution */} 严重程度分布 {result?.exceptions_by_severity && Object.keys(result.exceptions_by_severity).length > 0 ? (
{["critical", "high", "medium", "low"].map((severity) => { const count = result.exceptions_by_severity[severity] || 0; const config = severityConfig[severity as keyof typeof severityConfig]; return (
{config.label}: {count}
); })}
) : (

暂无数据

)}
{/* Execution Info */} 执行信息

创建时间

{new Date(task.created_at).toLocaleString()}

完成时间

{task.completed_at ? new Date(task.completed_at).toLocaleString() : "-"}

{result && ( <>

执行时间

{result.execution_time_ms?.toFixed(0) || '-'}ms

结果生成时间

{result.completed_at ? new Date(result.completed_at).toLocaleString() : "-"}

)}
{/* Exceptions Tab */}
异常列表 {exceptions.length} 条记录
{exceptions.length === 0 ? (

完美匹配!

所有记录都匹配成功

) : ( 员工 类型 严重程度 描述 差异金额 {exceptions.map((exc) => ( { setSelectedException(exc); setExceptionDetailOpen(true); }} >
{exc.employee_name}
{exc.employee_id}
{typeLabels[exc.exception_type] || typeLabels[exc.exception_type.toLowerCase()] || typeLabels[exc.exception_type.toUpperCase()] || exc.exception_type} {severityConfig[exc.severity].label} {exc.description} {exc.difference_amount != null && ( 0 ? "text-red-600" : "text-emerald-600"}> {exc.difference_amount > 0 ? "+" : ""} {exc.difference_amount.toFixed(2)} )}
))}
)}
{/* Matched Tab */}
已匹配列表 {matchedTotal} 条记录
{matchedTotal > 0 && (
)} {matchedLoading ? (
) : matchedRecords.length === 0 ? (

暂无已匹配记录

) : ( 员工 应发工资 社保 个税 实发工资 操作 {matchedRecords.map((record, index) => ( { setSelectedMatched(record); setMatchedDetailOpen(true); }} >
{record.employee_name}
{record.employee_id}
{record.salary_amount != null ? record.salary_amount.toFixed(2) : "-"} {record.social_security_amount != null ? record.social_security_amount.toFixed(2) : "-"} {record.tax_amount != null ? record.tax_amount.toFixed(2) : "-"} {record.net_salary != null ? record.net_salary.toFixed(2) : "-"}
))}
)}
{/* 异常详情弹窗 */} 异常详情 {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.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}

)}
{/* 已匹配记录明细弹窗 */} 已匹配记录明细 {selectedMatched && (

员工姓名

{selectedMatched.employee_name}

员工ID

{selectedMatched.employee_id}

金额详情

应发工资 {selectedMatched.salary_amount != null ? selectedMatched.salary_amount.toFixed(2) : "-"}
社保 {selectedMatched.social_security_amount != null ? selectedMatched.social_security_amount.toFixed(2) : "-"}
个税 {selectedMatched.tax_amount != null ? selectedMatched.tax_amount.toFixed(2) : "-"}
实发工资 {selectedMatched.net_salary != null ? selectedMatched.net_salary.toFixed(2) : "-"}
)}
); }