feat: migra Provas/Atividades (Exams.tsx) para 100% SQL-First com questoes e reverse sync
This commit is contained in:
parent
a0810b691a
commit
4903bad94d
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue