Procházet zdrojové kódy

holy shit teachers done?

Matthew Trejo před 4 měsíci
rodič
revize
ef81f6cec3

+ 6 - 5
ROADMAP.md

@@ -131,12 +131,14 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
 - **Estado**: Pendiente
 - **Descripción**: Implementación de funcionalidades para profesores
 - **Dashboard**: ✅ Completado
+- **Implementado**:
+  - ✅ Visualización de clases/secciones asignadas (`/teacher/assignments`)
+  - ✅ Sistema de registro de asistencia (`/teacher/attendance`)
+  - ✅ Reportes de asistencia por clase (`/teacher/reports`)
+  - ✅ Exportación de datos
 - **Por implementar**:
-  - Gestión de clases asignadas (`/teacher/classes`)
-  - Sistema de registro de asistencia (`/teacher/attendance`)
-  - Reportes de asistencia por clase (`/teacher/reports`)
   - Gestión de perfil (`/teacher/profile`)
-  - Exportación de datos
+  
 
 ### Funcionalidades del Rol Estudiante 📝
 - **Estado**: Pendiente
@@ -157,7 +159,6 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
   - Pruebas unitarias para componentes
   - Pruebas de integración para API
   - Pruebas end-to-end (Playwright/Cypress)
-  - Optimización de build
   - Configuración de CI/CD
   - Documentación de deployment
 

+ 1 - 1
src/app/admin/reports/page.tsx

