feat: novo layout de boletim, suporte a refazer provas e nomenclatura unificada

This commit is contained in:
Sidney 2026-04-29 09:39:12 -03:00
parent 2f50468cc5
commit bf4ebd8b6b
10 changed files with 177 additions and 76 deletions

View File

@ -32,3 +32,6 @@
10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. Notifications SHOULD include the motive text in the `message` field (JSON format: `{text, motivo}`) to allow previews in the manager bell. 10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. Notifications SHOULD include the motive text in the `message` field (JSON format: `{text, motivo}`) to allow previews in the manager bell.
11. **Modal Standards**: System modals should utilize `bg-transparent` (no background darkening or blur) with `shadow-2xl` for depth, ensuring a clean and float-like aesthetic. 11. **Modal Standards**: System modals should utilize `bg-transparent` (no background darkening or blur) with `shadow-2xl` for depth, ensuring a clean and float-like aesthetic.
12. **Messages Automation**: Preventive reminders and overdue settings MUST be managed within their respective template modals (contextual logic), with independent manual triggers for each phase (Overdue vs. Upcoming). 12. **Messages Automation**: Preventive reminders and overdue settings MUST be managed within their respective template modals (contextual logic), with independent manual triggers for each phase (Overdue vs. Upcoming).
13. **Grading & Evaluation Standards**: Assessments are categorized as `exam` (violet/violet labels) or `activity` (sky/blue labels). The report card (Boletim) MUST support multiple evaluations per period, showing the individual breakdown (name and value). The module MUST be named **"Atividades e Provas"** for clarity.
14. **Asaas Safety**: All financial generation forms MUST implement a loading state (`isCreating`) to disable submit buttons and prevent duplicate charges.
15. **Retake Policy**: Students ARE allowed to retake activities and exams. The system MUST delete the previous submission and overwrite the grade in `school_data.json` to ensure only the latest attempt is valid.

View File

@ -12,11 +12,12 @@
- [x] Correção das Imagens de Prova: Normalização das URLs nas questões de avaliações (Portal e Manager). - [x] Correção das Imagens de Prova: Normalização das URLs nas questões de avaliações (Portal e Manager).
- [x] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU. - [x] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU.
- [x] Correção do Sino de Notificações: Botões sempre visíveis, suporte a anexo via chave `arquivo` e exibição do **Motivo da Falta** direto na lista do sino. - [x] Correção do Sino de Notificações: Botões sempre visíveis, suporte a anexo via chave `arquivo` e exibição do **Motivo da Falta** direto na lista do sino.
- [x] **Segurança Financeira:** Implementado estado de carregamento (`isCreating`) no botão de gerar cobranças para impedir disparos duplicados ao Asaas por cliques múltiplos. - [x] **Segurança Financeira:** Implementada trava de segurança (`isCreating`) contra cliques múltiplos em formulários financeiros, resolvendo a duplicidade de cobranças no Asaas.
- [x] **Boletim & Avaliações:** Refatoração completa do sistema de notas para suportar múltiplas avaliações por período, integrando notas diretas e notas vindas de provas/atividades online. - [x] **Boletim Detalhado (Manager):** Upgrade para layout de lista (Full-Width) com cores distintas: **Violeta (Provas)** e **Azul (Atividades)**.
- [x] **Sincronia Portal/Manager:** Ajustada a submissão de provas no Portal para calcular notas via `maxScore` e injetar automaticamente no boletim do Gerenciador via `examId`. - [x] **Retake Logic:** Implementada possibilidade de refazer Provas/Atividades no Portal, com substituição automática da nota anterior e limpeza de submissão no banco.
- [x] **Padronização de Servidor:** Confirmado o uso de `server.selfhosted.js` em ambos os apps como ponto de entrada para garantir 100% de funcionalidades locais. - [x] **Mapeamento de Períodos:** Corrigido o bug que exibia UUIDs (códigos) no boletim do aluno; agora exibe os nomes amigáveis (ex: 1º Bimestre).
- [ ] Próximo Passo: Monitorar o desempenho das submissões de provas simultâneas no Portal. - [x] **Nomenclatura Unificada:** Alterado "Avaliações" para **"Atividades e Provas"** em todo o ecossistema (Portal e Manager).
- [ ] Próximo Passo: Analisar a necessidade de pesos diferenciados (médias ponderadas) entre Atividades e Provas no cálculo do boletim.
### 💳 Módulo Financeiro (Portal do Aluno) ### 💳 Módulo Financeiro (Portal do Aluno)
- **Funcionalidades Implementadas:** - **Funcionalidades Implementadas:**

