diff --git a/MEMORY.md b/MEMORY.md index 30ec632..6b5cb38 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -51,6 +51,7 @@ - [x] **Resolução de Condição de Corrida:** Implementado `await` no update duplo de cobranças do SQL na edição de cobranças do Manager (`Finance.tsx`), assegurando consistência na re-busca de dados. - [x] **Ordem de Exclusão e Notificação (Regra 34):** Removida a exclusão local imediata no endpoint `/api/excluir_cobranca`. A remoção local é delegada ao webhook do Asaas, permitindo que a mensagem de WhatsApp acesse os dados necessários antes da deleção. - [x] **Limpeza de Faltas no Reagendamento:** Ajustada a função `handleRescheduleLesson` no `LessonSchedule.tsx` para filtrar e deletar automaticamente do banco de dados/estado todas as faltas geradas de forma automática para uma aula que estava no passado e foi reagendada para o futuro. +- [x] **Correção das Presenças por Biometria:** Resolvido o bug onde as presenças por biometria apareciam como faltas. Corrigido o fuso horário de comparação do fim da aula no servidor (`processAutoAbsences`), adicionada a rotina de auto-limpeza de faltas duplicadas auto-geradas ao salvar ou confirmar presenças, priorizado a exibição de presenças sobre faltas nas interfaces (Manager e Portal), e garantida a gravação do ID da aula (`aula_id`) na sincronização relacional das frequências. - [x] **Validação de Build e Git Push:** Confirmada compilação bem-sucedida do frontend/backend e efetuado o push para a branch remota `main` sob autorização do usuário. ## 📋 Próximos Passos diff --git a/manager/components/AttendanceCapture.tsx b/manager/components/AttendanceCapture.tsx index 9d6eb29..d4d28c2 100644 --- a/manager/components/AttendanceCapture.tsx +++ b/manager/components/AttendanceCapture.tsx @@ -260,6 +260,15 @@ const AttendanceCapture: React.FC = ({ data, updateData String(nowLocal.getMinutes()).padStart(2, '0') + ':' + String(nowLocal.getSeconds()).padStart(2, '0'); + // Limpar qualquer falta auto-gerada para o mesmo aluno nesta mesma aula/dia + const filteredAttendance = (data.attendance || []).filter(a => { + const isAutoAbsence = a.studentId === detectedStudentId && + a.type === 'absence' && + (a.autoGenerated || (a.id && a.id.startsWith('auto-abs-'))) && + (a.lessonId === activeLesson.id || (a.date && a.date.startsWith(activeLesson.date))); + return !isAutoAbsence; + }); + const newAttendance: Attendance = { id: crypto.randomUUID(), studentId: detectedStudentId, @@ -271,7 +280,7 @@ const AttendanceCapture: React.FC = ({ data, updateData verified: true }; - const updatedAttendance = [...(data.attendance || []), newAttendance]; + const updatedAttendance = [...filteredAttendance, newAttendance]; updateData({ attendance: updatedAttendance }); // Sincronização em duas etapas: Local e Servidor (SQL) diff --git a/manager/components/AttendanceQuery.tsx b/manager/components/AttendanceQuery.tsx index c8d26da..db83662 100644 --- a/manager/components/AttendanceQuery.tsx +++ b/manager/components/AttendanceQuery.tsx @@ -410,13 +410,17 @@ const AttendanceQuery: React.FC = ({ data, updateData, dee const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00'); const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00'); - const matchedRecord = studentActualRecords.find(a => { + const matchingRecords = studentActualRecords.filter(a => { if ((a as any).lessonId === lesson.id) return true; if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true; const recordTime = new Date(a.date); return recordTime >= lessonStart && recordTime <= lessonEnd; }); + const matchedRecord = matchingRecords.find(a => a.type === 'presence' || !a.type) || + matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) || + matchingRecords[0]; + if (matchedRecord) { if (matchedRecord.type === 'absence') { if (matchedRecord.justificationAccepted) justified++; @@ -524,13 +528,17 @@ const AttendanceQuery: React.FC = ({ data, updateData, dee const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00'); const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00'); // Regra Estrita: Comparação exata com o horário da aula (sem 30 min de tolerância) - let record = actualRecords.find(a => { + const matchingRecords = actualRecords.filter(a => { if ((a as any).lessonId === lesson.id) return true; if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true; const recordTime = new Date(a.date); return recordTime >= lessonStart && recordTime <= lessonEnd; }); + let record = matchingRecords.find(a => a.type === 'presence' || !a.type) || + matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) || + matchingRecords[0]; + if (!record && now >= lessonStart) { const isFinished = now > lessonEnd; record = { diff --git a/manager/services/database.js b/manager/services/database.js index 09a681f..e4a991d 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -42,11 +42,29 @@ export async function processAutoAbsences(data) { const now = new Date(); let updated = false; + // 1. Auto-correção/Autolimpeza: remove faltas auto-geradas duplicadas se o aluno tiver uma presença registrada no mesmo dia/aula + const initialLength = data.attendance.length; + data.attendance = data.attendance.filter(a => { + if (a.type === 'absence' && (a.autoGenerated || (a.id && a.id.startsWith('auto-abs-')))) { + const hasPresence = data.attendance.some(p => + p.studentId === a.studentId && + (p.type === 'presence' || !p.type) && + (p.lessonId === a.lessonId || (p.date && a.date && p.date.substring(0, 10) === a.date.substring(0, 10))) + ); + return !hasPresence; + } + return true; + }); + if (data.attendance.length !== initialLength) { + updated = true; + } + // Cache de alunos por turma para performance const studentsByClass = {}; data.lessons.forEach(lesson => { - const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00`; + // RESOLUÇÃO DE FUSO HORÁRIO: Força fuso horário América/São_Paulo (-03:00) ao analisar a hora de término da aula no servidor (geralmente UTC) + const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00-03:00`; const lessonEnd = new Date(lessonEndStr); if (now > lessonEnd && lesson.status !== 'cancelled') { @@ -537,13 +555,13 @@ export async function syncJsonToRelationalTables() { for (const f of data.attendance) { if (!f.id || !f.studentId || !f.classId) continue; await client.query( - `INSERT INTO frequencias (id, aluno_id, turma_id, data, foto, verificado, tipo, justificativa, justificativa_aceita) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + `INSERT INTO frequencias (id, aluno_id, turma_id, aula_id, data, foto, verificado, tipo, justificativa, justificativa_aceita) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (id) DO UPDATE SET - aluno_id = EXCLUDED.aluno_id, turma_id = EXCLUDED.turma_id, data = EXCLUDED.data, + aluno_id = EXCLUDED.aluno_id, turma_id = EXCLUDED.turma_id, aula_id = EXCLUDED.aula_id, data = EXCLUDED.data, foto = EXCLUDED.foto, verificado = EXCLUDED.verificado, tipo = EXCLUDED.tipo, justificativa = EXCLUDED.justificativa, justificativa_aceita = EXCLUDED.justificativa_aceita`, - [f.id, f.studentId, f.classId, f.date, f.photo || '', f.verified || false, f.type || 'presence', f.justification || null, f.justificationAccepted || false] + [f.id, f.studentId, f.classId, f.lessonId || null, f.date, f.photo || '', f.verified || false, f.type || 'presence', f.justification || null, f.justificationAccepted || false] ); } } diff --git a/portal/src/pages/Dashboard.tsx b/portal/src/pages/Dashboard.tsx index 009fcc0..9673ccf 100644 --- a/portal/src/pages/Dashboard.tsx +++ b/portal/src/pages/Dashboard.tsx @@ -88,7 +88,7 @@ export default function Dashboard() { const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00'); const presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000); - const att = data.attendance.find(a => { + const matchingAtts = data.attendance.filter(a => { if (!a.date || typeof a.date !== 'string') return false; if ((a as any).lessonId === lesson.id) return true; if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true; @@ -96,6 +96,10 @@ export default function Dashboard() { return recordTime >= presenceStartWindow && recordTime <= lessonEnd; }); + const att = matchingAtts.find(a => a.type === 'presence' || (a.verified === true && a.type !== 'absence')) || + matchingAtts.find(a => a.type === 'absence' && a.justificationAccepted) || + matchingAtts[0]; + // Mesma lógica de presença da página de Frequência (exclui type: 'absence') const isPresent = att && (att.type === 'presence' || (att.verified === true && att.type !== 'absence')); if (isPresent) presencesCount++; diff --git a/portal/src/pages/Frequencia.tsx b/portal/src/pages/Frequencia.tsx index 55292ba..6c8c183 100644 --- a/portal/src/pages/Frequencia.tsx +++ b/portal/src/pages/Frequencia.tsx @@ -149,8 +149,8 @@ export default function Frequencia() { const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00', 0); const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '23:59', 23); - // Buscar registro com a MESMA lógica do Manager (find, não filter) - let record = attendance.find(a => { + // Buscar registros com a lógica priorizando presença + const matchingRecords = attendance.filter(a => { if (!a.date || typeof a.date !== 'string') return false; // 1. Match por lessonId (se existir) if ((a as any).lessonId === lesson.id) return true; @@ -161,14 +161,19 @@ export default function Frequencia() { return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs; }); + let record = matchingRecords.find(a => a.type === 'presence' || (a.verified === true && a.type !== 'absence')) || + matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) || + matchingRecords[0]; + // Se não encontrou registro real, verificar se precisa de justificativa associada if (!record) { - record = attendance.find(a => { + const matchingJustifications = attendance.filter(a => { if (!a.date || typeof a.date !== 'string' || !a.justification) return false; if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true; const recordTimeMs = new Date(a.date).getTime(); return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs; - }) || undefined; + }); + record = matchingJustifications[0]; } const { isInProgress, isCompleted } = getLessonTimeStatus(lesson, now);