147 lines
7.1 KiB
TypeScript
Executable File
147 lines
7.1 KiB
TypeScript
Executable File
import React, { useState, memo } from 'react';
|
|
import type { Submission, GeIdResult } from '../types';
|
|
import GeIdResultItem from './GeIdResultItem';
|
|
import TrashIcon from './TrashIcon';
|
|
import RetryIcon from './RetryIcon';
|
|
import ConfirmModal from './ConfirmModal';
|
|
|
|
interface HistoryItemProps {
|
|
submission: Submission;
|
|
onErrorClick: (details: string) => void;
|
|
onDelete: (id: string) => void;
|
|
onRetry: (submission: Submission, errorGeIds: string[], errorUsernames: string[]) => void;
|
|
onPaste: (username: string, geIdAndLang: string) => void;
|
|
hideNonErrors: boolean;
|
|
}
|
|
|
|
const HistoryItem: React.FC<HistoryItemProps> = ({ submission, onErrorClick, onDelete, onRetry, onPaste, hideNonErrors }) => {
|
|
const [showRetryConfirm, setShowRetryConfirm] = useState(false);
|
|
|
|
// Find GE IDs with errors
|
|
const errorResults = submission.results?.filter(r => r.details?.some(d => d.status === 'error')) ?? [];
|
|
const errorGeIds = errorResults.map(r => r.geIdAndLang);
|
|
const hasError = errorGeIds.length > 0;
|
|
|
|
// Get unique usernames from error results
|
|
const errorUsernames = [...new Set(
|
|
errorResults.flatMap(r => r.details?.filter(d => d.status === 'error').map(d => d.username) ?? [])
|
|
)];
|
|
|
|
// Filter results based on hideNonErrors toggle
|
|
const filteredResults = hideNonErrors
|
|
? submission.results?.filter(r => r.details?.some(d => d.status === 'error'))
|
|
: submission.results;
|
|
|
|
const handlePaste = () => {
|
|
onPaste(submission.username, submission.geIdAndLang);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
const handleRetryConfirm = () => {
|
|
setShowRetryConfirm(false);
|
|
onRetry(submission, errorGeIds, errorUsernames);
|
|
};
|
|
|
|
// If hideNonErrors is on and no errors, don't render
|
|
if (hideNonErrors && !hasError) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="bg-slate-800/50 border border-slate-700 rounded-lg shadow-md animate-fade-in overflow-hidden">
|
|
<div className="w-full flex justify-between items-center p-3 bg-slate-800">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-slate-400 flex-shrink-0">
|
|
{submission.timestamp.toLocaleString('vi-VN')}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handlePaste}
|
|
className="flex items-center gap-1.5 text-xs font-semibold text-cyan-400 bg-cyan-500/10 hover:bg-cyan-500/20 px-2 py-1 rounded-md transition-colors"
|
|
title="Dán username và GE ID & LANG vào form"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
</svg>
|
|
Dán
|
|
</button>
|
|
{hasError && (
|
|
<button
|
|
onClick={() => setShowRetryConfirm(true)}
|
|
className="flex items-center gap-1.5 text-xs font-semibold text-amber-400 bg-amber-500/10 hover:bg-amber-500/20 px-2 py-1 rounded-md transition-colors"
|
|
>
|
|
<RetryIcon className="w-4 h-4" />
|
|
Retry
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => onDelete(submission.id)}
|
|
className="flex items-center gap-1.5 text-xs font-semibold text-rose-400 bg-rose-500/10 hover:bg-rose-500/20 px-2 py-1 rounded-md transition-colors"
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
Xoá
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 space-y-4 border-t border-slate-700">
|
|
{filteredResults && filteredResults.length > 0 ? (
|
|
filteredResults.map((result, i) => (
|
|
<GeIdResultItem key={i} result={result} onErrorClick={onErrorClick} />
|
|
))
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<p className="text-slate-500 text-sm">
|
|
{hideNonErrors ? 'Không có lỗi trong lần submit này.' : 'Không có dữ liệu kết quả chi tiết cho lần submit này.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<style>{`
|
|
@keyframes fade-in {
|
|
from { opacity: 0; transform: translateY(-10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-fade-in {
|
|
animation: fade-in 0.3s ease-out forwards;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
|
|
{/* Retry Confirmation Modal */}
|
|
<ConfirmModal
|
|
isOpen={showRetryConfirm}
|
|
title="Xác nhận Retry"
|
|
message={
|
|
<div className="space-y-4">
|
|
<p className="text-slate-300">Bạn có muốn retry các GE ID & LANG sau?</p>
|
|
<div className="bg-slate-700/50 rounded-lg p-3 max-h-32 overflow-y-auto">
|
|
<p className="text-xs text-slate-400 mb-2">GE ID & LANG bị lỗi:</p>
|
|
{errorGeIds.map((geId, i) => (
|
|
<div key={i} className="text-sm text-indigo-300 font-mono">{geId}</div>
|
|
))}
|
|
</div>
|
|
<div className="bg-slate-700/50 rounded-lg p-3 max-h-32 overflow-y-auto">
|
|
<p className="text-xs text-slate-400 mb-2">Usernames:</p>
|
|
{errorUsernames.map((username, i) => (
|
|
<div key={i} className="text-sm text-cyan-300 font-mono">{username}</div>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-slate-400">
|
|
Các GE ID & LANG lỗi sẽ được tách thành record mới và bắt đầu cấp quyền ngay.
|
|
</p>
|
|
</div>
|
|
}
|
|
confirmText="Retry"
|
|
cancelText="Huỷ"
|
|
onConfirm={handleRetryConfirm}
|
|
onCancel={() => setShowRetryConfirm(false)}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default memo(HistoryItem); |