feat: implementacao de fechamento automatico de pauta e unificacao relacional de frequencia
This commit is contained in:
parent
9a09d7852a
commit
bafd1a6292
|
|
@ -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.
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
- [x] **Migração Relacional de Frequência:** Portal migrado para ler frequências diretamente da tabela SQL `frequencias`. **VERIFICADO.**
|
- [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] **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] **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.
|
- [ ] Próximo Passo: Expandir a migração relacional para o módulo de "Minhas Aulas" no Portal para manter a consistência arquitetural.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,71 @@ export async function getSchoolData() {
|
||||||
return rows[0]?.data || {};
|
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) {
|
export async function saveSchoolData(data) {
|
||||||
|
// Aplicar fechamento de pauta automático antes de salvar
|
||||||
|
const dataWithAbsences = await processAutoAbsences(data);
|
||||||
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO school_data (id, data, updated_at)
|
`INSERT INTO school_data (id, data, updated_at)
|
||||||
VALUES (1, $1, NOW())
|
VALUES (1, $1, NOW())
|
||||||
ON CONFLICT (id) DO UPDATE SET data = $1, updated_at = 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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,9 +172,9 @@ export default function Frequencia() {
|
||||||
|
|
||||||
// Stats calculation (aligned with list logic)
|
// Stats calculation (aligned with list logic)
|
||||||
const totalCourseLessons = lessons.length;
|
const totalCourseLessons = lessons.length;
|
||||||
const presences = attendance.filter(a => a.type === 'presence' || a.verified === true).length;
|
const presences = attendance.filter(a => a.type === 'presence').length;
|
||||||
const absences = attendance.filter(a => a.type === 'absence' && !a.verified && !a.justification).length;
|
const absences = attendance.filter(a => a.type === 'absence' && !a.justification).length;
|
||||||
const justified = attendance.filter(a => !!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 completedLessons = processedItems.filter(item => item.isCompleted && item.lesson.status !== 'cancelled').length;
|
||||||
const pendingLessons = 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;
|
const percentage = totalCourseLessons > 0 ? Math.round((presences / totalCourseLessons) * 100) : 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue