fix: strict attendance window and immediate cloud sync across manager and portal

This commit is contained in:
Sidney 2026-05-11 22:19:08 -03:00
parent a1b5075e41
commit bfb2bc12db
3 changed files with 37 additions and 33 deletions

View File

@ -231,7 +231,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
return; return;
} }
// Encontrar a aula ativa para esta turma/aluno no momento da captura // Encontrar a aula ativa para esta turma/aluno no momento exato da captura (Sem tolerância de 30 min)
const nowLocal = new Date(); const nowLocal = new Date();
const activeLesson = (data.lessons || []).find(l => { const activeLesson = (data.lessons || []).find(l => {
if (l.classId !== detectedClassId || l.status === 'cancelled') return false; if (l.classId !== detectedClassId || l.status === 'cancelled') return false;
@ -240,19 +240,19 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
const endStr = `${lessonDate}T${l.endTime || '23:59'}:00`; const endStr = `${lessonDate}T${l.endTime || '23:59'}:00`;
const lessonStart = new Date(startStr); const lessonStart = new Date(startStr);
const lessonEnd = new Date(endStr); const lessonEnd = new Date(endStr);
// Janela: 30 min antes do início até o fim da aula
const windowStart = new Date(lessonStart.getTime() - 30 * 60 * 1000); // REGRA ESTREITA: Apenas do início ao fim da aula
return nowLocal >= windowStart && nowLocal <= lessonEnd; return nowLocal >= lessonStart && nowLocal <= lessonEnd;
}); });
// REGRA ESTRITA: A presença só pode ser marcada se houver uma aula ativa // BLOQUEIO: Se não houver aula em andamento, impede o registro
if (!activeLesson) { if (!activeLesson) {
showAlert('Atenção', "Nenhuma aula ativa detectada para esta turma no momento. A presença só pode ser registrada a partir de 30 minutos antes do início até o término da aula.", 'warning'); showAlert('Atenção', "Nenhuma aula em andamento para esta turma no momento. O registro biométrico só é permitido durante o horário oficial da aula.", 'warning');
cancelCapture(); cancelCapture();
return; return;
} }
// Gerar string de data local (YYYY-MM-DDTHH:MM:SS) sem fuso UTC para evitar o bug do dia seguinte // Gerar string de data local para o banco de dados
const localDateStr = nowLocal.getFullYear() + '-' + const localDateStr = nowLocal.getFullYear() + '-' +
String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' + String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' +
String(nowLocal.getDate()).padStart(2, '0') + 'T' + String(nowLocal.getDate()).padStart(2, '0') + 'T' +
@ -264,7 +264,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
id: crypto.randomUUID(), id: crypto.randomUUID(),
studentId: detectedStudentId, studentId: detectedStudentId,
classId: detectedClassId, classId: detectedClassId,
lessonId: activeLesson.id, // Vínculo obrigatório agora lessonId: activeLesson.id,
date: localDateStr, date: localDateStr,
photo: capturedImage, photo: capturedImage,
type: 'presence', type: 'presence',
@ -273,16 +273,20 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
const updatedAttendance = [...(data.attendance || []), newAttendance]; const updatedAttendance = [...(data.attendance || []), newAttendance];
updateData({ attendance: updatedAttendance }); updateData({ attendance: updatedAttendance });
dbService.saveData({ ...data, attendance: updatedAttendance });
// Reset for next student // Sincronização em duas etapas: Local e Servidor (SQL)
const updatedData = { ...data, attendance: updatedAttendance };
dbService.saveData(updatedData);
dbService.saveToCloud(updatedData);
// Reset de interface
setCapturedImage(null); setCapturedImage(null);
setShowConfirmModal(false); setShowConfirmModal(false);
setDetectedStudentId(null); setDetectedStudentId(null);
setDetectedClassId(null); setDetectedClassId(null);
setIsProcessing(false); setIsProcessing(false);
closeModal(); closeModal();
showAlert('Sucesso', "Presença confirmada com sucesso!", 'success'); showAlert('Sucesso', "Presença registrada e sincronizada com o servidor!", 'success');
}; };
const cancelCapture = () => { const cancelCapture = () => {

View File

@ -118,7 +118,9 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
} }
updateData({ attendance: updatedAttendance }); updateData({ attendance: updatedAttendance });
dbService.saveData({ ...data, attendance: updatedAttendance }); const updatedData = { ...data, attendance: updatedAttendance };
dbService.saveData(updatedData);
dbService.saveToCloud(updatedData); // Sincronia imediata com SQL
showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success'); showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success');
}; };
@ -216,8 +218,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
updatedAttendance.push(newAbsence); updatedAttendance.push(newAbsence);
} }
const updatedData = { ...data, attendance: updatedAttendance };
updateData({ attendance: updatedAttendance }); updateData({ attendance: updatedAttendance });
dbService.saveData({ ...data, attendance: updatedAttendance }); dbService.saveData(updatedData);
dbService.saveToCloud(updatedData); // Sincronia imediata com SQL
setAbsenceStudentId(''); setAbsenceStudentId('');
setAbsenceJustification(''); setAbsenceJustification('');
@ -405,13 +409,12 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
deduplicatedLessons.forEach(lesson => { deduplicatedLessons.forEach(lesson => {
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 presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000);
const matchedRecord = studentActualRecords.find(a => { const matchedRecord = studentActualRecords.find(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 >= presenceStartWindow && recordTime <= lessonEnd; return recordTime >= lessonStart && recordTime <= lessonEnd;
}); });
if (matchedRecord) { if (matchedRecord) {
@ -520,16 +523,15 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
deduplicatedLessons.forEach(lesson => { deduplicatedLessons.forEach(lesson => {
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 presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000); // 30 mins before // Regra Estrita: Comparação exata com o horário da aula (sem 30 min de tolerância)
let record = actualRecords.find(a => { let record = actualRecords.find(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 >= presenceStartWindow && recordTime <= lessonEnd; return recordTime >= lessonStart && recordTime <= lessonEnd;
}); });
if (!record && now >= presenceStartWindow) { if (!record && now >= lessonStart) {
const isFinished = now > lessonEnd; const isFinished = now > lessonEnd;
record = { record = {
id: `v-${lesson.id}`, id: `v-${lesson.id}`,

View File

@ -148,7 +148,6 @@ export default function Frequencia() {
// Construir janela de tempo de forma resiliente // Construir janela de tempo de forma resiliente
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);
const presenceStartWindowMs = lessonStartMs - (30 * 60 * 1000); // 30 mins before
// Buscar registro com a MESMA lógica do Manager (find, não filter) // Buscar registro com a MESMA lógica do Manager (find, não filter)
let record = attendance.find(a => { let record = attendance.find(a => {
@ -157,19 +156,18 @@ export default function Frequencia() {
if ((a as any).lessonId === lesson.id) return true; if ((a as any).lessonId === lesson.id) return true;
// 2. Match exato de string (formato do JSON/sync) // 2. Match exato de string (formato do JSON/sync)
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;
// 3. Match por janela de tempo (30 min antes até fim da aula) // 3. Match por janela de tempo (Início ao Fim da aula - REGRA ESTREITA)
const recordTimeMs = new Date(a.date).getTime(); const recordTimeMs = new Date(a.date).getTime();
return recordTimeMs >= presenceStartWindowMs && recordTimeMs <= lessonEndMs; return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
}); });
// Se não encontrou registro real, verificar se precisa de justificativa associada // Se não encontrou registro real, verificar se precisa de justificativa associada
// (pode existir um registro de justificativa com data ligeiramente diferente)
if (!record) { if (!record) {
record = attendance.find(a => { record = attendance.find(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 >= presenceStartWindowMs && recordTimeMs <= lessonEndMs; return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
}) || undefined; }) || undefined;
} }