From a1b5075e41667f2806bcd7474beaab0b21ec6186 Mon Sep 17 00:00:00 2001 From: Sidney Date: Mon, 11 May 2026 21:43:31 -0300 Subject: [PATCH] fix: biometric attendance timezone shift and mandatory lesson binding --- manager/components/AttendanceCapture.tsx | 76 +++++++---- manager/components/AttendanceQuery.tsx | 164 ++++++++++++----------- manager/types.ts | 3 +- portal/src/types.ts | 3 +- 4 files changed, 146 insertions(+), 100 deletions(-) diff --git a/manager/components/AttendanceCapture.tsx b/manager/components/AttendanceCapture.tsx index feb942f..2e22004 100644 --- a/manager/components/AttendanceCapture.tsx +++ b/manager/components/AttendanceCapture.tsx @@ -19,7 +19,7 @@ const AttendanceCapture: React.FC = ({ data, updateData const [isProcessing, setIsProcessing] = useState(false); const [isClosing, setIsClosing] = useState(false); const [modelsLoaded, setModelsLoaded] = useState(false); - + // Auto-detected state const [detectedStudentId, setDetectedStudentId] = useState(null); const [detectedClassId, setDetectedClassId] = useState(null); @@ -75,18 +75,18 @@ const AttendanceCapture: React.FC = ({ data, updateData videoRef.current.srcObject = null; } - const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: facingMode } + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: facingMode } }); - + streamRef.current = stream; - + if (videoRef.current) { videoRef.current.srcObject = stream; try { - await videoRef.current.play(); + await videoRef.current.play(); } catch (e) { - console.error("Error playing video", e); + console.error("Error playing video", e); } } setCameraActive(true); @@ -146,7 +146,7 @@ const AttendanceCapture: React.FC = ({ data, updateData if (detections.length > 0) { // Find best match const bestMatch = findBestMatch(detections[0].descriptor); - + if (bestMatch) { // Found a student! setIsProcessing(true); @@ -174,7 +174,7 @@ const AttendanceCapture: React.FC = ({ data, updateData // Iterate through all active students who have a face descriptor for (const student of data.students) { if (student.status !== 'active' || !student.faceDescriptor) continue; - + const studentDescriptor = new Float32Array(student.faceDescriptor); const distance = faceapi.euclideanDistance(descriptor, studentDescriptor); @@ -202,7 +202,7 @@ const AttendanceCapture: React.FC = ({ data, updateData canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = canvas.toDataURL('image/jpeg'); - + setCapturedImage(imageData); setDetectedStudentId(studentId); setDetectedClassId(classId); @@ -231,11 +231,41 @@ const AttendanceCapture: React.FC = ({ data, updateData return; } + // Encontrar a aula ativa para esta turma/aluno no momento da captura + const nowLocal = new Date(); + const activeLesson = (data.lessons || []).find(l => { + if (l.classId !== detectedClassId || l.status === 'cancelled') return false; + const lessonDate = l.date; // YYYY-MM-DD + const startStr = `${lessonDate}T${l.startTime || '00:00'}:00`; + 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 ESTRITA: A presença só pode ser marcada se houver uma aula ativa + 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'); + cancelCapture(); + return; + } + + // Gerar string de data local (YYYY-MM-DDTHH:MM:SS) sem fuso UTC para evitar o bug do dia seguinte + const localDateStr = nowLocal.getFullYear() + '-' + + String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' + + String(nowLocal.getDate()).padStart(2, '0') + 'T' + + String(nowLocal.getHours()).padStart(2, '0') + ':' + + String(nowLocal.getMinutes()).padStart(2, '0') + ':' + + String(nowLocal.getSeconds()).padStart(2, '0'); + const newAttendance: Attendance = { id: crypto.randomUUID(), studentId: detectedStudentId, classId: detectedClassId, - date: new Date().toISOString(), + lessonId: activeLesson.id, // Vínculo obrigatório agora + date: localDateStr, photo: capturedImage, type: 'presence', verified: true @@ -275,15 +305,15 @@ const AttendanceCapture: React.FC = ({ data, updateData
{cameraActive ? ( <> -
- setSelectedDate(e.target.value)} /> - -
- )} -