View File

@ -139,7 +139,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
<div className="absolute top-14 right-0 w-80 sm:w-96 bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden animate-in slide-in-from-top-4 fade-in duration-200 flex flex-col max-h-[80vh]"> <div className="absolute top-14 right-0 w-80 sm:w-96 bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden animate-in slide-in-from-top-4 fade-in duration-200 flex flex-col max-h-[80vh]">
<div className="p-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between sticky top-0 z-10"> <div className="p-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between sticky top-0 z-10">
<div> <div>
<h3 className="font-black text-slate-800 flex items-center gap-2">Avaliações Pendentes <h3 className="font-black text-slate-800 flex items-center gap-2">Atividades/Provas Pendentes
{unreadCount > 0 && <span className="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full text-[10px] font-bold">{unreadCount}</span>} {unreadCount > 0 && <span className="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full text-[10px] font-bold">{unreadCount}</span>}
</h3> </h3>
</div> </div>

View File

@ -419,7 +419,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
<div> <div>
<h2 className="text-3xl font-black text-slate-800 tracking-tight flex items-center gap-3"> <h2 className="text-3xl font-black text-slate-800 tracking-tight flex items-center gap-3">
<BookOpen className="text-indigo-600" size={32} /> Avaliações <BookOpen className="text-indigo-600" size={32} /> Atividades e Provas
</h2> </h2>
<p className="text-slate-500 mt-2 font-medium">Gerencie as provas e testes das turmas.</p> <p className="text-slate-500 mt-2 font-medium">Gerencie as provas e testes das turmas.</p>
</div> </div>

View File

