diff --git a/GEMINI.md b/GEMINI.md index cf96c85..19e23ff 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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. 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). +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. diff --git a/MEMORY.md b/MEMORY.md index bcab768..8b2e81a 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -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] 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] **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] **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] **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] **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. -- [ ] Próximo Passo: Monitorar o desempenho das submissões de provas simultâneas no Portal. +- [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 Detalhado (Manager):** Upgrade para layout de lista (Full-Width) com cores distintas: **Violeta (Provas)** e **Azul (Atividades)**. +- [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] **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). +- [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) - **Funcionalidades Implementadas:** diff --git a/manager/components/AdminNotifications.tsx b/manager/components/AdminNotifications.tsx index 7845dfa..92e9e00 100644 --- a/manager/components/AdminNotifications.tsx +++ b/manager/components/AdminNotifications.tsx @@ -139,7 +139,7 @@ const AdminNotifications: React.FC = ({ data, updateData, setView, onNavi
-

Avaliações Pendentes +

Atividades/Provas Pendentes {unreadCount > 0 && {unreadCount}}

diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx index 1361900..4f1c5d2 100644 --- a/manager/components/Exams.tsx +++ b/manager/components/Exams.tsx @@ -419,7 +419,7 @@ const Exams: React.FC = ({ data, updateData }) => {

- Avaliações + Atividades e Provas

Gerencie as provas e testes das turmas.

diff --git a/manager/components/ReportCard.tsx b/manager/components/ReportCard.tsx index 1936653..1fd01c9 100644 --- a/manager/components/ReportCard.tsx +++ b/manager/components/ReportCard.tsx @@ -490,12 +490,27 @@ const ReportCard: React.FC = ({ data, updateData }) => {

{subject.name}

- {linkedExams.length > 0 && ( -
- - {linkedExams.length} {linkedExams.length === 1 ? 'Prova' : 'Provas'} -
- )} + {(() => { + const linkedExams = (data.exams || []).filter(e => e.subjectId === 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 ( + <> + {provasCount > 0 && ( +
+ + {provasCount} {provasCount === 1 ? 'Prova' : 'Provas'} +
+ )} + {atividadesCount > 0 && ( +
+ + {atividadesCount} {atividadesCount === 1 ? 'Atividade' : 'Atividades'} +
+ )} + + ); + })()}
MÉDIA: {(() => { const subjectGrades = studentGrades[subject.id] || {}; @@ -506,7 +521,7 @@ const ReportCard: React.FC = ({ data, updateData }) => {
-
+
{periods.map(period => { const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published'); const periodGrades = studentGrades[subject.id]?.[period.id] || {}; @@ -520,45 +535,53 @@ const ReportCard: React.FC = ({ data, updateData }) => {
{linkedExams.length > 0 ? ( -
+
{linkedExams.map(exam => { const isActivity = (exam as any).evaluationType === 'activity'; const maxScore = (exam as any).maxScore ?? 10; return ( -
-
- - - {isActivity ? 'Ativ' : 'Prova'} - - {exam.title} - - Vale {maxScore} +
+
+
+
+ + {isActivity ? 'Atividade' : 'Prova'} + + {exam.title} +
+ {exam.description && ( +

{exam.description}

+ )} +
- { - let val = parseFloat(e.target.value); - if (val > maxScore) val = maxScore; - if (val < 0) val = 0; - setStudentGrades(prev => ({ - ...prev, - [subject.id]: { - ...prev[subject.id], - [period.id]: { - ...prev[subject.id]?.[period.id], - [exam.id]: isNaN(val) ? '' : val + +
+ Nota (Máx {maxScore}) + { + let val = parseFloat(e.target.value); + if (val > maxScore) val = maxScore; + if (val < 0) val = 0; + setStudentGrades(prev => ({ + ...prev, + [subject.id]: { + ...prev[subject.id], + [period.id]: { + ...prev[subject.id]?.[period.id], + [exam.id]: isNaN(val) ? '' : val + } } - } - })); - }} - /> + })); + }} + /> +
) })} diff --git a/manager/components/Sidebar.tsx b/manager/components/Sidebar.tsx index a0b5dea..f12a884 100644 --- a/manager/components/Sidebar.tsx +++ b/manager/components/Sidebar.tsx @@ -44,7 +44,7 @@ const Sidebar: React.FC = ({ currentView, setView, user, logo, onL { id: View.Courses, icon: GraduationCap, label: 'Cursos' }, { id: View.Students, icon: Users, label: 'Alunos' }, { 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.Finance, icon: CircleDollarSign, label: 'Financeiro' }, { id: View.Contracts, icon: FileSignature, label: 'Contratos' }, diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index 7b41207..daa1af5 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -251,15 +251,17 @@ 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); return { ...g, subjectName: subject?.name || 'Disciplina desconhecida', examTitle: exam?.title, 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'); periods.sort(); 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; 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( - '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] ); - 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 exam = (schoolData.exams || []).find(e => e.id === examId); diff --git a/portal/src/components/Sidebar.tsx b/portal/src/components/Sidebar.tsx index 16a55db..a7933db 100644 --- a/portal/src/components/Sidebar.tsx +++ b/portal/src/components/Sidebar.tsx @@ -12,7 +12,7 @@ const navItems = [ { path: '/minhas-aulas', label: 'Cronograma', icon: CalendarClock }, { path: '/financeiro', label: 'Financeiro', icon: CreditCard }, { 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: '/contratos', label: 'Contratos', icon: FileText }, { path: '/certificados', label: 'Certificados', icon: Award }, diff --git a/portal/src/pages/Avaliacoes.tsx b/portal/src/pages/Avaliacoes.tsx index 1667942..289bbbf 100644 --- a/portal/src/pages/Avaliacoes.tsx +++ b/portal/src/pages/Avaliacoes.tsx @@ -26,6 +26,7 @@ export default function Avaliacoes() { const [exams, setExams] = useState([]); const [submissions, setSubmissions] = useState([]); const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<'exams' | 'activities'>('exams'); // Exam mode state const [view, setView] = useState('listing'); @@ -598,7 +599,7 @@ export default function Avaliacoes() { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, }} > - Voltar às Avaliações + Voltar às Atividades e Provas
@@ -608,14 +609,62 @@ export default function Avaliacoes() { // ========================================== // RENDER: Listing (Default) // ========================================== + const filteredExams = exams.filter(e => { + const isActivity = (e as any).evaluationType === 'activity'; + return activeTab === 'activities' ? isActivity : !isActivity; + }); + return (
-

Avaliações

-

Provas e avaliações disponíveis para você

+

Atividades e Provas

+

Provas e atividades disponíveis para você

- {exams.length === 0 ? ( + {/* Tabs */} +
+ + +
+ + {filteredExams.length === 0 ? (
- {exams.map(exam => { + {filteredExams.map(exam => { const sub = getSubmission(exam.id); const isDone = !!sub; @@ -699,23 +748,40 @@ export default function Avaliacoes() {
{isDone ? ( -
-
-

SUA NOTA

-

- {sub!.final_score.toFixed(1)} -

-
-
-

ACERTOS

-

- {sub!.correct_count}/{sub!.total_questions} -

+
+
+
+

SUA NOTA

+

+ {sub!.final_score.toFixed(1)} +

+
+
+

ACERTOS

+

+ {sub!.correct_count}/{sub!.total_questions} +

+
+
) : (