'use client'; import { useState, useEffect } from 'react'; import { MainLayout } from '@/components/layout/main-layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Progress } from '@/components/ui/progress'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Calendar, Clock, TrendingUp, Search, Filter, CheckCircle, XCircle, AlertCircle, User, BookOpen, BarChart3, CalendarDays, FileText, Download, ArrowUpDown } from 'lucide-react'; import { toast } from 'sonner'; import { AttendanceStatus } from '@prisma/client'; interface Teacher { id: string; firstName: string; lastName: string; email: string; } interface Period { id: string; name: string; isActive: boolean; } interface Class { id: string; name: string; code: string; period: Period; } interface Section { id: string; name: string; class: Class; teachers: Teacher[]; } interface AttendanceRecord { id: string; date: string; status: AttendanceStatus; reason: string | null; section: Section; } interface OverallStats { totalRecords: number; present: number; absent: number; justified: number; attendanceRate: number; } interface SectionStats { sectionId: string; sectionName: string; className: string; classCode: string; periodName: string; total: number; present: number; absent: number; justified: number; attendanceRate: number; } interface PeriodStats { periodId: string; periodName: string; isActive: boolean; total: number; present: number; absent: number; justified: number; attendanceRate: number; } interface FilterSection { id: string; name: string; className: string; classCode: string; periodName: string; } interface FilterPeriod { id: string; name: string; isActive: boolean; } interface Student { id: string; firstName: string; lastName: string; admissionNumber: string; } interface AttendanceResponse { student: Student; attendanceRecords: AttendanceRecord[]; statistics: { overall: OverallStats; bySections: SectionStats[]; byPeriods: PeriodStats[]; }; filters: { sections: FilterSection[]; periods: FilterPeriod[]; }; } export default function StudentAttendancePage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [selectedSection, setSelectedSection] = useState('all'); const [selectedPeriod, setSelectedPeriod] = useState('all'); const [selectedStatus, setSelectedStatus] = useState('all'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [sortField, setSortField] = useState<'date' | 'class' | 'status'>('date'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); useEffect(() => { fetchAttendance(); }, [selectedSection, selectedPeriod, selectedStatus, startDate, endDate]); const fetchAttendance = async () => { try { setLoading(true); const params = new URLSearchParams(); if (selectedSection !== 'all') params.append('sectionId', selectedSection); if (selectedPeriod !== 'all') params.append('periodId', selectedPeriod); if (selectedStatus !== 'all') params.append('status', selectedStatus); if (startDate) params.append('startDate', startDate); if (endDate) params.append('endDate', endDate); const response = await fetch(`/api/student/attendance?${params.toString()}`); if (!response.ok) { throw new Error('Error al cargar el historial de asistencia'); } const responseData: AttendanceResponse = await response.json(); setData(responseData); } catch (error) { console.error('Error:', error); toast.error('Error al cargar el historial de asistencia'); } finally { setLoading(false); } }; const filteredRecords = data?.attendanceRecords.filter(record => { const matchesSearch = record.section.class.name.toLowerCase().includes(searchTerm.toLowerCase()) || record.section.class.code.toLowerCase().includes(searchTerm.toLowerCase()) || record.section.name.toLowerCase().includes(searchTerm.toLowerCase()) || (record.reason && record.reason.toLowerCase().includes(searchTerm.toLowerCase())); return matchesSearch; }).sort((a, b) => { let aValue: string | number; let bValue: string | number; switch (sortField) { case 'date': aValue = new Date(a.date).getTime(); bValue = new Date(b.date).getTime(); break; case 'class': aValue = a.section.class.name; bValue = b.section.class.name; break; case 'status': aValue = a.status; bValue = b.status; break; default: return 0; } if (sortDirection === 'asc') { return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; } else { return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; } }) || []; const getStatusColor = (status: AttendanceStatus) => { switch (status) { case 'PRESENT': return 'text-green-600'; case 'ABSENT': return 'text-red-600'; case 'JUSTIFIED': return 'text-yellow-600'; default: return 'text-gray-600'; } }; const getStatusBadgeVariant = (status: AttendanceStatus): 'default' | 'secondary' | 'destructive' => { switch (status) { case 'PRESENT': return 'default'; case 'ABSENT': return 'destructive'; case 'JUSTIFIED': return 'secondary'; default: return 'secondary'; } }; const getStatusIcon = (status: AttendanceStatus) => { switch (status) { case 'PRESENT': return ; case 'ABSENT': return ; case 'JUSTIFIED': return ; default: return ; } }; const getStatusText = (status: AttendanceStatus) => { switch (status) { case 'PRESENT': return 'Presente'; case 'ABSENT': return 'Ausente'; case 'JUSTIFIED': return 'Justificado'; default: return status; } }; const getAttendanceColor = (rate: number) => { if (rate >= 90) return 'text-green-600'; if (rate >= 75) return 'text-yellow-600'; return 'text-red-600'; }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }); }; const formatShortDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('es-ES'); }; const getTeacherNames = (teachers: Teacher[]) => { return teachers.map(teacher => `${teacher.firstName} ${teacher.lastName}`).join(', '); }; const clearFilters = () => { setSearchTerm(''); setSelectedSection('all'); setSelectedPeriod('all'); setSelectedStatus('all'); setStartDate(''); setEndDate(''); }; const handleSort = (field: 'date' | 'class' | 'status') => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDirection('desc'); } }; if (loading) { return (
Cargando historial de asistencia...
); } if (!data) { return (
Error al cargar los datos
); } return (
{/* Header */}

Historial de Asistencia

{data.student.firstName} {data.student.lastName} - {data.student.admissionNumber}

{/* Estadísticas Generales */}
Total de Registros
{data.statistics.overall.totalRecords}

Histórico completo

Asistencia Promedio
{data.statistics.overall.attendanceRate}%

Todas las clases

Días Presente
{data.statistics.overall.present}

{Math.round((data.statistics.overall.present / data.statistics.overall.totalRecords) * 100)}% del total

Días Ausente
{data.statistics.overall.absent}

{data.statistics.overall.justified} justificados

{/* Filtros */} Filtros
setSearchTerm(e.target.value)} className="pl-10" />
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{/* Contenido con Tabs */} Registros ({filteredRecords.length}) Por Sección ({data.statistics.bySections.length}) Por Período ({data.statistics.byPeriods.length}) {/* Tab de Registros */} {filteredRecords.length > 0 ? ( handleSort('date')} >
Fecha
handleSort('class')} >
Clase
Sección Profesor handleSort('status')} >
Estado
Observación
{filteredRecords.map((record) => (
{formatShortDate(record.date)}
{new Date(record.date).toLocaleDateString('es-ES', { weekday: 'short' })}
{record.section.class.name}
{record.section.class.code} • {record.section.class.period.name}
{record.section.name}
{getTeacherNames(record.section.teachers) || 'Sin asignar'}
{getStatusIcon(record.status)} {getStatusText(record.status)}
{record.reason ? (
{record.reason}
) : ( - )}
))}
) : (

No se encontraron registros

No hay registros de asistencia que coincidan con los filtros aplicados.

)}
{/* Tab de Estadísticas por Sección */}
{data.statistics.bySections.map((section) => ( {section.className} {section.sectionName} • {section.periodName}
Asistencia = 90 ? 'default' : section.attendanceRate >= 75 ? 'secondary' : 'destructive'}> {section.attendanceRate}%
{section.present}
Presente
{section.absent}
Ausente
{section.justified}
Justificado
{section.total}
Total
))}
{/* Tab de Estadísticas por Período */}
{data.statistics.byPeriods.map((period) => (
{period.periodName} {period.total} registros de asistencia
{period.isActive ? 'Activo' : 'Inactivo'}
Asistencia = 90 ? 'default' : period.attendanceRate >= 75 ? 'secondary' : 'destructive'}> {period.attendanceRate}%
{period.present}
Presente
{period.absent}
Ausente
{period.justified}
Justificado
{period.total}
Total
))}
); }