import React, { useState, useRef } from 'react'; import { SchoolData, Exam, Question } from '../types'; import { FileText, Plus, Search, BookOpen, Upload, Trash2, ArrowLeft, Save, CheckCircle, Image as ImageIcon, X, RefreshCw, Lock, Unlock, AlertTriangle, Copy, Bell } from 'lucide-react'; import { uploadExamImage } from '../services/supabase'; import { useDialog } from '../DialogContext'; interface ExamsProps { data: SchoolData; updateData: (newData: Partial) => void; } const Exams: React.FC = ({ data, updateData }) => { const [searchTerm, setSearchTerm] = useState(''); const [currentView, setCurrentView] = useState<'list' | 'builder'>('list'); const [editingExam, setEditingExam] = useState(null); const [isUploading, setIsUploading] = useState(false); const [duplicatingExam, setDuplicatingExam] = useState(null); const [targetClassId, setTargetClassId] = useState(''); const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos'); const { showAlert, showConfirm } = useDialog(); const [dbClasses, setDbClasses] = useState(data?.classes || []); const [dbCourses, setDbCourses] = useState(data?.courses || []); const [dbSubjects, setDbSubjects] = useState(data?.subjects || []); const [dbExams, setDbExams] = useState(data.exams || []); const loadExams = async () => { try { const res = await fetch('/api/provas'); if (res.ok) { const { provas } = await res.json(); setDbExams(provas.map((p: any) => ({ id: p.id, classId: p.classId || p.turma_id, subjectId: p.subjectId || p.disciplina_id, periodId: p.periodId || p.periodo_id, title: p.title || p.titulo, durationMinutes: p.durationMinutes || p.duracao_minutos, status: p.status, allowRetake: p.allowRetake ?? p.permitir_refacao ?? false, isDeleted: p.isDeleted ?? p.is_deleted ?? false, evaluationType: p.evaluationType || p.evaluation_type || 'exam', questions: p.questions || [] }))); } } catch(e) { console.error(e); } }; React.useEffect(() => { loadExams(); }, []); React.useEffect(() => { Promise.all([ fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })), fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })), fetch('/api/disciplinas').catch(() => ({ ok: false, json: async () => ({}) })) ]).then(async (responses) => { const [resT, resC, resS] = responses; if (resT && resT.ok) { const jsonT = await resT.json(); if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({ id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0) }))); } if (resC && resC.ok) { const jsonC = await resC.json(); if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({ id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0) }))); } if (resS && resS.ok) { const jsonS = await resS.json(); if (jsonS.disciplinas) setDbSubjects(jsonS.disciplinas.map((d: any) => ({ id: d.id, name: d.nome }))); } }).catch(console.error); }, []); const normalizePhotoUrl = (url?: string) => { if (!url) return ''; if (url.startsWith('data:image') || url.startsWith('/storage')) return url; try { const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/); if (match) return `/storage/${match[1]}`; } catch (e) { } return url; }; const exams = dbExams || []; const filteredExams = exams.filter(exam => (activeTab === 'ativos' ? !exam.isDeleted : !!exam.isDeleted) && (exam.title.toLowerCase().includes(searchTerm.toLowerCase()) || dbClasses.find(c => c.id === exam.classId)?.name.toLowerCase().includes(searchTerm.toLowerCase())) ); const handleStartCreate = () => { setEditingExam({ id: Date.now().toString(), title: '', classId: dbClasses[0]?.id || '', durationMinutes: 60, status: 'draft', questions: [], evaluationType: 'exam', maxScore: 10, allowRetake: false } as any); setCurrentView('builder'); }; const handleEditExam = (exam: Exam) => { setEditingExam({ ...exam }); setCurrentView('builder'); }; const handleToggleRetake = async (examId: string) => { const targetExam = exams.find(e => e.id === examId); if (!targetExam) return; try { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, allowRetake: !targetExam.allowRetake }) }); await loadExams(); } catch (e) { console.error('Erro ao alterar refação:', e); } }; const handleDeleteExam = (examId: string) => { showConfirm( 'Mover para Lixeira', 'Tem certeza que deseja mover esta avaliação para a lixeira? Ela será ocultada para os alunos, mas as notas no boletim continuarão intactas.', async () => { try { const targetExam = exams.find(e => e.id === examId); if (targetExam) { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, isDeleted: true }) }); } } catch (e) { console.error('Erro ao deletar prova:', e); } await loadExams(); showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success'); } ); }; const handleRestoreExam = async (examId: string) => { try { const targetExam = exams.find(e => e.id === examId); if (targetExam) { await fetch(`/api/provas/${examId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...targetExam, isDeleted: false }) }); } } catch (e) { console.error('Erro ao restaurar prova:', e); } await loadExams(); showAlert('Sucesso', 'Avaliação reativada.', 'success'); }; const handlePermanentDelete = (examId: string) => { showConfirm( 'Excluir Permanentemente', '⚠️ Atenção: Esta ação irá apagar esta avaliação e todas as suas questões PERMANENTEMENTE do banco de dados. As submissões dos alunos também serão removidas. Esta ação NÃO pode ser desfeita. Deseja continuar?', async () => { try { const response = await fetch(`/api/provas/${examId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Falha ao excluir'); await loadExams(); showAlert('Sucesso', 'Avaliação excluída permanentemente.', 'success'); } catch (e) { console.error('Erro ao excluir permanentemente:', e); showAlert('Erro', 'Falha ao excluir a avaliação do servidor.', 'error'); } } ); }; const handleDuplicateExam = async () => { if (!duplicatingExam || !targetClassId) return; const newExam: Exam = { ...duplicatingExam, id: Date.now().toString() + Math.random().toString(36).substring(7), classId: targetClassId, status: 'draft', title: `${duplicatingExam.title} (Cópia)`, questions: (duplicatingExam.questions || []).map((q: any) => ({ ...q, id: Date.now().toString() + Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 6) })) }; try { const response = await fetch('/api/provas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newExam) }); if (!response.ok) throw new Error('Falha ao duplicar prova'); await loadExams(); showAlert('Sucesso', 'Avaliação duplicada com sucesso!', 'success'); } catch (e) { console.error('Erro ao duplicar:', e); showAlert('Erro', 'Falha ao duplicar a avaliação.', 'error'); } setDuplicatingExam(null); setTargetClassId(''); }; const handleAddQuestion = () => { if (!editingExam) return; setEditingExam({ ...editingExam, questions: [ ...editingExam.questions, { id: Date.now().toString() + Math.random().toString(36).substring(7), text: '', options: ['', '', '', ''], correctOptionIndex: 0 } ] }); }; const handleRemoveQuestion = (qIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions.splice(qIndex, 1); setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleQuestionChange = (qIndex: number, field: keyof Question, value: any) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex] = { ...newQuestions[qIndex], [field]: value }; setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleOptionChange = (qIndex: number, oIndex: number, value: string) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; const newOptions = [...newQuestions[qIndex].options]; newOptions[oIndex] = value; newQuestions[qIndex].options = newOptions; setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleAddOption = (qIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex].options.push(''); setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleRemoveOption = (qIndex: number, oIndex: number) => { if (!editingExam) return; const newQuestions = [...editingExam.questions]; newQuestions[qIndex].options.splice(oIndex, 1); // Adjust correctOptionIndex if needed if (newQuestions[qIndex].correctOptionIndex >= newQuestions[qIndex].options.length) { newQuestions[qIndex].correctOptionIndex = Math.max(0, newQuestions[qIndex].options.length - 1); } else if (newQuestions[qIndex].correctOptionIndex === oIndex) { newQuestions[qIndex].correctOptionIndex = 0; } setEditingExam({ ...editingExam, questions: newQuestions }); }; const handleImageUpload = async (qIndex: number, event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setIsUploading(true); try { const url = await uploadExamImage(file); if (url) { handleQuestionChange(qIndex, 'imageUrl', url); } else { alert('Falha ao obter URL pública da imagem após o upload.'); } } catch (error: any) { console.error(error); const errorMessage = error.message || 'Erro desconhecido'; alert(`Erro ao enviar imagem: ${errorMessage}\n\nVerifique sua conexão ou a configuração do bucket "exames" no MinIO.`); } finally { setIsUploading(false); if (event.target) { event.target.value = ''; // Reset file input } } }; const handleNotifyStudents = async (exam: Exam) => { const classObj = (dbClasses || []).find(c => c.id === exam.classId); if (!classObj) return; showConfirm( 'Notificar Turma', `Deseja enviar WhatsApp e notificação no portal para os alunos da turma ${classObj.name} informando sobre esta avaliação?`, async () => { try { const resp = await fetch('/api/exames/notificar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ examId: exam.id }) }); const resData = await resp.json(); if (resp.ok) { showAlert('Sucesso', 'Notificações enviadas com sucesso!', 'success'); } else { showAlert('Erro', resData.error || 'Erro ao notificar turma.', 'error'); } } catch (e) { showAlert('Erro', 'Erro de conexão.', 'error'); } } ); }; const handleSave = async (status: 'draft' | 'published') => { if (!editingExam) return; if (!editingExam.title || !editingExam.classId) { showAlert('Atenção', 'Preencha o título e a turma antes de salvar.', 'warning'); return; } if (status === 'published' && (!editingExam.subjectId || !editingExam.periodId)) { showAlert( 'Vínculo Obrigatório', 'Para PUBLICAR a avaliação e permitir que as notas entrem no Boletim Escolar, você precisa vincular uma Disciplina e um Período.', 'warning' ); return; } const finalExam = { ...editingExam, status }; const isNew = !dbExams.some(e => e.id === finalExam.id); const endpoint = isNew ? '/api/provas' : `/api/provas/${finalExam.id}`; const method = isNew ? 'POST' : 'PUT'; try { const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(finalExam) }); if (!response.ok) throw new Error('Falha ao salvar avaliação'); await loadExams(); showAlert('Sucesso', status === 'published' ? 'Avaliação publicada com sucesso!' : 'Rascunho salvo com sucesso!', 'success'); } catch (e) { console.error('Erro ao salvar prova:', e); showAlert('Erro', 'Falha ao salvar a avaliação no servidor.', 'error'); return; } setCurrentView('list'); setEditingExam(null); }; if (currentView === 'builder' && editingExam) { return (

Criador de Provas

Configure os detalhes e as questões da avaliação.

{/* Informações Básicas */}

Informações Básicas

setEditingExam({ ...editingExam, title: e.target.value })} placeholder="Ex: Prova Bimestral de Matemática" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800" />
setEditingExam({ ...editingExam, maxScore: parseFloat(e.target.value) || 0 } as any)} min="0" step="0.1" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800" />
setEditingExam({ ...editingExam, durationMinutes: parseInt(e.target.value) || 0 })} min="0" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800" />

Obrigatório para Publicar. A nota irá automaticamente para o boletim.

Obrigatório para Publicar. Define em qual coluna do boletim a nota entra.

{/* Questões */}
{editingExam.questions.map((question, qIndex) => (

{qIndex + 1} Questão

{/* Enunciado */}