import React, { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { BookOpen, FileText, Layers, Loader2, Plus, Search, Trash2, Edit2, AlertCircle, Check, Clock, XCircle } from 'lucide-react'; import { apiClient } from '../../services/apiClient'; import { templateService } from '../../services/templateService'; import { AssessmentTemplate } from '../../types'; import { useToast } from '../../contexts/ToastContext'; import { useConfirm } from '../../contexts/ConfirmContext'; import { useLanguage } from '../../contexts/LanguageContext'; interface QuestionBankViewProps { isAdmin?: boolean; } interface QuestionBankItem { id: string; name: string; description?: string; status: string; templateId?: string; createdAt: string; } type StatusFilter = 'ALL' | 'DRAFT' | 'PENDING_REVIEW' | 'PUBLISHED'; export default function QuestionBankView({ isAdmin: _isAdmin }: QuestionBankViewProps) { const navigate = useNavigate(); const { t } = useLanguage(); const { showSuccess, showError } = useToast(); const { confirm } = useConfirm(); const [banks, setBanks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [showDrawer, setShowDrawer] = useState(false); const [formData, setFormData] = useState({ name: '', description: '', templateId: '' }); const [saving, setSaving] = useState(false); const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); const [deletingId, setDeletingId] = useState(null); const [statusFilter, setStatusFilter] = useState('ALL'); const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { setLoading(true); const res = await apiClient.request('/question-banks', {}); if (!res.ok) throw new Error(res.status.toString()); const data = await res.json(); setBanks(Array.isArray(data) ? data : (data.data || [])); } catch (err: any) { setError(err.message || t('actionFailed')); showError(err.message || t('actionFailed')); } finally { setLoading(false); } }; const openDrawer = () => { setFormData({ name: '', description: '', templateId: '' }); setLoadingTemplates(true); templateService.getAll() .then(data => setTemplates(data)) .catch(() => showError(t('actionFailed'))) .finally(() => setLoadingTemplates(false)); setShowDrawer(true); }; const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name.trim()) return; setSaving(true); try { const payload: any = { name: formData.name, description: formData.description }; if (formData.templateId) payload.templateId = formData.templateId; const res = await apiClient.request('/question-banks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const errBody = await res.text().catch(() => ''); let msg = res.status.toString(); try { const parsed = JSON.parse(errBody); if (parsed.message) msg = parsed.message; } catch {} throw new Error(msg); } setShowDrawer(false); showSuccess(t('questionBankCreated')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } finally { setSaving(false); } }; const handleDelete = async (e: React.MouseEvent, bankId: string, bankName: string) => { e.stopPropagation(); const ok = await confirm({ message: t('confirmDeleteBank').replace('$1', bankName), confirmLabel: t('delete'), cancelLabel: t('cancel') }); if (!ok) return; setDeletingId(bankId); try { const res = await apiClient.request(`/question-banks/${bankId}`, { method: 'DELETE' }); if (!res.ok) { const errBody = await res.text().catch(() => ''); let msg = res.status.toString(); try { const parsed = JSON.parse(errBody); if (parsed.message) msg = parsed.message; } catch {} throw new Error(msg); } showSuccess(t('confirm')); fetchData(); } catch (err: any) { showError(err.message || t('questionBankDeleteFailed')); } finally { setDeletingId(null); } }; const handleCardClick = (bank: QuestionBankItem) => { navigate(`/question-banks/${bank.id}`); }; const filteredBanks = useMemo(() => { let result = banks; if (statusFilter !== 'ALL') result = result.filter(b => b.status === statusFilter); if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); result = result.filter(b => b.name.toLowerCase().includes(q) || (b.description || '').toLowerCase().includes(q)); } return result; }, [banks, statusFilter, searchQuery]); const STATUS_TABS: { key: StatusFilter; label: string; icon: React.ReactNode; count: (b: QuestionBankItem[]) => number }[] = [ { key: 'ALL', label: t('all'), icon: , count: (b) => b.length }, { key: 'PUBLISHED', label: t('published'), icon: , count: (b) => b.filter(i => i.status === 'PUBLISHED').length }, { key: 'DRAFT', label: t('draft'), icon: , count: (b) => b.filter(i => i.status === 'DRAFT').length }, { key: 'PENDING_REVIEW', label: t('pendingReview'), icon: , count: (b) => b.filter(i => i.status === 'PENDING_REVIEW').length }, ]; const stats = useMemo(() => ({ total: banks.length, published: banks.filter(b => b.status === 'PUBLISHED').length, draft: banks.filter(b => b.status === 'DRAFT').length, pending: banks.filter(b => b.status === 'PENDING_REVIEW').length, }), [banks]); const statusLabels: Record = { PUBLISHED: t('published'), PENDING_REVIEW: t('pendingReview'), REJECTED: t('rejected'), DRAFT: t('draft'), }; return (

{t('questionBankManagement')}

{t('questionBankManagementDesc')}

{!loading && !error && banks.length > 0 && (
{[ { label: t('totalBanks'), value: stats.total, color: 'bg-slate-50 border-slate-200 text-slate-700', icon: }, { label: t('published'), value: stats.published, color: 'bg-emerald-50 border-emerald-200/50 text-emerald-700', icon: }, { label: t('draft'), value: stats.draft, color: 'bg-slate-50 border-slate-200 text-slate-700', icon: }, { label: t('pendingReview'), value: stats.pending, color: 'bg-amber-50 border-amber-200/50 text-amber-700', icon: }, ].map((stat, i) => (
{stat.label} {stat.icon}
{stat.value}
))}
)} {!loading && !error && banks.length > 0 && (
setSearchQuery(e.target.value)} placeholder={t('searchQuestionBanksPlaceholder')} className="w-full pl-10 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" />
{STATUS_TABS.map((tab) => { const active = statusFilter === tab.key; return ( ); })}
)} {loading ? (
) : error ? (
{t('actionFailed')}
) : banks.length === 0 ? (

{t('noQuestionBanks')}

{t('createFirstBank')}

) : filteredBanks.length === 0 ? (

{t('noMatchingQuestionBanks')}

{t('tryChangingFilter')}

) : (
{filteredBanks.map((bank) => ( handleCardClick(bank)} className="bg-white border border-slate-200 rounded-3xl p-5 shadow-sm hover:shadow-md transition-all cursor-pointer group relative overflow-hidden" >

{bank.name}

{bank.description || t('noDescription')}

{statusLabels[bank.status] || bank.status} {new Date(bank.createdAt).toLocaleDateString('zh-CN')}
))}
)} {showDrawer && ( <> setShowDrawer(false)} className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-40" />

{t('createQuestionBank')}

setFormData({ ...formData, name: e.target.value })} className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" placeholder={t('name')} required autoFocus />
setFormData({ ...formData, description: e.target.value })} className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" placeholder={t('description')} />
{loadingTemplates && (
{t('loading')}
)}
)}
); }