Ver código fonte

holy shit we got reports

Matthew Trejo 4 meses atrás
pai
commit
e6971244b2

+ 3 - 1
ROADMAP.md

@@ -117,8 +117,10 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
   - ✅ Gestión de periodos académicos (`/admin/periods`) - CRUD completo con toggle activo/inactivo
   - ✅ Gestión de clases y materias (`/admin/classes`) - CRUD completo
   - ✅ Gestión de secciones (`/admin/sections`) - CRUD completo
+  - ✅ Gestión de asignaciones de profesores (`/admin/teacher-assignments`)
+  - ✅ Gestión de inscripciones de estudiantes (`/admin/student-enrollments`)
+  - ✅ Sistema de reportes (`/admin/reports`)
 - **Pendiente**:
-  - Sistema de reportes (`/admin/reports`)
   - Configuración del sistema (`/admin/settings`)
 
 ---

+ 31 - 0
package-lock.json

@@ -21,6 +21,7 @@
         "@radix-ui/react-separator": "^1.1.7",
         "@radix-ui/react-slot": "^1.2.3",
         "@radix-ui/react-switch": "^1.2.6",
+        "@radix-ui/react-tabs": "^1.1.13",
         "@radix-ui/react-tooltip": "^1.2.8",
         "@types/bcryptjs": "^2.4.6",
         "bcryptjs": "^3.0.2",
@@ -1831,6 +1832,36 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-tabs": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+      "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-direction": "1.1.1",
+        "@radix-ui/react-id": "1.1.1",
+        "@radix-ui/react-presence": "1.1.5",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-roving-focus": "1.1.11",
+        "@radix-ui/react-use-controllable-state": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-tooltip": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "@radix-ui/react-separator": "^1.1.7",
     "@radix-ui/react-slot": "^1.2.3",
     "@radix-ui/react-switch": "^1.2.6",
+    "@radix-ui/react-tabs": "^1.1.13",
     "@radix-ui/react-tooltip": "^1.2.8",
     "@types/bcryptjs": "^2.4.6",
     "bcryptjs": "^3.0.2",

+ 895 - 0
src/app/admin/reports/page.tsx

