Browse Source

student kinda worky

Matthew Trejo 3 months ago
parent
commit
8d1c149d62

+ 3 - 2
ROADMAP.md

@@ -144,10 +144,11 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
 - **Estado**: Pendiente
 - **Descripción**: Implementación de funcionalidades para estudiantes
 - **Dashboard**: ✅ Completado
+- **Implementado**:
+  - ✅ Vista de clases matriculadas (`/student/classes`)
+  - ✅ Historial de asistencia (`/student/attendance`)
 - **Por implementar**:
-  - Vista de clases matriculadas (`/student/classes`)
   - Consulta de horarios (`/student/schedule`)
-  - Historial de asistencia (`/student/attendance`)
   - Gestión de perfil (`/student/profile`)
   - Notificaciones de asistencia
 

+ 25 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "@radix-ui/react-dropdown-menu": "^2.1.16",
         "@radix-ui/react-label": "^2.1.7",
         "@radix-ui/react-navigation-menu": "^1.2.14",
+        "@radix-ui/react-progress": "^1.1.7",
         "@radix-ui/react-select": "^2.2.6",
         "@radix-ui/react-separator": "^1.1.7",
         "@radix-ui/react-slot": "^1.2.3",
@@ -1688,6 +1689,30 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-progress": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+      "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-roving-focus": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "@radix-ui/react-dropdown-menu": "^2.1.16",
     "@radix-ui/react-label": "^2.1.7",
     "@radix-ui/react-navigation-menu": "^1.2.14",
+    "@radix-ui/react-progress": "^1.1.7",
     "@radix-ui/react-select": "^2.2.6",
     "@radix-ui/react-separator": "^1.1.7",
     "@radix-ui/react-slot": "^1.2.3",

+ 260 - 0
src/app/api/student/attendance/route.ts