@@ -335,7 +335,7 @@ export default function ReportsPage() {
   };
 
   return (
-    <MainLayout>
+    <MainLayout title="Reportes">
       <div className="space-y-6">
         <div className="flex items-center justify-between">
           <div>

+ 1 - 1
src/app/admin/student-enrollments/page.tsx

@@ -293,7 +293,7 @@ export default function StudentEnrollmentsPage() {
   };
 
   return (
-    <MainLayout>
+    <MainLayout title="Inscripciones de Estudiantes">
       <div className="space-y-6">
         <div className="flex items-center justify-between">
           <div>

+ 1 - 1
src/app/admin/teacher-assignments/page.tsx

@@ -287,7 +287,7 @@ export default function TeacherAssignmentsPage() {
   };
 
   return (
-    <MainLayout>
+    <MainLayout title="Asignaciones de Profesores">
       <div className="space-y-6">
         <div className="flex items-center justify-between">
           <div>

+ 121 - 0
src/app/api/teacher/assignments/route.ts

@@ -0,0 +1,121 @@
+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?.email) {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    // Verificar que el usuario sea un profesor
+    const user = await prisma.user.findUnique({
+      where: { email: session.user.email },
+      include: { teacher: true }
+    });
+
+    if (!user || user.role !== 'TEACHER' || !user.teacher) {
+      return NextResponse.json(
+        { error: 'Acceso denegado. Solo profesores pueden acceder a esta información.' },
+        { status: 403 }
+      );
+    }
+
+    // Obtener las asignaciones del profesor con información detallada
+    const assignments = await prisma.teacherAssignment.findMany({
+      where: {
+        teacherId: user.teacher.id,
+        isActive: true
+      },
+      include: {
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true
+              }
+            },
+            studentEnrollments: {
+              where: { isActive: true },
+              include: {
+                student: {
+                  select: {
+                    id: true,
+                    firstName: true,
+                    lastName: true,
+                    cedula: true,
+                    email: true,
+                    admissionNumber: true
+                  }
+                }
+              }
+            },
+            _count: {
+              select: {
+                studentEnrollments: {
+                  where: { isActive: true }
+                }
+              }
+            }
+          }
+        }
+      },
+      orderBy: [
+        { section: { class: { period: { startDate: 'desc' } } } },
+        { section: { class: { name: 'asc' } } },
+        { section: { name: 'asc' } }
+      ]
+    });
+
+    // Formatear la respuesta
+    const formattedAssignments = assignments.map(assignment => ({
+      id: assignment.id,
+      isActive: assignment.isActive,
+      createdAt: assignment.createdAt,
+      section: {
+        id: assignment.section.id,
+        name: assignment.section.name,
+        isActive: assignment.section.isActive,
+        studentCount: assignment.section._count.studentEnrollments,
+        students: assignment.section.studentEnrollments.map(enrollment => enrollment.student),
+        class: {
+          id: assignment.section.class.id,
+          name: assignment.section.class.name,
+          code: assignment.section.class.code,
+          description: assignment.section.class.description,
+          isActive: assignment.section.class.isActive,
+          period: {
+            id: assignment.section.class.period.id,
+            name: assignment.section.class.period.name,
+            startDate: assignment.section.class.period.startDate,
+            endDate: assignment.section.class.period.endDate,
+            isActive: assignment.section.class.period.isActive
+          }
+        }
+      }
+    }));
+
+    return NextResponse.json({
+      assignments: formattedAssignments,
+      teacher: {
+        id: user.teacher.id,
+        firstName: user.teacher.firstName,
+        lastName: user.teacher.lastName,
+        email: user.teacher.email
+      }
+    });
+
+  } catch (error) {
+    console.error('Error al obtener asignaciones del profesor:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 336 - 0
src/app/api/teacher/attendance/route.ts

@@ -0,0 +1,336 @@
+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(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    // Verificar que el usuario sea un profesor
+    const teacher = await prisma.teacher.findUnique({
+      where: { userId: session.user.id },
+      select: {
+        id: true,
+        firstName: true,
+        lastName: true,
+        email: true,
+        isActive: true
+      }
+    });
+
+    if (!teacher) {
+      return NextResponse.json(
+        { error: 'Acceso denegado. Solo profesores pueden acceder.' },
+        { status: 403 }
+      );
+    }
+
+    const { searchParams } = new URL(request.url);
+    const sectionId = searchParams.get('sectionId');
+    const date = searchParams.get('date');
+
+    if (!sectionId) {
+      return NextResponse.json(
+        { error: 'ID de sección es requerido' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar que el profesor esté asignado a esta sección
+    const teacherAssignment = await prisma.teacherAssignment.findFirst({
+      where: {
+        teacherId: teacher.id,
+        sectionId: sectionId,
+        isActive: true
+      }
+    });
+
+    if (!teacherAssignment) {
+      return NextResponse.json(
+        { error: 'No tienes acceso a esta sección' },
+        { status: 403 }
+      );
+    }
+
+    // Construir filtros para la consulta
+    const whereClause: any = {
+      sectionId: sectionId
+    };
+
+    if (date) {
+      const targetDate = new Date(date);
+      targetDate.setHours(0, 0, 0, 0);
+      const nextDay = new Date(targetDate);
+      nextDay.setDate(nextDay.getDate() + 1);
+      
+      whereClause.date = {
+        gte: targetDate,
+        lt: nextDay
+      };
+    }
+
+    // Obtener registros de asistencia
+    const attendanceRecords = await prisma.attendance.findMany({
+      where: whereClause,
+      include: {
+        student: {
+          select: {
+            id: true,
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            email: true,
+            admissionNumber: true
+          }
+        }
+      },
+      orderBy: [
+        { date: 'desc' },
+        { student: { lastName: 'asc' } },
+        { student: { firstName: 'asc' } }
+      ]
+    });
+
+    // Obtener información de la sección
+    const section = await prisma.section.findUnique({
+      where: { id: sectionId },
+      include: {
+        class: {
+          include: {
+            period: true
+          }
+        },
+        studentEnrollments: {
+          where: { isActive: true },
+          include: {
+            student: {
+              select: {
+                id: true,
+                firstName: true,
+                lastName: true,
+                cedula: true,
+                email: true,
+                admissionNumber: true
+              }
+            }
+          },
+          orderBy: [
+            { student: { lastName: 'asc' } },
+            { student: { firstName: 'asc' } }
+          ]
+        }
+      }
+    });
+
+    if (!section) {
+      return NextResponse.json(
+        { error: 'Sección no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Si se especifica una fecha, obtener o crear registros para esa fecha
+    if (date) {
+      const targetDate = new Date(date);
+      targetDate.setHours(0, 0, 0, 0);
+      
+      const enrolledStudents = section.studentEnrollments.map(enrollment => enrollment.student);
+      const attendanceMap = new Map(attendanceRecords.map(record => [record.studentId, record]));
+      
+      // Crear registros de asistencia para estudiantes que no tienen registro en esa fecha
+      const attendanceForDate = enrolledStudents.map(student => {
+        const existingRecord = attendanceMap.get(student.id);
+        return existingRecord || {
+          id: null,
+          studentId: student.id,
+          sectionId: sectionId,
+          date: targetDate,
+          status: null,
+          reason: null,
+          student: student
+        };
+      });
+
+      return NextResponse.json({
+        section: {
+          id: section.id,
+          name: section.name,
+          class: {
+            id: section.class.id,
+            name: section.class.name,
+            code: section.class.code,
+            period: section.class.period
+          }
+        },
+        teacher: {
+          id: teacher.id,
+          firstName: teacher.firstName,
+          lastName: teacher.lastName
+        },
+        date: targetDate,
+        attendance: attendanceForDate
+      });
+    }
+
+    // Si no se especifica fecha, devolver todos los registros
+    return NextResponse.json({
+      section: {
+        id: section.id,
+        name: section.name,
+        class: {
+          id: section.class.id,
+          name: section.class.name,
+          code: section.class.code,
+          period: section.class.period
+        }
+      },
+      teacher: {
+        id: teacher.id,
+        firstName: teacher.firstName,
+        lastName: teacher.lastName
+      },
+      attendance: attendanceRecords
+    });
+
+  } catch (error) {
+    console.error('Error al obtener asistencia:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user?.id) {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    // Verificar que el usuario sea un profesor
+    const teacher = await prisma.teacher.findUnique({
+      where: { userId: session.user.id }
+    });
+
+    if (!teacher) {
+      return NextResponse.json(
+        { error: 'Acceso denegado. Solo profesores pueden registrar asistencia.' },
+        { status: 403 }
+      );
+    }
+
+    const body = await request.json();
+    const { sectionId, date, attendanceData } = body;
+
+    if (!sectionId || !date || !Array.isArray(attendanceData)) {
+      return NextResponse.json(
+        { error: 'Datos incompletos. Se requiere sectionId, date y attendanceData.' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar que el profesor esté asignado a esta sección
+    const teacherAssignment = await prisma.teacherAssignment.findFirst({
+      where: {
+        teacherId: teacher.id,
+        sectionId: sectionId,
+        isActive: true
+      }
+    });
+
+    if (!teacherAssignment) {
+      return NextResponse.json(
+        { error: 'No tienes acceso a esta sección' },
+        { status: 403 }
+      );
+    }
+
+    const targetDate = new Date(date);
+    targetDate.setHours(0, 0, 0, 0);
+
+    // Procesar cada registro de asistencia
+    const results = [];
+    
+    for (const record of attendanceData) {
+      const { studentId, status, reason } = record;
+      
+      if (!studentId || !status) {
+        continue; // Saltar registros incompletos
+      }
+
+      // Verificar que el estudiante esté inscrito en la sección
+      const enrollment = await prisma.studentEnrollment.findFirst({
+        where: {
+          studentId: studentId,
+          sectionId: sectionId,
+          isActive: true
+        }
+      });
+
+      if (!enrollment) {
+        continue; // Saltar estudiantes no inscritos
+      }
+
+      // Crear o actualizar registro de asistencia
+      const attendanceRecord = await prisma.attendance.upsert({
+        where: {
+          studentId_sectionId_date: {
+            studentId: studentId,
+            sectionId: sectionId,
+            date: targetDate
+          }
+        },
+        update: {
+          status: status,
+          reason: reason || null,
+          updatedAt: new Date()
+        },
+        create: {
+          studentId: studentId,
+          sectionId: sectionId,
+          date: targetDate,
+          status: status,
+          reason: reason || null
+        },
+        include: {
+          student: {
+            select: {
+              id: true,
+              firstName: true,
+              lastName: true,
+              cedula: true
+            }
+          }
+        }
+      });
+
+      results.push(attendanceRecord);
+    }
+
+    return NextResponse.json({
+      message: 'Asistencia registrada exitosamente',
+      recordsProcessed: results.length,
+      records: results
+    });
+
+  } catch (error) {
+    console.error('Error al registrar asistencia:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 432 - 0
src/app/api/teacher/reports/route.ts

@@ -0,0 +1,432 @@
+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(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    // Verificar que el usuario sea un profesor
+    const teacher = await prisma.teacher.findUnique({
+      where: { userId: session.user.id },
+      select: {
+        id: true,
+        firstName: true,
+        lastName: true,
+        email: true,
+        isActive: true
+      }
+    });
+
+    if (!teacher) {
+      return NextResponse.json(
+        { error: 'Acceso denegado. Solo profesores pueden acceder.' },
+        { status: 403 }
+      );
+    }
+
+    const { searchParams } = new URL(request.url);
+    const reportType = searchParams.get('type') || 'summary';
+    const sectionId = searchParams.get('sectionId');
+    const startDate = searchParams.get('startDate');
+    const endDate = searchParams.get('endDate');
+    const studentId = searchParams.get('studentId');
+
+    // Verificar acceso a la sección si se especifica
+    if (sectionId) {
+      const teacherAssignment = await prisma.teacherAssignment.findFirst({
+        where: {
+          teacherId: teacher.id,
+          sectionId: sectionId,
+          isActive: true
+        }
+      });
+
+      if (!teacherAssignment) {
+        return NextResponse.json(
+          { error: 'No tienes acceso a esta sección' },
+          { status: 403 }
+        );
+      }
+    }
+
+    // Construir filtros base
+    const whereClause: any = {};
+    
+    if (sectionId) {
+      whereClause.sectionId = sectionId;
+    } else {
+      // Si no se especifica sección, obtener solo las secciones asignadas al profesor
+      const teacherSections = await prisma.teacherAssignment.findMany({
+        where: {
+          teacherId: teacher.id,
+          isActive: true
+        },
+        select: { sectionId: true }
+      });
+      
+      whereClause.sectionId = {
+        in: teacherSections.map(assignment => assignment.sectionId)
+      };
+    }
+
+    if (startDate && endDate) {
+      const start = new Date(startDate);
+      const end = new Date(endDate);
+      end.setHours(23, 59, 59, 999);
+      
+      whereClause.date = {
+        gte: start,
+        lte: end
+      };
+    }
+
+    if (studentId) {
+      whereClause.studentId = studentId;
+    }
+
+    switch (reportType) {
+      case 'summary':
+        return await generateSummaryReport(whereClause, teacher);
+      
+      case 'detailed':
+        return await generateDetailedReport(whereClause, teacher);
+      
+      case 'student':
+        return await generateStudentReport(whereClause, teacher, studentId);
+      
+      case 'statistics':
+        return await generateStatisticsReport(whereClause, teacher);
+      
+      default:
+        return NextResponse.json(
+          { error: 'Tipo de reporte no válido' },
+          { status: 400 }
+        );
+    }
+
+  } catch (error) {
+    console.error('Error al generar reporte:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// Reporte resumen por sección
+async function generateSummaryReport(whereClause: any, teacher: any) {
+  const attendanceRecords = await prisma.attendance.findMany({
+    where: whereClause,
+    include: {
+      section: {
+        include: {
+          class: {
+            include: {
+              period: true
+            }
+          },
+          studentEnrollments: {
+            where: { isActive: true },
+            include: {
+              student: {
+                select: {
+                  id: true,
+                  firstName: true,
+                  lastName: true,
+                  cedula: true
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  // Agrupar por sección
+  const sectionStats = new Map();
+  
+  attendanceRecords.forEach(record => {
+    const sectionId = record.sectionId;
+    if (!sectionStats.has(sectionId)) {
+      sectionStats.set(sectionId, {
+        section: record.section,
+        totalRecords: 0,
+        present: 0,
+        absent: 0,
+        justified: 0,
+        attendanceRate: 0
+      });
+    }
+    
+    const stats = sectionStats.get(sectionId);
+    stats.totalRecords++;
+    
+    switch (record.status) {
+      case 'PRESENT':
+        stats.present++;
+        break;
+      case 'ABSENT':
+        stats.absent++;
+        break;
+      case 'JUSTIFIED':
+        stats.justified++;
+        break;
+    }
+    
+    stats.attendanceRate = ((stats.present + stats.justified) / stats.totalRecords) * 100;
+  });
+
+  return NextResponse.json({
+    reportType: 'summary',
+    teacher: {
+      id: teacher.id,
+      firstName: teacher.firstName,
+      lastName: teacher.lastName
+    },
+    generatedAt: new Date(),
+    data: Array.from(sectionStats.values())
+  });
+}
+
+// Reporte detallado por fecha
+async function generateDetailedReport(whereClause: any, teacher: any) {
+  const attendanceRecords = await prisma.attendance.findMany({
+    where: whereClause,
+    include: {
+      student: {
+        select: {
+          id: true,
+          firstName: true,
+          lastName: true,
+          cedula: true,
+          admissionNumber: true
+        }
+      },
+      section: {
+        include: {
+          class: {
+            include: {
+              period: true
+            }
+          }
+        }
+      }
+    },
+    orderBy: [
+      { date: 'desc' },
+      { section: { class: { name: 'asc' } } },
+      { student: { lastName: 'asc' } }
+    ]
+  });
+
+  return NextResponse.json({
+    reportType: 'detailed',
+    teacher: {
+      id: teacher.id,
+      firstName: teacher.firstName,
+      lastName: teacher.lastName
+    },
+    generatedAt: new Date(),
+    totalRecords: attendanceRecords.length,
+    data: attendanceRecords
+  });
+}
+
+// Reporte por estudiante específico
+async function generateStudentReport(whereClause: any, teacher: any, studentId: string | null) {
+  if (!studentId) {
+    return NextResponse.json(
+      { error: 'ID de estudiante requerido para este tipo de reporte' },
+      { status: 400 }
+    );
+  }
+
+  const student = await prisma.student.findUnique({
+    where: { id: studentId },
+    select: {
+      id: true,
+      firstName: true,
+      lastName: true,
+      cedula: true,
+      admissionNumber: true,
+      email: true
+    }
+  });
+
+  if (!student) {
+    return NextResponse.json(
+      { error: 'Estudiante no encontrado' },
+      { status: 404 }
+    );
+  }
+
+  const attendanceRecords = await prisma.attendance.findMany({
+    where: whereClause,
+    include: {
+      section: {
+        include: {
+          class: {
+            include: {
+              period: true
+            }
+          }
+        }
+      }
+    },
+    orderBy: [
+      { date: 'desc' }
+    ]
+  });
+
+  // Calcular estadísticas del estudiante
+  const stats = {
+    totalRecords: attendanceRecords.length,
+    present: attendanceRecords.filter(r => r.status === 'PRESENT').length,
+    absent: attendanceRecords.filter(r => r.status === 'ABSENT').length,
+    justified: attendanceRecords.filter(r => r.status === 'JUSTIFIED').length,
+    attendanceRate: 0
+  };
+
+  if (stats.totalRecords > 0) {
+    stats.attendanceRate = ((stats.present + stats.justified) / stats.totalRecords) * 100;
+  }
+
+  return NextResponse.json({
+    reportType: 'student',
+    teacher: {
+      id: teacher.id,
+      firstName: teacher.firstName,
+      lastName: teacher.lastName
+    },
+    student: student,
+    generatedAt: new Date(),
+    statistics: stats,
+    data: attendanceRecords
+  });
+}
+
+// Reporte de estadísticas generales
+async function generateStatisticsReport(whereClause: any, teacher: any) {
+  const attendanceRecords = await prisma.attendance.findMany({
+    where: whereClause,
+    include: {
+      section: {
+        include: {
+          class: {
+            include: {
+              period: true
+            }
+          }
+        }
+      }
+    }
+  });
+
+  // Estadísticas generales
+  const totalRecords = attendanceRecords.length;
+  const present = attendanceRecords.filter(r => r.status === 'PRESENT').length;
+  const absent = attendanceRecords.filter(r => r.status === 'ABSENT').length;
+  const justified = attendanceRecords.filter(r => r.status === 'JUSTIFIED').length;
+  
+  const overallAttendanceRate = totalRecords > 0 ? ((present + justified) / totalRecords) * 100 : 0;
+
+  // Estadísticas por período
+  const periodStats = new Map();
+  attendanceRecords.forEach(record => {
+    const periodId = record.section.class.period.id;
+    const periodName = record.section.class.period.name;
+    
+    if (!periodStats.has(periodId)) {
+      periodStats.set(periodId, {
+        period: { id: periodId, name: periodName },
+        total: 0,
+        present: 0,
+        absent: 0,
+        justified: 0,
+        rate: 0
+      });
+    }
+    
+    const stats = periodStats.get(periodId);
+    stats.total++;
+    
+    switch (record.status) {
+      case 'PRESENT':
+        stats.present++;
+        break;
+      case 'ABSENT':
+        stats.absent++;
+        break;
+      case 'JUSTIFIED':
+        stats.justified++;
+        break;
+    }
+    
+    stats.rate = ((stats.present + stats.justified) / stats.total) * 100;
+  });
+
+  // Estadísticas por clase
+  const classStats = new Map();
+  attendanceRecords.forEach(record => {
+    const classId = record.section.class.id;
+    const className = record.section.class.name;
+    const classCode = record.section.class.code;
+    
+    if (!classStats.has(classId)) {
+      classStats.set(classId, {
+        class: { id: classId, name: className, code: classCode },
+        total: 0,
+        present: 0,
+        absent: 0,
+        justified: 0,
+        rate: 0
+      });
+    }
+    
+    const stats = classStats.get(classId);
+    stats.total++;
+    
+    switch (record.status) {
+      case 'PRESENT':
+        stats.present++;
+        break;
+      case 'ABSENT':
+        stats.absent++;
+        break;
+      case 'JUSTIFIED':
+        stats.justified++;
+        break;
+    }
+    
+    stats.rate = ((stats.present + stats.justified) / stats.total) * 100;
+  });
+
+  return NextResponse.json({
+    reportType: 'statistics',
+    teacher: {
+      id: teacher.id,
+      firstName: teacher.firstName,
+      lastName: teacher.lastName
+    },
+    generatedAt: new Date(),
+    overall: {
+      totalRecords,
+      present,
+      absent,
+      justified,
+      attendanceRate: overallAttendanceRate
+    },
+    byPeriod: Array.from(periodStats.values()),
+    byClass: Array.from(classStats.values())
+  });
+}

+ 408 - 0
src/app/teacher/assignments/page.tsx

@@ -0,0 +1,408 @@
+'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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { CalendarDays, Users, BookOpen, Search, Filter, Eye } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  cedula: string;
+  email: string;
+  admissionNumber: string;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  description: string | null;
+  isActive: boolean;
+  period: Period;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  isActive: boolean;
+  studentCount: number;
+  students: Student[];
+  class: Class;
+}
+
+interface Assignment {
+  id: string;
+  isActive: boolean;
+  createdAt: string;
+  section: Section;
+}
+
+interface Teacher {
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+}
+
+interface AssignmentsResponse {
+  assignments: Assignment[];
+  teacher: Teacher;
+}
+
+export default function TeacherAssignmentsPage() {
+  const [assignments, setAssignments] = useState<Assignment[]>([]);
+  const [teacher, setTeacher] = useState<Teacher | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [selectedPeriod, setSelectedPeriod] = useState<string>('all');
+  const [selectedStatus, setSelectedStatus] = useState<string>('all');
+  const [selectedAssignment, setSelectedAssignment] = useState<Assignment | null>(null);
+  const [showStudentDetails, setShowStudentDetails] = useState(false);
+
+  useEffect(() => {
+    fetchAssignments();
+  }, []);
+
+  const fetchAssignments = async () => {
+    try {
+      setLoading(true);
+      const response = await fetch('/api/teacher/assignments');
+      
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'Error al cargar asignaciones');
+      }
+
+      const data: AssignmentsResponse = await response.json();
+      setAssignments(data.assignments);
+      setTeacher(data.teacher);
+    } catch (error) {
+      console.error('Error:', error);
+      toast.error(error instanceof Error ? error.message : 'Error al cargar asignaciones');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const filteredAssignments = assignments.filter(assignment => {
+    const matchesSearch = 
+      assignment.section.class.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      assignment.section.class.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      assignment.section.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      assignment.section.class.period.name.toLowerCase().includes(searchTerm.toLowerCase());
+    
+    const matchesPeriod = selectedPeriod === 'all' || assignment.section.class.period.id === selectedPeriod;
+    const matchesStatus = selectedStatus === 'all' || 
+      (selectedStatus === 'active' && assignment.section.class.period.isActive) ||
+      (selectedStatus === 'inactive' && !assignment.section.class.period.isActive);
+    
+    return matchesSearch && matchesPeriod && matchesStatus;
+  });
+
+  const periods = Array.from(new Set(assignments.map(a => a.section.class.period)))
+    .filter((period, index, self) => self.findIndex(p => p.id === period.id) === index);
+
+  const activeAssignments = filteredAssignments.filter(a => a.section.class.period.isActive);
+  const totalStudents = filteredAssignments.reduce((sum, a) => sum + a.section.studentCount, 0);
+  const totalClasses = new Set(filteredAssignments.map(a => a.section.class.id)).size;
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric'
+    });
+  };
+
+  if (loading) {
+    return (
+      <div className="container mx-auto p-6">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-center">
+            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
+            <p>Cargando asignaciones...</p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <MainLayout title="Mis Asignaciones">
+
+      <div className="container mx-auto p-6 space-y-6">
+      {/* Header */}
+      <div className="flex flex-col gap-4">
+        <div>
+          <h1 className="text-3xl font-bold">Mis Asignaciones</h1>
+          <p className="text-muted-foreground">
+            Visualiza las clases y secciones que tienes asignadas
+            {teacher && ` - ${teacher.firstName} ${teacher.lastName}`}
+          </p>
+        </div>
+
+        {/* Stats Cards */}
+        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+          <Card>
+            <CardContent className="p-4">
+              <div className="flex items-center gap-2">
+                <BookOpen className="h-4 w-4 text-primary" />
+                <div>
+                  <p className="text-sm text-muted-foreground">Total Asignaciones</p>
+                  <p className="text-2xl font-bold">{filteredAssignments.length}</p>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardContent className="p-4">
+              <div className="flex items-center gap-2">
+                <CalendarDays className="h-4 w-4 text-green-600" />
+                <div>
+                  <p className="text-sm text-muted-foreground">Períodos Activos</p>
+                  <p className="text-2xl font-bold">{activeAssignments.length}</p>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardContent className="p-4">
+              <div className="flex items-center gap-2">
+                <Users className="h-4 w-4 text-blue-600" />
+                <div>
+                  <p className="text-sm text-muted-foreground">Total Estudiantes</p>
+                  <p className="text-2xl font-bold">{totalStudents}</p>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+          
+          <Card>
+            <CardContent className="p-4">
+              <div className="flex items-center gap-2">
+                <BookOpen className="h-4 w-4 text-purple-600" />
+                <div>
+                  <p className="text-sm text-muted-foreground">Clases Únicas</p>
+                  <p className="text-2xl font-bold">{totalClasses}</p>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+
+      {/* Filters */}
+      <Card>
+        <CardHeader>
+          <CardTitle className="flex items-center gap-2">
+            <Filter className="h-4 w-4" />
+            Filtros
+          </CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+            <div className="relative">
+              <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar por clase, código o sección..."
+                value={searchTerm}
+                onChange={(e) => setSearchTerm(e.target.value)}
+                className="pl-10"
+              />
+            </div>
+            
+            <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>
+            
+            <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>
+            
+            <Button 
+              variant="outline" 
+              onClick={() => {
+                setSearchTerm('');
+                setSelectedPeriod('all');
+                setSelectedStatus('all');
+              }}
+            >
+              Limpiar filtros
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Assignments List */}
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        {filteredAssignments.map((assignment) => (
+          <Card key={assignment.id} className="hover:shadow-md transition-shadow">
+            <CardHeader>
+              <div className="flex items-start justify-between">
+                <div className="space-y-1">
+                  <CardTitle className="text-lg">
+                    {assignment.section.class.name}
+                  </CardTitle>
+                  <CardDescription>
+                    Código: {assignment.section.class.code} • Sección: {assignment.section.name}
+                  </CardDescription>
+                </div>
+                <div className="flex flex-col gap-2">
+                  <Badge variant={assignment.section.class.period.isActive ? 'default' : 'secondary'}>
+                    {assignment.section.class.period.isActive ? 'Activo' : 'Inactivo'}
+                  </Badge>
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={() => {
+                      setSelectedAssignment(assignment);
+                      setShowStudentDetails(true);
+                    }}
+                  >
+                    <Eye className="h-4 w-4 mr-1" />
+                    Ver detalles
+                  </Button>
+                </div>
+              </div>
+            </CardHeader>
+            <CardContent>
+              <div className="space-y-3">
+                <div>
+                  <p className="text-sm font-medium text-muted-foreground">Período Académico</p>
+                  <p className="text-sm">{assignment.section.class.period.name}</p>
+                  <p className="text-xs text-muted-foreground">
+                    {formatDate(assignment.section.class.period.startDate)} - {formatDate(assignment.section.class.period.endDate)}
+                  </p>
+                </div>
+                
+                {assignment.section.class.description && (
+                  <div>
+                    <p className="text-sm font-medium text-muted-foreground">Descripción</p>
+                    <p className="text-sm">{assignment.section.class.description}</p>
+                  </div>
+                )}
+                
+                <div className="flex items-center justify-between pt-2 border-t">
+                  <div className="flex items-center gap-2">
+                    <Users className="h-4 w-4 text-muted-foreground" />
+                    <span className="text-sm">
+                      {assignment.section.studentCount} estudiante{assignment.section.studentCount !== 1 ? 's' : ''}
+                    </span>
+                  </div>
+                  <div className="text-xs text-muted-foreground">
+                    Asignado: {formatDate(assignment.createdAt)}
+                  </div>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        ))}
+      </div>
+
+      {filteredAssignments.length === 0 && (
+        <Card>
+          <CardContent className="p-8 text-center">
+            <BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+            <h3 className="text-lg font-medium mb-2">No se encontraron asignaciones</h3>
+            <p className="text-muted-foreground">
+              {assignments.length === 0 
+                ? 'No tienes asignaciones registradas en el sistema.'
+                : 'No hay asignaciones que coincidan con los filtros aplicados.'}
+            </p>
+          </CardContent>
+        </Card>
+      )}
+
+      {/* Student Details Modal */}
+      {showStudentDetails && selectedAssignment && (
+        <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
+          <Card className="w-full max-w-4xl max-h-[80vh] overflow-hidden">
+            <CardHeader>
+              <div className="flex items-center justify-between">
+                <div>
+                  <CardTitle>
+                    {selectedAssignment.section.class.name} - {selectedAssignment.section.name}
+                  </CardTitle>
+                  <CardDescription>
+                    Lista de estudiantes inscritos
+                  </CardDescription>
+                </div>
+                <Button 
+                  variant="outline" 
+                  onClick={() => setShowStudentDetails(false)}
+                >
+                  Cerrar
+                </Button>
+              </div>
+            </CardHeader>
+            <CardContent className="overflow-y-auto">
+              {selectedAssignment.section.students.length > 0 ? (
+                <div className="space-y-4">
+                  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    {selectedAssignment.section.students.map((student) => (
+                      <Card key={student.id} className="p-4">
+                        <div className="space-y-2">
+                          <h4 className="font-medium">
+                            {student.firstName} {student.lastName}
+                          </h4>
+                          <div className="text-sm text-muted-foreground space-y-1">
+                            <p>Cédula: {student.cedula}</p>
+                            <p>Email: {student.email}</p>
+                            <p>N° Admisión: {student.admissionNumber}</p>
+                          </div>
+                        </div>
+                      </Card>
+                    ))}
+                  </div>
+                </div>
+              ) : (
+                <div className="text-center py-8">
+                  <Users className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+                  <p className="text-muted-foreground">
+                    No hay estudiantes inscritos en esta sección.
+                  </p>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+        </div>
+      )}
+      </div>
+    </MainLayout>
+  );
+}

+ 555 - 0
src/app/teacher/attendance/page.tsx

@@ -0,0 +1,555 @@
+'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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Textarea } from '@/components/ui/textarea';
+import { CalendarDays, Users, BookOpen, Save, Download, Search, Filter, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  cedula: string;
+  email: string;
+  admissionNumber: string;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  period: Period;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  class: Class;
+}
+
+interface Assignment {
+  id: string;
+  section: Section;
+}
+
+interface AttendanceRecord {
+  id: string | null;
+  studentId: string;
+  sectionId: string;
+  date: Date;
+  status: 'PRESENT' | 'ABSENT' | 'JUSTIFIED' | null;
+  reason: string | null;
+  student: Student;
+}
+
+interface AttendanceData {
+  section: Section;
+  teacher: {
+    id: string;
+    firstName: string;
+    lastName: string;
+  };
+  date: Date;
+  attendance: AttendanceRecord[];
+}
+
+type AttendanceStatus = 'PRESENT' | 'ABSENT' | 'JUSTIFIED';
+
+export default function AttendancePage() {
+  const [assignments, setAssignments] = useState<Assignment[]>([]);
+  const [selectedSectionId, setSelectedSectionId] = useState<string>('');
+  const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0]);
+  const [attendanceData, setAttendanceData] = useState<AttendanceData | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [statusFilter, setStatusFilter] = useState<string>('all');
+
+  // Cargar asignaciones del profesor
+  useEffect(() => {
+    const fetchAssignments = async () => {
+      try {
+        const response = await fetch('/api/teacher/assignments');
+        if (!response.ok) {
+          throw new Error('Error al cargar asignaciones');
+        }
+        const data = await response.json();
+        setAssignments(data.assignments || []);
+      } catch (error) {
+        console.error('Error:', error);
+        toast.error('Error al cargar las asignaciones');
+      }
+    };
+
+    fetchAssignments();
+  }, []);
+
+  // Cargar datos de asistencia cuando se selecciona sección y fecha
+  const loadAttendanceData = async () => {
+    if (!selectedSectionId || !selectedDate) return;
+
+    setLoading(true);
+    try {
+      const response = await fetch(
+        `/api/teacher/attendance?sectionId=${selectedSectionId}&date=${selectedDate}`
+      );
+      
+      if (!response.ok) {
+        throw new Error('Error al cargar datos de asistencia');
+      }
+      
+      const data = await response.json();
+      setAttendanceData(data);
+    } catch (error) {
+      console.error('Error:', error);
+      toast.error('Error al cargar los datos de asistencia');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    loadAttendanceData();
+  }, [selectedSectionId, selectedDate]);
+
+  // Actualizar estado de asistencia de un estudiante
+  const updateAttendanceStatus = (studentId: string, status: AttendanceStatus, reason?: string) => {
+    if (!attendanceData) return;
+
+    const updatedAttendance = attendanceData.attendance.map(record => {
+      if (record.studentId === studentId) {
+        return {
+          ...record,
+          status,
+          reason: reason || null
+        };
+      }
+      return record;
+    });
+
+    setAttendanceData({
+      ...attendanceData,
+      attendance: updatedAttendance
+    });
+  };
+
+  // Guardar asistencia
+  const saveAttendance = async () => {
+    if (!attendanceData) return;
+
+    setSaving(true);
+    try {
+      const attendanceToSave = attendanceData.attendance
+        .filter(record => record.status !== null)
+        .map(record => ({
+          studentId: record.studentId,
+          status: record.status,
+          reason: record.reason
+        }));
+
+      const response = await fetch('/api/teacher/attendance', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          sectionId: selectedSectionId,
+          date: selectedDate,
+          attendanceData: attendanceToSave
+        })
+      });
+
+      if (!response.ok) {
+        throw new Error('Error al guardar asistencia');
+      }
+
+      const result = await response.json();
+      toast.success(`Asistencia guardada exitosamente. ${result.recordsProcessed} registros procesados.`);
+      
+      // Recargar datos
+      await loadAttendanceData();
+    } catch (error) {
+      console.error('Error:', error);
+      toast.error('Error al guardar la asistencia');
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  // Marcar todos como presente/ausente
+  const markAllAs = (status: AttendanceStatus) => {
+    if (!attendanceData) return;
+
+    const updatedAttendance = attendanceData.attendance.map(record => ({
+      ...record,
+      status,
+      reason: null
+    }));
+
+    setAttendanceData({
+      ...attendanceData,
+      attendance: updatedAttendance
+    });
+  };
+
+  // Filtrar estudiantes
+  const filteredAttendance = attendanceData?.attendance.filter(record => {
+    const matchesSearch = 
+      record.student.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      record.student.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+      record.student.cedula.includes(searchTerm) ||
+      record.student.admissionNumber.includes(searchTerm);
+    
+    const matchesStatus = statusFilter === 'all' || record.status === statusFilter;
+    
+    return matchesSearch && matchesStatus;
+  }) || [];
+
+  // Estadísticas
+  const stats = attendanceData ? {
+    total: attendanceData.attendance.length,
+    present: attendanceData.attendance.filter(r => r.status === 'PRESENT').length,
+    absent: attendanceData.attendance.filter(r => r.status === 'ABSENT').length,
+    justified: attendanceData.attendance.filter(r => r.status === 'JUSTIFIED').length,
+    pending: attendanceData.attendance.filter(r => r.status === null).length
+  } : { total: 0, present: 0, absent: 0, justified: 0, pending: 0 };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric'
+    });
+  };
+
+  const getStatusBadge = (status: AttendanceStatus | null) => {
+    switch (status) {
+      case 'PRESENT':
+        return <Badge variant="default" className="bg-green-100 text-green-800">Presente</Badge>;
+      case 'ABSENT':
+        return <Badge variant="destructive">Ausente</Badge>;
+      case 'JUSTIFIED':
+        return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800">Justificado</Badge>;
+      default:
+        return <Badge variant="outline">Pendiente</Badge>;
+    }
+  };
+
+  const getStatusIcon = (status: AttendanceStatus | null) => {
+    switch (status) {
+      case 'PRESENT':
+        return <CheckCircle className="h-4 w-4 text-green-600" />;
+      case 'ABSENT':
+        return <XCircle className="h-4 w-4 text-red-600" />;
+      case 'JUSTIFIED':
+        return <AlertCircle className="h-4 w-4 text-yellow-600" />;
+      default:
+        return <AlertCircle className="h-4 w-4 text-gray-400" />;
+    }
+  };
+
+  return (
+    <MainLayout title="Registro de Asistencia">
+      <div className="container mx-auto p-6 space-y-6">
+        {/* Header */}
+        <div className="flex flex-col gap-4">
+          <div>
+            <h1 className="text-3xl font-bold">Registro de Asistencia</h1>
+            <p className="text-muted-foreground">
+              Registra la asistencia de los estudiantes por sección y fecha
+            </p>
+          </div>
+
+          {/* Selección de sección y fecha */}
+          <Card>
+            <CardHeader>
+              <CardTitle>Seleccionar Sección y Fecha</CardTitle>
+              <CardDescription>
+                Elige la sección y fecha para registrar asistencia
+              </CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div className="space-y-2">
+                  <Label htmlFor="section">Sección</Label>
+                  <Select value={selectedSectionId} onValueChange={setSelectedSectionId}>
+                    <SelectTrigger>
+                      <SelectValue placeholder="Selecciona una sección" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {assignments.map((assignment) => (
+                        <SelectItem key={assignment.id} value={assignment.section.id}>
+                          {assignment.section.class.name} - {assignment.section.name}
+                          {' '}({assignment.section.class.period.name})
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+                
+                <div className="space-y-2">
+                  <Label htmlFor="date">Fecha</Label>
+                  <Input
+                    id="date"
+                    type="date"
+                    value={selectedDate}
+                    onChange={(e) => setSelectedDate(e.target.value)}
+                  />
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Estadísticas */}
+        {attendanceData && (
+          <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+            <Card>
+              <CardContent className="p-4">
+                <div className="flex items-center gap-2">
+                  <Users className="h-4 w-4 text-blue-600" />
+                  <div>
+                    <p className="text-sm text-muted-foreground">Total</p>
+                    <p className="text-2xl font-bold">{stats.total}</p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+            
+            <Card>
+              <CardContent className="p-4">
+                <div className="flex items-center gap-2">
+                  <CheckCircle className="h-4 w-4 text-green-600" />
+                  <div>
+                    <p className="text-sm text-muted-foreground">Presentes</p>
+                    <p className="text-2xl font-bold text-green-600">{stats.present}</p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+            
+            <Card>
+              <CardContent className="p-4">
+                <div className="flex items-center gap-2">
+                  <XCircle className="h-4 w-4 text-red-600" />
+                  <div>
+                    <p className="text-sm text-muted-foreground">Ausentes</p>
+                    <p className="text-2xl font-bold text-red-600">{stats.absent}</p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+            
+            <Card>
+              <CardContent className="p-4">
+                <div className="flex items-center gap-2">
+                  <AlertCircle className="h-4 w-4 text-yellow-600" />
+                  <div>
+                    <p className="text-sm text-muted-foreground">Justificados</p>
+                    <p className="text-2xl font-bold text-yellow-600">{stats.justified}</p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+            
+            <Card>
+              <CardContent className="p-4">
+                <div className="flex items-center gap-2">
+                  <AlertCircle className="h-4 w-4 text-gray-400" />
+                  <div>
+                    <p className="text-sm text-muted-foreground">Pendientes</p>
+                    <p className="text-2xl font-bold text-gray-600">{stats.pending}</p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </div>
+        )}
+
+        {/* Lista de asistencia */}
+        {loading ? (
+          <Card>
+            <CardContent className="p-8">
+              <div className="flex items-center justify-center">
+                <div className="text-center">
+                  <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
+                  <p>Cargando datos de asistencia...</p>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        ) : attendanceData ? (
+          <Card>
+            <CardHeader>
+              <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
+                <div>
+                  <CardTitle>
+                    {attendanceData.section.class.name} - {attendanceData.section.name}
+                  </CardTitle>
+                  <CardDescription>
+                    Asistencia para el {formatDate(selectedDate)}
+                  </CardDescription>
+                </div>
+                
+                <div className="flex flex-wrap gap-2">
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={() => markAllAs('PRESENT')}
+                  >
+                    <CheckCircle className="h-4 w-4 mr-2" />
+                    Todos Presentes
+                  </Button>
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={() => markAllAs('ABSENT')}
+                  >
+                    <XCircle className="h-4 w-4 mr-2" />
+                    Todos Ausentes
+                  </Button>
+                  <Button
+                    onClick={saveAttendance}
+                    disabled={saving}
+                  >
+                    <Save className="h-4 w-4 mr-2" />
+                    {saving ? 'Guardando...' : 'Guardar Asistencia'}
+                  </Button>
+                </div>
+              </div>
+              
+              {/* Filtros */}
+              <div className="flex flex-col md:flex-row gap-4">
+                <div className="flex-1">
+                  <div className="relative">
+                    <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                    <Input
+                      placeholder="Buscar por nombre, cédula o número de admisión..."
+                      value={searchTerm}
+                      onChange={(e) => setSearchTerm(e.target.value)}
+                      className="pl-10"
+                    />
+                  </div>
+                </div>
+                
+                <Select value={statusFilter} onValueChange={setStatusFilter}>
+                  <SelectTrigger className="w-full md:w-48">
+                    <Filter className="h-4 w-4 mr-2" />
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los estados</SelectItem>
+                    <SelectItem value="PRESENT">Presentes</SelectItem>
+                    <SelectItem value="ABSENT">Ausentes</SelectItem>
+                    <SelectItem value="JUSTIFIED">Justificados</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+            </CardHeader>
+            
+            <CardContent>
+              {filteredAttendance.length > 0 ? (
+                <div className="space-y-4">
+                  {filteredAttendance.map((record) => (
+                    <Card key={record.studentId} className="p-4">
+                      <div className="flex flex-col md:flex-row md:items-center gap-4">
+                        <div className="flex-1">
+                          <div className="flex items-center gap-2 mb-2">
+                            {getStatusIcon(record.status)}
+                            <h4 className="font-medium">
+                              {record.student.firstName} {record.student.lastName}
+                            </h4>
+                            {getStatusBadge(record.status)}
+                          </div>
+                          <div className="text-sm text-muted-foreground space-y-1">
+                            <p>Cédula: {record.student.cedula}</p>
+                            <p>N° Admisión: {record.student.admissionNumber}</p>
+                          </div>
+                        </div>
+                        
+                        <div className="flex flex-col gap-2">
+                          <div className="flex gap-2">
+                            <Button
+                              size="sm"
+                              variant={record.status === 'PRESENT' ? 'default' : 'outline'}
+                              onClick={() => updateAttendanceStatus(record.studentId, 'PRESENT')}
+                            >
+                              <CheckCircle className="h-4 w-4 mr-1" />
+                              Presente
+                            </Button>
+                            <Button
+                              size="sm"
+                              variant={record.status === 'ABSENT' ? 'destructive' : 'outline'}
+                              onClick={() => updateAttendanceStatus(record.studentId, 'ABSENT')}
+                            >
+                              <XCircle className="h-4 w-4 mr-1" />
+                              Ausente
+                            </Button>
+                            <Button
+                              size="sm"
+                              variant={record.status === 'JUSTIFIED' ? 'secondary' : 'outline'}
+                              onClick={() => updateAttendanceStatus(record.studentId, 'JUSTIFIED')}
+                            >
+                              <AlertCircle className="h-4 w-4 mr-1" />
+                              Justificado
+                            </Button>
+                          </div>
+                          
+                          {(record.status === 'ABSENT' || record.status === 'JUSTIFIED') && (
+                            <Textarea
+                              placeholder="Razón (opcional)"
+                              value={record.reason || ''}
+                              onChange={(e) => updateAttendanceStatus(record.studentId, record.status!, e.target.value)}
+                              className="text-sm"
+                              rows={2}
+                            />
+                          )}
+                        </div>
+                      </div>
+                    </Card>
+                  ))}
+                </div>
+              ) : (
+                <div className="text-center py-8">
+                  <Users className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+                  <p className="text-muted-foreground">
+                    {searchTerm || statusFilter !== 'all' 
+                      ? 'No se encontraron estudiantes con los filtros aplicados.'
+                      : 'No hay estudiantes inscritos en esta sección.'}
+                  </p>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+        ) : selectedSectionId && selectedDate ? (
+          <Card>
+            <CardContent className="p-8">
+              <div className="text-center">
+                <BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+                <p className="text-muted-foreground">
+                  Selecciona una sección y fecha para comenzar a registrar asistencia.
+                </p>
+              </div>
+            </CardContent>
+          </Card>
+        ) : null}
+      </div>
+    </MainLayout>
+  );
+}

+ 778 - 0
src/app/teacher/reports/page.tsx

@@ -0,0 +1,778 @@
+'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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { 
+  BarChart3, 
+  FileText, 
+  Download, 
+  Calendar, 
+  Users, 
+  TrendingUp, 
+  CheckCircle, 
+  XCircle, 
+  AlertCircle,
+  BookOpen,
+  Clock
+} from 'lucide-react';
+import { toast } from 'sonner';
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  cedula: string;
+  email: string;
+  admissionNumber: string;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  period: Period;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  class: Class;
+  studentCount?: number;
+  students?: Student[];
+}
+
+interface Assignment {
+  id: string;
+  section: Section;
+}
+
+interface AttendanceRecord {
+  id: string;
+  studentId: string;
+  sectionId: string;
+  date: string;
+  status: 'PRESENT' | 'ABSENT' | 'JUSTIFIED';
+  reason: string | null;
+  student: Student;
+  section: Section;
+}
+
+interface SummaryData {
+  section: Section;
+  totalRecords: number;
+  present: number;
+  absent: number;
+  justified: number;
+  attendanceRate: number;
+}
+
+interface StatisticsData {
+  overall: {
+    totalRecords: number;
+    present: number;
+    absent: number;
+    justified: number;
+    attendanceRate: number;
+  };
+  byPeriod: Array<{
+    period: { id: string; name: string };
+    total: number;
+    present: number;
+    absent: number;
+    justified: number;
+    rate: number;
+  }>;
+  byClass: Array<{
+    class: { id: string; name: string; code: string };
+    total: number;
+    present: number;
+    absent: number;
+    justified: number;
+    rate: number;
+  }>;
+}
+
+interface StudentReportData {
+  student: Student;
+  statistics: {
+    totalRecords: number;
+    present: number;
+    absent: number;
+    justified: number;
+    attendanceRate: number;
+  };
+  data: AttendanceRecord[];
+}
+
+interface BaseReportResponse {
+  reportType: string;
+  teacher: {
+    id: string;
+    firstName: string;
+    lastName: string;
+  };
+  generatedAt: string;
+}
+
+interface SummaryReportResponse extends BaseReportResponse {
+  reportType: 'summary';
+  data: SummaryData[];
+  totalRecords: number;
+}
+
+interface DetailedReportResponse extends BaseReportResponse {
+  reportType: 'detailed';
+  data: AttendanceRecord[];
+  totalRecords: number;
+}
+
+interface StatisticsReportResponse extends BaseReportResponse {
+  reportType: 'statistics';
+  data: StatisticsData;
+  overall: StatisticsData['overall'];
+  byPeriod: StatisticsData['byPeriod'];
+  byClass: StatisticsData['byClass'];
+}
+
+interface StudentReportResponse extends BaseReportResponse {
+  reportType: 'student';
+  data: AttendanceRecord[];
+  student: Student;
+  statistics: StudentReportData['statistics'];
+}
+
+type ReportResponse = SummaryReportResponse | DetailedReportResponse | StatisticsReportResponse | StudentReportResponse;
+
+export default function ReportsPage() {
+  const [assignments, setAssignments] = useState<Assignment[]>([]);
+  const [students, setStudents] = useState<Student[]>([]);
+  const [reportData, setReportData] = useState<ReportResponse | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [activeTab, setActiveTab] = useState('summary');
+  
+  // Filtros
+  const [selectedSectionId, setSelectedSectionId] = useState<string>('all');
+  const [selectedStudentId, setSelectedStudentId] = useState<string>('all');
+  const [startDate, setStartDate] = useState<string>('');
+  const [endDate, setEndDate] = useState<string>('');
+
+  // Cargar asignaciones del profesor
+  useEffect(() => {
+    const fetchAssignments = async () => {
+      try {
+        const response = await fetch('/api/teacher/assignments');
+        if (!response.ok) {
+          throw new Error('Error al cargar asignaciones');
+        }
+        const data: { assignments: Assignment[] } = await response.json();
+        setAssignments(data.assignments || []);
+      } catch (error) {
+        console.error('Error:', error);
+        toast.error('Error al cargar las asignaciones');
+      }
+    };
+
+    fetchAssignments();
+  }, []);
+
+  // Cargar estudiantes cuando se selecciona una sección
+  useEffect(() => {
+    if (selectedSectionId && selectedSectionId !== 'all') {
+      const selectedAssignment = assignments.find(a => a.section.id === selectedSectionId);
+      if (selectedAssignment?.section.students) {
+        // Los estudiantes ya están incluidos en la respuesta de assignments
+        const sectionStudents = selectedAssignment.section.students;
+        setStudents(sectionStudents);
+      } else {
+        setStudents([]);
+      }
+    } else {
+      setStudents([]);
+    }
+    setSelectedStudentId('all');
+  }, [selectedSectionId, assignments]);
+
+  const generateReport = async (reportType: string) => {
+    setLoading(true);
+    try {
+      const params = new URLSearchParams({
+        type: reportType
+      });
+
+      if (selectedSectionId && selectedSectionId !== 'all') {
+        params.append('sectionId', selectedSectionId);
+      }
+      if (startDate) {
+        params.append('startDate', startDate);
+      }
+      if (endDate) {
+        params.append('endDate', endDate);
+      }
+      if (selectedStudentId && selectedStudentId !== 'all' && reportType === 'student') {
+        params.append('studentId', selectedStudentId);
+      }
+
+      const response = await fetch(`/api/teacher/reports?${params.toString()}`);
+      if (!response.ok) {
+        throw new Error('Error al generar reporte');
+      }
+
+      const data: ReportResponse = await response.json();
+      setReportData(data);
+      setActiveTab(reportType);
+      toast.success('Reporte generado exitosamente');
+    } catch (error) {
+      console.error('Error:', error);
+      toast.error('Error al generar el reporte');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const exportToCSV = (data: SummaryData[] | AttendanceRecord[], filename: string) => {
+    if (!data || data.length === 0) {
+      toast.error('No hay datos para exportar');
+      return;
+    }
+
+    const headers = Object.keys(data[0]);
+    const csvContent = [
+      headers.join(','),
+      ...data.map(row => 
+        headers.map(header => {
+          const value = row[header as keyof typeof row];
+          if (typeof value === 'object' && value !== null) {
+            return JSON.stringify(value).replace(/"/g, '""');
+          }
+          return `"${String(value).replace(/"/g, '""')}"`;
+        }).join(',')
+      )
+    ].join('\n');
+
+    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+    const link = document.createElement('a');
+    const url = URL.createObjectURL(blob);
+    link.setAttribute('href', url);
+    link.setAttribute('download', `${filename}_${new Date().toISOString().split('T')[0]}.csv`);
+    link.style.visibility = 'hidden';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    
+    toast.success('Reporte exportado exitosamente');
+  };
+
+  const getStatusBadge = (status: 'PRESENT' | 'ABSENT' | 'JUSTIFIED') => {
+    switch (status) {
+      case 'PRESENT':
+        return <Badge variant="default" className="bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1" />Presente</Badge>;
+      case 'ABSENT':
+        return <Badge variant="destructive"><XCircle className="w-3 h-3 mr-1" />Ausente</Badge>;
+      case 'JUSTIFIED':
+        return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800"><AlertCircle className="w-3 h-3 mr-1" />Justificado</Badge>;
+    }
+  };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'short',
+      day: 'numeric'
+    });
+  };
+
+  const formatPercentage = (value: number) => {
+    return `${value.toFixed(1)}%`;
+  };
+
+  return (
+    <MainLayout title="Reportes de Asistencia">
+
+      <div className="container mx-auto p-6 space-y-6">
+        {/* Header */}
+        <div className="flex items-center justify-between">
+          <div>
+            <h1 className="text-3xl font-bold text-gray-900">Reportes de Asistencia</h1>
+            <p className="text-gray-600 mt-1">Genera y visualiza reportes detallados de asistencia por clase</p>
+          </div>
+          <div className="flex items-center space-x-2">
+            <BarChart3 className="h-8 w-8 text-blue-600" />
+          </div>
+        </div>
+
+        {/* Filtros */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Calendar className="h-5 w-5" />
+              Filtros de Reporte
+            </CardTitle>
+            <CardDescription>
+              Configura los parámetros para generar reportes personalizados
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+              <div className="space-y-2">
+                <Label htmlFor="section">Sección</Label>
+                <Select value={selectedSectionId} onValueChange={setSelectedSectionId}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Todas las secciones" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todas las secciones</SelectItem>
+                    {assignments.map((assignment) => (
+                      <SelectItem key={assignment.section.id} value={assignment.section.id}>
+                        {assignment.section.class.code} - {assignment.section.name}
+                      </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 className="space-y-2">
+                <Label htmlFor="student">Estudiante (para reporte individual)</Label>
+                <Select value={selectedStudentId} onValueChange={setSelectedStudentId}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Seleccionar estudiante" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los estudiantes</SelectItem>
+                    {students.map((student) => (
+                      <SelectItem key={student.id} value={student.id}>
+                        {student.lastName}, {student.firstName}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+            </div>
+
+            <div className="flex flex-wrap gap-2 mt-4">
+              <Button 
+                onClick={() => generateReport('summary')} 
+                disabled={loading}
+                variant="default"
+              >
+                <BarChart3 className="w-4 h-4 mr-2" />
+                Resumen por Sección
+              </Button>
+              <Button 
+                onClick={() => generateReport('detailed')} 
+                disabled={loading}
+                variant="outline"
+              >
+                <FileText className="w-4 h-4 mr-2" />
+                Reporte Detallado
+              </Button>
+              <Button 
+                onClick={() => generateReport('statistics')} 
+                disabled={loading}
+                variant="outline"
+              >
+                <TrendingUp className="w-4 h-4 mr-2" />
+                Estadísticas Generales
+              </Button>
+              <Button 
+                onClick={() => generateReport('student')} 
+                disabled={loading || selectedStudentId === 'all'}
+                variant="outline"
+              >
+                <Users className="w-4 h-4 mr-2" />
+                Reporte por Estudiante
+              </Button>
+            </div>
+          </CardContent>
+        </Card>
+
+        {/* Resultados */}
+        {reportData && (
+          <Card>
+            <CardHeader>
+              <div className="flex items-center justify-between">
+                <div>
+                  <CardTitle className="flex items-center gap-2">
+                    <FileText className="h-5 w-5" />
+                    Resultados del Reporte
+                  </CardTitle>
+                  <CardDescription>
+                    Generado el {formatDate(reportData.generatedAt)} por {reportData.teacher.firstName} {reportData.teacher.lastName}
+                  </CardDescription>
+                </div>
+                <Button 
+                  onClick={() => {
+                    if (reportData.reportType === 'summary') {
+                      exportToCSV(reportData.data, 'reporte_resumen');
+                    } else if (reportData.reportType === 'detailed') {
+                      exportToCSV(reportData.data, 'reporte_detallado');
+                    }
+                  }}
+                  variant="outline"
+                  size="sm"
+                  disabled={reportData.reportType !== 'summary' && reportData.reportType !== 'detailed'}
+                >
+                  <Download className="w-4 h-4 mr-2" />
+                  Exportar CSV
+                </Button>
+              </div>
+            </CardHeader>
+            <CardContent>
+              <Tabs value={activeTab} onValueChange={setActiveTab}>
+                <TabsList className="grid w-full grid-cols-4">
+                  <TabsTrigger value="summary">Resumen</TabsTrigger>
+                  <TabsTrigger value="detailed">Detallado</TabsTrigger>
+                  <TabsTrigger value="statistics">Estadísticas</TabsTrigger>
+                  <TabsTrigger value="student">Por Estudiante</TabsTrigger>
+                </TabsList>
+
+                {/* Reporte Resumen */}
+                <TabsContent value="summary" className="space-y-4">
+                  {reportData.reportType === 'summary' && (
+                    <div className="space-y-4">
+                      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+                        {reportData.data.map((sectionData) => (
+                          <Card key={sectionData.section.id}>
+                            <CardHeader className="pb-2">
+                              <CardTitle className="text-lg flex items-center gap-2">
+                                <BookOpen className="h-4 w-4" />
+                                {sectionData.section.class.code} - {sectionData.section.name}
+                              </CardTitle>
+                              <CardDescription>
+                                {sectionData.section.class.name}
+                              </CardDescription>
+                            </CardHeader>
+                            <CardContent>
+                              <div className="space-y-2">
+                                <div className="flex justify-between">
+                                  <span className="text-sm text-gray-600">Total Registros:</span>
+                                  <span className="font-medium">{sectionData.totalRecords}</span>
+                                </div>
+                                <div className="flex justify-between">
+                                  <span className="text-sm text-green-600">Presentes:</span>
+                                  <span className="font-medium text-green-600">{sectionData.present}</span>
+                                </div>
+                                <div className="flex justify-between">
+                                  <span className="text-sm text-red-600">Ausentes:</span>
+                                  <span className="font-medium text-red-600">{sectionData.absent}</span>
+                                </div>
+                                <div className="flex justify-between">
+                                  <span className="text-sm text-yellow-600">Justificados:</span>
+                                  <span className="font-medium text-yellow-600">{sectionData.justified}</span>
+                                </div>
+                                <div className="border-t pt-2 mt-2">
+                                  <div className="flex justify-between">
+                                    <span className="text-sm font-medium">Tasa de Asistencia:</span>
+                                    <span className="font-bold text-blue-600">
+                                      {formatPercentage(sectionData.attendanceRate)}
+                                    </span>
+                                  </div>
+                                </div>
+                              </div>
+                            </CardContent>
+                          </Card>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+                </TabsContent>
+
+                {/* Reporte Detallado */}
+                <TabsContent value="detailed" className="space-y-4">
+                  {reportData.reportType === 'detailed' && (
+                    <div className="space-y-4">
+                      <div className="flex items-center justify-between">
+                        <h3 className="text-lg font-semibold">Registros Detallados</h3>
+                        <Badge variant="outline">{reportData.totalRecords} registros</Badge>
+                      </div>
+                      <div className="overflow-x-auto">
+                        <Table>
+                          <TableHeader>
+                            <TableRow>
+                              <TableHead>Fecha</TableHead>
+                              <TableHead>Estudiante</TableHead>
+                              <TableHead>Cédula</TableHead>
+                              <TableHead>Clase</TableHead>
+                              <TableHead>Sección</TableHead>
+                              <TableHead>Estado</TableHead>
+                              <TableHead>Observación</TableHead>
+                            </TableRow>
+                          </TableHeader>
+                          <TableBody>
+                            {reportData.data.map((record) => (
+                              <TableRow key={record.id}>
+                                <TableCell>{formatDate(record.date)}</TableCell>
+                                <TableCell>
+                                  {record.student.lastName}, {record.student.firstName}
+                                </TableCell>
+                                <TableCell>{record.student.cedula}</TableCell>
+                                <TableCell>{record.section.class.code}</TableCell>
+                                <TableCell>{record.section.name}</TableCell>
+                                <TableCell>{getStatusBadge(record.status)}</TableCell>
+                                <TableCell>{record.reason || '-'}</TableCell>
+                              </TableRow>
+                            ))}
+                          </TableBody>
+                        </Table>
+                      </div>
+                    </div>
+                  )}
+                </TabsContent>
+
+                {/* Estadísticas */}
+                <TabsContent value="statistics" className="space-y-4">
+                  {reportData.reportType === 'statistics' && (
+                    <div className="space-y-6">
+                      {/* Estadísticas Generales */}
+                      <Card>
+                        <CardHeader>
+                          <CardTitle className="flex items-center gap-2">
+                            <TrendingUp className="h-5 w-5" />
+                            Estadísticas Generales
+                          </CardTitle>
+                        </CardHeader>
+                        <CardContent>
+                          <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-blue-600">{reportData.overall.totalRecords}</div>
+                              <div className="text-sm text-gray-600">Total Registros</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-green-600">{reportData.overall.present}</div>
+                              <div className="text-sm text-gray-600">Presentes</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-red-600">{reportData.overall.absent}</div>
+                              <div className="text-sm text-gray-600">Ausentes</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-yellow-600">{reportData.overall.justified}</div>
+                              <div className="text-sm text-gray-600">Justificados</div>
+                            </div>
+                          </div>
+                          <div className="mt-4 text-center">
+                            <div className="text-3xl font-bold text-blue-600">
+                              {formatPercentage(reportData.overall.attendanceRate)}
+                            </div>
+                            <div className="text-sm text-gray-600">Tasa de Asistencia General</div>
+                          </div>
+                        </CardContent>
+                      </Card>
+
+                      {/* Por Período */}
+                      {reportData.byPeriod && reportData.byPeriod.length > 0 && (
+                        <Card>
+                          <CardHeader>
+                            <CardTitle className="flex items-center gap-2">
+                              <Clock className="h-5 w-5" />
+                              Estadísticas por Período
+                            </CardTitle>
+                          </CardHeader>
+                          <CardContent>
+                            <div className="overflow-x-auto">
+                              <Table>
+                                <TableHeader>
+                                  <TableRow>
+                                    <TableHead>Período</TableHead>
+                                    <TableHead>Total</TableHead>
+                                    <TableHead>Presentes</TableHead>
+                                    <TableHead>Ausentes</TableHead>
+                                    <TableHead>Justificados</TableHead>
+                                    <TableHead>Tasa</TableHead>
+                                  </TableRow>
+                                </TableHeader>
+                                <TableBody>
+                                  {reportData.byPeriod.map((period) => (
+                                    <TableRow key={period.period.id}>
+                                      <TableCell className="font-medium">{period.period.name}</TableCell>
+                                      <TableCell>{period.total}</TableCell>
+                                      <TableCell className="text-green-600">{period.present}</TableCell>
+                                      <TableCell className="text-red-600">{period.absent}</TableCell>
+                                      <TableCell className="text-yellow-600">{period.justified}</TableCell>
+                                      <TableCell className="font-medium">{formatPercentage(period.rate)}</TableCell>
+                                    </TableRow>
+                                  ))}
+                                </TableBody>
+                              </Table>
+                            </div>
+                          </CardContent>
+                        </Card>
+                      )}
+
+                      {/* Por Clase */}
+                      {reportData.byClass && reportData.byClass.length > 0 && (
+                        <Card>
+                          <CardHeader>
+                            <CardTitle className="flex items-center gap-2">
+                              <BookOpen className="h-5 w-5" />
+                              Estadísticas por Clase
+                            </CardTitle>
+                          </CardHeader>
+                          <CardContent>
+                            <div className="overflow-x-auto">
+                              <Table>
+                                <TableHeader>
+                                  <TableRow>
+                                    <TableHead>Código</TableHead>
+                                    <TableHead>Clase</TableHead>
+                                    <TableHead>Total</TableHead>
+                                    <TableHead>Presentes</TableHead>
+                                    <TableHead>Ausentes</TableHead>
+                                    <TableHead>Justificados</TableHead>
+                                    <TableHead>Tasa</TableHead>
+                                  </TableRow>
+                                </TableHeader>
+                                <TableBody>
+                                  {reportData.byClass.map((classData) => (
+                                    <TableRow key={classData.class.id}>
+                                      <TableCell className="font-medium">{classData.class.code}</TableCell>
+                                      <TableCell>{classData.class.name}</TableCell>
+                                      <TableCell>{classData.total}</TableCell>
+                                      <TableCell className="text-green-600">{classData.present}</TableCell>
+                                      <TableCell className="text-red-600">{classData.absent}</TableCell>
+                                      <TableCell className="text-yellow-600">{classData.justified}</TableCell>
+                                      <TableCell className="font-medium">{formatPercentage(classData.rate)}</TableCell>
+                                    </TableRow>
+                                  ))}
+                                </TableBody>
+                              </Table>
+                            </div>
+                          </CardContent>
+                        </Card>
+                      )}
+                    </div>
+                  )}
+                </TabsContent>
+
+                {/* Reporte por Estudiante */}
+                <TabsContent value="student" className="space-y-4">
+                  {reportData.reportType === 'student' && (
+                    <div className="space-y-4">
+                      {/* Información del Estudiante */}
+                      <Card>
+                        <CardHeader>
+                          <CardTitle className="flex items-center gap-2">
+                            <Users className="h-5 w-5" />
+                            {reportData.student.lastName}, {reportData.student.firstName}
+                          </CardTitle>
+                          <CardDescription>
+                            Cédula: {reportData.student.cedula} | Matrícula: {reportData.student.admissionNumber}
+                          </CardDescription>
+                        </CardHeader>
+                        <CardContent>
+                          <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-blue-600">{reportData.statistics.totalRecords}</div>
+                              <div className="text-sm text-gray-600">Total Registros</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-green-600">{reportData.statistics.present}</div>
+                              <div className="text-sm text-gray-600">Presentes</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-red-600">{reportData.statistics.absent}</div>
+                              <div className="text-sm text-gray-600">Ausentes</div>
+                            </div>
+                            <div className="text-center">
+                              <div className="text-2xl font-bold text-yellow-600">{reportData.statistics.justified}</div>
+                              <div className="text-sm text-gray-600">Justificados</div>
+                            </div>
+                          </div>
+                          <div className="mt-4 text-center">
+                            <div className="text-3xl font-bold text-blue-600">
+                              {formatPercentage(reportData.statistics.attendanceRate)}
+                            </div>
+                            <div className="text-sm text-gray-600">Tasa de Asistencia</div>
+                          </div>
+                        </CardContent>
+                      </Card>
+
+                      {/* Historial de Asistencia */}
+                      <Card>
+                        <CardHeader>
+                          <CardTitle>Historial de Asistencia</CardTitle>
+                        </CardHeader>
+                        <CardContent>
+                          <div className="overflow-x-auto">
+                            <Table>
+                              <TableHeader>
+                                <TableRow>
+                                  <TableHead>Fecha</TableHead>
+                                  <TableHead>Clase</TableHead>
+                                  <TableHead>Sección</TableHead>
+                                  <TableHead>Estado</TableHead>
+                                  <TableHead>Observación</TableHead>
+                                </TableRow>
+                              </TableHeader>
+                              <TableBody>
+                                {reportData.data.map((record) => (
+                                  <TableRow key={record.id}>
+                                    <TableCell>{formatDate(record.date)}</TableCell>
+                                    <TableCell>{record.section.class.code}</TableCell>
+                                    <TableCell>{record.section.name}</TableCell>
+                                    <TableCell>{getStatusBadge(record.status)}</TableCell>
+                                    <TableCell>{record.reason || '-'}</TableCell>
+                                  </TableRow>
+                                ))}
+                              </TableBody>
+                            </Table>
+                          </div>
+                        </CardContent>
+                      </Card>
+                    </div>
+                  )}
+                </TabsContent>
+              </Tabs>
+            </CardContent>
+          </Card>
+        )}
+
+        {loading && (
+          <Card>
+            <CardContent className="flex items-center justify-center py-8">
+              <div className="text-center">
+                <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
+                <p className="text-gray-600">Generando reporte...</p>
+              </div>
+            </CardContent>
+          </Card>
+        )}
+      </div>
+    </MainLayout>
+  );
+}

+ 40 - 0
src/components/animated-counter.tsx

@@ -0,0 +1,40 @@
+"use client"
+
+import { useEffect, useState } from "react"
+
+interface AnimatedCounterProps {
+  end: number
+  duration?: number
+  suffix?: string
+}
+
+export function AnimatedCounter({ end, duration = 2000, suffix = "" }: AnimatedCounterProps) {
+  const [count, setCount] = useState(0)
+
+  useEffect(() => {
+    let startTime: number
+    let animationFrame: number
+
+    const animate = (currentTime: number) => {
+      if (!startTime) startTime = currentTime
+      const progress = Math.min((currentTime - startTime) / duration, 1)
+
+      setCount(Math.floor(progress * end))
+
+      if (progress < 1) {
+        animationFrame = requestAnimationFrame(animate)
+      }
+    }
+
+    animationFrame = requestAnimationFrame(animate)
+
+    return () => cancelAnimationFrame(animationFrame)
+  }, [end, duration])
+
+  return (
+    <span>
+      {count.toLocaleString()}
+      {suffix}
+    </span>
+  )
+}

+ 108 - 0
src/components/feature-showcase.tsx

@@ -0,0 +1,108 @@
+"use client"
+
+import { useState } from "react"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Users, BarChart3, Clock, Shield } from "lucide-react"
+
+const features = [
+  {
+    id: "dashboard",
+    title: "Dashboard Administrativo",
+    description: "Panel de control completo con métricas en tiempo real",
+    icon: BarChart3,
+    image: "/university-admin-dashboard.png",
+    badge: "Más Popular",
+  },
+  {
+    id: "attendance",
+    title: "Control de Asistencia",
+    description: "Registro intuitivo y seguimiento detallado de asistencia",
+    icon: Clock,
+    image: "/attendance-interface.png",
+    badge: "Nuevo",
+  },
+  {
+    id: "users",
+    title: "Gestión de Usuarios",
+    description: "Administración completa de estudiantes y docentes",
+    icon: Users,
+    image: "/user-management-interface.png",
+    badge: null,
+  },
+  {
+    id: "security",
+    title: "Seguridad Avanzada",
+    description: "Sistema de roles y permisos granulares",
+    icon: Shield,
+    image: "/security-settings-interface.png",
+    badge: "Empresarial",
+  },
+]
+
+export function FeatureShowcase() {
+  const [activeFeature, setActiveFeature] = useState("dashboard")
+
+  const currentFeature = features.find((f) => f.id === activeFeature) || features[0]
+
+  return (
+    <div className="grid lg:grid-cols-2 gap-12 items-center">
+      <div className="space-y-6">
+        <div className="space-y-4">
+          {features.map((feature) => {
+            const Icon = feature.icon
+            const isActive = activeFeature === feature.id
+
+            return (
+              <Card
+                key={feature.id}
+                className={`cursor-pointer transition-all duration-300 hover:shadow-lg ${
+                  isActive ? "ring-2 ring-primary shadow-lg" : ""
+                }`}
+                onClick={() => setActiveFeature(feature.id)}
+              >
+                <CardContent className="p-6">
+                  <div className="flex items-start gap-4">
+                    <div
+                      className={`p-3 rounded-lg transition-colors ${
+                        isActive ? "bg-primary text-primary-foreground" : "bg-muted"
+                      }`}
+                    >
+                      <Icon className="w-6 h-6" />
+                    </div>
+                    <div className="flex-1">
+                      <div className="flex items-center gap-2 mb-2">
+                        <h3 className="font-semibold text-lg">{feature.title}</h3>
+                        {feature.badge && (
+                          <Badge variant={isActive ? "default" : "secondary"} className="text-xs">
+                            {feature.badge}
+                          </Badge>
+                        )}
+                      </div>
+                      <p className="text-muted-foreground">{feature.description}</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            )
+          })}
+        </div>
+      </div>
+
+      <div className="relative">
+        <div className="relative overflow-hidden rounded-xl shadow-2xl bg-gradient-to-br from-primary/5 to-accent/5 p-1">
+          <img
+            src={currentFeature.image || "/placeholder.svg"}
+            alt={currentFeature.title}
+            className="w-full h-auto rounded-lg transition-all duration-500"
+          />
+          <div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent rounded-lg" />
+        </div>
+
+        {/* Floating elements for visual appeal */}
+        <div className="absolute -top-4 -right-4 w-20 h-20 bg-primary/10 rounded-full blur-xl animate-pulse" />
+        <div className="absolute -bottom-4 -left-4 w-16 h-16 bg-accent/10 rounded-full blur-xl animate-pulse delay-1000" />
+      </div>
+    </div>
+  )
+}

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

@@ -43,10 +43,10 @@ const adminMenuItems = [
 
 const teacherMenuItems = [
   { icon: Home, label: 'Dashboard', href: '/teacher' },
-  { icon: BookOpen, label: 'Mis Clases', href: '/teacher/classes' },
+  { icon: BookOpen, label: 'Mis Clases', href: '/teacher/assignments' },
   { icon: UserCheck, label: 'Asistencia', href: '/teacher/attendance' },
   { icon: ClipboardList, label: 'Reportes', href: '/teacher/reports' },
-  { icon: Settings, label: 'Perfil', href: '/teacher/profile' },
+  // { icon: Settings, label: 'Perfil', href: '/teacher/profile' },
 ]
 
 const studentMenuItems = [

+ 121 - 0
src/components/pricing-section.tsx

@@ -0,0 +1,121 @@
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { CheckCircle, Star } from "lucide-react"
+
+const plans = [
+  {
+    name: "Básico",
+    price: "$49",
+    period: "/mes",
+    description: "Perfecto para instituciones pequeñas",
+    features: [
+      "Hasta 500 estudiantes",
+      "3 usuarios administrativos",
+      "Reportes básicos",
+      "Soporte por email",
+      "Backup diario",
+    ],
+    popular: false,
+  },
+  {
+    name: "Profesional",
+    price: "$99",
+    period: "/mes",
+    description: "Ideal para universidades medianas",
+    features: [
+      "Hasta 2,000 estudiantes",
+      "10 usuarios administrativos",
+      "Reportes avanzados",
+      "Soporte prioritario",
+      "Backup en tiempo real",
+      "API personalizada",
+      "Integraciones",
+    ],
+    popular: true,
+  },
+  {
+    name: "Empresarial",
+    price: "Personalizado",
+    period: "",
+    description: "Para grandes instituciones",
+    features: [
+      "Estudiantes ilimitados",
+      "Usuarios ilimitados",
+      "Reportes personalizados",
+      "Soporte 24/7",
+      "Implementación dedicada",
+      "Capacitación incluida",
+      "SLA garantizado",
+    ],
+    popular: false,
+  },
+]
+
+export function PricingSection() {
+  return (
+    <section className="py-20 px-4">
+      <div className="container mx-auto max-w-6xl">
+        <div className="text-center mb-16">
+          <Badge variant="secondary" className="mb-4">
+            Precios Transparentes
+          </Badge>
+          <h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
+            Planes que se adaptan a tu institución
+          </h2>
+          <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
+            Desde pequeñas escuelas hasta grandes universidades, tenemos el plan perfecto para ti
+          </p>
+        </div>
+
+        <div className="grid md:grid-cols-3 gap-8">
+          {plans.map((plan, index) => (
+            <Card key={index} className={`relative ${plan.popular ? "ring-2 ring-primary shadow-xl scale-105" : ""}`}>
+              {plan.popular && (
+                <div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
+                  <Badge className="bg-primary text-primary-foreground px-4 py-1">
+                    <Star className="w-4 h-4 mr-1" />
+                    Más Popular
+                  </Badge>
+                </div>
+              )}
+
+              <CardHeader className="text-center pb-8">
+                <CardTitle className="text-2xl">{plan.name}</CardTitle>
+                <CardDescription className="text-base">{plan.description}</CardDescription>
+                <div className="mt-4">
+                  <span className="text-4xl font-bold text-foreground">{plan.price}</span>
+                  <span className="text-muted-foreground">{plan.period}</span>
+                </div>
+              </CardHeader>
+
+              <CardContent className="space-y-6">
+                <ul className="space-y-3">
+                  {plan.features.map((feature, featureIndex) => (
+                    <li key={featureIndex} className="flex items-center gap-3">
+                      <CheckCircle className="w-5 h-5 text-accent flex-shrink-0" />
+                      <span className="text-sm">{feature}</span>
+                    </li>
+                  ))}
+                </ul>
+
+                <Button className="w-full" variant={plan.popular ? "default" : "outline"} size="lg">
+                  {plan.name === "Empresarial" ? "Contactar Ventas" : "Comenzar Prueba"}
+                </Button>
+              </CardContent>
+            </Card>
+          ))}
+        </div>
+
+        <div className="text-center mt-12">
+          <p className="text-muted-foreground mb-4">
+            ¿Necesitas más información? Todos los planes incluyen 14 días de prueba gratuita
+          </p>
+          <Button variant="outline" size="lg">
+            Comparar Todos los Planes
+          </Button>
+        </div>
+      </div>
+    </section>
+  )
+}