229 lines
14 KiB
TypeScript
Executable File
229 lines
14 KiB
TypeScript
Executable File
import React, { useState } from 'react';
|
|
|
|
interface CheckResult {
|
|
ge_id: string;
|
|
lang: string;
|
|
chapter: string;
|
|
status: 'FOUND' | 'NOT_FOUND' | 'ERROR';
|
|
message: string;
|
|
tms_url?: string;
|
|
}
|
|
|
|
interface CheckRecord {
|
|
id: string;
|
|
created_at: string;
|
|
input: any;
|
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
results: CheckResult[];
|
|
error?: string;
|
|
}
|
|
|
|
interface CheckHistoryProps {
|
|
history: CheckRecord[];
|
|
onDelete?: (id: string) => void;
|
|
}
|
|
|
|
const CheckHistory: React.FC<CheckHistoryProps> = ({ history, onDelete }) => {
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [hideCompleted, setHideCompleted] = useState(false);
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('vi-VN');
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'completed': return 'text-green-400';
|
|
case 'processing': return 'text-yellow-400';
|
|
case 'failed': return 'text-red-400';
|
|
default: return 'text-slate-400';
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
switch (status) {
|
|
case 'completed': return 'Hoàn thành';
|
|
case 'processing': return 'Đang xử lý';
|
|
case 'failed': return 'Thất bại';
|
|
default: return 'Chờ xử lý';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="w-full mt-8">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-semibold text-white">Lịch sử Check Upload</h2>
|
|
<label className="flex items-center gap-2 text-sm text-slate-400 cursor-pointer">
|
|
<span>Ẩn chap đã hoàn thành</span>
|
|
<button
|
|
onClick={() => setHideCompleted(!hideCompleted)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${hideCompleted ? 'bg-indigo-600' : 'bg-slate-600'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${hideCompleted ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
{history.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{history.map((record) => {
|
|
const isExpanded = expandedId === record.id;
|
|
const foundCount = record.results.filter(r => r.status === 'FOUND').length;
|
|
const notFoundCount = record.results.filter(r => r.status === 'NOT_FOUND').length;
|
|
const errorCount = record.results.filter(r => r.status === 'ERROR').length;
|
|
|
|
// Filter results based on toggle - hide FOUND (completed) when toggle is on
|
|
let displayResults = hideCompleted
|
|
? record.results.filter(r => r.status !== 'FOUND')
|
|
: record.results;
|
|
|
|
// Sort results by GE ID (ascending)
|
|
displayResults = [...displayResults].sort((a, b) => {
|
|
const aId = parseInt(a.ge_id) || 0;
|
|
const bId = parseInt(b.ge_id) || 0;
|
|
return aId - bId;
|
|
});
|
|
|
|
// Skip record if no results to show when filter is on
|
|
if (hideCompleted && displayResults.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={record.id}
|
|
className="bg-slate-800/50 border border-slate-700 rounded-lg overflow-hidden"
|
|
>
|
|
<div
|
|
className="p-4 cursor-pointer hover:bg-slate-700/30 transition-colors"
|
|
onClick={() => setExpandedId(isExpanded ? null : record.id)}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<span className="text-sm text-slate-400">{formatDate(record.created_at)}</span>
|
|
<span className={`text-sm font-medium ${getStatusColor(record.status)}`}>
|
|
{getStatusText(record.status)}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-4 text-sm">
|
|
<span className="text-slate-300">
|
|
Tổng: <span className="font-medium text-white">{record.results.length}</span>
|
|
</span>
|
|
{foundCount > 0 && (
|
|
<span className="text-green-400">
|
|
Đã có: <span className="font-medium">{foundCount}</span>
|
|
</span>
|
|
)}
|
|
{notFoundCount > 0 && (
|
|
<span className="text-yellow-400">
|
|
Chưa có: <span className="font-medium">{notFoundCount}</span>
|
|
</span>
|
|
)}
|
|
{errorCount > 0 && (
|
|
<span className="text-red-400">
|
|
Lỗi: <span className="font-medium">{errorCount}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{onDelete && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete(record.id);
|
|
}}
|
|
className="text-slate-500 hover:text-red-400 transition-colors p-1"
|
|
title="Xoá"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
<button className="text-slate-400 hover:text-white transition-colors">
|
|
<svg
|
|
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && displayResults.length > 0 && (
|
|
<div className="border-t border-slate-700">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-700/50">
|
|
<tr>
|
|
<th className="py-2 px-4 text-left text-slate-300">GE ID</th>
|
|
<th className="py-2 px-4 text-left text-slate-300">LANG</th>
|
|
<th className="py-2 px-4 text-left text-slate-300">CHAP</th>
|
|
<th className="py-2 px-4 text-left text-slate-300">Trạng thái</th>
|
|
<th className="py-2 px-4 text-left text-slate-300">Ghi chú</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{displayResults.map((result, idx) => (
|
|
<tr key={idx} className="border-t border-slate-700/50 hover:bg-slate-700/30">
|
|
<td className="py-2 px-4">
|
|
{result.tms_url ? (
|
|
<a
|
|
href={result.tms_url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-blue-400 hover:underline"
|
|
>
|
|
{result.ge_id}
|
|
</a>
|
|
) : (
|
|
<span className="text-white">{result.ge_id}</span>
|
|
)}
|
|
</td>
|
|
<td className="py-2 px-4 text-slate-300">{result.lang}</td>
|
|
<td className="py-2 px-4 text-slate-300">{result.chapter}</td>
|
|
<td className="py-2 px-4">
|
|
<span
|
|
className={`px-2 py-1 rounded text-xs font-bold ${result.status === 'FOUND'
|
|
? 'bg-green-900 text-green-300'
|
|
: result.status === 'NOT_FOUND'
|
|
? 'bg-yellow-900 text-yellow-300'
|
|
: 'bg-red-900 text-red-300'
|
|
}`}
|
|
>
|
|
{result.status === 'FOUND' ? 'ĐÃ CÓ' : result.status === 'NOT_FOUND' ? 'CHƯA CÓ' : 'LỖI'}
|
|
</span>
|
|
</td>
|
|
<td className="py-2 px-4 text-slate-400">{result.message}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-10 px-6 bg-slate-800/50 border border-slate-700 rounded-lg">
|
|
<p className="text-slate-400">Chưa có lịch sử check.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CheckHistory;
|