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] **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] **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] **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.
|
- [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
|
## 📋 Próximos Passos
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,15 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
||||||
String(nowLocal.getMinutes()).padStart(2, '0') + ':' +
|
String(nowLocal.getMinutes()).padStart(2, '0') + ':' +
|
||||||
String(nowLocal.getSeconds()).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 = {
|
const newAttendance: Attendance = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
studentId: detectedStudentId,
|
studentId: detectedStudentId,
|
||||||
|
|
@ -271,7 +280,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
||||||
verified: true
|
verified: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedAttendance = [...(data.attendance || []), newAttendance];
|
const updatedAttendance = [...filteredAttendance, newAttendance];
|
||||||
updateData({ attendance: updatedAttendance });
|
updateData({ attendance: updatedAttendance });
|
||||||
|
|
||||||
// Sincronização em duas etapas: Local e Servidor (SQL)
|
// 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 lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00');
|
||||||
const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':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 as any).lessonId === lesson.id) return true;
|
||||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||||
const recordTime = new Date(a.date);
|
const recordTime = new Date(a.date);
|
||||||
return recordTime >= lessonStart && recordTime <= lessonEnd;
|
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) {
|
||||||
if (matchedRecord.type === 'absence') {
|
if (matchedRecord.type === 'absence') {
|
||||||
if (matchedRecord.justificationAccepted) justified++;
|
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 lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00');
|
||||||
const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':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)
|
// 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 as any).lessonId === lesson.id) return true;
|
||||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||||
const recordTime = new Date(a.date);
|
const recordTime = new Date(a.date);
|
||||||
return recordTime >= lessonStart && recordTime <= lessonEnd;
|
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) {
|
if (!record && now >= lessonStart) {
|
||||||
const isFinished = now > lessonEnd;
|
const isFinished = now > lessonEnd;
|
||||||
record = {
|
record = {
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,29 @@ export async function processAutoAbsences(data) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let updated = false;
|
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
|
// Cache de alunos por turma para performance
|
||||||
const studentsByClass = {};
|
const studentsByClass = {};
|
||||||
|
|
||||||
data.lessons.forEach(lesson => {
|
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);
|
const lessonEnd = new Date(lessonEndStr);
|
||||||
|
|
||||||
if (now > lessonEnd && lesson.status !== 'cancelled') {
|
if (now > lessonEnd && lesson.status !== 'cancelled') {
|
||||||
|
|
@ -537,13 +555,13 @@ export async function syncJsonToRelationalTables() {
|
||||||
for (const f of data.attendance) {
|
for (const f of data.attendance) {
|
||||||
if (!f.id || !f.studentId || !f.classId) continue;
|
if (!f.id || !f.studentId || !f.classId) continue;
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO frequencias (id, aluno_id, turma_id, data, foto, verificado, tipo, justificativa, justificativa_aceita)
|
`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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
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,
|
foto = EXCLUDED.foto, verificado = EXCLUDED.verificado, tipo = EXCLUDED.tipo,
|
||||||
justificativa = EXCLUDED.justificativa, justificativa_aceita = EXCLUDED.justificativa_aceita`,
|
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 lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00');
|
||||||
const presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000);
|
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.date || typeof a.date !== 'string') return false;
|
||||||
if ((a as any).lessonId === lesson.id) return true;
|
if ((a as any).lessonId === lesson.id) return true;
|
||||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) 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;
|
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')
|
// 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'));
|
const isPresent = att && (att.type === 'presence' || (att.verified === true && att.type !== 'absence'));
|
||||||
if (isPresent) presencesCount++;
|
if (isPresent) presencesCount++;
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,8 @@ export default function Frequencia() {
|
||||||
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00', 0);
|
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00', 0);
|
||||||
const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '23:59', 23);
|
const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '23:59', 23);
|
||||||
|
|
||||||
// Buscar registro com a MESMA lógica do Manager (find, não filter)
|
// Buscar registros com a lógica priorizando presença
|
||||||
let record = attendance.find(a => {
|
const matchingRecords = attendance.filter(a => {
|
||||||
if (!a.date || typeof a.date !== 'string') return false;
|
if (!a.date || typeof a.date !== 'string') return false;
|
||||||
// 1. Match por lessonId (se existir)
|
// 1. Match por lessonId (se existir)
|
||||||
if ((a as any).lessonId === lesson.id) return true;
|
if ((a as any).lessonId === lesson.id) return true;
|
||||||
|
|
@ -161,14 +161,19 @@ export default function Frequencia() {
|
||||||
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
|
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
|
// Se não encontrou registro real, verificar se precisa de justificativa associada
|
||||||
if (!record) {
|
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 || typeof a.date !== 'string' || !a.justification) return false;
|
||||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||||
const recordTimeMs = new Date(a.date).getTime();
|
const recordTimeMs = new Date(a.date).getTime();
|
||||||
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
|
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
|
||||||
}) || undefined;
|
});
|
||||||
|
record = matchingJustifications[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isInProgress, isCompleted } = getLessonTimeStatus(lesson, now);
|
const { isInProgress, isCompleted } = getLessonTimeStatus(lesson, now);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue