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 { 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<ExamsProps> = ({ data, updateData }) => {
setCurrentView('builder');
};
const handleToggleRetake = (examId: string) => {
const updatedExams = exams.map(e => {
if (e.id === examId) {
return { ...e, allowRetake: !e.allowRetake };
}
return e;
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 })
});
updateData({ exams: updatedExams });
dbService.saveData({ ...data, exams: updatedExams });
await loadExams();
} catch (e) {
console.error('Erro ao alterar refação:', e);
}
};
const handleDeleteExam = (examId: string) => {
@ -149,9 +153,7 @@ const Exams: React.FC<ExamsProps> = ({ 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<ExamsProps> = ({ 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<ExamsProps> = ({ 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<ExamsProps> = ({ 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);
};

View File

@ -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);

View File

@ -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');