@ -490,12 +490,27 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
<h4 className="font-black text-slate-800 uppercase tracking-wider text-sm">{subject.name}</h4> <h4 className="font-black text-slate-800 uppercase tracking-wider text-sm">{subject.name}</h4>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{linkedExams.length > 0 && ( {(() => {
<div className="px-3 py-1 bg-violet-50 border border-violet-200 rounded-lg text-[10px] font-black text-violet-600 flex items-center gap-1"> const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.status === 'published');
<FileText size={12} /> const provasCount = linkedExams.filter(e => (e as any).evaluationType !== 'activity').length;
{linkedExams.length} {linkedExams.length === 1 ? 'Prova' : 'Provas'} const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length;
</div> return (
)} <>
{provasCount > 0 && (
<div className="px-3 py-1 bg-violet-50 border border-violet-200 rounded-lg text-[10px] font-black text-violet-600 flex items-center gap-1">
<FileText size={12} />
{provasCount} {provasCount === 1 ? 'Prova' : 'Provas'}
</div>
)}
{atividadesCount > 0 && (
<div className="px-3 py-1 bg-sky-50 border border-sky-200 rounded-lg text-[10px] font-black text-sky-600 flex items-center gap-1">
<FileText size={12} />
{atividadesCount} {atividadesCount === 1 ? 'Atividade' : 'Atividades'}
</div>
)}
</>
);
})()}
<div className="px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-black text-slate-500"> <div className="px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-black text-slate-500">
MÉDIA: {(() => { MÉDIA: {(() => {
const subjectGrades = studentGrades[subject.id] || {}; const subjectGrades = studentGrades[subject.id] || {};
@ -506,7 +521,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 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 => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published');
const periodGrades = studentGrades[subject.id]?.[period.id] || {}; const periodGrades = studentGrades[subject.id]?.[period.id] || {};
@ -520,45 +535,53 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
</div> </div>
{linkedExams.length > 0 ? ( {linkedExams.length > 0 ? (
<div className="space-y-3"> <div className="space-y-4">
{linkedExams.map(exam => { {linkedExams.map(exam => {
const isActivity = (exam as any).evaluationType === 'activity'; const isActivity = (exam as any).evaluationType === 'activity';
const maxScore = (exam as any).maxScore ?? 10; const maxScore = (exam as any).maxScore ?? 10;
return ( return (
<div key={exam.id} className="space-y-1"> <div key={exam.id} className={`p-4 rounded-xl border flex flex-col md:flex-row md:items-center justify-between gap-4 ${isActivity ? 'bg-sky-50/50 border-sky-100' : 'bg-violet-50/50 border-violet-100'}`}>
<div className="flex items-center justify-between mb-1"> <div className="flex-1">
<span className="text-[10px] font-bold text-slate-600 truncate pr-2" title={exam.title}> <div className="flex flex-col">
<span className={`mr-1 px-1.5 py-0.5 rounded text-[8px] uppercase tracking-wider ${isActivity ? 'bg-sky-100 text-sky-700' : 'bg-violet-100 text-violet-700'}`}> <div className="text-sm font-bold text-slate-800 leading-tight mb-1 flex items-center gap-2">
{isActivity ? 'Ativ' : 'Prova'} <span className={`px-2 py-0.5 rounded text-[9px] uppercase tracking-wider font-black shrink-0 ${isActivity ? 'bg-sky-200 text-sky-800' : 'bg-violet-200 text-violet-800'}`}>
</span> {isActivity ? 'Atividade' : 'Prova'}
{exam.title} </span>
</span> {exam.title}
<span className="text-[9px] text-slate-400 font-bold whitespace-nowrap shrink-0">Vale {maxScore}</span> </div>
{exam.description && (
<p className="text-xs text-slate-500 leading-snug pr-2">{exam.description}</p>
)}
</div>
</div> </div>
<input
type="number" <div className="flex items-center gap-3 shrink-0">
min="0" <span className="text-[10px] font-bold text-slate-500 uppercase">Nota (Máx {maxScore})</span>
max={maxScore} <input
step="0.1" type="number"
placeholder="—" min="0"
className="w-full px-2 py-1.5 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all text-sm font-bold text-center" max={maxScore}
value={studentGrades[subject.id]?.[period.id]?.[exam.id] ?? ''} step="0.1"
onChange={(e) => { placeholder="—"
let val = parseFloat(e.target.value); className={`w-24 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 transition-all text-sm font-black text-center ${isActivity ? 'bg-white border-sky-200 focus:ring-sky-500' : 'bg-white border-violet-200 focus:ring-violet-500'}`}
if (val > maxScore) val = maxScore; value={studentGrades[subject.id]?.[period.id]?.[exam.id] ?? ''}
if (val < 0) val = 0; onChange={(e) => {
setStudentGrades(prev => ({ let val = parseFloat(e.target.value);
...prev, if (val > maxScore) val = maxScore;
[subject.id]: { if (val < 0) val = 0;
...prev[subject.id], setStudentGrades(prev => ({
[period.id]: { ...prev,
...prev[subject.id]?.[period.id], [subject.id]: {
[exam.id]: isNaN(val) ? '' : val ...prev[subject.id],
[period.id]: {
...prev[subject.id]?.[period.id],
[exam.id]: isNaN(val) ? '' : val
}
} }
} }));
})); }}
}} />
/> </div>
</div> </div>
) )
})} })}

View File

@ -44,7 +44,7 @@ const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo, onL
{ id: View.Courses, icon: GraduationCap, label: 'Cursos' }, { id: View.Courses, icon: GraduationCap, label: 'Cursos' },
{ id: View.Students, icon: Users, label: 'Alunos' }, { id: View.Students, icon: Users, label: 'Alunos' },
{ id: View.Classes, icon: BookOpen, label: 'Turmas' }, { id: View.Classes, icon: BookOpen, label: 'Turmas' },
{ id: View.Exams, icon: ClipboardList, label: 'Avaliações' }, { id: View.Exams, icon: ClipboardList, label: 'Atividades e Provas' },
{ id: View.ReportCard, icon: FileText, label: 'Boletim Escolar' }, { id: View.ReportCard, icon: FileText, label: 'Boletim Escolar' },
{ id: View.Finance, icon: CircleDollarSign, label: 'Financeiro' }, { id: View.Finance, icon: CircleDollarSign, label: 'Financeiro' },
{ id: View.Contracts, icon: FileSignature, label: 'Contratos' }, { id: View.Contracts, icon: FileSignature, label: 'Contratos' },

View File

@ -251,15 +251,17 @@ 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) => s.id === g.subjectId);
const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null; const exam = g.examId ? (schoolData.exams || []).find(e => e.id === g.examId) : null;
const periodObj = (schoolData.periods || []).find(p => p.id === g.period);
return { return {
...g, ...g,
subjectName: subject?.name || 'Disciplina desconhecida', subjectName: subject?.name || 'Disciplina desconhecida',
examTitle: exam?.title, examTitle: exam?.title,
evaluationType: exam?.evaluationType || 'exam', evaluationType: exam?.evaluationType || 'exam',
maxScore: exam?.maxScore maxScore: exam?.maxScore,
periodName: periodObj ? periodObj.name : g.period
}; };
}); });
const periods = [...new Set(grades.map((g) => g.period))]; const periods = [...new Set(enrichedGrades.map((g) => g.periodName))];
if (periods.length === 0) periods.push('1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre'); if (periods.length === 0) periods.push('1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre');
periods.sort(); periods.sort();
res.json({ grades: enrichedGrades, periods, allSubjects: courseSubjects }); res.json({ grades: enrichedGrades, periods, allSubjects: courseSubjects });
@ -519,12 +521,17 @@ app.post('/api/portal/avaliacoes/submeter', authMiddleware, async (req, res) =>
const { examId, answers } = req.body; const { examId, answers } = req.body;
if (!examId || !answers) return res.status(400).json({ error: 'Dados obrigatórios' }); if (!examId || !answers) return res.status(400).json({ error: 'Dados obrigatórios' });
// Verificar se já submeteu // Verificar se já submeteu e deletar a submissão anterior para permitir refazer
const { rows: existing } = await pool.query( const { rows: existing } = await pool.query(
'SELECT id FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1', 'SELECT * FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1',
[req.user.studentId, examId] [req.user.studentId, examId]
); );
if (existing.length > 0) return res.status(409).json({ error: 'Você já realizou esta prova.' }); if (existing.length > 0) {
await pool.query(
'DELETE FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2',
[req.user.studentId, examId]
);
}
const schoolData = await getSchoolData(); const schoolData = await getSchoolData();
const exam = (schoolData.exams || []).find(e => e.id === examId); const exam = (schoolData.exams || []).find(e => e.id === examId);

View File

@ -12,7 +12,7 @@ const navItems = [
{ path: '/minhas-aulas', label: 'Cronograma', icon: CalendarClock }, { path: '/minhas-aulas', label: 'Cronograma', icon: CalendarClock },
{ path: '/financeiro', label: 'Financeiro', icon: CreditCard }, { path: '/financeiro', label: 'Financeiro', icon: CreditCard },
{ path: '/notas', label: 'Notas', icon: BookOpen }, { path: '/notas', label: 'Notas', icon: BookOpen },
{ path: '/avaliacoes', label: 'Avaliações', icon: ClipboardList }, { path: '/avaliacoes', label: 'Atividades e Provas', icon: ClipboardList },
{ path: '/frequencia', label: 'Frequência', icon: CalendarCheck }, { path: '/frequencia', label: 'Frequência', icon: CalendarCheck },
{ path: '/contratos', label: 'Contratos', icon: FileText }, { path: '/contratos', label: 'Contratos', icon: FileText },
{ path: '/certificados', label: 'Certificados', icon: Award }, { path: '/certificados', label: 'Certificados', icon: Award },

View File

@ -26,6 +26,7 @@ export default function Avaliacoes() {
const [exams, setExams] = useState<Exam[]>([]); const [exams, setExams] = useState<Exam[]>([]);
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]); const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'exams' | 'activities'>('exams');
// Exam mode state // Exam mode state
const [view, setView] = useState<ExamView>('listing'); const [view, setView] = useState<ExamView>('listing');
@ -598,7 +599,7 @@ export default function Avaliacoes() {
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
}} }}
> >
<ArrowLeft size={18} /> Voltar às Avaliações <ArrowLeft size={18} /> Voltar às Atividades e Provas
</button> </button>
</div> </div>
</div> </div>
@ -608,14 +609,62 @@ export default function Avaliacoes() {
// ========================================== // ==========================================
// RENDER: Listing (Default) // RENDER: Listing (Default)
// ========================================== // ==========================================
const filteredExams = exams.filter(e => {
const isActivity = (e as any).evaluationType === 'activity';
return activeTab === 'activities' ? isActivity : !isActivity;
});
return ( return (
<div className="page-container"> <div className="page-container">
<div className="animate-fade-in" style={{ marginBottom: '1.5rem' }}> <div className="animate-fade-in" style={{ marginBottom: '1.5rem' }}>
<h1 className="page-title">Avaliações</h1> <h1 className="page-title">Atividades e Provas</h1>
<p className="page-subtitle">Provas e avaliações disponíveis para você</p> <p className="page-subtitle">Provas e atividades disponíveis para você</p>
</div> </div>
{exams.length === 0 ? ( {/* Tabs */}
<div className="animate-fade-in" style={{
display: 'flex', gap: '0.5rem', marginBottom: '2rem',
background: 'var(--color-surface-light)', padding: '0.5rem',
borderRadius: '12px', border: '1px solid var(--glass-border)',
width: 'fit-content'
}}>
<button
onClick={() => setActiveTab('exams')}
style={{
padding: '0.5rem 1.5rem',
borderRadius: '8px',
border: 'none',
fontSize: '0.85rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s',
background: activeTab === 'exams' ? 'var(--color-primary)' : 'transparent',
color: activeTab === 'exams' ? 'white' : 'var(--color-text-secondary)',
boxShadow: activeTab === 'exams' ? '0 2px 8px var(--bg-primary-alpha)' : 'none'
}}
>
Provas ({exams.filter(e => (e as any).evaluationType !== 'activity').length})
</button>
<button
onClick={() => setActiveTab('activities')}
style={{
padding: '0.5rem 1.5rem',
borderRadius: '8px',
border: 'none',
fontSize: '0.85rem',
fontWeight: 700,
cursor: 'pointer',
transition: 'all 0.2s',
background: activeTab === 'activities' ? 'var(--color-info, #0369a1)' : 'transparent',
color: activeTab === 'activities' ? 'white' : 'var(--color-text-secondary)',
boxShadow: activeTab === 'activities' ? '0 2px 8px rgba(3, 105, 161, 0.2)' : 'none'
}}
>
Atividades ({exams.filter(e => (e as any).evaluationType === 'activity').length})
</button>
</div>
{filteredExams.length === 0 ? (
<div className="glass-card animate-fade-in" style={{ <div className="glass-card animate-fade-in" style={{
padding: '4rem 2rem', textAlign: 'center', padding: '4rem 2rem', textAlign: 'center',
color: 'var(--color-text-secondary)', color: 'var(--color-text-secondary)',
@ -630,7 +679,7 @@ export default function Avaliacoes() {
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: '1rem', gap: '1rem',
}} className="animate-fade-in stagger-children"> }} className="animate-fade-in stagger-children">
{exams.map(exam => { {filteredExams.map(exam => {
const sub = getSubmission(exam.id); const sub = getSubmission(exam.id);
const isDone = !!sub; const isDone = !!sub;
@ -699,23 +748,40 @@ export default function Avaliacoes() {
</div> </div>
{isDone ? ( {isDone ? (
<div style={{ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
display: 'flex', justifyContent: 'space-between', alignItems: 'center', <div style={{
padding: '0.75rem 1rem', borderRadius: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
background: 'var(--bg-success-alpha)', padding: '0.75rem 1rem', borderRadius: 10,
}}> background: 'var(--bg-success-alpha)',
<div> }}>
<p style={{ fontSize: '0.7rem', color: 'var(--color-text-secondary)', fontWeight: 600 }}>SUA NOTA</p> <div>
<p style={{ fontSize: '1.25rem', fontWeight: 800, color: 'var(--color-success)' }}> <p style={{ fontSize: '0.7rem', color: 'var(--color-text-secondary)', fontWeight: 600 }}>SUA NOTA</p>
{sub!.final_score.toFixed(1)} <p style={{ fontSize: '1.25rem', fontWeight: 800, color: 'var(--color-success)' }}>
</p> {sub!.final_score.toFixed(1)}
</div> </p>
<div style={{ textAlign: 'right' }}> </div>
<p style={{ fontSize: '0.7rem', color: 'var(--color-text-secondary)', fontWeight: 600 }}>ACERTOS</p> <div style={{ textAlign: 'right' }}>
<p style={{ fontSize: '1rem', fontWeight: 700 }}> <p style={{ fontSize: '0.7rem', color: 'var(--color-text-secondary)', fontWeight: 600 }}>ACERTOS</p>
{sub!.correct_count}/{sub!.total_questions} <p style={{ fontSize: '1rem', fontWeight: 700 }}>
</p> {sub!.correct_count}/{sub!.total_questions}
</p>
</div>
</div> </div>
<button
onClick={() => {
showAppConfirm('Deseja realmente refazer? Sua nota anterior será substituída.', () => startExam(exam));
}}
style={{
width: '100%', padding: '0.65rem',
borderRadius: 10, border: '1px solid var(--color-primary-alpha)',
background: 'transparent', color: 'var(--color-primary)',
fontSize: '0.8rem', fontWeight: 700,
cursor: 'pointer', transition: 'all 0.2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
}}
>
Refazer {(exam as any).evaluationType === 'activity' ? 'Atividade' : 'Prova'}
</button>
</div> </div>
) : ( ) : (
<button <button

View File

@ -8,6 +8,7 @@ interface GradeWithSubject extends Grade {
examTitle?: string; examTitle?: string;
evaluationType?: string; evaluationType?: string;
maxScore?: number; maxScore?: number;
periodName?: string;
} }
export default function Notas() { export default function Notas() {
@ -139,7 +140,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.period === period); const periodGrades = subjectGrades.filter(g => (g.periodName || g.period) === 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);