Files
freedakgmail b93929eb1a feat: UI优化和功能完善
- 添加公共分页组件,支持上边显示、分页大小10
- 侧边栏/Header按Liner风格优化,添加动画效果
- 异常处理列表支持分页和明细弹窗
- 任务结果页面支持已匹配列表分页
- 修复Dialog弹窗背景透明问题
- 新增dropdown-menu组件
- 添加parsed_file_record模型和回填脚本
- 前端枚举中文化
2026-07-07 12:57:15 +08:00

827 lines
33 KiB
TypeScript

"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<string, number>;
exceptions_by_severity: Record<string, number>;
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<string, string> = {
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<TaskDetail | null>(null);
const [result, setResult] = useState<ReconciliationResult | null>(null);
const [exceptions, setExceptions] = useState<ExceptionItem[]>([]);
const [matchedRecords, setMatchedRecords] = useState<MatchedItem[]>([]);
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<ExceptionItem | null>(null);
const [matchedDetailOpen, setMatchedDetailOpen] = useState(false);
const [selectedMatched, setSelectedMatched] = useState<MatchedItem | null>(null);
const fetchResultRef = useRef<(() => Promise<void>) | undefined>(undefined);
const fetchTaskDetail = useCallback(async () => {
try {
const res = await api.get<TaskDetail>(`/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<ReconciliationResult>(`/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<Blob>(`/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<Blob>(`/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 (
<div className="min-h-full flex items-center justify-center">
<div className="flex items-center gap-2 text-muted-foreground">
<RefreshCw className="h-5 w-5 animate-spin" />
...
</div>
</div>
);
}
if (!task) {
return (
<div className="min-h-full flex items-center justify-center">
<Card className="max-w-md">
<CardContent className="py-12 text-center">
<p className="text-muted-foreground"></p>
<Button className="mt-4 btn-press" onClick={() => router.back()}>
</Button>
</CardContent>
</Card>
</div>
);
}
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 (
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto space-y-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="flex items-center justify-between"
>
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()} className="hover:bg-muted">
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-heading-1 text-foreground">{task.name}</h1>
<p className="text-muted-foreground mt-1">
: {task.period}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" onClick={handleExport} className="btn-press">
<Download className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={handleExportKingdee} className="btn-press">
<FileText className="h-4 w-4 mr-2" />
</Button>
</div>
</motion.div>
{/* Stats Cards */}
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
>
<motion.div variants={item}>
<Card className="hover-lift">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-caption text-muted-foreground uppercase tracking-wide"></p>
<p className="text-3xl font-semibold mt-1">{task.total_employees}</p>
</div>
<div className="p-2.5 rounded-lg bg-blue-50 dark:bg-blue-950/30">
<Users className="w-5 h-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div variants={item}>
<Card className="hover-lift">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-caption text-muted-foreground uppercase tracking-wide"></p>
<p className="text-3xl font-semibold mt-1 text-emerald-600">{task.matched_count}</p>
</div>
<div className="p-2.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/30">
<CheckCircle className="w-5 h-5 text-emerald-600" />
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div variants={item}>
<Card className="hover-lift">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-caption text-muted-foreground uppercase tracking-wide"></p>
<p className="text-3xl font-semibold mt-1 text-red-600">{task.exception_count}</p>
</div>
<div className="p-2.5 rounded-lg bg-red-50 dark:bg-red-950/30">
<AlertOctagon className="w-5 h-5 text-red-600" />
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div variants={item}>
<Card className="hover-lift">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-caption text-muted-foreground uppercase tracking-wide"></p>
<p className="text-3xl font-semibold mt-1">{matchRate}%</p>
</div>
<div className="p-2.5 rounded-lg bg-violet-50 dark:bg-violet-950/30">
<TrendingUp className="w-5 h-5 text-violet-600" />
</div>
</div>
{/* Progress bar */}
<div className="mt-3 h-2 bg-muted rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${matchRate}%` }}
transition={{ duration: 0.8, delay: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="h-full bg-gradient-to-r from-emerald-500 to-emerald-400"
/>
</div>
</CardContent>
</Card>
</motion.div>
</motion.div>
{/* Tabs */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2, ease: [0.16, 1, 0.3, 1] }}
>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList className="bg-muted/50">
<TabsTrigger value="overview" className="data-[state=active]:bg-background"></TabsTrigger>
<TabsTrigger value="exceptions" className="data-[state=active]:bg-background">
{task.exception_count > 0 && (
<Badge variant="destructive" className="ml-2">
{task.exception_count}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="matched" className="data-[state=active]:bg-background"></TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-4">
<div className="grid lg:grid-cols-2 gap-4">
{/* Exception Types */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
{result?.exceptions_by_type && Object.keys(result.exceptions_by_type).length > 0 ? (
<div className="space-y-3">
{Object.entries(result.exceptions_by_type).map(([type, count]) => (
<div key={type} className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{typeLabels[type] || typeLabels[type.toLowerCase()] || typeLabels[type.toUpperCase()] || type}
</span>
<Badge variant="secondary">{count}</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</CardContent>
</Card>
{/* Severity Distribution */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
{result?.exceptions_by_severity && Object.keys(result.exceptions_by_severity).length > 0 ? (
<div className="flex flex-wrap gap-3">
{["critical", "high", "medium", "low"].map((severity) => {
const count = result.exceptions_by_severity[severity] || 0;
const config = severityConfig[severity as keyof typeof severityConfig];
return (
<div key={severity} className={`flex items-center gap-2 px-3 py-1.5 rounded-full ${config.bg}`}>
<div className={`w-2 h-2 rounded-full bg-current ${config.text}`} />
<span className={`text-sm font-medium ${config.text}`}>
{config.label}: {count}
</span>
</div>
);
})}
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</CardContent>
</Card>
</div>
{/* Execution Info */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">{new Date(task.created_at).toLocaleString()}</p>
</div>
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">
{task.completed_at ? new Date(task.completed_at).toLocaleString() : "-"}
</p>
</div>
{result && (
<>
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">{result.execution_time_ms?.toFixed(0) || '-'}ms</p>
</div>
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="text-sm font-medium mt-1">
{result.completed_at ? new Date(result.completed_at).toLocaleString() : "-"}
</p>
</div>
</>
)}
</div>
</CardContent>
</Card>
</TabsContent>
{/* Exceptions Tab */}
<TabsContent value="exceptions">
<Card>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium"></CardTitle>
<Badge variant="secondary">{exceptions.length} </Badge>
</div>
</CardHeader>
<CardContent className="p-0">
{exceptions.length === 0 ? (
<div className="py-16 text-center">
<div className="w-16 h-16 rounded-full bg-emerald-50 dark:bg-emerald-950/30 mx-auto mb-4 flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-emerald-500" />
</div>
<p className="text-lg font-medium text-foreground">!</p>
<p className="text-sm text-muted-foreground mt-1"></p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{exceptions.map((exc) => (
<TableRow
key={exc.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => {
setSelectedException(exc);
setExceptionDetailOpen(true);
}}
>
<TableCell>
<div>
<div className="font-medium">{exc.employee_name}</div>
<div className="text-xs text-muted-foreground">{exc.employee_id}</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="font-normal">
{typeLabels[exc.exception_type] || typeLabels[exc.exception_type.toLowerCase()] || typeLabels[exc.exception_type.toUpperCase()] || exc.exception_type}
</Badge>
</TableCell>
<TableCell>
<Badge className={`${severityConfig[exc.severity].bg} ${severityConfig[exc.severity].text}`}>
{severityConfig[exc.severity].label}
</Badge>
</TableCell>
<TableCell className="max-w-[250px]">
<span className="truncate block">{exc.description}</span>
</TableCell>
<TableCell className="text-right">
{exc.difference_amount != null && (
<span className={exc.difference_amount > 0 ? "text-red-600" : "text-emerald-600"}>
{exc.difference_amount > 0 ? "+" : ""}
{exc.difference_amount.toFixed(2)}
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
{/* Matched Tab */}
<TabsContent value="matched">
<Card>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium"></CardTitle>
<Badge variant="secondary">{matchedTotal} </Badge>
</div>
</CardHeader>
<CardContent>
{matchedTotal > 0 && (
<div className="mb-4">
<Pagination
total={matchedTotal}
page={matchedPage}
pageSize={matchedPageSize}
onChange={onMatchedPageChange}
/>
</div>
)}
{matchedLoading ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : matchedRecords.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground"></p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="w-[60px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{matchedRecords.map((record, index) => (
<TableRow
key={`${record.employee_id}-${index}`}
className="cursor-pointer hover:bg-muted/50"
onClick={() => {
setSelectedMatched(record);
setMatchedDetailOpen(true);
}}
>
<TableCell>
<div>
<div className="font-medium">{record.employee_name}</div>
<div className="text-xs text-muted-foreground">{record.employee_id}</div>
</div>
</TableCell>
<TableCell className="text-right">
{record.salary_amount != null ? record.salary_amount.toFixed(2) : "-"}
</TableCell>
<TableCell className="text-right">
{record.social_security_amount != null ? record.social_security_amount.toFixed(2) : "-"}
</TableCell>
<TableCell className="text-right">
{record.tax_amount != null ? record.tax_amount.toFixed(2) : "-"}
</TableCell>
<TableCell className="text-right font-medium">
{record.net_salary != null ? record.net_salary.toFixed(2) : "-"}
</TableCell>
<TableCell>
<Eye className="h-4 w-4 text-muted-foreground" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</motion.div>
{/* 异常详情弹窗 */}
<Dialog open={exceptionDetailOpen} onOpenChange={setExceptionDetailOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{selectedException && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="font-medium">{selectedException.employee_name}</p>
</div>
<div>
<p className="text-caption text-muted-foreground">ID</p>
<p className="font-medium">{selectedException.employee_id}</p>
</div>
<div>
<p className="text-caption text-muted-foreground"></p>
<Badge variant="outline">
{typeLabels[selectedException.exception_type] || typeLabels[selectedException.exception_type.toLowerCase()] || typeLabels[selectedException.exception_type.toUpperCase()] || selectedException.exception_type}
</Badge>
</div>
<div>
<p className="text-caption text-muted-foreground"></p>
<Badge className={`${severityConfig[selectedException.severity].bg} ${severityConfig[selectedException.severity].text}`}>
{severityConfig[selectedException.severity].label}
</Badge>
</div>
</div>
<div className="border-t pt-4">
<h4 className="font-medium mb-3"></h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{selectedException.salary_amount != null && (
<div className="p-3 rounded-lg bg-muted/50">
<p className="text-caption text-muted-foreground"></p>
<p className="font-mono font-medium mt-1">{selectedException.salary_amount.toFixed(2)}</p>
</div>
)}
{selectedException.social_security_amount != null && (
<div className="p-3 rounded-lg bg-muted/50">
<p className="text-caption text-muted-foreground"></p>
<p className="font-mono font-medium mt-1">{selectedException.social_security_amount.toFixed(2)}</p>
</div>
)}
{selectedException.tax_amount != null && (
<div className="p-3 rounded-lg bg-muted/50">
<p className="text-caption text-muted-foreground"></p>
<p className="font-mono font-medium mt-1">{selectedException.tax_amount.toFixed(2)}</p>
</div>
)}
{selectedException.bank_amount != null && (
<div className="p-3 rounded-lg bg-muted/50">
<p className="text-caption text-muted-foreground"></p>
<p className="font-mono font-medium mt-1">{selectedException.bank_amount.toFixed(2)}</p>
</div>
)}
</div>
{selectedException.difference_amount != null && (
<div className="mt-4 p-4 rounded-lg bg-muted/50">
<p className="text-caption text-muted-foreground"></p>
<p className={`text-2xl font-mono font-semibold mt-1 ${selectedException.difference_amount > 0 ? "text-red-600" : "text-emerald-600"}`}>
{selectedException.difference_amount > 0 ? "+" : ""}
{selectedException.difference_amount.toFixed(2)}
</p>
</div>
)}
</div>
<div className="border-t pt-4">
<div className="space-y-3">
<div>
<p className="text-caption text-muted-foreground mb-1"></p>
<p>{selectedException.description}</p>
</div>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* 已匹配记录明细弹窗 */}
<Dialog open={matchedDetailOpen} onOpenChange={setMatchedDetailOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{selectedMatched && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-caption text-muted-foreground"></p>
<p className="font-medium">{selectedMatched.employee_name}</p>
</div>
<div>
<p className="text-caption text-muted-foreground">ID</p>
<p className="font-medium">{selectedMatched.employee_id}</p>
</div>
</div>
<div className="border-t pt-4">
<h4 className="font-medium mb-3"></h4>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 rounded-lg bg-muted/50">
<span className="text-muted-foreground"></span>
<span className="font-mono font-medium">
{selectedMatched.salary_amount != null ? selectedMatched.salary_amount.toFixed(2) : "-"}
</span>
</div>
<div className="flex justify-between items-center p-3 rounded-lg bg-muted/50">
<span className="text-muted-foreground"></span>
<span className="font-mono font-medium">
{selectedMatched.social_security_amount != null ? selectedMatched.social_security_amount.toFixed(2) : "-"}
</span>
</div>
<div className="flex justify-between items-center p-3 rounded-lg bg-muted/50">
<span className="text-muted-foreground"></span>
<span className="font-mono font-medium">
{selectedMatched.tax_amount != null ? selectedMatched.tax_amount.toFixed(2) : "-"}
</span>
</div>
<div className="flex justify-between items-center p-3 rounded-lg bg-primary/10 border border-primary/20">
<span className="font-medium"></span>
<span className="font-mono font-semibold text-lg">
{selectedMatched.net_salary != null ? selectedMatched.net_salary.toFixed(2) : "-"}
</span>
</div>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}