fix: normalização de IDs e melhoria no feedback de envio de provas (Portal/Manager)
This commit is contained in:
parent
5d1e7876be
commit
26dc4210eb
|
|
@ -144,7 +144,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
initialGrades[subject.id] = {};
|
initialGrades[subject.id] = {};
|
||||||
periods.forEach(period => {
|
periods.forEach(period => {
|
||||||
const periodGrades: any = {};
|
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) {
|
if (linkedExams.length > 0) {
|
||||||
linkedExams.forEach(exam => {
|
linkedExams.forEach(exam => {
|
||||||
|
|
@ -518,7 +518,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{subjects.map(subject => {
|
{subjects.map(subject => {
|
||||||
// Encontrar provas vinculadas a esta disciplina
|
// 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 (
|
return (
|
||||||
<div key={subject.id} className="bg-slate-50 rounded-2xl p-6 border border-slate-100 space-y-4">
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<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 provasCount = linkedExams.filter(e => (e as any).evaluationType !== 'activity').length;
|
||||||
const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length;
|
const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length;
|
||||||
return (
|
return (
|
||||||
|
|
@ -561,7 +561,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{periods.map(period => {
|
{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 periodGrades = studentGrades[subject.id]?.[period.id] || {};
|
||||||
const periodSum: number = Object.values(periodGrades).reduce<number>((a, b: any) => a + (b !== '' ? Number(b) : 0), 0);
|
const periodSum: number = Object.values(periodGrades).reduce<number>((a, b: any) => a + (b !== '' ? Number(b) : 0), 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -306,8 +306,8 @@ app.get('/api/student-submissions/:studentId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { studentId } = req.params;
|
const { studentId } = req.params;
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
'SELECT prova_id, acertos, erros FROM provas_submissoes WHERE aluno_id = $1',
|
'SELECT prova_id as "prova_id", acertos, erros FROM provas_submissoes WHERE aluno_id = $1',
|
||||||
[studentId]
|
[String(studentId)]
|
||||||
);
|
);
|
||||||
res.json({ submissions: rows });
|
res.json({ submissions: rows });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -321,7 +321,12 @@ app.get('/api/student-submissions/:studentId', async (req, res) => {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
app.get('/api/notas/:alunoId', async (req, res) => {
|
app.get('/api/notas/:alunoId', async (req, res) => {
|
||||||
try {
|
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 });
|
res.json({ notas });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao buscar notas do aluno:', err);
|
console.error('Erro ao buscar notas do aluno:', err);
|
||||||
|
|
|
||||||
|
|
@ -264,9 +264,9 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const enrichedGrades = grades.map((g) => {
|
const enrichedGrades = grades.map((g) => {
|
||||||
const subject = subjects.find((s) => s.id === g.subjectId);
|
const subject = subjects.find((s) => String(s.id) === String(g.subjectId));
|
||||||
const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null;
|
const exam = g.examId ? (schoolData.exams || []).find(e => String(e.id) === String(g.examId)) : null;
|
||||||
const periodObj = (schoolData.periods || []).find(p => p.id === g.period);
|
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;
|
const submission = g.examId ? submissions.find(s => String(s.prova_id) === String(g.examId)) : null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useAuth } from '../context/AuthContext';
|
||||||
import type { Exam, ExamSubmission } from '../types';
|
import type { Exam, ExamSubmission } from '../types';
|
||||||
import {
|
import {
|
||||||
ClipboardList, Clock, ChevronLeft, ChevronRight, Send, CheckCircle2,
|
ClipboardList, Clock, ChevronLeft, ChevronRight, Send, CheckCircle2,
|
||||||
XCircle, Award, AlertTriangle, Timer, ArrowLeft
|
XCircle, Award, AlertTriangle, Timer, ArrowLeft, Loader2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { normalizePhotoUrl } from '../helpers';
|
import { normalizePhotoUrl } from '../helpers';
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function Avaliacoes() {
|
||||||
|
|
||||||
// In-app modal state (replaces native alert/confirm)
|
// In-app modal state (replaces native alert/confirm)
|
||||||
const [modalMsg, setModalMsg] = useState('');
|
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 [showModal, setShowModal] = useState(false);
|
||||||
const [confirmCallback, setConfirmCallback] = useState<(() => void) | null>(null);
|
const [confirmCallback, setConfirmCallback] = useState<(() => void) | null>(null);
|
||||||
|
|
||||||
|
|
@ -120,6 +120,13 @@ export default function Avaliacoes() {
|
||||||
const handleSubmit = async (autoSubmit = false) => {
|
const handleSubmit = async (autoSubmit = false) => {
|
||||||
if (submitting || !activeExam) return;
|
if (submitting || !activeExam) return;
|
||||||
setSubmitting(true);
|
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);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
|
|
||||||
|
|
@ -136,16 +143,28 @@ export default function Avaliacoes() {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setResult(data.result);
|
// Show Success Modal
|
||||||
setView('result');
|
setModalType('info');
|
||||||
fetchExams();
|
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 {
|
} 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');
|
if (!autoSubmit) setView('listing');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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 {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -445,14 +464,17 @@ export default function Avaliacoes() {
|
||||||
? <XCircle size={28} color="var(--color-danger)" />
|
? <XCircle size={28} color="var(--color-danger)" />
|
||||||
: modalType === 'confirm'
|
: modalType === 'confirm'
|
||||||
? <AlertTriangle size={28} color="var(--color-warning)" />
|
? <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)" />
|
: <CheckCircle2 size={28} color="var(--color-primary)" />
|
||||||
}
|
}
|
||||||
</div>
|
</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}
|
{modalMsg}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
|
{modalType !== 'loading' && (
|
||||||
{modalType === 'confirm' ? (
|
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
|
||||||
|
{modalType === 'confirm' ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowModal(false)}
|
onClick={() => setShowModal(false)}
|
||||||
|
|
@ -492,8 +514,9 @@ export default function Avaliacoes() {
|
||||||
>
|
>
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export default function Notas() {
|
||||||
? allSubjects
|
? allSubjects
|
||||||
: [...new Set(grades.map(g => g.subjectId))].map(id => ({
|
: [...new Set(grades.map(g => g.subjectId))].map(id => ({
|
||||||
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
|
// General average logic
|
||||||
|
|
@ -132,7 +132,7 @@ export default function Notas() {
|
||||||
{displaySubjects.map((subject, idx) => {
|
{displaySubjects.map((subject, idx) => {
|
||||||
const subjectId = typeof subject === 'string' ? subject : subject.id;
|
const subjectId = typeof subject === 'string' ? subject : subject.id;
|
||||||
const subjectName = typeof subject === 'string' ? subject : subject.name;
|
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 (
|
return (
|
||||||
<div key={subjectId} className="glass-card animate-fade-in" style={{ marginBottom: '1.5rem', overflow: 'hidden' }}>
|
<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' }}>
|
<div style={{ padding: '1.5rem' }}>
|
||||||
{periods.map(period => {
|
{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;
|
if (periodGrades.length === 0) return null;
|
||||||
|
|
||||||
const periodTotal = periodGrades.reduce((sum, g) => sum + g.value, 0);
|
const periodTotal = periodGrades.reduce((sum, g) => sum + g.value, 0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue