From 4903bad94ddb40230068eeabfa554d0195421cff Mon Sep 17 00:00:00 2001 From: Sidney Date: Mon, 25 May 2026 19:14:59 -0300 Subject: [PATCH] feat: migra Provas/Atividades (Exams.tsx) para 100% SQL-First com questoes e reverse sync --- manager/components/Exams.tsx | 80 +++++++++++++++++++++--------------- manager/server.selfhosted.js | 46 +++++++++++++++++++-- manager/services/database.js | 4 +- 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx index 1387a73..2c5dcb0 100644 --- a/manager/components/Exams.tsx +++ b/manager/components/Exams.tsx @@ -3,7 +3,7 @@ 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; @@ -122,15 +122,19 @@ const Exams: React.FC = ({ data, updateData }) => { 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 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) => { @@ -149,9 +153,7 @@ const Exams: React.FC = ({ data, updateData }) => { } } catch (e) { console.error('Erro ao deletar prova:', e); } - const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: true } : e); - updateData({ exams: updatedExams }); - dbService.saveData({ ...data, exams: updatedExams }); + await loadExams(); showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success'); } ); @@ -169,30 +171,37 @@ const Exams: React.FC = ({ data, updateData }) => { } } catch (e) { console.error('Erro ao restaurar prova:', e); } - const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: false } : e); - updateData({ exams: updatedExams }); - dbService.saveData({ ...data, exams: updatedExams }); + await loadExams(); showAlert('Sucesso', 'Avaliação reativada.', 'success'); }; - const handleDuplicateExam = () => { + 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', // Sempre começa como rascunho para segurança + status: 'draft', title: `${duplicatingExam.title} (Cópia)` }; - const updatedExams = [...exams, newExam]; - updateData({ exams: updatedExams }); - dbService.saveData({ ...data, exams: updatedExams }); + 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(''); - showAlert('Sucesso', 'Avaliação duplicada com sucesso!', 'success'); }; const handleAddQuestion = () => { @@ -306,7 +315,7 @@ const Exams: React.FC = ({ data, updateData }) => { ); }; - const handleSave = (status: 'draft' | 'published') => { + const handleSave = async (status: 'draft' | 'published') => { if (!editingExam) return; if (!editingExam.title || !editingExam.classId) { @@ -324,18 +333,25 @@ const Exams: React.FC = ({ data, updateData }) => { } const finalExam = { ...editingExam, status }; - const currentExams = data.exams || []; - const existingIndex = currentExams.findIndex(e => e.id === finalExam.id); + const isNew = !dbExams.some(e => e.id === finalExam.id); + const endpoint = isNew ? '/api/provas' : `/api/provas/${finalExam.id}`; + const method = isNew ? 'POST' : 'PUT'; - let newExams; - if (existingIndex >= 0) { - newExams = [...currentExams]; - newExams[existingIndex] = finalExam; - } else { - newExams = [...currentExams, finalExam]; + 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; } - updateData({ exams: newExams }); setCurrentView('list'); setEditingExam(null); }; diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index a48eac8..9571e5a 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -733,7 +733,21 @@ app.get('/api/provas/:id/questoes', async (req, res) => { app.post('/api/provas', async (req, res) => { try { - await insertProva(req.body); + const prova = req.body; + await insertProva(prova); + + // Sync questions if provided + if (prova.questions && prova.questions.length > 0) { + await syncQuestoesProva(prova.id, prova.questions); + } + + // Reverse sync to legacy JSON + const appData = await getSchoolData(); + const dbProvas = await getProvas(); + appData.exams = dbProvas; + appData.lastUpdated = new Date().toISOString(); + await saveSchoolData(appData); + res.json({ success: true }); } catch (error) { console.error('Erro ao criar prova:', error); @@ -743,7 +757,22 @@ app.post('/api/provas', async (req, res) => { app.put('/api/provas/:id', async (req, res) => { try { - await updateProva(req.params.id, req.body); + const { id } = req.params; + const prova = req.body; + await updateProva(id, prova); + + // Sync questions if provided + if (prova.questions) { + await syncQuestoesProva(id, prova.questions); + } + + // Reverse sync to legacy JSON + const appData = await getSchoolData(); + const dbProvas = await getProvas(); + appData.exams = dbProvas; + appData.lastUpdated = new Date().toISOString(); + await saveSchoolData(appData); + res.json({ success: true }); } catch (error) { console.error('Erro ao atualizar prova:', error); @@ -753,7 +782,18 @@ app.put('/api/provas/:id', async (req, res) => { app.delete('/api/provas/:id', async (req, res) => { try { - await deleteProva(req.params.id); + const { id } = req.params; + // Cleanup relational dependencies + await pool.query('DELETE FROM questoes_provas WHERE prova_id = $1', [id]); + await pool.query('DELETE FROM provas_submissoes WHERE prova_id = $1', [id]); + await deleteProva(id); + + // Reverse sync to legacy JSON + const appData = await getSchoolData(); + appData.exams = (appData.exams || []).filter(e => e.id !== id); + appData.lastUpdated = new Date().toISOString(); + await saveSchoolData(appData); + res.json({ success: true }); } catch (error) { console.error('Erro ao deletar prova:', error); diff --git a/manager/services/database.js b/manager/services/database.js index 26c74d6..5b217b7 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -917,7 +917,7 @@ export async function getProvas() { examId: q.prova_id, text: q.texto, options: q.opcoes || [], - correctAnswer: q.indice_correto, + correctOptionIndex: q.indice_correto ?? 0, order: q.ordem, imageUrl: q.imagem_url })) @@ -975,7 +975,7 @@ export async function syncQuestoesProva(provaId, questoes) { await client.query( `INSERT INTO questoes_provas (id, prova_id, texto, imagem_url, opcoes, indice_correto, ordem) VALUES ($1, $2, $3, $4, $5, $6, $7)`, - [q.id || require('crypto').randomUUID(), provaId, q.texto || q.text, q.imagem_url || q.imageUrl, JSON.stringify(q.opcoes || q.options || []), q.indice_correto ?? q.correctIndex ?? 0, i] + [q.id || require('crypto').randomUUID(), provaId, q.texto || q.text, q.imagem_url || q.imageUrl, JSON.stringify(q.opcoes || q.options || []), q.indice_correto ?? q.correctOptionIndex ?? q.correctIndex ?? 0, i] ); } await client.query('COMMIT');