@@ -0,0 +1,260 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { AttendanceStatus } from '@prisma/client';
+
+export async function GET(request: NextRequest) {
+  try {
+    // Verificar sesión
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.id) {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    // Verificar que el usuario sea estudiante
+    const user = await prisma.user.findUnique({
+      where: { id: session.user.id },
+      include: { student: true }
+    });
+
+    if (!user || user.role !== 'STUDENT' || !user.student) {
+      return NextResponse.json(
+        { error: 'Acceso denegado. Solo estudiantes pueden acceder.' },
+        { status: 403 }
+      );
+    }
+
+    // Obtener parámetros de consulta
+    const { searchParams } = new URL(request.url);
+    const sectionId = searchParams.get('sectionId');
+    const status = searchParams.get('status') as AttendanceStatus | 'all' | null;
+    const startDate = searchParams.get('startDate');
+    const endDate = searchParams.get('endDate');
+    const periodId = searchParams.get('periodId');
+
+    // Construir filtros
+    const whereClause: any = {
+      studentId: user.student.id,
+    };
+
+    if (sectionId && sectionId !== 'all') {
+      whereClause.sectionId = sectionId;
+    }
+
+    if (status && status !== 'all') {
+      whereClause.status = status;
+    }
+
+    if (startDate) {
+      whereClause.date = {
+        ...whereClause.date,
+        gte: new Date(startDate)
+      };
+    }
+
+    if (endDate) {
+      whereClause.date = {
+        ...whereClause.date,
+        lte: new Date(endDate)
+      };
+    }
+
+    // Si se especifica un período, filtrar por secciones de ese período
+    if (periodId && periodId !== 'all') {
+      whereClause.section = {
+        class: {
+          periodId: periodId
+        }
+      };
+    }
+
+    // Obtener registros de asistencia
+    const attendanceRecords = await prisma.attendance.findMany({
+      where: whereClause,
+      include: {
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true
+              }
+            },
+            teacherAssignments: {
+              where: { isActive: true },
+              include: {
+                teacher: true
+              }
+            }
+          }
+        }
+      },
+      orderBy: {
+        date: 'desc'
+      }
+    });
+
+    // Obtener estadísticas generales
+    const totalRecords = attendanceRecords.length;
+    const presentCount = attendanceRecords.filter(r => r.status === 'PRESENT').length;
+    const absentCount = attendanceRecords.filter(r => r.status === 'ABSENT').length;
+    const justifiedCount = attendanceRecords.filter(r => r.status === 'JUSTIFIED').length;
+    const attendanceRate = totalRecords > 0 ? Math.round((presentCount / totalRecords) * 100) : 0;
+
+    // Estadísticas por sección
+    const sectionStats = attendanceRecords.reduce((acc, record) => {
+      const sectionId = record.sectionId;
+      if (!acc[sectionId]) {
+        acc[sectionId] = {
+          sectionId,
+          sectionName: record.section.name,
+          className: record.section.class.name,
+          classCode: record.section.class.code,
+          periodName: record.section.class.period.name,
+          total: 0,
+          present: 0,
+          absent: 0,
+          justified: 0,
+          attendanceRate: 0
+        };
+      }
+      
+      acc[sectionId].total++;
+      if (record.status === 'PRESENT') acc[sectionId].present++;
+      if (record.status === 'ABSENT') acc[sectionId].absent++;
+      if (record.status === 'JUSTIFIED') acc[sectionId].justified++;
+      
+      acc[sectionId].attendanceRate = Math.round(
+        (acc[sectionId].present / acc[sectionId].total) * 100
+      );
+      
+      return acc;
+    }, {} as Record<string, any>);
+
+    // Estadísticas por período
+    const periodStats = attendanceRecords.reduce((acc, record) => {
+      const periodId = record.section.class.period.id;
+      if (!acc[periodId]) {
+        acc[periodId] = {
+          periodId,
+          periodName: record.section.class.period.name,
+          isActive: record.section.class.period.isActive,
+          total: 0,
+          present: 0,
+          absent: 0,
+          justified: 0,
+          attendanceRate: 0
+        };
+      }
+      
+      acc[periodId].total++;
+      if (record.status === 'PRESENT') acc[periodId].present++;
+      if (record.status === 'ABSENT') acc[periodId].absent++;
+      if (record.status === 'JUSTIFIED') acc[periodId].justified++;
+      
+      acc[periodId].attendanceRate = Math.round(
+        (acc[periodId].present / acc[periodId].total) * 100
+      );
+      
+      return acc;
+    }, {} as Record<string, any>);
+
+    // Obtener todas las secciones matriculadas para filtros
+    const enrolledSections = await prisma.studentEnrollment.findMany({
+      where: {
+        studentId: user.student.id,
+        isActive: true
+      },
+      include: {
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true
+              }
+            }
+          }
+        }
+      }
+    });
+
+    // Obtener períodos únicos
+    const periods = Array.from(
+      new Set(
+        enrolledSections.map(e => e.section.class.period)
+      )
+    ).filter((period, index, self) => 
+      self.findIndex(p => p.id === period.id) === index
+    );
+
+    return NextResponse.json({
+      student: {
+        id: user.student.id,
+        firstName: user.student.firstName,
+        lastName: user.student.lastName,
+        admissionNumber: user.student.admissionNumber
+      },
+      attendanceRecords: attendanceRecords.map(record => ({
+        id: record.id,
+        date: record.date,
+        status: record.status,
+        reason: record.reason,
+        section: {
+          id: record.section.id,
+          name: record.section.name,
+          class: {
+            id: record.section.class.id,
+            name: record.section.class.name,
+            code: record.section.class.code,
+            period: {
+              id: record.section.class.period.id,
+              name: record.section.class.period.name,
+              isActive: record.section.class.period.isActive
+            }
+          },
+          teachers: record.section.teacherAssignments.map(ta => ({
+            id: ta.teacher.id,
+            firstName: ta.teacher.firstName,
+            lastName: ta.teacher.lastName,
+            email: ta.teacher.email
+          }))
+        }
+      })),
+      statistics: {
+        overall: {
+          totalRecords,
+          present: presentCount,
+          absent: absentCount,
+          justified: justifiedCount,
+          attendanceRate
+        },
+        bySections: Object.values(sectionStats),
+        byPeriods: Object.values(periodStats)
+      },
+      filters: {
+        sections: enrolledSections.map(e => ({
+          id: e.section.id,
+          name: e.section.name,
+          className: e.section.class.name,
+          classCode: e.section.class.code,
+          periodName: e.section.class.period.name
+        })),
+        periods: periods.map(p => ({
+          id: p.id,
+          name: p.name,
+          isActive: p.isActive
+        }))
+      }
+    });
+
+  } catch (error) {
+    console.error('Error al obtener historial de asistencia:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 195 - 0
src/app/api/student/classes/route.ts

@@ -0,0 +1,195 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user?.id) {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    if (session.user.role !== 'STUDENT') {
+      return NextResponse.json(
+        { message: 'Acceso denegado. Solo estudiantes pueden acceder a esta información.' },
+        { status: 403 }
+      );
+    }
+
+    // Buscar el estudiante por userId
+    const student = await prisma.student.findUnique({
+      where: {
+        userId: session.user.id,
+        isActive: true,
+      },
+    });
+
+    if (!student) {
+      return NextResponse.json(
+        { message: 'Estudiante no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Obtener las inscripciones activas del estudiante con toda la información relacionada
+    const enrollments = await prisma.studentEnrollment.findMany({
+      where: {
+        studentId: student.id,
+        isActive: true,
+        section: {
+          isActive: true,
+          class: {
+            isActive: true,
+          },
+        },
+      },
+      include: {
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true,
+              },
+            },
+            teacherAssignments: {
+              where: {
+                isActive: true,
+              },
+              include: {
+                teacher: {
+                  select: {
+                    id: true,
+                    firstName: true,
+                    lastName: true,
+                    email: true,
+                  },
+                },
+              },
+            },
+            _count: {
+              select: {
+                studentEnrollments: {
+                  where: {
+                    isActive: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      orderBy: [
+        {
+          section: {
+            class: {
+              period: {
+                isActive: 'desc',
+              },
+            },
+          },
+        },
+        {
+          section: {
+            class: {
+              name: 'asc',
+            },
+          },
+        },
+      ],
+    });
+
+    // Obtener estadísticas de asistencia para cada clase
+    const classesWithStats = await Promise.all(
+      enrollments.map(async (enrollment) => {
+        const attendanceStats = await prisma.attendance.groupBy({
+          by: ['status'],
+          where: {
+            studentId: student.id,
+            sectionId: enrollment.sectionId,
+          },
+          _count: {
+            status: true,
+          },
+        });
+
+        const totalAttendance = attendanceStats.reduce(
+          (sum, stat) => sum + stat._count.status,
+          0
+        );
+
+        const presentCount = attendanceStats.find(
+          (stat) => stat.status === 'PRESENT'
+        )?._count.status || 0;
+
+        const absentCount = attendanceStats.find(
+          (stat) => stat.status === 'ABSENT'
+        )?._count.status || 0;
+
+        const justifiedCount = attendanceStats.find(
+          (stat) => stat.status === 'JUSTIFIED'
+        )?._count.status || 0;
+
+        const attendanceRate = totalAttendance > 0 
+          ? Math.round((presentCount / totalAttendance) * 100) 
+          : 0;
+
+        return {
+          id: enrollment.id,
+          enrolledAt: enrollment.createdAt,
+          section: {
+            id: enrollment.section.id,
+            name: enrollment.section.name,
+            studentCount: enrollment.section._count.studentEnrollments,
+            class: {
+              id: enrollment.section.class.id,
+              name: enrollment.section.class.name,
+              code: enrollment.section.class.code,
+              description: enrollment.section.class.description,
+              period: {
+                id: enrollment.section.class.period.id,
+                name: enrollment.section.class.period.name,
+                startDate: enrollment.section.class.period.startDate,
+                endDate: enrollment.section.class.period.endDate,
+                isActive: enrollment.section.class.period.isActive,
+              },
+            },
+            teachers: enrollment.section.teacherAssignments.map((assignment) => ({
+              id: assignment.teacher.id,
+              firstName: assignment.teacher.firstName,
+              lastName: assignment.teacher.lastName,
+              email: assignment.teacher.email,
+            })),
+          },
+          attendanceStats: {
+            totalRecords: totalAttendance,
+            present: presentCount,
+            absent: absentCount,
+            justified: justifiedCount,
+            attendanceRate,
+          },
+        };
+      })
+    );
+
+    return NextResponse.json({
+      classes: classesWithStats,
+      student: {
+        id: student.id,
+        firstName: student.firstName,
+        lastName: student.lastName,
+        admissionNumber: student.admissionNumber,
+      },
+    });
+  } catch (error) {
+    console.error('Error al obtener clases del estudiante:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 648 - 0
src/app/student/attendance/page.tsx

@@ -0,0 +1,648 @@
+'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 {
+  Calendar,
+  Clock,
+  TrendingUp,
+  Search,
+  Filter,
+  CheckCircle,
+  XCircle,
+  AlertCircle,
+  User,
+  BookOpen,
+  BarChart3,
+  CalendarDays,
+  FileText,
+  Download
+} 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<AttendanceResponse | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [selectedSection, setSelectedSection] = useState<string>('all');
+  const [selectedPeriod, setSelectedPeriod] = useState<string>('all');
+  const [selectedStatus, setSelectedStatus] = useState<string>('all');
+  const [startDate, setStartDate] = useState('');
+  const [endDate, setEndDate] = useState('');
+
+  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;
+  }) || [];
+
+  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 <CheckCircle className="h-4 w-4" />;
+      case 'ABSENT': return <XCircle className="h-4 w-4" />;
+      case 'JUSTIFIED': return <AlertCircle className="h-4 w-4" />;
+      default: return <Clock className="h-4 w-4" />;
+    }
+  };
+
+  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('');
+  };
+
+  if (loading) {
+    return (
+      <MainLayout requiredRole="STUDENT" title="Historial de Asistencia">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando historial de asistencia...</div>
+        </div>
+      </MainLayout>
+    );
+  }
+
+  if (!data) {
+    return (
+      <MainLayout requiredRole="STUDENT" title="Historial de Asistencia">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg text-red-600">Error al cargar los datos</div>
+        </div>
+      </MainLayout>
+    );
+  }
+
+  return (
+    <MainLayout requiredRole="STUDENT" title="Historial de Asistencia">
+      <div className="space-y-6">
+        {/* Header */}
+        <div className="flex justify-between items-center">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Historial de Asistencia</h1>
+            <p className="text-muted-foreground">
+              {data.student.firstName} {data.student.lastName} - {data.student.admissionNumber}
+            </p>
+          </div>
+        </div>
+
+        {/* Estadísticas Generales */}
+        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Total de Registros</CardTitle>
+              <Calendar className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{data.statistics.overall.totalRecords}</div>
+              <p className="text-xs text-muted-foreground">
+                Histórico completo
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Asistencia Promedio</CardTitle>
+              <TrendingUp className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className={`text-2xl font-bold ${getAttendanceColor(data.statistics.overall.attendanceRate)}`}>
+                {data.statistics.overall.attendanceRate}%
+              </div>
+              <p className="text-xs text-muted-foreground">
+                Todas las clases
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Días Presente</CardTitle>
+              <CheckCircle className="h-4 w-4 text-green-600" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold text-green-600">{data.statistics.overall.present}</div>
+              <p className="text-xs text-muted-foreground">
+                {Math.round((data.statistics.overall.present / data.statistics.overall.totalRecords) * 100)}% del total
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Días Ausente</CardTitle>
+              <XCircle className="h-4 w-4 text-red-600" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold text-red-600">{data.statistics.overall.absent}</div>
+              <p className="text-xs text-muted-foreground">
+                {data.statistics.overall.justified} justificados
+              </p>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Filtros */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Filter className="h-5 w-5" />
+              Filtros
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
+              <div className="space-y-2">
+                <Label htmlFor="search">Buscar</Label>
+                <div className="relative">
+                  <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
+                  <Input
+                    id="search"
+                    placeholder="Buscar..."
+                    value={searchTerm}
+                    onChange={(e) => setSearchTerm(e.target.value)}
+                    className="pl-10"
+                  />
+                </div>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="section">Sección</Label>
+                <Select value={selectedSection} onValueChange={setSelectedSection}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Todas las secciones" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todas las secciones</SelectItem>
+                    {data.filters.sections.map((section) => (
+                      <SelectItem key={section.id} value={section.id}>
+                        {section.className} - {section.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="period">Período</Label>
+                <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Todos los períodos" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los períodos</SelectItem>
+                    {data.filters.periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name} {period.isActive && '(Activo)'}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="status">Estado</Label>
+                <Select value={selectedStatus} onValueChange={setSelectedStatus}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Todos los estados" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los estados</SelectItem>
+                    <SelectItem value="PRESENT">Presente</SelectItem>
+                    <SelectItem value="ABSENT">Ausente</SelectItem>
+                    <SelectItem value="JUSTIFIED">Justificado</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="startDate">Fecha Inicio</Label>
+                <Input
+                  id="startDate"
+                  type="date"
+                  value={startDate}
+                  onChange={(e) => setStartDate(e.target.value)}
+                />
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="endDate">Fecha Fin</Label>
+                <Input
+                  id="endDate"
+                  type="date"
+                  value={endDate}
+                  onChange={(e) => setEndDate(e.target.value)}
+                />
+              </div>
+            </div>
+            
+            <div className="flex justify-end mt-4">
+              <Button variant="outline" onClick={clearFilters}>
+                Limpiar filtros
+              </Button>
+            </div>
+          </CardContent>
+        </Card>
+
+        {/* Contenido con Tabs */}
+        <Tabs defaultValue="records" className="space-y-4">
+          <TabsList>
+            <TabsTrigger value="records" className="flex items-center gap-2">
+              <FileText className="h-4 w-4" />
+              Registros ({filteredRecords.length})
+            </TabsTrigger>
+            <TabsTrigger value="sections" className="flex items-center gap-2">
+              <BookOpen className="h-4 w-4" />
+              Por Sección ({data.statistics.bySections.length})
+            </TabsTrigger>
+            <TabsTrigger value="periods" className="flex items-center gap-2">
+              <BarChart3 className="h-4 w-4" />
+              Por Período ({data.statistics.byPeriods.length})
+            </TabsTrigger>
+          </TabsList>
+
+          {/* Tab de Registros */}
+          <TabsContent value="records" className="space-y-4">
+            {filteredRecords.length > 0 ? (
+              <div className="grid grid-cols-1 gap-4">
+                {filteredRecords.map((record) => (
+                  <Card key={record.id} className="hover:shadow-md transition-shadow">
+                    <CardContent className="p-4">
+                      <div className="flex justify-between items-start">
+                        <div className="space-y-2 flex-1">
+                          <div className="flex items-center gap-3">
+                            <div className={`flex items-center gap-2 ${getStatusColor(record.status)}`}>
+                              {getStatusIcon(record.status)}
+                              <Badge variant={getStatusBadgeVariant(record.status)}>
+                                {getStatusText(record.status)}
+                              </Badge>
+                            </div>
+                            <div className="text-sm text-muted-foreground">
+                              {formatDate(record.date)}
+                            </div>
+                          </div>
+                          
+                          <div className="space-y-1">
+                            <div className="font-medium">
+                              {record.section.class.name} - {record.section.name}
+                            </div>
+                            <div className="text-sm text-muted-foreground">
+                              {record.section.class.code} • {record.section.class.period.name}
+                            </div>
+                            <div className="text-sm text-muted-foreground">
+                              <User className="h-3 w-3 inline mr-1" />
+                              {getTeacherNames(record.section.teachers) || 'Sin profesor asignado'}
+                            </div>
+                          </div>
+                          
+                          {record.reason && (
+                            <div className="text-sm bg-muted p-2 rounded">
+                              <strong>Observación:</strong> {record.reason}
+                            </div>
+                          )}
+                        </div>
+                      </div>
+                    </CardContent>
+                  </Card>
+                ))}
+              </div>
+            ) : (
+              <Card>
+                <CardContent className="flex flex-col items-center justify-center py-12">
+                  <Calendar className="h-12 w-12 text-muted-foreground mb-4" />
+                  <h3 className="text-lg font-medium mb-2">No se encontraron registros</h3>
+                  <p className="text-muted-foreground text-center">
+                    No hay registros de asistencia que coincidan con los filtros aplicados.
+                  </p>
+                </CardContent>
+              </Card>
+            )}
+          </TabsContent>
+
+          {/* Tab de Estadísticas por Sección */}
+          <TabsContent value="sections" className="space-y-4">
+            <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+              {data.statistics.bySections.map((section) => (
+                <Card key={section.sectionId}>
+                  <CardHeader>
+                    <CardTitle className="text-lg">{section.className}</CardTitle>
+                    <CardDescription>
+                      {section.sectionName} • {section.periodName}
+                    </CardDescription>
+                  </CardHeader>
+                  <CardContent className="space-y-4">
+                    <div className="flex justify-between items-center">
+                      <span className="text-sm font-medium">Asistencia</span>
+                      <Badge variant={section.attendanceRate >= 90 ? 'default' : section.attendanceRate >= 75 ? 'secondary' : 'destructive'}>
+                        {section.attendanceRate}%
+                      </Badge>
+                    </div>
+                    
+                    <Progress value={section.attendanceRate} className="h-2" />
+                    
+                    <div className="grid grid-cols-4 gap-2 text-xs">
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <CheckCircle className="h-3 w-3 text-green-600" />
+                          <span className="font-medium">{section.present}</span>
+                        </div>
+                        <div className="text-muted-foreground">Presente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <XCircle className="h-3 w-3 text-red-600" />
+                          <span className="font-medium">{section.absent}</span>
+                        </div>
+                        <div className="text-muted-foreground">Ausente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <AlertCircle className="h-3 w-3 text-yellow-600" />
+                          <span className="font-medium">{section.justified}</span>
+                        </div>
+                        <div className="text-muted-foreground">Justificado</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <Calendar className="h-3 w-3 text-blue-600" />
+                          <span className="font-medium">{section.total}</span>
+                        </div>
+                        <div className="text-muted-foreground">Total</div>
+                      </div>
+                    </div>
+                  </CardContent>
+                </Card>
+              ))}
+            </div>
+          </TabsContent>
+
+          {/* Tab de Estadísticas por Período */}
+          <TabsContent value="periods" className="space-y-4">
+            <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+              {data.statistics.byPeriods.map((period) => (
+                <Card key={period.periodId}>
+                  <CardHeader>
+                    <div className="flex justify-between items-start">
+                      <div>
+                        <CardTitle className="text-lg">{period.periodName}</CardTitle>
+                        <CardDescription>
+                          {period.total} registros de asistencia
+                        </CardDescription>
+                      </div>
+                      <Badge variant={period.isActive ? 'default' : 'secondary'}>
+                        {period.isActive ? 'Activo' : 'Inactivo'}
+                      </Badge>
+                    </div>
+                  </CardHeader>
+                  <CardContent className="space-y-4">
+                    <div className="flex justify-between items-center">
+                      <span className="text-sm font-medium">Asistencia</span>
+                      <Badge variant={period.attendanceRate >= 90 ? 'default' : period.attendanceRate >= 75 ? 'secondary' : 'destructive'}>
+                        {period.attendanceRate}%
+                      </Badge>
+                    </div>
+                    
+                    <Progress value={period.attendanceRate} className="h-2" />
+                    
+                    <div className="grid grid-cols-4 gap-2 text-xs">
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <CheckCircle className="h-3 w-3 text-green-600" />
+                          <span className="font-medium">{period.present}</span>
+                        </div>
+                        <div className="text-muted-foreground">Presente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <XCircle className="h-3 w-3 text-red-600" />
+                          <span className="font-medium">{period.absent}</span>
+                        </div>
+                        <div className="text-muted-foreground">Ausente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <AlertCircle className="h-3 w-3 text-yellow-600" />
+                          <span className="font-medium">{period.justified}</span>
+                        </div>
+                        <div className="text-muted-foreground">Justificado</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <Calendar className="h-3 w-3 text-blue-600" />
+                          <span className="font-medium">{period.total}</span>
+                        </div>
+                        <div className="text-muted-foreground">Total</div>
+                      </div>
+                    </div>
+                  </CardContent>
+                </Card>
+              ))}
+            </div>
+          </TabsContent>
+        </Tabs>
+      </div>
+    </MainLayout>
+  );
+}

+ 436 - 0
src/app/student/classes/page.tsx

@@ -0,0 +1,436 @@
+'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 {
+  BookOpen,
+  Users,
+  Calendar,
+  Clock,
+  TrendingUp,
+  Search,
+  Filter,
+  CheckCircle,
+  XCircle,
+  AlertCircle,
+  User,
+  GraduationCap
+} from 'lucide-react';
+import { toast } from 'sonner';
+
+interface Teacher {
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  description: string | null;
+  period: Period;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  studentCount: number;
+  class: Class;
+  teachers: Teacher[];
+}
+
+interface AttendanceStats {
+  totalRecords: number;
+  present: number;
+  absent: number;
+  justified: number;
+  attendanceRate: number;
+}
+
+interface EnrolledClass {
+  id: string;
+  enrolledAt: string;
+  section: Section;
+  attendanceStats: AttendanceStats;
+}
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  admissionNumber: string;
+}
+
+interface ClassesResponse {
+  classes: EnrolledClass[];
+  student: Student;
+}
+
+export default function StudentClassesPage() {
+  const [classes, setClasses] = useState<EnrolledClass[]>([]);
+  const [student, setStudent] = useState<Student | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [selectedPeriod, setSelectedPeriod] = useState<string>('all');
+  const [selectedStatus, setSelectedStatus] = useState<string>('all');
+
+  useEffect(() => {
+    fetchClasses();
+  }, []);
+
+  const fetchClasses = async () => {
+    try {
+      setLoading(true);
+      const response = await fetch('/api/student/classes');
+      if (!response.ok) {
+        throw new Error('Error al cargar las clases');
+      }
+      const data: ClassesResponse = await response.json();
+      setClasses(data.classes);
+      setStudent(data.student);
+    } catch (error) {
+      console.error('Error:', error);
+      toast.error('Error al cargar las clases matriculadas');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const filteredClasses = classes.filter(enrolledClass => {
+    const matchesSearch = 
+      enrolledClass.section.class.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      enrolledClass.section.class.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      enrolledClass.section.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      enrolledClass.section.class.period.name.toLowerCase().includes(searchTerm.toLowerCase());
+    
+    const matchesPeriod = selectedPeriod === 'all' || enrolledClass.section.class.period.id === selectedPeriod;
+    const matchesStatus = selectedStatus === 'all' || 
+      (selectedStatus === 'active' && enrolledClass.section.class.period.isActive) ||
+      (selectedStatus === 'inactive' && !enrolledClass.section.class.period.isActive);
+    
+    return matchesSearch && matchesPeriod && matchesStatus;
+  });
+
+  const periods = Array.from(new Set(classes.map(c => c.section.class.period)))
+    .filter((period, index, self) => self.findIndex(p => p.id === period.id) === index);
+
+  const activeClasses = filteredClasses.filter(c => c.section.class.period.isActive);
+  const totalAttendanceRecords = filteredClasses.reduce((sum, c) => sum + c.attendanceStats.totalRecords, 0);
+  const averageAttendanceRate = filteredClasses.length > 0 
+    ? Math.round(filteredClasses.reduce((sum, c) => sum + c.attendanceStats.attendanceRate, 0) / filteredClasses.length)
+    : 0;
+
+  const getAttendanceColor = (rate: number) => {
+    if (rate >= 90) return 'text-green-600';
+    if (rate >= 75) return 'text-yellow-600';
+    return 'text-red-600';
+  };
+
+  const getAttendanceBadgeVariant = (rate: number): 'default' | 'secondary' | 'destructive' => {
+    if (rate >= 90) return 'default';
+    if (rate >= 75) return 'secondary';
+    return 'destructive';
+  };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric'
+    });
+  };
+
+  const getTeacherNames = (teachers: Teacher[]) => {
+    return teachers.map(teacher => `${teacher.firstName} ${teacher.lastName}`).join(', ');
+  };
+
+  if (loading) {
+    return (
+      <MainLayout requiredRole="STUDENT" title="Mis Clases">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando clases matriculadas...</div>
+        </div>
+      </MainLayout>
+    );
+  }
+
+  return (
+    <MainLayout requiredRole="STUDENT" title="Mis Clases">
+      <div className="space-y-6">
+        {/* Header */}
+        <div className="flex justify-between items-center">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Mis Clases Matriculadas</h1>
+            <p className="text-muted-foreground">
+              {student && `${student.firstName} ${student.lastName} - ${student.admissionNumber}`}
+            </p>
+          </div>
+        </div>
+
+        {/* Estadísticas Generales */}
+        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Total de Clases</CardTitle>
+              <BookOpen className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{filteredClasses.length}</div>
+              <p className="text-xs text-muted-foreground">
+                {activeClasses.length} activas
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Registros de Asistencia</CardTitle>
+              <Calendar className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{totalAttendanceRecords}</div>
+              <p className="text-xs text-muted-foreground">
+                Total acumulado
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Asistencia Promedio</CardTitle>
+              <TrendingUp className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className={`text-2xl font-bold ${getAttendanceColor(averageAttendanceRate)}`}>
+                {averageAttendanceRate}%
+              </div>
+              <p className="text-xs text-muted-foreground">
+                Todas las clases
+              </p>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Períodos Únicos</CardTitle>
+              <Clock className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{periods.length}</div>
+              <p className="text-xs text-muted-foreground">
+                Histórico
+              </p>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Filtros */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Filter className="h-5 w-5" />
+              Filtros
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+              <div className="space-y-2">
+                <Label htmlFor="search">Buscar</Label>
+                <div className="relative">
+                  <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
+                  <Input
+                    id="search"
+                    placeholder="Buscar por clase, código o sección..."
+                    value={searchTerm}
+                    onChange={(e) => setSearchTerm(e.target.value)}
+                    className="pl-10"
+                  />
+                </div>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="period">Período</Label>
+                <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Filtrar por período" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los períodos</SelectItem>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name} {period.isActive && '(Activo)'}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              
+              <div className="space-y-2">
+                <Label htmlFor="status">Estado</Label>
+                <Select value={selectedStatus} onValueChange={setSelectedStatus}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Filtrar por estado" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los estados</SelectItem>
+                    <SelectItem value="active">Períodos activos</SelectItem>
+                    <SelectItem value="inactive">Períodos inactivos</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+            </div>
+          </CardContent>
+        </Card>
+
+        {/* Lista de Clases */}
+        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+          {filteredClasses.length > 0 ? (
+            filteredClasses.map((enrolledClass) => (
+              <Card key={enrolledClass.id} className="hover:shadow-md transition-shadow">
+                <CardHeader>
+                  <div className="flex justify-between items-start">
+                    <div className="space-y-1">
+                      <CardTitle className="flex items-center gap-2">
+                        <GraduationCap className="h-5 w-5" />
+                        {enrolledClass.section.class.name}
+                      </CardTitle>
+                      <CardDescription>
+                        {enrolledClass.section.class.code} - {enrolledClass.section.name}
+                      </CardDescription>
+                    </div>
+                    <Badge variant={enrolledClass.section.class.period.isActive ? 'default' : 'secondary'}>
+                      {enrolledClass.section.class.period.name}
+                    </Badge>
+                  </div>
+                </CardHeader>
+                
+                <CardContent className="space-y-4">
+                  {/* Información de la clase */}
+                  <div className="space-y-2">
+                    {enrolledClass.section.class.description && (
+                      <p className="text-sm text-muted-foreground">
+                        {enrolledClass.section.class.description}
+                      </p>
+                    )}
+                    
+                    <div className="flex items-center gap-4 text-sm text-muted-foreground">
+                      <div className="flex items-center gap-1">
+                        <Users className="h-4 w-4" />
+                        {enrolledClass.section.studentCount} estudiantes
+                      </div>
+                      <div className="flex items-center gap-1">
+                        <User className="h-4 w-4" />
+                        {getTeacherNames(enrolledClass.section.teachers) || 'Sin profesor asignado'}
+                      </div>
+                    </div>
+                    
+                    <div className="text-sm text-muted-foreground">
+                      <strong>Matriculado:</strong> {formatDate(enrolledClass.enrolledAt)}
+                    </div>
+                    
+                    <div className="text-sm text-muted-foreground">
+                      <strong>Período:</strong> {formatDate(enrolledClass.section.class.period.startDate)} - {formatDate(enrolledClass.section.class.period.endDate)}
+                    </div>
+                  </div>
+
+                  {/* Estadísticas de Asistencia */}
+                  <div className="space-y-3">
+                    <div className="flex justify-between items-center">
+                      <h4 className="text-sm font-medium">Asistencia</h4>
+                      <Badge variant={getAttendanceBadgeVariant(enrolledClass.attendanceStats.attendanceRate)}>
+                        {enrolledClass.attendanceStats.attendanceRate}%
+                      </Badge>
+                    </div>
+                    
+                    <Progress 
+                      value={enrolledClass.attendanceStats.attendanceRate} 
+                      className="h-2"
+                    />
+                    
+                    <div className="grid grid-cols-4 gap-2 text-xs">
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <CheckCircle className="h-3 w-3 text-green-600" />
+                          <span className="font-medium">{enrolledClass.attendanceStats.present}</span>
+                        </div>
+                        <div className="text-muted-foreground">Presente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <XCircle className="h-3 w-3 text-red-600" />
+                          <span className="font-medium">{enrolledClass.attendanceStats.absent}</span>
+                        </div>
+                        <div className="text-muted-foreground">Ausente</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <AlertCircle className="h-3 w-3 text-yellow-600" />
+                          <span className="font-medium">{enrolledClass.attendanceStats.justified}</span>
+                        </div>
+                        <div className="text-muted-foreground">Justificado</div>
+                      </div>
+                      
+                      <div className="text-center">
+                        <div className="flex items-center justify-center gap-1">
+                          <Calendar className="h-3 w-3 text-blue-600" />
+                          <span className="font-medium">{enrolledClass.attendanceStats.totalRecords}</span>
+                        </div>
+                        <div className="text-muted-foreground">Total</div>
+                      </div>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            ))
+          ) : (
+            <div className="col-span-full">
+              <Card>
+                <CardContent className="flex flex-col items-center justify-center py-12">
+                  <BookOpen className="h-12 w-12 text-muted-foreground mb-4" />
+                  <h3 className="text-lg font-medium mb-2">No se encontraron clases</h3>
+                  <p className="text-muted-foreground text-center">
+                    {searchTerm || selectedPeriod !== 'all' || selectedStatus !== 'all'
+                      ? 'No hay clases que coincidan con los filtros aplicados.'
+                      : 'No tienes clases matriculadas en este momento.'}
+                  </p>
+                  {(searchTerm || selectedPeriod !== 'all' || selectedStatus !== 'all') && (
+                    <Button 
+                      variant="outline" 
+                      className="mt-4"
+                      onClick={() => {
+                        setSearchTerm('');
+                        setSelectedPeriod('all');
+                        setSelectedStatus('all');
+                      }}
+                    >
+                      Limpiar filtros
+                    </Button>
+                  )}
+                </CardContent>
+              </Card>
+            </div>
+          )}
+        </div>
+      </div>
+    </MainLayout>
+  );
+}

+ 2 - 2
src/components/layout/sidebar.tsx

@@ -52,9 +52,9 @@ const teacherMenuItems = [
 const studentMenuItems = [
   { icon: Home, label: 'Dashboard', href: '/student' },
   { icon: BookOpen, label: 'Mis Clases', href: '/student/classes' },
-  { icon: Calendar, label: 'Horarios', href: '/student/schedule' },
+  // { icon: Calendar, label: 'Horarios', href: '/student/schedule' },
   { icon: ClipboardList, label: 'Mi Asistencia', href: '/student/attendance' },
-  { icon: Settings, label: 'Perfil', href: '/student/profile' },
+  // { icon: Settings, label: 'Perfil', href: '/student/profile' },
 ]
 
 function SidebarContent({ className }: SidebarProps) {

+ 31 - 0
src/components/ui/progress.tsx

@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+  className,
+  value,
+  ...props
+}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
+  return (
+    <ProgressPrimitive.Root
+      data-slot="progress"
+      className={cn(
+        "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
+        className
+      )}
+      {...props}
+    >
+      <ProgressPrimitive.Indicator
+        data-slot="progress-indicator"
+        className="bg-primary h-full w-full flex-1 transition-all"
+        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+      />
+    </ProgressPrimitive.Root>
+  )
+}
+
+export { Progress }