fix: normalização de IDs e melhoria no feedback de envio de provas (Portal/Manager)

This commit is contained in:
Sidney 2026-05-01 10:41:02 -03:00
parent 5d1e7876be
commit 26dc4210eb
5 changed files with 53 additions and 25 deletions

View File

@ -144,7 +144,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
initialGrades[subject.id] = {};
periods.forEach(period => {
const periodGrades: any = {};
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published');
const linkedExams = (data.exams || []).filter(e => String(e.subjectId) === String(subject.id) && String(e.periodId) === String(period.id) && e.status === 'published');
if (linkedExams.length > 0) {
linkedExams.forEach(exam => {
@ -518,7 +518,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
<div className="space-y-6">
{subjects.map(subject => {
// Encontrar provas vinculadas a esta disciplina
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.status === 'published');
const linkedExams = (data.exams || []).filter(e => String(e.subjectId) === String(subject.id) && e.status === 'published');
return (
<div key={subject.id} className="bg-slate-50 rounded-2xl p-6 border border-slate-100 space-y-4">
@ -529,7 +529,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
</div>
<div className="flex items-center gap-2">
{(() => {
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.status === 'published');
const linkedExams = (data.exams || []).filter(e => String(e.subjectId) === String(subject.id) && e.status === 'published');
const provasCount = linkedExams.filter(e => (e as any).evaluationType !== 'activity').length;
const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length;
return (
@ -561,7 +561,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
</div>
<div className="flex flex-col gap-6">
{periods.map(period => {
const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published');
const linkedExams = (data.exams || []).filter(e => String(e.subjectId) === String(subject.id) && String(e.periodId) === String(period.id) && e.status === 'published');
const periodGrades = studentGrades[subject.id]?.[period.id] || {};
const periodSum: number = Object.values(periodGrades).reduce<number>((a, b: any) => a + (b !== '' ? Number(b) : 0), 0);

View File

@ -306,8 +306,8 @@ app.get('/api/student-submissions/:studentId', async (req, res) => {
try {
const { studentId } = req.params;
const { rows } = await pool.query(
'SELECT prova_id, acertos, erros FROM provas_submissoes WHERE aluno_id = $1',
[studentId]
'SELECT prova_id as "prova_id", acertos, erros FROM provas_submissoes WHERE aluno_id = $1',
[String(studentId)]
);
res.json({ submissions: rows });
} catch (err) {
@ -321,7 +321,12 @@ app.get('/api/student-submissions/:studentId', async (req, res) => {
// ============================================================
app.get('/api/notas/:alunoId', async (req, res) => {
try {
const notas = await getNotasByAluno(req.params.alunoId);
const { rows: dbNotas } = await pool.query(
'SELECT id, aluno_id as "aluno_id", disciplina_id as "disciplina_id", periodo_id as "periodo_id", prova_id as "prova_id", valor as "valor" FROM notas_boletim WHERE aluno_id = $1',
[req.params.alunoId]
);
// Garantir cast numérico para evitar erro de .toFixed no frontend
const notas = dbNotas.map(n => ({ ...n, valor: Number(n.valor) }));
res.json({ notas });
} catch (err) {
console.error('Erro ao buscar notas do aluno:', err);

View File

@ -264,9 +264,9 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => {
);
const enrichedGrades = grades.map((g) => {
const subject = subjects.find((s) => s.id === g.subjectId);
const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null;
const periodObj = (schoolData.periods || []).find(p => p.id === g.period);
const subject = subjects.find((s) => String(s.id) === String(g.subjectId));
const exam = g.examId ? (schoolData.exams || []).find(e => String(e.id) === String(g.examId)) : null;
const periodObj = (schoolData.periods || []).find(p => String(p.id) === String(g.period));
const submission = g.examId ? submissions.find(s => String(s.prova_id) === String(g.examId)) : null;

View File

@ -3,7 +3,7 @@ import { useAuth } from '../context/AuthContext';
import type { Exam, ExamSubmission } from '../types';
import {
ClipboardList, Clock, ChevronLeft, ChevronRight, Send, CheckCircle2,
XCircle, Award, AlertTriangle, Timer, ArrowLeft
XCircle, Award, AlertTriangle, Timer, ArrowLeft, Loader2
} from 'lucide-react';
import { normalizePhotoUrl } from '../helpers';
@ -40,7 +40,7 @@ export default function Avaliacoes() {
// In-app modal state (replaces native alert/confirm)
const [modalMsg, setModalMsg] = useState('');
const [modalType, setModalType] = useState<'info' | 'error' | 'confirm'>('info');
const [modalType, setModalType] = useState<'info' | 'error' | 'confirm' | 'loading'>('info');
const [showModal, setShowModal] = useState(false);
const [confirmCallback, setConfirmCallback] = useState<(() => void) | null>(null);
@ -120,6 +120,13 @@ export default function Avaliacoes() {
const handleSubmit = async (autoSubmit = false) => {
if (submitting || !activeExam) return;
setSubmitting(true);
const typeLabel = (activeExam as any).evaluationType === 'activity' ? 'atividade' : 'prova';
// Show Loading Modal
setModalType('loading');
setModalMsg(`Enviando sua ${typeLabel}... Por favor, aguarde e não feche esta janela.`);
setShowModal(true);
if (timerRef.current) clearInterval(timerRef.current);
@ -136,16 +143,28 @@ export default function Avaliacoes() {
const data = await res.json();
if (data.success) {
setResult(data.result);
setView('result');
fetchExams();
// Show Success Modal
setModalType('info');
setModalMsg(`Sua ${typeLabel} foi enviada com sucesso! Clique em OK para ver seu resultado.`);
setShowModal(true);
// Wait for user to click OK before showing result?
// No, user wants result. But let's follow the "sent successfully" request.
// We'll set a callback to OK button to show result
setConfirmCallback(() => {
setResult(data.result);
setView('result');
fetchExams();
});
} else {
showAppAlert(data.error || 'Erro ao enviar prova.', 'error');
const errorCode = `ERR-${activeExam.id.substring(0, 4)}-${new Date().getTime().toString().slice(-4)}`;
showAppAlert(`Não foi possível enviar sua nota. Tente novamente ou contate o suporte. (Código: ${errorCode})`, 'error');
if (!autoSubmit) setView('listing');
}
} catch (err) {
console.error(err);
showAppAlert('Erro de conexão ao enviar prova.', 'error');
const errorCode = `CONN-ERR-${new Date().getTime().toString().slice(-4)}`;
showAppAlert(`Erro de conexão ao enviar prova. Verifique sua internet. (Código: ${errorCode})`, 'error');
} finally {
setSubmitting(false);
}
@ -445,14 +464,17 @@ export default function Avaliacoes() {
? <XCircle size={28} color="var(--color-danger)" />
: modalType === 'confirm'
? <AlertTriangle size={28} color="var(--color-warning)" />
: modalType === 'loading'
? <Loader2 size={28} color="var(--color-primary)" className="animate-spin" />
: <CheckCircle2 size={28} color="var(--color-primary)" />
}
</div>
<p style={{ fontSize: '0.9rem', fontWeight: 500, marginBottom: '1.5rem', lineHeight: 1.5 }}>
<p style={{ fontSize: '0.9rem', fontWeight: 500, marginBottom: modalType === 'loading' ? 0 : '1.5rem', lineHeight: 1.5 }}>
{modalMsg}
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
{modalType === 'confirm' ? (
{modalType !== 'loading' && (
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
{modalType === 'confirm' ? (
<>
<button
onClick={() => setShowModal(false)}
@ -492,8 +514,9 @@ export default function Avaliacoes() {
>
OK
</button>
)}
</div>
)}
</div>
)}
</div>
</div>
)}

View File

@ -52,7 +52,7 @@ export default function Notas() {
? allSubjects
: [...new Set(grades.map(g => g.subjectId))].map(id => ({
id,
name: grades.find(g => g.subjectId === id)?.subjectName || id
name: grades.find(g => String(g.subjectId) === String(id))?.subjectName || id
}));
// General average logic
@ -132,7 +132,7 @@ export default function Notas() {
{displaySubjects.map((subject, idx) => {
const subjectId = typeof subject === 'string' ? subject : subject.id;
const subjectName = typeof subject === 'string' ? subject : subject.name;
const subjectGrades = grades.filter(g => g.subjectId === subjectId);
const subjectGrades = grades.filter(g => String(g.subjectId) === String(subjectId));
return (
<div key={subjectId} className="glass-card animate-fade-in" style={{ marginBottom: '1.5rem', overflow: 'hidden' }}>
@ -142,7 +142,7 @@ export default function Notas() {
<div style={{ padding: '1.5rem' }}>
{periods.map(period => {
const periodGrades = subjectGrades.filter(g => (g.periodName || g.period) === period);
const periodGrades = subjectGrades.filter(g => String(g.periodName || g.period) === String(period));
if (periodGrades.length === 0) return null;
const periodTotal = periodGrades.reduce((sum, g) => sum + g.value, 0);