@@ -0,0 +1,895 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { MainLayout } from '@/components/layout/main-layout';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+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 { toast } from 'sonner';
+import { 
+  BarChart3, 
+  Users, 
+  GraduationCap, 
+  BookOpen, 
+  UserCheck, 
+  Calendar, 
+  Download,
+  TrendingUp,
+  PieChart,
+  Activity,
+  FileText
+} from 'lucide-react';
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface OverviewData {
+  overview: {
+    totalUsers: number;
+    totalStudents: number;
+    totalTeachers: number;
+    totalClasses: number;
+    totalSections: number;
+    activePeriods: number;
+    recentEnrollments: number;
+    recentAttendance: number;
+  };
+  usersByRole: Array<{
+    role: string;
+    count: number;
+  }>;
+}
+
+interface StudentReportData {
+  students: Array<{
+    id: string;
+    firstName: string;
+    lastName: string;
+    cedula: string;
+    email: string;
+    admissionNumber: string;
+    enrollmentsCount: number;
+    attendanceRate: number;
+    totalAttendances: number;
+    presentAttendances: number;
+    enrollments: Array<{
+      sectionName: string;
+      className: string;
+      classCode: string;
+      periodName: string;
+    }>;
+  }>;
+  summary: {
+    totalStudents: number;
+    averageEnrollments: number;
+    averageAttendanceRate: number;
+  };
+}
+
+interface TeacherReportData {
+  teachers: Array<{
+    id: string;
+    firstName: string;
+    lastName: string;
+    cedula: string;
+    email: string;
+    phone: string;
+    assignmentsCount: number;
+    totalStudents: number;
+    assignments: Array<{
+      sectionName: string;
+      className: string;
+      classCode: string;
+      periodName: string;
+      studentsCount: number;
+    }>;
+  }>;
+  summary: {
+    totalTeachers: number;
+    averageAssignments: number;
+    totalStudentsManaged: number;
+  };
+}
+
+interface AttendanceReportData {
+  attendances: Array<{
+    id: string;
+    date: string;
+    status: string;
+    reason: string | null;
+    student: {
+      firstName: string;
+      lastName: string;
+      cedula: string;
+      admissionNumber: string;
+    };
+    section: {
+      name: string;
+      className: string;
+      classCode: string;
+      periodName: string;
+    };
+  }>;
+  summary: {
+    totalRecords: number;
+    byStatus: Array<{
+      status: string;
+      count: number;
+    }>;
+    byDate: Array<{
+      date: string;
+      count: number;
+    }>;
+  };
+}
+
+interface EnrollmentReportData {
+  enrollments: Array<{
+    id: string;
+    isActive: boolean;
+    createdAt: string;
+    student: {
+      firstName: string;
+      lastName: string;
+      cedula: string;
+      admissionNumber: string;
+    };
+    section: {
+      name: string;
+      className: string;
+      classCode: string;
+      periodName: string;
+    };
+  }>;
+  summary: {
+    totalEnrollments: number;
+    byStatus: Array<{
+      isActive: boolean;
+      count: number;
+    }>;
+    bySectionCount: number;
+  };
+}
+
+interface ClassReportData {
+  classes: Array<{
+    id: string;
+    name: string;
+    code: string;
+    description: string | null;
+    isActive: boolean;
+    period: {
+      name: string;
+      isActive: boolean;
+    };
+    totalSections: number;
+    totalStudents: number;
+    totalTeachers: number;
+    sections: Array<{
+      name: string;
+      studentsCount: number;
+      teachersCount: number;
+      teachers: Array<{
+        firstName: string;
+        lastName: string;
+      }>;
+    }>;
+  }>;
+  summary: {
+    totalClasses: number;
+    totalSections: number;
+    totalStudents: number;
+    averageStudentsPerClass: number;
+  };
+}
+
+export default function ReportsPage() {
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [selectedPeriod, setSelectedPeriod] = useState<string>('');
+  const [startDate, setStartDate] = useState<string>('');
+  const [endDate, setEndDate] = useState<string>('');
+  const [loading, setLoading] = useState(false);
+  const [activeTab, setActiveTab] = useState('overview');
+  
+  // Data states
+  const [overviewData, setOverviewData] = useState<OverviewData | null>(null);
+  const [studentsData, setStudentsData] = useState<StudentReportData | null>(null);
+  const [teachersData, setTeachersData] = useState<TeacherReportData | null>(null);
+  const [attendanceData, setAttendanceData] = useState<AttendanceReportData | null>(null);
+  const [enrollmentsData, setEnrollmentsData] = useState<EnrollmentReportData | null>(null);
+  const [classesData, setClassesData] = useState<ClassReportData | null>(null);
+
+  useEffect(() => {
+    fetchPeriods();
+    fetchReport('overview');
+  }, []);
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/reports/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data);
+      }
+    } catch (error) {
+      console.error('Error fetching periods:', error);
+    }
+  };
+
+  const fetchReport = async (reportType: string) => {
+    setLoading(true);
+    try {
+      const params = new URLSearchParams({
+        type: reportType,
+        ...(selectedPeriod && selectedPeriod !== 'all' && { periodId: selectedPeriod }),
+        ...(startDate && { startDate }),
+        ...(endDate && { endDate })
+      });
+
+      const response = await fetch(`/api/admin/reports?${params}`);
+      if (response.ok) {
+        const data = await response.json();
+        
+        switch (reportType) {
+          case 'overview':
+            setOverviewData(data);
+            break;
+          case 'students':
+            setStudentsData(data);
+            break;
+          case 'teachers':
+            setTeachersData(data);
+            break;
+          case 'attendance':
+            setAttendanceData(data);
+            break;
+          case 'enrollments':
+            setEnrollmentsData(data);
+            break;
+          case 'classes':
+            setClassesData(data);
+            break;
+        }
+      } else {
+        toast.error('Error al cargar el reporte');
+      }
+    } catch (error) {
+      console.error('Error fetching report:', error);
+      toast.error('Error al cargar el reporte');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleTabChange = (value: string) => {
+    setActiveTab(value);
+    fetchReport(value);
+  };
+
+  const handleFilterChange = () => {
+    fetchReport(activeTab);
+  };
+
+  const exportToCSV = (data: any[], 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];
+          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 getAttendanceStatusBadge = (status: string) => {
+    switch (status) {
+      case 'PRESENT':
+        return <Badge variant="default">Presente</Badge>;
+      case 'ABSENT':
+        return <Badge variant="destructive">Ausente</Badge>;
+      case 'JUSTIFIED':
+        return <Badge variant="secondary">Justificado</Badge>;
+      default:
+        return <Badge variant="outline">{status}</Badge>;
+    }
+  };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES');
+  };
+
+  const formatPercentage = (value: number) => {
+    return `${value.toFixed(1)}%`;
+  };
+
+  return (
+    <MainLayout>
+      <div className="space-y-6">
+        <div className="flex items-center justify-between">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Sistema de Reportes</h1>
+            <p className="text-muted-foreground">
+              Genera y visualiza reportes detallados del sistema educativo
+            </p>
+          </div>
+        </div>
+
+        {/* Filtros */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Activity className="h-5 w-5" />
+              Filtros de Reporte
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+              <div className="space-y-2">
+                <Label>Periodo Académico</Label>
+                <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Todos los periodos" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los periodos</SelectItem>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name} {period.isActive && '(Activo)'}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              <div className="space-y-2">
+                <Label>Fecha Inicio</Label>
+                <Input
+                  type="date"
+                  value={startDate}
+                  onChange={(e) => setStartDate(e.target.value)}
+                />
+              </div>
+              <div className="space-y-2">
+                <Label>Fecha Fin</Label>
+                <Input
+                  type="date"
+                  value={endDate}
+                  onChange={(e) => setEndDate(e.target.value)}
+                />
+              </div>
+              <div className="flex items-end">
+                <Button onClick={handleFilterChange} className="w-full">
+                  Aplicar Filtros
+                </Button>
+              </div>
+            </div>
+          </CardContent>
+        </Card>
+
+        {/* Tabs de Reportes */}
+        <Tabs value={activeTab} onValueChange={handleTabChange}>
+          <TabsList className="grid w-full grid-cols-6">
+            <TabsTrigger value="overview">Resumen</TabsTrigger>
+            <TabsTrigger value="students">Estudiantes</TabsTrigger>
+            <TabsTrigger value="teachers">Profesores</TabsTrigger>
+            <TabsTrigger value="attendance">Asistencia</TabsTrigger>
+            <TabsTrigger value="enrollments">Inscripciones</TabsTrigger>
+            <TabsTrigger value="classes">Clases</TabsTrigger>
+          </TabsList>
+
+          {/* Reporte de Resumen */}
+          <TabsContent value="overview">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : overviewData ? (
+              <div className="space-y-6">
+                <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+                  <Card>
+                    <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                      <CardTitle className="text-sm font-medium">Total Usuarios</CardTitle>
+                      <Users className="h-4 w-4 text-muted-foreground" />
+                    </CardHeader>
+                    <CardContent>
+                      <div className="text-2xl font-bold">{overviewData.overview.totalUsers}</div>
+                    </CardContent>
+                  </Card>
+                  <Card>
+                    <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                      <CardTitle className="text-sm font-medium">Estudiantes Activos</CardTitle>
+                      <GraduationCap className="h-4 w-4 text-muted-foreground" />
+                    </CardHeader>
+                    <CardContent>
+                      <div className="text-2xl font-bold">{overviewData.overview.totalStudents}</div>
+                    </CardContent>
+                  </Card>
+                  <Card>
+                    <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                      <CardTitle className="text-sm font-medium">Profesores Activos</CardTitle>
+                      <Users className="h-4 w-4 text-muted-foreground" />
+                    </CardHeader>
+                    <CardContent>
+                      <div className="text-2xl font-bold">{overviewData.overview.totalTeachers}</div>
+                    </CardContent>
+                  </Card>
+                  <Card>
+                    <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                      <CardTitle className="text-sm font-medium">Clases Activas</CardTitle>
+                      <BookOpen className="h-4 w-4 text-muted-foreground" />
+                    </CardHeader>
+                    <CardContent>
+                      <div className="text-2xl font-bold">{overviewData.overview.totalClasses}</div>
+                    </CardContent>
+                  </Card>
+                </div>
+
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                  <Card>
+                    <CardHeader>
+                      <CardTitle>Usuarios por Rol</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <div className="space-y-2">
+                        {overviewData.usersByRole.map((item) => (
+                          <div key={item.role} className="flex justify-between items-center">
+                            <span className="capitalize">{item.role.toLowerCase()}</span>
+                            <Badge variant="outline">{item.count}</Badge>
+                          </div>
+                        ))}
+                      </div>
+                    </CardContent>
+                  </Card>
+                  <Card>
+                    <CardHeader>
+                      <CardTitle>Actividad Reciente</CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <div className="space-y-4">
+                        <div className="flex justify-between items-center">
+                          <span>Inscripciones (últimos 30 días)</span>
+                          <Badge variant="default">{overviewData.overview.recentEnrollments}</Badge>
+                        </div>
+                        <div className="flex justify-between items-center">
+                          <span>Registros de asistencia (últimos 7 días)</span>
+                          <Badge variant="default">{overviewData.overview.recentAttendance}</Badge>
+                        </div>
+                      </div>
+                    </CardContent>
+                  </Card>
+                </div>
+              </div>
+            ) : null}
+          </TabsContent>
+
+          {/* Reporte de Estudiantes */}
+          <TabsContent value="students">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : studentsData ? (
+              <div className="space-y-6">
+                <div className="flex justify-between items-center">
+                  <div className="grid grid-cols-3 gap-4">
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{studentsData.summary.totalStudents}</div>
+                        <p className="text-xs text-muted-foreground">Total Estudiantes</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{studentsData.summary.averageEnrollments.toFixed(1)}</div>
+                        <p className="text-xs text-muted-foreground">Promedio Inscripciones</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{formatPercentage(studentsData.summary.averageAttendanceRate)}</div>
+                        <p className="text-xs text-muted-foreground">Asistencia Promedio</p>
+                      </CardContent>
+                    </Card>
+                  </div>
+                  <Button onClick={() => exportToCSV(studentsData.students, 'reporte_estudiantes')}>
+                    <Download className="mr-2 h-4 w-4" />
+                    Exportar CSV
+                  </Button>
+                </div>
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Detalle de Estudiantes</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>Estudiante</TableHead>
+                          <TableHead>Cédula</TableHead>
+                          <TableHead>Matrícula</TableHead>
+                          <TableHead>Inscripciones</TableHead>
+                          <TableHead>Asistencia</TableHead>
+                          <TableHead>Tasa Asistencia</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {studentsData.students.map((student) => (
+                          <TableRow key={student.id}>
+                            <TableCell className="font-medium">
+                              {student.firstName} {student.lastName}
+                            </TableCell>
+                            <TableCell>{student.cedula}</TableCell>
+                            <TableCell>
+                              <Badge variant="outline">{student.admissionNumber}</Badge>
+                            </TableCell>
+                            <TableCell>{student.enrollmentsCount}</TableCell>
+                            <TableCell>{student.presentAttendances}/{student.totalAttendances}</TableCell>
+                            <TableCell>
+                              <Badge variant={student.attendanceRate >= 80 ? 'default' : 'destructive'}>
+                                {formatPercentage(student.attendanceRate)}
+                              </Badge>
+                            </TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                  </CardContent>
+                </Card>
+              </div>
+            ) : null}
+          </TabsContent>
+
+          {/* Reporte de Profesores */}
+          <TabsContent value="teachers">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : teachersData ? (
+              <div className="space-y-6">
+                <div className="flex justify-between items-center">
+                  <div className="grid grid-cols-3 gap-4">
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{teachersData.summary.totalTeachers}</div>
+                        <p className="text-xs text-muted-foreground">Total Profesores</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{teachersData.summary.averageAssignments.toFixed(1)}</div>
+                        <p className="text-xs text-muted-foreground">Promedio Asignaciones</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{teachersData.summary.totalStudentsManaged}</div>
+                        <p className="text-xs text-muted-foreground">Estudiantes Gestionados</p>
+                      </CardContent>
+                    </Card>
+                  </div>
+                  <Button onClick={() => exportToCSV(teachersData.teachers, 'reporte_profesores')}>
+                    <Download className="mr-2 h-4 w-4" />
+                    Exportar CSV
+                  </Button>
+                </div>
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Detalle de Profesores</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>Profesor</TableHead>
+                          <TableHead>Cédula</TableHead>
+                          <TableHead>Email</TableHead>
+                          <TableHead>Asignaciones</TableHead>
+                          <TableHead>Total Estudiantes</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {teachersData.teachers.map((teacher) => (
+                          <TableRow key={teacher.id}>
+                            <TableCell className="font-medium">
+                              {teacher.firstName} {teacher.lastName}
+                            </TableCell>
+                            <TableCell>{teacher.cedula}</TableCell>
+                            <TableCell>{teacher.email}</TableCell>
+                            <TableCell>
+                              <Badge variant="outline">{teacher.assignmentsCount}</Badge>
+                            </TableCell>
+                            <TableCell>{teacher.totalStudents}</TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                  </CardContent>
+                </Card>
+              </div>
+            ) : null}
+          </TabsContent>
+
+          {/* Reporte de Asistencia */}
+          <TabsContent value="attendance">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : attendanceData ? (
+              <div className="space-y-6">
+                <div className="flex justify-between items-center">
+                  <div className="grid grid-cols-4 gap-4">
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{attendanceData.summary.totalRecords}</div>
+                        <p className="text-xs text-muted-foreground">Total Registros</p>
+                      </CardContent>
+                    </Card>
+                    {attendanceData.summary.byStatus.map((status) => (
+                      <Card key={status.status}>
+                        <CardContent className="pt-6">
+                          <div className="text-2xl font-bold">{status.count}</div>
+                          <p className="text-xs text-muted-foreground">
+                            {status.status === 'PRESENT' ? 'Presentes' : 
+                             status.status === 'ABSENT' ? 'Ausentes' : 'Justificados'}
+                          </p>
+                        </CardContent>
+                      </Card>
+                    ))}
+                  </div>
+                  <Button onClick={() => exportToCSV(attendanceData.attendances, 'reporte_asistencia')}>
+                    <Download className="mr-2 h-4 w-4" />
+                    Exportar CSV
+                  </Button>
+                </div>
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Registros de Asistencia</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>Fecha</TableHead>
+                          <TableHead>Estudiante</TableHead>
+                          <TableHead>Matrícula</TableHead>
+                          <TableHead>Sección</TableHead>
+                          <TableHead>Estado</TableHead>
+                          <TableHead>Razón</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {attendanceData.attendances.slice(0, 100).map((attendance) => (
+                          <TableRow key={attendance.id}>
+                            <TableCell>{formatDate(attendance.date)}</TableCell>
+                            <TableCell className="font-medium">
+                              {attendance.student.firstName} {attendance.student.lastName}
+                            </TableCell>
+                            <TableCell>
+                              <Badge variant="outline">{attendance.student.admissionNumber}</Badge>
+                            </TableCell>
+                            <TableCell>
+                              {attendance.section.classCode} - {attendance.section.name}
+                            </TableCell>
+                            <TableCell>{getAttendanceStatusBadge(attendance.status)}</TableCell>
+                            <TableCell>{attendance.reason || '-'}</TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                    {attendanceData.attendances.length > 100 && (
+                      <div className="text-center py-4 text-muted-foreground">
+                        Mostrando los primeros 100 registros de {attendanceData.attendances.length} total
+                      </div>
+                    )}
+                  </CardContent>
+                </Card>
+              </div>
+            ) : null}
+          </TabsContent>
+
+          {/* Reporte de Inscripciones */}
+          <TabsContent value="enrollments">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : enrollmentsData ? (
+              <div className="space-y-6">
+                <div className="flex justify-between items-center">
+                  <div className="grid grid-cols-3 gap-4">
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{enrollmentsData.summary.totalEnrollments}</div>
+                        <p className="text-xs text-muted-foreground">Total Inscripciones</p>
+                      </CardContent>
+                    </Card>
+                    {enrollmentsData.summary.byStatus.map((status) => (
+                      <Card key={String(status.isActive)}>
+                        <CardContent className="pt-6">
+                          <div className="text-2xl font-bold">{status.count}</div>
+                          <p className="text-xs text-muted-foreground">
+                            {status.isActive ? 'Activas' : 'Inactivas'}
+                          </p>
+                        </CardContent>
+                      </Card>
+                    ))}
+                  </div>
+                  <Button onClick={() => exportToCSV(enrollmentsData.enrollments, 'reporte_inscripciones')}>
+                    <Download className="mr-2 h-4 w-4" />
+                    Exportar CSV
+                  </Button>
+                </div>
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Detalle de Inscripciones</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>Estudiante</TableHead>
+                          <TableHead>Matrícula</TableHead>
+                          <TableHead>Clase</TableHead>
+                          <TableHead>Sección</TableHead>
+                          <TableHead>Estado</TableHead>
+                          <TableHead>Fecha Inscripción</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {enrollmentsData.enrollments.slice(0, 100).map((enrollment) => (
+                          <TableRow key={enrollment.id}>
+                            <TableCell className="font-medium">
+                              {enrollment.student.firstName} {enrollment.student.lastName}
+                            </TableCell>
+                            <TableCell>
+                              <Badge variant="outline">{enrollment.student.admissionNumber}</Badge>
+                            </TableCell>
+                            <TableCell>
+                              {enrollment.section.classCode} - {enrollment.section.className}
+                            </TableCell>
+                            <TableCell>{enrollment.section.name}</TableCell>
+                            <TableCell>
+                              <Badge variant={enrollment.isActive ? 'default' : 'secondary'}>
+                                {enrollment.isActive ? 'Activa' : 'Inactiva'}
+                              </Badge>
+                            </TableCell>
+                            <TableCell>{formatDate(enrollment.createdAt)}</TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                    {enrollmentsData.enrollments.length > 100 && (
+                      <div className="text-center py-4 text-muted-foreground">
+                        Mostrando las primeras 100 inscripciones de {enrollmentsData.enrollments.length} total
+                      </div>
+                    )}
+                  </CardContent>
+                </Card>
+              </div>
+            ) : null}
+          </TabsContent>
+
+          {/* Reporte de Clases */}
+          <TabsContent value="classes">
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando reporte...</div>
+              </div>
+            ) : classesData ? (
+              <div className="space-y-6">
+                <div className="flex justify-between items-center">
+                  <div className="grid grid-cols-4 gap-4">
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{classesData.summary.totalClasses}</div>
+                        <p className="text-xs text-muted-foreground">Total Clases</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{classesData.summary.totalSections}</div>
+                        <p className="text-xs text-muted-foreground">Total Secciones</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{classesData.summary.totalStudents}</div>
+                        <p className="text-xs text-muted-foreground">Total Estudiantes</p>
+                      </CardContent>
+                    </Card>
+                    <Card>
+                      <CardContent className="pt-6">
+                        <div className="text-2xl font-bold">{classesData.summary.averageStudentsPerClass.toFixed(1)}</div>
+                        <p className="text-xs text-muted-foreground">Promedio por Clase</p>
+                      </CardContent>
+                    </Card>
+                  </div>
+                  <Button onClick={() => exportToCSV(classesData.classes, 'reporte_clases')}>
+                    <Download className="mr-2 h-4 w-4" />
+                    Exportar CSV
+                  </Button>
+                </div>
+                <Card>
+                  <CardHeader>
+                    <CardTitle>Detalle de Clases</CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <Table>
+                      <TableHeader>
+                        <TableRow>
+                          <TableHead>Clase</TableHead>
+                          <TableHead>Código</TableHead>
+                          <TableHead>Periodo</TableHead>
+                          <TableHead>Secciones</TableHead>
+                          <TableHead>Estudiantes</TableHead>
+                          <TableHead>Profesores</TableHead>
+                          <TableHead>Estado</TableHead>
+                        </TableRow>
+                      </TableHeader>
+                      <TableBody>
+                        {classesData.classes.map((classItem) => (
+                          <TableRow key={classItem.id}>
+                            <TableCell className="font-medium">{classItem.name}</TableCell>
+                            <TableCell>
+                              <Badge variant="outline">{classItem.code}</Badge>
+                            </TableCell>
+                            <TableCell>
+                              <Badge variant={classItem.period.isActive ? 'default' : 'secondary'}>
+                                {classItem.period.name}
+                              </Badge>
+                            </TableCell>
+                            <TableCell>{classItem.totalSections}</TableCell>
+                            <TableCell>{classItem.totalStudents}</TableCell>
+                            <TableCell>{classItem.totalTeachers}</TableCell>
+                            <TableCell>
+                              <Badge variant={classItem.isActive ? 'default' : 'secondary'}>
+                                {classItem.isActive ? 'Activa' : 'Inactiva'}
+                              </Badge>
+                            </TableCell>
+                          </TableRow>
+                        ))}
+                      </TableBody>
+                    </Table>
+                  </CardContent>
+                </Card>
+              </div>
+            ) : null}
+          </TabsContent>
+        </Tabs>
+      </div>
+    </MainLayout>
+  );
+}

+ 41 - 0
src/app/api/admin/reports/periods/route.ts

@@ -0,0 +1,41 @@
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No tienes permisos para acceder a esta información' },
+        { status: 403 }
+      );
+    }
+
+    const periods = await prisma.period.findMany({
+      where: {
+        deletedAt: null
+      },
+      select: {
+        id: true,
+        name: true,
+        startDate: true,
+        endDate: true,
+        isActive: true
+      },
+      orderBy: {
+        startDate: 'desc'
+      }
+    });
+
+    return NextResponse.json(periods);
+  } catch (error) {
+    console.error('Error fetching periods:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 549 - 0
src/app/api/admin/reports/route.ts

@@ -0,0 +1,549 @@
+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 || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No tienes permisos para acceder a los reportes' },
+        { status: 403 }
+      );
+    }
+
+    const { searchParams } = new URL(request.url);
+    const reportType = searchParams.get('type');
+    const periodId = searchParams.get('periodId');
+    const startDate = searchParams.get('startDate');
+    const endDate = searchParams.get('endDate');
+
+    switch (reportType) {
+      case 'overview':
+        return await getOverviewReport();
+      case 'students':
+        return await getStudentsReport(periodId);
+      case 'teachers':
+        return await getTeachersReport(periodId);
+      case 'attendance':
+        return await getAttendanceReport(periodId, startDate, endDate);
+      case 'enrollments':
+        return await getEnrollmentsReport(periodId);
+      case 'classes':
+        return await getClassesReport(periodId);
+      default:
+        return await getOverviewReport();
+    }
+  } catch (error) {
+    console.error('Error generating report:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// Reporte general del sistema
+async function getOverviewReport() {
+  try {
+    const [totalUsers, totalStudents, totalTeachers, totalClasses, totalSections, activePeriods] = await Promise.all([
+      prisma.user.count(),
+      prisma.student.count({ where: { isActive: true } }),
+      prisma.teacher.count({ where: { isActive: true } }),
+      prisma.class.count({ where: { isActive: true } }),
+      prisma.section.count({ where: { isActive: true } }),
+      prisma.period.count({ where: { isActive: true } })
+    ]);
+
+    const usersByRole = await prisma.user.groupBy({
+      by: ['role'],
+      _count: {
+        id: true
+      }
+    });
+
+    const recentEnrollments = await prisma.studentEnrollment.count({
+      where: {
+        createdAt: {
+          gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Últimos 30 días
+        }
+      }
+    });
+
+    const recentAttendance = await prisma.attendance.count({
+      where: {
+        createdAt: {
+          gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Últimos 7 días
+        }
+      }
+    });
+
+    return NextResponse.json({
+      overview: {
+        totalUsers,
+        totalStudents,
+        totalTeachers,
+        totalClasses,
+        totalSections,
+        activePeriods,
+        recentEnrollments,
+        recentAttendance
+      },
+      usersByRole: usersByRole.map(item => ({
+        role: item.role,
+        count: item._count.id
+      }))
+    });
+  } catch (error) {
+    console.error('Error in overview report:', error);
+    throw error;
+  }
+}
+
+// Reporte de estudiantes
+async function getStudentsReport(periodId: string | null) {
+  try {
+    const whereClause = periodId ? {
+      enrollments: {
+        some: {
+          section: {
+            class: {
+              periodId: periodId
+            }
+          }
+        }
+      }
+    } : {};
+
+    const students = await prisma.student.findMany({
+      where: {
+        isActive: true,
+        ...whereClause
+      },
+      include: {
+        enrollments: {
+          where: {
+            isActive: true,
+            ...(periodId ? {
+              section: {
+                class: {
+                  periodId: periodId
+                }
+              }
+            } : {})
+          },
+          include: {
+            section: {
+              include: {
+                class: {
+                  include: {
+                    period: true
+                  }
+                }
+              }
+            }
+          }
+        },
+        attendances: {
+          where: {
+            ...(periodId ? {
+              section: {
+                class: {
+                  periodId: periodId
+                }
+              }
+            } : {})
+          }
+        }
+      },
+      orderBy: {
+        lastName: 'asc'
+      }
+    });
+
+    const studentsWithStats = students.map(student => {
+      const totalAttendances = student.attendances.length;
+      const presentAttendances = student.attendances.filter(a => a.status === 'PRESENT').length;
+      const attendanceRate = totalAttendances > 0 ? (presentAttendances / totalAttendances) * 100 : 0;
+
+      return {
+        id: student.id,
+        firstName: student.firstName,
+        lastName: student.lastName,
+        cedula: student.cedula,
+        email: student.email,
+        admissionNumber: student.admissionNumber,
+        enrollmentsCount: student.enrollments.length,
+        attendanceRate: Math.round(attendanceRate * 100) / 100,
+        totalAttendances,
+        presentAttendances,
+        enrollments: student.enrollments.map(enrollment => ({
+          sectionName: enrollment.section.name,
+          className: enrollment.section.class.name,
+          classCode: enrollment.section.class.code,
+          periodName: enrollment.section.class.period.name
+        }))
+      };
+    });
+
+    return NextResponse.json({
+      students: studentsWithStats,
+      summary: {
+        totalStudents: students.length,
+        averageEnrollments: students.length > 0 ? students.reduce((sum, s) => sum + s.enrollments.length, 0) / students.length : 0,
+        averageAttendanceRate: studentsWithStats.length > 0 ? studentsWithStats.reduce((sum, s) => sum + s.attendanceRate, 0) / studentsWithStats.length : 0
+      }
+    });
+  } catch (error) {
+    console.error('Error in students report:', error);
+    throw error;
+  }
+}
+
+// Reporte de profesores
+async function getTeachersReport(periodId: string | null) {
+  try {
+    const whereClause = periodId ? {
+      assignments: {
+        some: {
+          section: {
+            class: {
+              periodId: periodId
+            }
+          }
+        }
+      }
+    } : {};
+
+    const teachers = await prisma.teacher.findMany({
+      where: {
+        isActive: true,
+        ...whereClause
+      },
+      include: {
+        assignments: {
+          where: {
+            isActive: true,
+            ...(periodId ? {
+              section: {
+                class: {
+                  periodId: periodId
+                }
+              }
+            } : {})
+          },
+          include: {
+            section: {
+              include: {
+                class: {
+                  include: {
+                    period: true
+                  }
+                },
+                studentEnrollments: {
+                  where: {
+                    isActive: true
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      orderBy: {
+        lastName: 'asc'
+      }
+    });
+
+    const teachersWithStats = teachers.map(teacher => {
+      const totalStudents = teacher.assignments.reduce((sum, assignment) => 
+        sum + assignment.section.studentEnrollments.length, 0
+      );
+
+      return {
+        id: teacher.id,
+        firstName: teacher.firstName,
+        lastName: teacher.lastName,
+        cedula: teacher.cedula,
+        email: teacher.email,
+        phone: teacher.phone,
+        assignmentsCount: teacher.assignments.length,
+        totalStudents,
+        assignments: teacher.assignments.map(assignment => ({
+          sectionName: assignment.section.name,
+          className: assignment.section.class.name,
+          classCode: assignment.section.class.code,
+          periodName: assignment.section.class.period.name,
+          studentsCount: assignment.section.studentEnrollments.length
+        }))
+      };
+    });
+
+    return NextResponse.json({
+      teachers: teachersWithStats,
+      summary: {
+        totalTeachers: teachers.length,
+        averageAssignments: teachers.length > 0 ? teachers.reduce((sum, t) => sum + t.assignments.length, 0) / teachers.length : 0,
+        totalStudentsManaged: teachersWithStats.reduce((sum, t) => sum + t.totalStudents, 0)
+      }
+    });
+  } catch (error) {
+    console.error('Error in teachers report:', error);
+    throw error;
+  }
+}
+
+// Reporte de asistencia
+async function getAttendanceReport(periodId: string | null, startDate: string | null, endDate: string | null) {
+  try {
+    const dateFilter: any = {};
+    if (startDate) dateFilter.gte = new Date(startDate);
+    if (endDate) dateFilter.lte = new Date(endDate);
+
+    const whereClause: any = {
+      ...(Object.keys(dateFilter).length > 0 && { date: dateFilter }),
+      ...(periodId && {
+        section: {
+          class: {
+            periodId: periodId
+          }
+        }
+      })
+    };
+
+    const attendances = await prisma.attendance.findMany({
+      where: whereClause,
+      include: {
+        student: true,
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true
+              }
+            }
+          }
+        }
+      },
+      orderBy: {
+        date: 'desc'
+      }
+    });
+
+    const attendanceByStatus = await prisma.attendance.groupBy({
+      by: ['status'],
+      where: whereClause,
+      _count: {
+        id: true
+      }
+    });
+
+    const attendanceByDate = await prisma.attendance.groupBy({
+      by: ['date'],
+      where: whereClause,
+      _count: {
+        id: true
+      },
+      orderBy: {
+        date: 'asc'
+      }
+    });
+
+    return NextResponse.json({
+      attendances: attendances.map(attendance => ({
+        id: attendance.id,
+        date: attendance.date,
+        status: attendance.status,
+        reason: attendance.reason,
+        student: {
+          firstName: attendance.student.firstName,
+          lastName: attendance.student.lastName,
+          cedula: attendance.student.cedula,
+          admissionNumber: attendance.student.admissionNumber
+        },
+        section: {
+          name: attendance.section.name,
+          className: attendance.section.class.name,
+          classCode: attendance.section.class.code,
+          periodName: attendance.section.class.period.name
+        }
+      })),
+      summary: {
+        totalRecords: attendances.length,
+        byStatus: attendanceByStatus.map(item => ({
+          status: item.status,
+          count: item._count.id
+        })),
+        byDate: attendanceByDate.map(item => ({
+          date: item.date,
+          count: item._count.id
+        }))
+      }
+    });
+  } catch (error) {
+    console.error('Error in attendance report:', error);
+    throw error;
+  }
+}
+
+// Reporte de inscripciones
+async function getEnrollmentsReport(periodId: string | null) {
+  try {
+    const whereClause = periodId ? {
+      section: {
+        class: {
+          periodId: periodId
+        }
+      }
+    } : {};
+
+    const enrollments = await prisma.studentEnrollment.findMany({
+      where: whereClause,
+      include: {
+        student: true,
+        section: {
+          include: {
+            class: {
+              include: {
+                period: true
+              }
+            }
+          }
+        }
+      },
+      orderBy: {
+        createdAt: 'desc'
+      }
+    });
+
+    const enrollmentsByStatus = await prisma.studentEnrollment.groupBy({
+      by: ['isActive'],
+      where: whereClause,
+      _count: {
+        id: true
+      }
+    });
+
+    const enrollmentsBySection = await prisma.studentEnrollment.groupBy({
+      by: ['sectionId'],
+      where: whereClause,
+      _count: {
+        id: true
+      }
+    });
+
+    return NextResponse.json({
+      enrollments: enrollments.map(enrollment => ({
+        id: enrollment.id,
+        isActive: enrollment.isActive,
+        createdAt: enrollment.createdAt,
+        student: {
+          firstName: enrollment.student.firstName,
+          lastName: enrollment.student.lastName,
+          cedula: enrollment.student.cedula,
+          admissionNumber: enrollment.student.admissionNumber
+        },
+        section: {
+          name: enrollment.section.name,
+          className: enrollment.section.class.name,
+          classCode: enrollment.section.class.code,
+          periodName: enrollment.section.class.period.name
+        }
+      })),
+      summary: {
+        totalEnrollments: enrollments.length,
+        byStatus: enrollmentsByStatus.map(item => ({
+          isActive: item.isActive,
+          count: item._count.id
+        })),
+        bySectionCount: enrollmentsBySection.length
+      }
+    });
+  } catch (error) {
+    console.error('Error in enrollments report:', error);
+    throw error;
+  }
+}
+
+// Reporte de clases
+async function getClassesReport(periodId: string | null) {
+  try {
+    const whereClause = periodId ? { periodId } : { isActive: true };
+
+    const classes = await prisma.class.findMany({
+      where: whereClause,
+      include: {
+        period: true,
+        sections: {
+          include: {
+            studentEnrollments: {
+              where: {
+                isActive: true
+              }
+            },
+            teacherAssignments: {
+              where: {
+                isActive: true
+              },
+              include: {
+                teacher: true
+              }
+            }
+          }
+        }
+      },
+      orderBy: {
+        name: 'asc'
+      }
+    });
+
+    const classesWithStats = classes.map(classItem => {
+      const totalSections = classItem.sections.length;
+      const totalStudents = classItem.sections.reduce((sum, section) => 
+        sum + section.studentEnrollments.length, 0
+      );
+      const totalTeachers = classItem.sections.reduce((sum, section) => 
+        sum + section.teacherAssignments.length, 0
+      );
+
+      return {
+        id: classItem.id,
+        name: classItem.name,
+        code: classItem.code,
+        description: classItem.description,
+        isActive: classItem.isActive,
+        period: {
+          name: classItem.period.name,
+          isActive: classItem.period.isActive
+        },
+        totalSections,
+        totalStudents,
+        totalTeachers,
+        sections: classItem.sections.map(section => ({
+          name: section.name,
+          studentsCount: section.studentEnrollments.length,
+          teachersCount: section.teacherAssignments.length,
+          teachers: section.teacherAssignments.map(assignment => ({
+            firstName: assignment.teacher.firstName,
+            lastName: assignment.teacher.lastName
+          }))
+        }))
+      };
+    });
+
+    return NextResponse.json({
+      classes: classesWithStats,
+      summary: {
+        totalClasses: classes.length,
+        totalSections: classesWithStats.reduce((sum, c) => sum + c.totalSections, 0),
+        totalStudents: classesWithStats.reduce((sum, c) => sum + c.totalStudents, 0),
+        averageStudentsPerClass: classes.length > 0 ? classesWithStats.reduce((sum, c) => sum + c.totalStudents, 0) / classes.length : 0
+      }
+    });
+  } catch (error) {
+    console.error('Error in classes report:', error);
+    throw error;
+  }
+}

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

@@ -35,10 +35,10 @@ const adminMenuItems = [
   { icon: School, label: 'Periodos', href: '/admin/periods' },
   { icon: BookOpen, label: 'Clases', href: '/admin/classes' },
   { icon: Layers, label: 'Secciones', href: '/admin/sections' },
-  { icon: GraduationCap, label: 'Profesores', href: '/admin/teachers' },
-  { icon: User, label: 'Estudiantes', href: '/admin/students' },
+  { icon: GraduationCap, label: 'Profesores', href: '/admin/teacher-assignments' },
+  { icon: User, label: 'Estudiantes', href: '/admin/student-enrollments' },
   { icon: BarChart3, label: 'Reportes', href: '/admin/reports' },
-  { icon: Settings, label: 'Configuración', href: '/admin/settings' },
+  // { icon: Settings, label: 'Configuración', href: '/admin/settings' },
 ]
 
 const teacherMenuItems = [

+ 66 - 0
src/components/ui/tabs.tsx

@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Root>) {
+  return (
+    <TabsPrimitive.Root
+      data-slot="tabs"
+      className={cn("flex flex-col gap-2", className)}
+      {...props}
+    />
+  )
+}
+
+function TabsList({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.List>) {
+  return (
+    <TabsPrimitive.List
+      data-slot="tabs-list"
+      className={cn(
+        "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TabsTrigger({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
+  return (
+    <TabsPrimitive.Trigger
+      data-slot="tabs-trigger"
+      className={cn(
+        "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TabsContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof TabsPrimitive.Content>) {
+  return (
+    <TabsPrimitive.Content
+      data-slot="tabs-content"
+      className={cn("flex-1 outline-none", className)}
+      {...props}
+    />
+  )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }