feat: migra Provas/Atividades (Exams.tsx) para 100% SQL-First com questoes e reverse sync

This commit is contained in:
Sidney 2026-05-25 19:14:59 -03:00
parent a0810b691a
commit 4903bad94d
3 changed files with 93 additions and 37 deletions

View File

@ -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 { 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 { uploadExamImage } from '../services/supabase';
import { useDialog } from '../DialogContext'; import { useDialog } from '../DialogContext';
import { dbService } from '../services/dbService';
interface ExamsProps { interface ExamsProps {
data: SchoolData; data: SchoolData;
@ -122,15 +122,19 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
setCurrentView('builder'); setCurrentView('builder');
}; };
const handleToggleRetake = (examId: string) => { const handleToggleRetake = async (examId: string) => {
const updatedExams = exams.map(e => { const targetExam = exams.find(e => e.id === examId);
if (e.id === examId) { if (!targetExam) return;
return { ...e, allowRetake: !e.allowRetake }; try {
} await fetch(`/api/provas/${examId}`, {
return e; method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...targetExam, allowRetake: !targetExam.allowRetake })
}); });
updateData({ exams: updatedExams }); await loadExams();
dbService.saveData({ ...data, exams: updatedExams }); } catch (e) {
console.error('Erro ao alterar refação:', e);
}
}; };
const handleDeleteExam = (examId: string) => { const handleDeleteExam = (examId: string) => {
@ -149,9 +153,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} }
} catch (e) { console.error('Erro ao deletar prova:', e); } } catch (e) { console.error('Erro ao deletar prova:', e); }
const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: true } : e); await loadExams();
updateData({ exams: updatedExams });
dbService.saveData({ ...data, exams: updatedExams });
showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success'); showAlert('Sucesso', 'Avaliação movida para a lixeira.', 'success');
} }
); );
@ -169,30 +171,37 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} }
} catch (e) { console.error('Erro ao restaurar prova:', e); } } catch (e) { console.error('Erro ao restaurar prova:', e); }
const updatedExams = exams.map(e => e.id === examId ? { ...e, isDeleted: false } : e); await loadExams();
updateData({ exams: updatedExams });
dbService.saveData({ ...data, exams: updatedExams });
showAlert('Sucesso', 'Avaliação reativada.', 'success'); showAlert('Sucesso', 'Avaliação reativada.', 'success');
}; };
const handleDuplicateExam = () => { const handleDuplicateExam = async () => {
if (!duplicatingExam || !targetClassId) return; if (!duplicatingExam || !targetClassId) return;
const newExam: Exam = { const newExam: Exam = {
...duplicatingExam, ...duplicatingExam,
id: Date.now().toString() + Math.random().toString(36).substring(7), id: Date.now().toString() + Math.random().toString(36).substring(7),
classId: targetClassId, classId: targetClassId,
status: 'draft', // Sempre começa como rascunho para segurança status: 'draft',
title: `${duplicatingExam.title} (Cópia)` title: `${duplicatingExam.title} (Cópia)`
}; };
const updatedExams = [...exams, newExam]; try {
updateData({ exams: updatedExams }); const response = await fetch('/api/provas', {
dbService.saveData({ ...data, exams: updatedExams }); 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); setDuplicatingExam(null);
setTargetClassId(''); setTargetClassId('');
showAlert('Sucesso', 'Avaliação duplicada com sucesso!', 'success');
}; };
const handleAddQuestion = () => { const handleAddQuestion = () => {
@ -306,7 +315,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
); );
}; };
const handleSave = (status: 'draft' | 'published') => { const handleSave = async (status: 'draft' | 'published') => {
if (!editingExam) return; if (!editingExam) return;
if (!editingExam.title || !editingExam.classId) { if (!editingExam.title || !editingExam.classId) {
@ -324,18 +333,25 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
} }
const finalExam = { ...editingExam, status }; const finalExam = { ...editingExam, status };
const currentExams = data.exams || []; const isNew = !dbExams.some(e => e.id === finalExam.id);
const existingIndex = currentExams.findIndex(e => e.id === finalExam.id); const endpoint = isNew ? '/api/provas' : `/api/provas/${finalExam.id}`;
const method = isNew ? 'POST' : 'PUT';
let newExams; try {
if (existingIndex >= 0) { const response = await fetch(endpoint, {
newExams = [...currentExams]; method,
newExams[existingIndex] = finalExam; headers: { 'Content-Type': 'application/json' },
} else { body: JSON.stringify(finalExam)
newExams = [...currentExams, 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'); setCurrentView('list');
setEditingExam(null); setEditingExam(null);
}; };

View File

@ -733,7 +733,21 @@ app.get('/api/provas/:id/questoes', async (req, res) => {
app.post('/api/provas', async (req, res) => { app.post('/api/provas', async (req, res) => {
try { 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 }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao criar prova:', 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) => { app.put('/api/provas/:id', async (req, res) => {
try { 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 }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao atualizar prova:', 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) => { app.delete('/api/provas/:id', async (req, res) => {
try { 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 }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao deletar prova:', error); console.error('Erro ao deletar prova:', error);

View File

@ -917,7 +917,7 @@ export async function getProvas() {
examId: q.prova_id, examId: q.prova_id,
text: q.texto, text: q.texto,
options: q.opcoes || [], options: q.opcoes || [],
correctAnswer: q.indice_correto, correctOptionIndex: q.indice_correto ?? 0,
order: q.ordem, order: q.ordem,
imageUrl: q.imagem_url imageUrl: q.imagem_url
})) }))
@ -975,7 +975,7 @@ export async function syncQuestoesProva(provaId, questoes) {
await client.query( await client.query(
`INSERT INTO questoes_provas (id, prova_id, texto, imagem_url, opcoes, indice_correto, ordem) `INSERT INTO questoes_provas (id, prova_id, texto, imagem_url, opcoes, indice_correto, ordem)
VALUES ($1, $2, $3, $4, $5, $6, $7)`, 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'); await client.query('COMMIT');