fix: unify frequency deduplication and fix dashboard crash on portal
This commit is contained in:
parent
ef8b7c51a8
commit
9ba2e11e27
|
|
@ -52,4 +52,5 @@
|
||||||
23. **Soft Delete Policy (Lixeira)**: Evaluations (exams/activities) MUST NOT be hard-deleted to preserve grade history. Use the `isDeleted` flag to hide them from students and active management views, keeping them accessible in the "Lixeira" for restoration and ensuring reference integrity in the Report Card.
|
23. **Soft Delete Policy (Lixeira)**: Evaluations (exams/activities) MUST NOT be hard-deleted to preserve grade history. Use the `isDeleted` flag to hide them from students and active management views, keeping them accessible in the "Lixeira" for restoration and ensuring reference integrity in the Report Card.
|
||||||
24. **Grading Arithmetic Logic**: All grade averages (Period, Subject, and General) MUST be calculated as arithmetic means (Average of Means). Summing grades for a final period result is prohibited to ensure consistency between Portal and Manager.
|
24. **Grading Arithmetic Logic**: All grade averages (Period, Subject, and General) MUST be calculated as arithmetic means (Average of Means). Summing grades for a final period result is prohibited to ensure consistency between Portal and Manager.
|
||||||
25. **SQL Date Handling**: When fetching timestamps from PostgreSQL to be matched with JSON dates/ISO strings in the frontend, ALWAYS use `TO_CHAR(column, 'YYYY-MM-DD"T"HH24:MI:SS')` in the SQL query to prevent Node.js timezone shifts from corrupting string-based matching logic.
|
25. **SQL Date Handling**: When fetching timestamps from PostgreSQL to be matched with JSON dates/ISO strings in the frontend, ALWAYS use `TO_CHAR(column, 'YYYY-MM-DD"T"HH24:MI:SS')` in the SQL query to prevent Node.js timezone shifts from corrupting string-based matching logic.
|
||||||
|
26. **SQL-First Architecture Pattern**: New features or module refactors SHOULD prioritize 100% SQL persistence (using `notas_boletim` as a template). The legacy JSON `school_data` should be treated as secondary or deprecated for these modules, ensuring real-time consistency between Manager and Portal.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,12 @@
|
||||||
- [x] **Blindagem de Fuso Horário (Postgres):** Rota de frequência do portal atualizada para usar `TO_CHAR` no SQL, eliminando deslocamentos de horas causados pela conversão UTC automática do driver.
|
- [x] **Blindagem de Fuso Horário (Postgres):** Rota de frequência do portal atualizada para usar `TO_CHAR` no SQL, eliminando deslocamentos de horas causados pela conversão UTC automática do driver.
|
||||||
- [x] **Unificação de Janela de Presença:** Portal e Manager agora utilizam a mesma janela de 30 minutos de tolerância para correlacionar presenças e faltas às aulas.
|
- [x] **Unificação de Janela de Presença:** Portal e Manager agora utilizam a mesma janela de 30 minutos de tolerância para correlacionar presenças e faltas às aulas.
|
||||||
- [x] **Sincronia de Estatísticas (Portal):** O cálculo de porcentagem no Dashboard do Portal agora usa o mesmo motor lógico da página de Frequência, garantindo números idênticos.
|
- [x] **Sincronia de Estatísticas (Portal):** O cálculo de porcentagem no Dashboard do Portal agora usa o mesmo motor lógico da página de Frequência, garantindo números idênticos.
|
||||||
- [ ] Próximo Passo: Expandir a migração relacional para o módulo de "Minhas Aulas" no Portal para manter a consistência arquitetural.
|
- [x] **Consolidação do Modelo Relacional (Notas):** Confirmado que o módulo de Notas/Boletim é o primeiro 100% SQL, servindo de template para futuras migrações. O JSON `school_data.grades` foi oficialmente substituído pela tabela `notas_boletim`.
|
||||||
|
- [x] **Unificação de Pauta (Deduplicação):** Implementado filtro de deduplicação de aulas no Portal (`Frequencia.tsx` e `Dashboard.tsx`) para ignorar aulas conflitantes, igualando os totais aos do Admin.
|
||||||
|
- [x] **Regra de Registro Único:** Portal agora exibe apenas a primeira batida válida por aula, eliminando duplicidade visual de biometria.
|
||||||
|
- [x] **Sincronia de Justificativas:** Ajustada a contagem matemática do Portal para contabilizar faltas justificadas apenas após o aceite do Admin.
|
||||||
|
- [x] **Fix Dashboard Crash:** Corrigido erro de "tela preta" no Dashboard causado por acesso inseguro a propriedades nulas durante falhas de API.
|
||||||
|
- [ ] Próximo Passo: Iniciar a migração do módulo Financeiro para 100% SQL seguindo o padrão do Boletim.
|
||||||
|
|
||||||
|
|
||||||
## 📅 Histórico Anterior (06/05/2026)
|
## 📅 Histórico Anterior (06/05/2026)
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ export default function Dashboard() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingPayments = data?.payments.filter(p => p.status === 'pending' || p.status === 'overdue') || [];
|
const pendingPayments = data?.payments?.filter(p => p.status === 'pending' || p.status === 'overdue') || [];
|
||||||
const overduePayments = data?.payments.filter(p => p.status === 'overdue') || [];
|
const overduePayments = data?.payments?.filter(p => p.status === 'overdue') || [];
|
||||||
const totalPending = pendingPayments.reduce((s, p) => s + (p.amount - (p.discount || 0)), 0);
|
const totalPending = pendingPayments.reduce((s, p) => s + (p.amount - (p.discount || 0)), 0);
|
||||||
|
|
||||||
// Synchronized Frequency Calculation (Matches Frequencia.tsx & Manager)
|
// Synchronized Frequency Calculation (Matches Frequencia.tsx & Manager)
|
||||||
|
|
@ -73,8 +73,13 @@ export default function Dashboard() {
|
||||||
let validLessonsCount = 0;
|
let validLessonsCount = 0;
|
||||||
|
|
||||||
if (data?.lessons && data?.attendance) {
|
if (data?.lessons && data?.attendance) {
|
||||||
const nowLocal = new Date();
|
const deduplicatedLessons = data.lessons.filter((lesson, index, self) =>
|
||||||
data.lessons.forEach(lesson => {
|
index === self.findIndex((t) => (
|
||||||
|
t.date === lesson.date && t.startTime === lesson.startTime
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
deduplicatedLessons.forEach(lesson => {
|
||||||
if (lesson.status === 'cancelled') return;
|
if (lesson.status === 'cancelled') return;
|
||||||
validLessonsCount++;
|
validLessonsCount++;
|
||||||
|
|
||||||
|
|
@ -95,7 +100,7 @@ export default function Dashboard() {
|
||||||
if (isPresent) presencesCount++;
|
if (isPresent) presencesCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const totalAttendance = data?.attendance.length || 0;
|
const totalAttendance = data?.attendance?.length || 0;
|
||||||
const frequencyPercent = validLessonsCount > 0 ? Math.round((presencesCount / validLessonsCount) * 100) : 0;
|
const frequencyPercent = validLessonsCount > 0 ? Math.round((presencesCount / validLessonsCount) * 100) : 0;
|
||||||
|
|
||||||
const nextDue = pendingPayments
|
const nextDue = pendingPayments
|
||||||
|
|
|
||||||
|
|
@ -136,13 +136,21 @@ export default function Frequencia() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Deduplicar aulas exatamente como no Manager
|
||||||
|
const deduplicatedLessons = lessons.filter((lesson, index, self) =>
|
||||||
|
index === self.findIndex((t) => (
|
||||||
|
t.date === lesson.date && t.startTime === lesson.startTime
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
// Merge and Categorize
|
// Merge and Categorize
|
||||||
const processedItems = lessons.map(lesson => {
|
const processedItems = deduplicatedLessons.map(lesson => {
|
||||||
const lessonFullISO = new Date(parseLessonDateTime(lesson.date, lesson.startTime || '00:00:00')).toISOString();
|
const lessonFullISO = new Date(parseLessonDateTime(lesson.date, lesson.startTime || '00:00:00')).toISOString();
|
||||||
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00:00');
|
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00:00');
|
||||||
const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '00:00:00', lesson.endTime ? 0 : 60);
|
const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '00:00:00', lesson.endTime ? 0 : 60);
|
||||||
|
|
||||||
const atts = attendance.filter(a => {
|
// No Manager, ele procura o primeiro registro válido
|
||||||
|
const att = attendance.find(a => {
|
||||||
if (!a.date || typeof a.date !== 'string') return false;
|
if (!a.date || typeof a.date !== 'string') return false;
|
||||||
|
|
||||||
// 1. Exact Match (Including Manager DB fallback format)
|
// 1. Exact Match (Including Manager DB fallback format)
|
||||||
|
|
@ -158,7 +166,7 @@ export default function Frequencia() {
|
||||||
const { isInProgress, isCompleted } = getLessonTimeStatus(lesson, now);
|
const { isInProgress, isCompleted } = getLessonTimeStatus(lesson, now);
|
||||||
return {
|
return {
|
||||||
lesson,
|
lesson,
|
||||||
attendances: atts,
|
attendances: att ? [att] : [], // Simulando o comportamento do manager (apenas 1 registro)
|
||||||
isInProgress,
|
isInProgress,
|
||||||
isCompleted
|
isCompleted
|
||||||
};
|
};
|
||||||
|
|
@ -179,14 +187,14 @@ export default function Frequencia() {
|
||||||
|
|
||||||
if (isPresent) {
|
if (isPresent) {
|
||||||
presences++;
|
presences++;
|
||||||
} else if (hasJustification) {
|
} else if (activeJustification?.justificationAccepted) {
|
||||||
justified++;
|
justified++;
|
||||||
} else if (isCompleted || parseLessonDateTime(lesson.date || '', '23:59:59') < now.getTime()) {
|
} else if (isCompleted || parseLessonDateTime(lesson.date || '', '23:59:59') < now.getTime()) {
|
||||||
absences++;
|
absences++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCourseLessons = lessons.filter(l => l.status !== 'cancelled').length;
|
const totalCourseLessons = deduplicatedLessons.filter(l => l.status !== 'cancelled').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;
|
||||||
|
|
@ -206,7 +214,7 @@ export default function Frequencia() {
|
||||||
const displayItems = activeTab === 'scheduled' ? activeItems : historyItems;
|
const displayItems = activeTab === 'scheduled' ? activeItems : historyItems;
|
||||||
|
|
||||||
// Collect lessons available for justification modal dropdown
|
// Collect lessons available for justification modal dropdown
|
||||||
const justifiableLessons = lessons.filter(l => {
|
const justifiableLessons = deduplicatedLessons.filter(l => {
|
||||||
if (l.status === 'cancelled') return false;
|
if (l.status === 'cancelled') return false;
|
||||||
|
|
||||||
// Check window (uses new 24h before/after logic)
|
// Check window (uses new 24h before/after logic)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue