diff --git a/GEMINI.md b/GEMINI.md index d815880..4693669 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -47,4 +47,5 @@ 18. **Automated Messaging (Cron Jobs)**: The system uses `node-cron` for independent message scheduling (Preventive vs. Overdue). Overdue logic MUST implement safety checks using `overdue_warnings_count` and `last_overdue_warning_at` to avoid spamming the student. Immediate webhook triggers for `PAYMENT_OVERDUE` are disabled in favor of scheduled routines. 19. **Numerical Data Integrity**: When retrieving data from PostgreSQL `NUMERIC` or `DECIMAL` columns, values MUST be explicitly cast to `Number()` in the backend before being sent to the frontend to prevent crashes when using `.toFixed()` in React. 20. **Exam Duplication**: The system supports cloning evaluations. Duplicated exams MUST default to `draft` status and include `(Cópia)` in the title to allow verification before deployment to new classes. +21. **Automatic Attendance Closure**: The system implements an automatic closure routine (`processAutoAbsences`) that generates physical "Absence" records in the PostgreSQL database for any past lesson where a student has no presence or justification. This routine is triggered during data save operations to ensure retroactive consistency between lessons and records. diff --git a/MEMORY.md b/MEMORY.md index 21b011d..52ea257 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -12,6 +12,7 @@ - [x] **Migração Relacional de Frequência:** Portal migrado para ler frequências diretamente da tabela SQL `frequencias`. **VERIFICADO.** - [x] **Sincronização Bidirecional (Frequência):** Garantido que justificativas enviadas pelo Portal atualizem instantaneamente a tabela relacional via `ON CONFLICT`. - [x] **Auto-Migração de Esquema:** Implementada lógica de auto-correção de colunas (`ALTER TABLE`) na rotina de sincronização do banco de dados (`database.js`). +- [x] **Fechamento Automático de Pauta:** Implementada rotina `processAutoAbsences` que gera registros físicos de falta para aulas passadas sem registro, garantindo consistência entre Portal e Manager. - [ ] Próximo Passo: Expandir a migração relacional para o módulo de "Minhas Aulas" no Portal para manter a consistência arquitetural. diff --git a/manager/services/database.js b/manager/services/database.js index cf027f9..bcce101 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -29,12 +29,71 @@ export async function getSchoolData() { return rows[0]?.data || {}; } +/** + * Percorre as aulas concluídas e gera registros de falta para alunos que não compareceram. + * Isso transforma faltas "virtuais" em registros físicos no banco de dados. + */ +export async function processAutoAbsences(data) { + if (!data.lessons || !data.students || !data.attendance) return data; + + const now = new Date(); + let updated = false; + + // Cache de alunos por turma para performance + const studentsByClass = {}; + + data.lessons.forEach(lesson => { + const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00`; + const lessonEnd = new Date(lessonEndStr); + + if (now > lessonEnd && lesson.status !== 'cancelled') { + if (!studentsByClass[lesson.classId]) { + studentsByClass[lesson.classId] = data.students.filter(s => s.classId === lesson.classId && s.status === 'active'); + } + + studentsByClass[lesson.classId].forEach(student => { + const hasRecord = data.attendance.some(a => + a.studentId === student.id && + (a.lessonId === lesson.id || (a.date && a.date.startsWith(lesson.date))) + ); + + if (!hasRecord) { + data.attendance.push({ + id: `auto-abs-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + studentId: student.id, + classId: lesson.classId, + lessonId: lesson.id, + date: `${lesson.date}T${lesson.startTime || '00:00'}:00`, + type: 'absence', + verified: true, + autoGenerated: true + }); + updated = true; + } + }); + } + }); + + if (updated) { + data.lastUpdated = new Date().toISOString(); + } + return data; +} + export async function saveSchoolData(data) { + // Aplicar fechamento de pauta automático antes de salvar + const dataWithAbsences = await processAutoAbsences(data); + await pool.query( `INSERT INTO school_data (id, data, updated_at) VALUES (1, $1, NOW()) ON CONFLICT (id) DO UPDATE SET data = $1, updated_at = NOW()`, - [JSON.stringify(data)] + [JSON.stringify(dataWithAbsences)] + ); + + // Sincronizar tabelas relacionais em background para não travar o salvamento + syncJsonToRelationalTables(dataWithAbsences).catch(err => + console.error('[Database:Sync] Erro na sincronização automática:', err) ); } diff --git a/portal/src/pages/Frequencia.tsx b/portal/src/pages/Frequencia.tsx index 2f17de0..c84b91a 100644 --- a/portal/src/pages/Frequencia.tsx +++ b/portal/src/pages/Frequencia.tsx @@ -172,9 +172,9 @@ export default function Frequencia() { // Stats calculation (aligned with list logic) const totalCourseLessons = lessons.length; - const presences = attendance.filter(a => a.type === 'presence' || a.verified === true).length; - const absences = attendance.filter(a => a.type === 'absence' && !a.verified && !a.justification).length; - const justified = attendance.filter(a => !!a.justification).length; + const presences = attendance.filter(a => a.type === 'presence').length; + const absences = attendance.filter(a => a.type === 'absence' && !a.justification).length; + const justified = attendance.filter(a => a.type === 'absence' && !!a.justification).length; const completedLessons = processedItems.filter(item => item.isCompleted && item.lesson.status !== 'cancelled').length; const pendingLessons = processedItems.filter(item => !item.isCompleted && item.lesson.status !== 'cancelled').length; const percentage = totalCourseLessons > 0 ? Math.round((presences / totalCourseLessons) * 100) : 0;