import React, { useState, useRef, useEffect } from 'react'; import { SchoolData, Attendance, Student } from '../types'; import { dbService } from '../services/dbService'; import { useDialog } from '../DialogContext'; import { Camera, CheckCircle, XCircle, User, SwitchCamera, Loader2, Search, RefreshCw } from 'lucide-react'; import * as faceapi from '@vladmandic/face-api'; interface AttendanceCaptureProps { data: SchoolData; updateData: (newData: Partial) => void; } const AttendanceCapture: React.FC = ({ data, updateData }) => { const { showAlert } = useDialog(); const [cameraActive, setCameraActive] = useState(false); const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user'); const [capturedImage, setCapturedImage] = useState(null); const [showConfirmModal, setShowConfirmModal] = useState(false); 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); const closeModal = () => { setIsClosing(true); setTimeout(() => { setCapturedImage(null); setShowConfirmModal(false); setDetectedStudentId(null); setDetectedClassId(null); setIsProcessing(false); setIsClosing(false); stopCamera(); }, 400); }; const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const intervalRef = useRef(null); // Load Models useEffect(() => { const loadModels = async () => { const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/'; try { await Promise.all([ faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL), faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL), faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL), faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL), ]); setModelsLoaded(true); } catch (err) { console.error("Error loading face-api models", err); showAlert('Erro', "Erro ao carregar modelos de reconhecimento facial. Verifique sua conexão.", 'error'); } }; loadModels(); }, []); // Start Camera const startCamera = async () => { try { // Stop any existing stream first if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); } if (videoRef.current && videoRef.current.srcObject) { const oldStream = videoRef.current.srcObject as MediaStream; oldStream.getTracks().forEach(track => track.stop()); videoRef.current.srcObject = null; } const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingMode } }); streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; try { await videoRef.current.play(); } catch (e) { console.error("Error playing video", e); } } setCameraActive(true); setIsProcessing(false); } catch (err) { console.error("Error accessing camera:", err); showAlert('Erro', "Erro ao acessar a câmera. Verifique as permissões.", 'error'); } }; // Attach stream to video when active useEffect(() => { if (cameraActive && videoRef.current && streamRef.current) { videoRef.current.srcObject = streamRef.current; videoRef.current.play().catch(e => console.error("Error playing video", e)); } }, [cameraActive]); // Stop Camera on unmount useEffect(() => { return () => { stopCamera(); }; }, []); const stopCamera = () => { if (intervalRef.current) clearInterval(intervalRef.current); if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } setCameraActive(false); }; const switchCamera = () => { setFacingMode(prev => prev === 'user' ? 'environment' : 'user'); }; // Restart camera when facing mode changes useEffect(() => { if (cameraActive) { startCamera(); } }, [facingMode]); // Face Detection Loop useEffect(() => { if (cameraActive && modelsLoaded && videoRef.current) { const detectFace = async () => { if (!videoRef.current || videoRef.current.paused || videoRef.current.ended || isProcessing || showConfirmModal) return; try { const detections = await faceapi.detectAllFaces(videoRef.current, new faceapi.TinyFaceDetectorOptions()) .withFaceLandmarks() .withFaceDescriptors(); if (detections.length > 0) { // Find best match const bestMatch = findBestMatch(detections[0].descriptor); if (bestMatch) { // Found a student! setIsProcessing(true); capturePhoto(bestMatch.studentId, bestMatch.classId); } } } catch (e) { console.error("Detection error", e); } }; intervalRef.current = setInterval(detectFace, 1000); // Check every 1s } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [cameraActive, modelsLoaded, isProcessing, showConfirmModal, data.students]); const findBestMatch = (descriptor: Float32Array) => { let bestDistance = 0.6; // Threshold let bestStudentId = null; let bestClassId = null; // 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); if (distance < bestDistance) { bestDistance = distance; bestStudentId = student.id; bestClassId = student.classId; } } if (bestStudentId && bestClassId) { return { studentId: bestStudentId, classId: bestClassId }; } return null; }; const capturePhoto = (studentId: string, classId: string) => { if (videoRef.current && canvasRef.current) { const video = videoRef.current; const canvas = canvasRef.current; const context = canvas.getContext('2d'); if (context) { canvas.width = video.videoWidth; 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); setShowConfirmModal(true); } } }; const confirmPresence = () => { if (!detectedStudentId || !detectedClassId || !capturedImage) return; // Check if already present for THIS class within a 1-hour window // (Allows multiple presences per day if lessons are at different times) const now = new Date(); const alreadyPresent = data.attendance.some(a => { if (a.studentId !== detectedStudentId || a.classId !== detectedClassId) return false; const attDate = new Date(a.date); const isSameDay = attDate.toDateString() === now.toDateString(); const diffMs = Math.abs(now.getTime() - attDate.getTime()); return isSameDay && diffMs < (5 * 60 * 1000); // Intervalo de 5 minutos }); if (alreadyPresent) { showAlert('Atenção', "Aluno já marcou presença hoje!", 'warning'); cancelCapture(); return; } const newAttendance: Attendance = { id: crypto.randomUUID(), studentId: detectedStudentId, classId: detectedClassId, date: new Date().toISOString(), photo: capturedImage, type: 'presence', verified: true }; const updatedAttendance = [...(data.attendance || []), newAttendance]; updateData({ attendance: updatedAttendance }); dbService.saveData({ ...data, attendance: updatedAttendance }); // Reset for next student setCapturedImage(null); setShowConfirmModal(false); setDetectedStudentId(null); setDetectedClassId(null); setIsProcessing(false); closeModal(); showAlert('Sucesso', "Presença confirmada com sucesso!", 'success'); }; const cancelCapture = () => { closeModal(); }; const detectedStudent = data.students.find(s => s.id === detectedStudentId); const detectedClass = data.classes.find(c => c.id === detectedClassId); return (

Registro de Presença

Posicione o rosto para identificação automática.

{/* Camera View Container */}
{cameraActive ? ( <>
{/* Main Action Button */} {!cameraActive ? ( ) : ( )}
{/* System Status (Minimalist) */} {!cameraActive && (
{modelsLoaded ? 'IA Pronta' : 'Carregando IA'}
{data.students.filter(s => s.faceDescriptor).length} Faces Cadastradas
)}
{/* Confirmation Modal */} {showConfirmModal && capturedImage && detectedStudent && (
{/* Blue Top Bar */}

Identificado!

Confirmar presença para:

Captured

{detectedStudent.name}

{detectedClass?.name}

)}
); }; export default AttendanceCapture;