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;
}
// 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 activeLesson = (data.lessons || []).find(l => {
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 lessonStart = new Date(startStr);
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);
return nowLocal >= windowStart && nowLocal <= lessonEnd;
// REGRA ESTREITA: Apenas do início ao fim da aula
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) {
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();
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() + '-' +
String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' +
String(nowLocal.getDate()).padStart(2, '0') + 'T' +
@ -264,7 +264,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
id: crypto.randomUUID(),
studentId: detectedStudentId,
classId: detectedClassId,
lessonId: activeLesson.id, // Vínculo obrigatório agora
lessonId: activeLesson.id,
date: localDateStr,
photo: capturedImage,
type: 'presence',
@ -273,16 +273,20 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
const updatedAttendance = [...(data.attendance || []), newAttendance];
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);
setShowConfirmModal(false);
setDetectedStudentId(null);
setDetectedClassId(null);
setIsProcessing(false);
closeModal();
showAlert('Sucesso', "Presença confirmada com sucesso!", 'success');
showAlert('Sucesso', "Presença registrada e sincronizada com o servidor!", 'success');
};
const cancelCapture = () => {

View File

@ -118,7 +118,9 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
}
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');
};
@ -216,8 +218,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
updatedAttendance.push(newAbsence);
}
const updatedData = { ...data, attendance: updatedAttendance };
updateData({ attendance: updatedAttendance });
dbService.saveData({ ...data, attendance: updatedAttendance });
dbService.saveData(updatedData);
dbService.saveToCloud(updatedData); // Sincronia imediata com SQL
setAbsenceStudentId('');
setAbsenceJustification('');
@ -402,17 +406,16 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
let justified = 0;
const now = new Date();
deduplicatedLessons.forEach(lesson => {
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 presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000);
deduplicatedLessons.forEach(lesson => {
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 => {
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 >= presenceStartWindow && recordTime <= lessonEnd;
});
const matchedRecord = studentActualRecords.find(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;
});
if (matchedRecord) {
if (matchedRecord.type === 'absence') {
@ -520,16 +523,15 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
deduplicatedLessons.forEach(lesson => {
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 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 => {
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 >= presenceStartWindow && recordTime <= lessonEnd;
return recordTime >= lessonStart && recordTime <= lessonEnd;
});
if (!record && now >= presenceStartWindow) {
if (!record && now >= lessonStart) {
const isFinished = now > lessonEnd;
record = {
id: `v-${lesson.id}`,

View File

@ -143,12 +143,11 @@ export default function Frequencia() {
))
);
// Merge and Categorize — Clone EXATO do Manager (AttendanceQuery.tsx)
// Merge and Categorize — Clone EXATO do Manager (AttendanceQuery.tsx)
const processedItems = deduplicatedLessons.map(lesson => {
// Construir janela de tempo de forma resiliente
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00', 0);
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)
let record = attendance.find(a => {
@ -157,19 +156,18 @@ export default function Frequencia() {
if ((a as any).lessonId === lesson.id) return true;
// 2. Match exato de string (formato do JSON/sync)
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();
return recordTimeMs >= presenceStartWindowMs && recordTimeMs <= lessonEndMs;
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
});
// Se não encontrou registro real, verificar se precisa de justificativa associada
// (pode existir um registro de justificativa com data ligeiramente diferente)
if (!record) {
record = attendance.find(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 >= presenceStartWindowMs && recordTimeMs <= lessonEndMs;
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
}) || undefined;
}