fix(attendance): resolve biometric presence showing as absence, fix timezone offset in processAutoAbsences, deduplicate auto-generated absences, prioritize presence in frontend matching, and sync aula_id
This commit is contained in:
parent
2f2dec63d5
commit
9fe6882174
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -260,6 +260,15 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ 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<AttendanceCaptureProps> = ({ 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)
|
||||
|
|
|
|||
|
|
@ -410,13 +410,17 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ 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<AttendanceQueryProps> = ({ 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 = {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue