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'; import { dbService } from '../services/dbService'; 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.turma_id, subjectId: p.disciplina_id, periodId: p.periodo_id, title: p.titulo, durationMinutes: p.duracao_minutos, status: p.status, allowRetake: p.permitir_refacao, isDeleted: p.is_deleted, evaluationType: p.evaluation_type || 'exam', questions: [] // questoes carregadas sob demanda }))); } } 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 = (examId: string) => { const updatedExams = exams.map(e => { if (e.id === examId) { return { ...e, allowRetake: !e.allowRetake }; } return e; }); updateData({ exams: updatedExams }); dbService.saveData({ ...data, exams: updatedExams }); }; 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.', () => { const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: true } : e); updateData({ exams: updatedExams }); dbService.saveData({ ...data, exams: updatedExams }); showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success'); } ); }; const handleRestoreExam = (examId: string) => { const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: false } : e); updateData({ exams: updatedExams }); dbService.saveData({ ...data, exams: updatedExams }); showAlert('Sucesso', 'Avaliação reativada.', 'success'); }; const handleDuplicateExam = () => { if (!duplicatingExam || !targetClassId) return; const newExam: Exam = { ...duplicatingExam, id: Date.now().toString() + Math.random().toString(36).substring(7), classId: targetClassId, status: 'draft', // Sempre começa como rascunho para segurança title: `${duplicatingExam.title} (Cópia)` }; const updatedExams = [...exams, newExam]; updateData({ exams: updatedExams }); dbService.saveData({ ...data, exams: updatedExams }); setDuplicatingExam(null); setTargetClassId(''); showAlert('Sucesso', 'Avaliação duplicada com sucesso!', 'success'); }; 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 = (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 currentExams = data.exams || []; const existingIndex = currentExams.findIndex(e => e.id === finalExam.id); let newExams; if (existingIndex >= 0) { newExams = [...currentExams]; newExams[existingIndex] = finalExam; } else { newExams = [...currentExams, finalExam]; } updateData({ exams: newExams }); 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 */}