Files

449 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { Sidebar } from "@/components/sidebar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"
import { FileText, Calendar, Search, Eye, Download, Trash2, Loader2 } from "lucide-react"
import Link from "next/link"
import { useToast } from "@/hooks/use-toast"
// 歷史記錄數據類型
interface HistoryItem {
id: string;
title: string;
type: string;
score: number;
grade: string;
date: string;
status: 'completed' | 'processing';
evaluation_id?: number;
description?: string;
created_at: string;
updated_at: string;
}
// 統計數據類型
interface StatsData {
totalProjects: number;
completedProjects: number;
processingProjects: number;
averageScore: number;
}
export default function HistoryPage() {
const [searchTerm, setSearchTerm] = useState("")
const [filterType, setFilterType] = useState("all")
const [filterStatus, setFilterStatus] = useState("all")
const [historyData, setHistoryData] = useState<HistoryItem[]>([])
const [statsData, setStatsData] = useState<StatsData>({
totalProjects: 0,
completedProjects: 0,
processingProjects: 0,
averageScore: 0
})
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [deletingId, setDeletingId] = useState<string | null>(null)
const { toast } = useToast()
// 載入歷史記錄數據
useEffect(() => {
const loadHistoryData = async () => {
try {
setLoading(true);
setError(null);
// 並行獲取歷史記錄和統計數據
const [historyResponse, statsResponse] = await Promise.all([
fetch('/api/history'),
fetch('/api/history/stats')
]);
if (!historyResponse.ok || !statsResponse.ok) {
throw new Error('獲取數據失敗');
}
const historyResult = await historyResponse.json();
const statsResult = await statsResponse.json();
if (historyResult.success && statsResult.success) {
setHistoryData(historyResult.data);
setStatsData(statsResult.data);
} else {
throw new Error(historyResult.error || statsResult.error || '數據載入失敗');
}
} catch (err) {
console.error('載入歷史記錄失敗:', err);
setError(err instanceof Error ? err.message : '載入數據時發生錯誤');
} finally {
setLoading(false);
}
};
loadHistoryData();
}, []);
const filteredHistory = historyData.filter((item) => {
const matchesSearch = item.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = filterType === "all" || item.type === filterType;
const matchesStatus = filterStatus === "all" || item.status === filterStatus;
return matchesSearch && matchesType && matchesStatus;
});
const getGradeColor = (grade: string) => {
if (grade.startsWith("A")) return "bg-green-100 text-green-800";
if (grade.startsWith("B")) return "bg-blue-100 text-blue-800";
if (grade.startsWith("C")) return "bg-yellow-100 text-yellow-800";
return "bg-gray-100 text-gray-800";
};
const getTypeIcon = (type: string) => {
switch (type) {
case "PPT":
return <FileText className="h-4 w-4" />;
case "Video":
return <FileText className="h-4 w-4" />;
case "Website":
return <FileText className="h-4 w-4" />;
default:
return <FileText className="h-4 w-4" />;
}
};
// 下載評審報告
const handleDownload = async (evaluationId: number, projectTitle: string) => {
try {
toast({
title: "報告下載中",
description: "評審報告 PDF 正在生成,請稍候...",
});
// 調用下載 API
const response = await fetch(`/api/evaluation/${evaluationId}/download`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '下載失敗');
}
// 獲取 PDF Blob
const pdfBlob = await response.blob();
// 創建下載連結
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
// 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱
const contentDisposition = response.headers.get('Content-Disposition');
let fileName = `評審報告_${projectTitle}_${new Date().toISOString().split('T')[0]}.pdf`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
// 顯示成功提示
toast({
title: "下載完成",
description: "評審報告已成功下載",
});
} catch (error) {
console.error('下載失敗:', error);
toast({
title: "下載失敗",
description: error instanceof Error ? error.message : '下載過程中發生錯誤',
variant: "destructive",
});
}
};
// 刪除評審報告
const handleDelete = async (evaluationId: number, projectTitle: string) => {
try {
setDeletingId(evaluationId.toString());
const response = await fetch(`/api/evaluation/${evaluationId}/delete`, {
method: 'DELETE',
});
const result = await response.json();
if (result.success) {
// 從本地狀態中移除已刪除的項目
setHistoryData(prev => prev.filter(item => item.evaluation_id !== evaluationId));
// 重新載入統計數據
const statsResponse = await fetch('/api/history/stats');
const statsResult = await statsResponse.json();
if (statsResult.success) {
setStatsData(statsResult.data);
}
toast({
title: "刪除成功",
description: `評審報告「${projectTitle}」已成功刪除`,
});
} else {
throw new Error(result.error || '刪除失敗');
}
} catch (error) {
console.error('刪除失敗:', error);
toast({
title: "刪除失敗",
description: error instanceof Error ? error.message : '刪除過程中發生錯誤',
variant: "destructive",
});
} finally {
setDeletingId(null);
}
};
return (
<div className="min-h-screen bg-background">
<Sidebar />
<main className="md:ml-64 p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8 pt-8 md:pt-0">
<h1 className="text-3xl font-bold text-foreground mb-2 font-[var(--font-playfair)]"></h1>
<p className="text-muted-foreground"></p>
</div>
{/* Filters */}
<Card className="mb-6">
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜尋專案標題..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="類型篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="PPT">PPT</SelectItem>
<SelectItem value="Video"></SelectItem>
<SelectItem value="Website"></SelectItem>
</SelectContent>
</Select>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="狀態篩選" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="processing"></SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Statistics */}
<div className="grid md:grid-cols-4 gap-4 mb-6">
<Card>
<CardContent className="pt-6 text-center">
<div className="text-2xl font-bold text-primary mb-1">{statsData.totalProjects}</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{statsData.completedProjects}
</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{statsData.averageScore}
</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-2xl font-bold text-yellow-600 mb-1">
{statsData.processingProjects}
</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
</div>
{/* Loading State */}
{loading && (
<Card>
<CardContent className="pt-6 text-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-4" />
<h3 className="font-medium mb-2">...</h3>
<p className="text-muted-foreground"></p>
</CardContent>
</Card>
)}
{/* Error State */}
{error && (
<Card>
<CardContent className="pt-6 text-center py-12">
<FileText className="h-12 w-12 text-destructive mx-auto mb-4" />
<h3 className="font-medium mb-2 text-destructive"></h3>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={() => window.location.reload()}>
</Button>
</CardContent>
</Card>
)}
{/* History List */}
{!loading && !error && (
<div className="space-y-4">
{filteredHistory.map((item) => (
<Card key={item.id} className="hover:shadow-md transition-shadow">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center">
{getTypeIcon(item.type)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium truncate">{item.title}</h3>
<div className="flex items-center gap-4 mt-1">
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Calendar className="h-3 w-3" />
{item.date}
</div>
<Badge variant="outline">{item.type}</Badge>
<Badge variant={item.status === "completed" ? "default" : "secondary"}>
{item.status === "completed" ? "已完成" : "處理中"}
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-4">
{item.status === "completed" && (
<div className="text-center">
<div className="text-2xl font-bold text-primary">{item.score}</div>
<Badge className={getGradeColor(item.grade)}>{item.grade}</Badge>
</div>
)}
<div className="flex gap-2">
{item.status === "completed" ? (
<>
<Button asChild variant="outline" size="sm">
<Link href={`/results?id=${item.evaluation_id}`}>
<Eye className="h-4 w-4" />
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDownload(item.evaluation_id!, item.title)}
>
<Download className="h-4 w-4" />
</Button>
</>
) : (
<Button variant="outline" size="sm" disabled>
...
</Button>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
disabled={deletingId === item.evaluation_id?.toString()}
>
{deletingId === item.evaluation_id?.toString() ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{item.title}
<br />
<br />
<br />
<br />
<br />
<strong></strong>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => item.evaluation_id && handleDelete(item.evaluation_id, item.title)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</div>
</CardContent>
</Card>
))}
{filteredHistory.length === 0 && (
<Card>
<CardContent className="pt-6 text-center py-12">
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="font-medium mb-2"></h3>
<p className="text-muted-foreground mb-4">調</p>
<Button asChild>
<Link href="/upload"></Link>
</Button>
</CardContent>
</Card>
)}
</div>
)}
</div>
</main>
</div>
)
}