import React, { useState, useEffect } from 'react'; import { SchoolData, Attendance, Class, Student } from '../types'; import { dbService } from '../services/dbService'; import { useDialog } from '../DialogContext'; import { Search, Calendar, User, Clock, CheckCircle, XCircle, FileDown, BookOpen, Plus, X, AlertCircle, RefreshCw, ChevronRight, Trash2, FileSignature, Paperclip } from 'lucide-react'; import jsPDF from 'jspdf'; import 'jspdf-autotable'; import { addHeader } from '../services/pdfService'; import SearchableSelect from './SearchableSelect'; interface AttendanceQueryProps { data: SchoolData; updateData: (newData: Partial) => void; deepLinkStudentId?: string | null; clearDeepLink?: () => void; } const AttendanceQuery: React.FC = ({ data, updateData, deepLinkStudentId, clearDeepLink }) => { const { showAlert } = useDialog(); useEffect(() => { if (deepLinkStudentId) { const student = data.students.find(s => s.id === deepLinkStudentId); if (student) { const classObj = data.classes.find(c => c.id === student.classId); if (classObj) { setSelectedClass(classObj); setSelectedStudent(student); setShowStudentHistoryModal(true); if (clearDeepLink) clearDeepLink(); } } } }, [deepLinkStudentId, data.students, data.classes, clearDeepLink]); const [selectedClass, setSelectedClass] = useState(null); const [showStudentListModal, setShowStudentListModal] = useState(false); const [selectedStudent, setSelectedStudent] = useState(null); const [showStudentHistoryModal, setShowStudentHistoryModal] = useState(false); const [showAbsenceModal, setShowAbsenceModal] = useState(false); const [isClosing, setIsClosing] = useState(false); const [isClosing2, setIsClosing2] = useState(false); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [absenceStudentId, setAbsenceStudentId] = useState(''); const [absenceJustification, setAbsenceJustification] = useState(''); const [absenceDate, setAbsenceDate] = useState(new Date().toISOString().split('T')[0]); const [absenceLessonId, setAbsenceLessonId] = useState(''); const [viewingAttachment, setViewingAttachment] = useState(null); const [attendanceForAttachment, setAttendanceForAttachment] = useState(null); // Helper para normalizar URLs de fotos (vacina contra cache antigo) const normalizePhotoUrl = (url?: string) => { if (!url || typeof url !== 'string') return ''; if (url.startsWith('data:image') || url.startsWith('blob:')) return url; if (url.startsWith('/storage/')) return url; try { const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/); if (match) return `/storage/${match[1]}`; } catch(e) {} return url; }; const toggleAttendanceStatus = (record: any) => { let updatedAttendance = [...(data.attendance || [])]; if (record.isVirtual) { // Ação do botão do Admin: criar o registro real a partir do virtual const lesson = data.lessons.find(l => l.id === record.id.replace('v-', '')); const newType = (record.type === 'absence' || record.type === 'awaiting') ? 'presence' : 'absence'; const newRecord: Attendance = { id: crypto.randomUUID(), studentId: record.studentId, classId: record.classId, date: lesson ? `${lesson.date}T${lesson.startTime || '00:00'}:00` : new Date().toISOString(), verified: true, type: newType, ...(lesson ? { lessonId: lesson.id } : {}) // Vinculação rígida para não haver duplicidade }; // Garantir que não duplica se já houver por algum erro const existingIdx = updatedAttendance.findIndex(a => a.studentId === record.studentId && ((a as any).lessonId === lesson?.id || a.date === newRecord.date) ); if (existingIdx >= 0) { updatedAttendance[existingIdx] = { ...updatedAttendance[existingIdx], type: newType, justification: undefined, justificationAccepted: undefined }; } else { updatedAttendance.push(newRecord); } } else { // Toggle existing record const newType = record.type === 'absence' ? 'presence' : 'absence'; updatedAttendance = updatedAttendance.map(a => a.id === record.id ? { ...a, type: newType, justification: undefined, justificationAccepted: undefined } : a ); } updateData({ attendance: updatedAttendance }); dbService.saveData({ ...data, attendance: updatedAttendance }); showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success'); }; const handleDeleteAttachmentRecord = () => { if (!attendanceForAttachment || !attendanceForAttachment.justification) return; try { const parsed = JSON.parse(attendanceForAttachment.justification); delete parsed.arquivo_base64; const updatedJustification = JSON.stringify(parsed); const updatedAttendance = (data.attendance || []).map(a => a.id === attendanceForAttachment.id ? { ...a, justification: updatedJustification } : a ); updateData({ attendance: updatedAttendance }); dbService.saveData({ ...data, attendance: updatedAttendance }); setViewingAttachment(null); setAttendanceForAttachment(null); showAlert('Sucesso', 'Arquivo removido com sucesso.', 'success'); } catch(e) { console.error('Erro ao excluir anexo do registro', e); } }; const closeModal = () => { setIsClosing(true); setTimeout(() => { setShowStudentListModal(false); setShowAbsenceModal(false); setIsClosing(false); setAbsenceStudentId(''); setAbsenceJustification(''); setAbsenceLessonId(''); }, 400); }; const closeHistoryModal = () => { setIsClosing2(true); setTimeout(() => { setShowStudentHistoryModal(false); setSelectedStudent(null); setIsClosing2(false); }, 400); }; const handleAddAbsence = () => { if (!absenceStudentId || !absenceJustification || !absenceLessonId) { showAlert('Atenção', "⚠️ Por favor, preencha todos os campos da justificativa.", 'warning'); return; } const student = data.students.find(s => s.id === absenceStudentId); if (!student) { showAlert('Erro', "Aluno não encontrado.", 'error'); return; } const lesson = data.lessons.find(l => l.id === absenceLessonId); if (!lesson) { showAlert('Atenção', "⚠️ Por favor, selecione a aula para justificar.", 'warning'); return; } // Check if there is already a record for this lesson specifically const existingIndex = (data.attendance || []).findIndex(a => a.studentId === absenceStudentId && ((a as any).lessonId === lesson.id || a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) ); let updatedAttendance = [...(data.attendance || [])]; if (existingIndex >= 0) { updatedAttendance[existingIndex] = { ...updatedAttendance[existingIndex], type: 'absence', justification: absenceJustification, justificationAccepted: true, verified: true, lessonId: lesson.id as any }; } else { const newAbsence: Attendance = { id: crypto.randomUUID(), studentId: absenceStudentId, classId: student.classId, date: `${lesson.date}T${lesson.startTime || '00:00'}:00`, verified: true, type: 'absence', justification: absenceJustification, justificationAccepted: true, ...(lesson ? { lessonId: lesson.id } : {}) as any }; updatedAttendance.push(newAbsence); } updateData({ attendance: updatedAttendance }); dbService.saveData({ ...data, attendance: updatedAttendance }); setAbsenceStudentId(''); setAbsenceJustification(''); setAbsenceLessonId(''); closeModal(); showAlert('Sucesso', "Falta justificada registrada com sucesso!", 'success'); }; const handleExportPDF = async (classObj: Class) => { setIsGeneratingPDF(true); try { const doc = new jsPDF(); const startY = await addHeader(doc, data); doc.setFontSize(18); doc.text('Relatório de Frequência', 14, startY + 10); doc.setFontSize(11); doc.text(`Data: ${new Date(selectedDate).toLocaleDateString()}`, 14, startY + 18); doc.text(`Turma: ${classObj.name}`, 14, startY + 24); const classAttendance = (data.attendance || []).filter(record => record.classId === classObj.id && record.date.startsWith(selectedDate) ); const tableData = classAttendance.map(record => { const student = data.students.find(s => s.id === record.studentId); const time = new Date(record.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); let justMotivo = record.justification || '-'; if (justMotivo.startsWith('{')) { try { const parsed = JSON.parse(justMotivo); justMotivo = parsed.motivo || justMotivo; } catch(e) {} } return [ student?.name || 'Desconhecido', time, record.type === 'absence' ? (record.justificationAccepted ? 'Falta Justificada' : 'Falta') : 'Presente', justMotivo ]; }); (doc as any).autoTable({ startY: startY + 30, head: [['Aluno', 'Horário', 'Status', 'Justificativa']], body: tableData, }); doc.save(`frequencia_${classObj.name}_${selectedDate}.pdf`); } catch (error) { console.error('Error exporting PDF:', error); } finally { setIsGeneratingPDF(false); } }; return (

Registro de Frequência

Gerencie a frequência por turma e registre faltas justificadas.

setSelectedDate(e.target.value)} />
{/* Class Cards Grid */}
{data.classes.map(classObj => { const classStudents = data.students.filter(s => s.classId === classObj.id && s.status === 'active'); const attendanceCount = (data.attendance || []).filter(a => a.classId === classObj.id && a.date.startsWith(selectedDate)).length; const course = data.courses.find(c => c.id === classObj.courseId); return (
{ setSelectedClass(classObj); setShowStudentListModal(true); }} className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:shadow-xl hover:border-indigo-300 transition-all cursor-pointer group relative overflow-hidden" >

{classObj.name}

{course?.name}

{classStudents.length} Alunos • {attendanceCount} Registros
Ver Alunos
); })}
{/* === MODAL 1: Lista de Alunos da Turma === */} {showStudentListModal && selectedClass && (

Alunos: {selectedClass.name}

Clique em um aluno para ver seu histórico individual.

{(() => { const classStudents = data.students .filter(s => s.classId === selectedClass.id && s.status === 'active') .sort((a, b) => a.name.localeCompare(b.name)); if (classStudents.length === 0) { return (

Nenhum aluno ativo nesta turma.

); } return classStudents.map(student => { const studentActualRecords = (data.attendance || []).filter(a => a.studentId === student.id && a.classId === selectedClass.id); const classLessonsRaw = (data.lessons || []).filter(l => l.classId === selectedClass.id && l.status !== 'cancelled'); const deduplicatedLessons = classLessonsRaw.filter((lesson, index, self) => index === self.findIndex((t) => ( t.date === lesson.date && t.startTime === lesson.startTime )) ); let presences = 0; let absences = 0; 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); 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; }); if (matchedRecord) { if (matchedRecord.type === 'absence') { if (matchedRecord.justificationAccepted) justified++; else absences++; } else if (matchedRecord.type === 'presence' || !matchedRecord.type) { presences++; } } else if (now > lessonEnd) { absences++; } }); return (
{ setSelectedStudent(student); setShowStudentHistoryModal(true); }} className="flex items-center justify-between p-4 bg-slate-50 hover:bg-indigo-50 rounded-xl border border-slate-100 hover:border-indigo-200 cursor-pointer transition-all group" >
{student.photo ? ( {student.name} ) : ( student.name.charAt(0).toUpperCase() )}

{student.name}

Matrícula: {student.enrollmentNumber || '—'}

{presences}P {absences}F {justified > 0 && {justified}J}
); }); })()}
)} {/* === MODAL 2: Histórico Individual do Aluno === */} {showStudentHistoryModal && selectedStudent && selectedClass && (
{selectedStudent.photo ? ( {selectedStudent.name} ) : (
{selectedStudent.name.charAt(0).toUpperCase()}
)}

{selectedStudent.name}

Histórico de Frequência • {selectedClass.name}

{(() => { const now = new Date(); const actualRecords = (data.attendance || []) .filter(a => a.studentId === selectedStudent.id && a.classId === selectedClass.id); const classLessonsRaw = (data.lessons || []) .filter(l => l.classId === selectedClass.id && l.status !== 'cancelled'); const deduplicatedLessons = classLessonsRaw.filter((lesson, index, self) => index === self.findIndex((t) => ( t.date === lesson.date && t.startTime === lesson.startTime )) ); const tableRows: any[] = []; 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 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; }); if (!record && now >= presenceStartWindow) { const isFinished = now > lessonEnd; record = { id: `v-${lesson.id}`, studentId: selectedStudent.id, classId: selectedClass.id, date: `${lesson.date}T${lesson.startTime || '00:00'}:00`, type: isFinished ? 'absence' : 'awaiting', isVirtual: true, lessonId: lesson.id, awaiting: !isFinished }; } if (record) { tableRows.push({ lesson, record }); } }); tableRows.sort((a, b) => new Date(b.lesson.date + 'T' + (b.lesson.startTime || '00:00') + ':00').getTime() - new Date(a.lesson.date + 'T' + (a.lesson.startTime || '00:00') + ':00').getTime()); if (tableRows.length === 0) { return (

Nenhum registro de frequência ou aula agendada.

); } let presences = 0; let absences = 0; let justified = 0; tableRows.forEach(row => { if (row.record.type === 'absence') { if (row.record.justificationAccepted) justified++; else absences++; } else if (row.record.type === 'presence' || (!row.record.type && !row.record.isVirtual)) { presences++; } }); return ( <> {/* Summary bar */}
{presences} Presenças
{absences} Faltas
{justified} Justificadas
{tableRows.length} Aulas
{/* Attendance table */}
{tableRows.map(({lesson, record}) => { const recordDate = new Date(record.date); const time = recordDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); let justMotivo = record.justification || ''; let justAttachment: string | null = null; if (justMotivo.startsWith('{')) { try { const parsed = JSON.parse(justMotivo); justMotivo = parsed.motivo || justMotivo; justAttachment = parsed.arquivo_base64 || null; } catch(e) {} } const isAbsence = record.type === 'absence'; const isAwaiting = record.type === 'awaiting'; const isJustified = isAbsence && record.justificationAccepted; const hasPendingJustification = isAbsence && record.justification && !record.justificationAccepted; const isPendente = record.awaiting; return ( ); })}
Data Início (Aula) Término (Aula) Registro Status Justificativa Anexo Ação
{recordDate.toLocaleDateString('pt-BR')}
{lesson?.startTime || '--:--'}
{lesson?.endTime || '--:--'}
{record.isVirtual ? "--:--" : time}
{isAwaiting ? ( Aguardando Justificativa ) : isJustified ? ( Falta Justificada ) : (isAbsence || isPendente) ? (
Falta {isPendente && ( Aguardando Justificativa )}
) : ( Presente )}
{isAwaiting || isPendente ? ( Aguardando registro ou justificativa... ) : justMotivo ? (

{justMotivo}

) : ( )}
{justAttachment ? ( ) : ( )}
{(isAbsence || isPendente || isAwaiting) && ( )} {hasPendingJustification && ( )}
); })()}
)} {/* Justified Absence Modal */} {showAbsenceModal && (
{/* Blue Top Bar */}

Justificar Falta

setAbsenceStudentId(val)} options={data.students .filter(s => s.status === 'active') .sort((a, b) => a.name.localeCompare(b.name)) .map(student => ({ id: student.id, name: student.name }))} />
{ setAbsenceDate(e.target.value); setAbsenceLessonId(''); }} />