feat: implementacao de fechamento automatico de pauta e unificacao relacional de frequencia

This commit is contained in:
Sidney 2026-05-05 09:25:12 -03:00
parent 9a09d7852a
commit bafd1a6292
4 changed files with 65 additions and 4 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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)
);
}

View File

@ -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;