Matthew Trejo 4 luni în urmă
părinte
comite
4cc329b1f6

+ 252 - 0
ROADMAP.md

@@ -0,0 +1,252 @@
+# TAPIR - Sistema de Gestión de Asistencia Estudiantil
+## Roadmap de Desarrollo
+
+### 📋 Resumen del Proyecto
+TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado con Next.js 15, TypeScript, Prisma ORM y PostgreSQL. El sistema maneja tres roles principales: Administradores, Profesores y Estudiantes.
+
+---
+
+## ✅ Completado
+
+### 1. Estructura Base del Proyecto ✅
+- **Estado**: Completado
+- **Descripción**: Proyecto Next.js 15 con TypeScript configurado
+- **Archivos principales**:
+  - `package.json` - Configuración de dependencias y scripts
+  - `tsconfig.json` - Configuración de TypeScript
+  - `next.config.ts` - Configuración de Next.js
+  - `eslint.config.mjs` - Configuración de ESLint
+  - `postcss.config.mjs` - Configuración de PostCSS
+
+### 2. Base de Datos y ORM ✅
+- **Estado**: Completado
+- **Descripción**: Prisma ORM configurado con PostgreSQL
+- **Archivos implementados**:
+  - `prisma/schema.prisma` - Esquema completo de base de datos
+  - `prisma/migrations/` - Migraciones de base de datos
+  - `prisma/seed.ts` - Script de población inicial
+  - `src/lib/prisma.ts` - Cliente de Prisma
+- **Modelos implementados**:
+  - Users (Usuarios con roles)
+  - Periods (Periodos académicos)
+  - Partials (Parciales)
+  - Classes (Materias)
+  - Sections (Secciones)
+  - Teachers (Profesores)
+  - Students (Estudiantes)
+  - TeacherAssignments (Asignaciones)
+  - StudentEnrollments (Inscripciones)
+  - Attendances (Asistencias)
+
+### 3. UI Framework y Componentes ✅
+- **Estado**: Completado
+- **Descripción**: shadcn/ui configurado con TailwindCSS v4
+- **Archivos implementados**:
+  - `components.json` - Configuración de shadcn/ui
+  - `src/app/globals.css` - Estilos globales
+  - `src/lib/utils.ts` - Utilidades para clases CSS
+- **Componentes UI disponibles**:
+  - Button, Card, Input, Label, Form
+  - Dialog, Alert Dialog, Sheet
+  - Table, Separator, Switch
+  - Tooltip, Sidebar
+
+### 4. Sistema de Autenticación ✅
+- **Estado**: Completado
+- **Descripción**: NextAuth.js con autenticación por credenciales
+- **Archivos implementados**:
+  - `src/lib/auth.ts` - Configuración de NextAuth
+  - `src/middleware.ts` - Middleware de protección de rutas
+  - `src/app/api/auth/[...nextauth]/route.ts` - API de autenticación
+  - `src/app/auth/signin/page.tsx` - Página de inicio de sesión
+  - `src/components/providers/session-provider.tsx` - Proveedor de sesión
+- **Características**:
+  - Autenticación basada en JWT
+  - Protección de rutas por rol
+  - Redirección automática según rol
+  - Encriptación de contraseñas con bcrypt
+
+### 5. Layout y Componentes Base ✅
+- **Estado**: Completado
+- **Descripción**: Sistema de layout responsivo con sidebar
+- **Archivos implementados**:
+  - `src/components/layout/main-layout.tsx` - Layout principal
+  - `src/components/layout/sidebar.tsx` - Sidebar con navegación por rol
+  - `src/components/layout/header.tsx` - Header con título y acciones
+- **Características**:
+  - Sidebar responsivo con menús específicos por rol
+  - Header dinámico con información de usuario
+  - Layout adaptativo para móviles
+  - Navegación contextual según permisos
+
+### 6. Dashboards por Rol ✅
+- **Estado**: Completado
+- **Descripción**: Dashboards específicos para cada tipo de usuario
+- **Archivos implementados**:
+  - `src/app/admin/page.tsx` - Dashboard administrativo
+  - `src/app/teacher/page.tsx` - Dashboard de profesor
+  - `src/app/student/page.tsx` - Dashboard de estudiante
+  - `src/app/page.tsx` - Página de bienvenida con redirección
+- **Características**:
+  - Estadísticas específicas por rol
+  - Datos mock para demostración
+  - Interfaz intuitiva y moderna
+  - Protección de acceso por rol
+
+### 7. Datos de Prueba ✅
+- **Estado**: Completado
+- **Descripción**: Base de datos poblada con datos iniciales
+- **Usuarios creados**:
+  - Administrador: `admin@universidad.edu` / `admin123`
+  - Profesor: `profesor@universidad.edu` / `teacher123`
+  - Estudiante: `estudiante@universidad.edu` / `student123`
+- **Datos incluidos**:
+  - Periodo académico 2024
+  - 3 parciales configurados
+  - 3 materias con secciones
+  - Usuarios de ejemplo para cada rol
+
+---
+
+## 🚧 En Progreso
+
+### Funcionalidades del Rol Administrador 🔄
+- **Estado**: En progreso
+- **Descripción**: Implementación de funcionalidades administrativas
+- **Dashboard**: ✅ Completado
+- **Pendiente**:
+  - Gestión de usuarios (`/admin/users`)
+  - Gestión de periodos académicos (`/admin/periods`)
+  - Gestión de clases y materias (`/admin/classes`)
+  - Gestión de profesores (`/admin/teachers`)
+  - Gestión de estudiantes (`/admin/students`)
+  - Sistema de reportes (`/admin/reports`)
+  - Configuración del sistema (`/admin/settings`)
+
+---
+
+## 📋 Pendiente
+
+### Funcionalidades del Rol Profesor 📝
+- **Estado**: Pendiente
+- **Descripción**: Implementación de funcionalidades para profesores
+- **Dashboard**: ✅ Completado
+- **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
+- **Descripción**: Implementación de funcionalidades para estudiantes
+- **Dashboard**: ✅ Completado
+- **Por implementar**:
+  - Vista de clases matriculadas (`/student/classes`)
+  - Consulta de horarios (`/student/schedule`)
+  - Historial de asistencia (`/student/attendance`)
+  - Gestión de perfil (`/student/profile`)
+  - Notificaciones de asistencia
+
+### Testing y Build 📝
+- **Estado**: Pendiente
+- **Descripción**: Pruebas y preparación para producción
+- **Por implementar**:
+  - Configuración de testing (Jest/Vitest)
+  - 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
+
+---
+
+## 🎯 Próximos Pasos
+
+### Prioridad Alta
+1. **Completar funcionalidades de Administrador**
+   - Gestión CRUD de usuarios
+   - Gestión de periodos y clases
+   - Sistema de reportes básico
+
+2. **Implementar funcionalidades de Profesor**
+   - Sistema de registro de asistencia
+   - Gestión de clases asignadas
+
+3. **Implementar funcionalidades de Estudiante**
+   - Consulta de asistencia personal
+   - Vista de horarios
+
+### Prioridad Media
+1. **Mejoras de UX/UI**
+   - Notificaciones en tiempo real
+   - Mejoras de responsividad
+   - Optimización de rendimiento
+
+2. **Funcionalidades Avanzadas**
+   - Exportación de reportes (PDF/Excel)
+   - Sistema de notificaciones por email
+   - Dashboard con gráficos avanzados
+
+### Prioridad Baja
+1. **Testing y Calidad**
+   - Cobertura de pruebas completa
+   - Documentación técnica
+   - Guías de usuario
+
+---
+
+## 📊 Progreso General
+
+- **Completado**: 62.5% (5/8 tareas principales)
+- **En progreso**: 12.5% (1/8 tareas principales)
+- **Pendiente**: 25% (2/8 tareas principales)
+
+### Arquitectura Técnica ✅
+- ✅ Next.js 15 con App Router
+- ✅ TypeScript para type safety
+- ✅ Prisma ORM con PostgreSQL
+- ✅ NextAuth.js para autenticación
+- ✅ shadcn/ui + TailwindCSS para UI
+- ✅ Middleware para protección de rutas
+- ✅ Estructura modular y escalable
+
+### Base de Datos ✅
+- ✅ Esquema completo definido
+- ✅ Relaciones entre entidades
+- ✅ Migraciones configuradas
+- ✅ Datos de prueba poblados
+- ✅ Índices y constraints
+
+---
+
+## 🔧 Configuración de Desarrollo
+
+### Comandos Disponibles
+```bash
+# Desarrollo
+npm run dev          # Servidor de desarrollo
+npm run build        # Build de producción
+npm run start        # Servidor de producción
+npm run lint         # Linting
+
+# Base de datos
+npm run db:seed      # Poblar base de datos
+npx prisma studio    # Interface gráfica de BD
+npx prisma migrate   # Ejecutar migraciones
+```
+
+### Variables de Entorno Requeridas
+```env
+DATABASE_URL="postgresql://..."
+NEXTAUTH_SECRET="..."
+NEXTAUTH_URL="http://localhost:3000"
+```
+
+---
+
+**Última actualización**: Enero 2025  
+**Versión**: 0.1.0  
+**Estado**: En desarrollo activo

+ 11 - 0
package-lock.json

@@ -33,6 +33,7 @@
         "react": "19.1.0",
         "react-dom": "19.1.0",
         "react-hook-form": "^7.62.0",
+        "sonner": "^2.0.7",
         "tailwind-merge": "^3.3.1",
         "zod": "^4.0.17"
       },
@@ -7211,6 +7212,16 @@
         "is-arrayish": "^0.3.1"
       }
     },
+    "node_modules/sonner": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
+      "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+        "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+      }
+    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

+ 1 - 0
package.json

@@ -38,6 +38,7 @@
     "react": "19.1.0",
     "react-dom": "19.1.0",
     "react-hook-form": "^7.62.0",
+    "sonner": "^2.0.7",
     "tailwind-merge": "^3.3.1",
     "zod": "^4.0.17"
   },

+ 449 - 0
src/app/admin/classes/page.tsx

@@ -0,0 +1,449 @@
+'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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { toast } from 'sonner';
+import { Plus, Search, Edit, Trash2, Users, Calendar, Clock } from 'lucide-react';
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  description: string | null;
+  period: Period;
+  _count: {
+    sections: number;
+  };
+}
+
+interface CreateClassData {
+  name: string;
+  code: string;
+  description: string;
+  periodId: string;
+}
+
+export default function ClassesPage() {
+  const [classes, setClasses] = useState<Class[]>([]);
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [editingClass, setEditingClass] = useState<Class | null>(null);
+  const [formData, setFormData] = useState<CreateClassData>({
+    name: '',
+    code: '',
+    description: '',
+    periodId: '',
+  });
+
+  useEffect(() => {
+    fetchClasses();
+    fetchPeriods();
+  }, []);
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes');
+      if (response.ok) {
+        const data = await response.json();
+        setClasses(data);
+      } else {
+        toast.error('Error al cargar las clases');
+      }
+    } catch (error) {
+      toast.error('Error al cargar las clases');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data.filter((period: Period) => period.isActive));
+      }
+    } catch (error) {
+      console.error('Error fetching periods:', error);
+    }
+  };
+
+  const handleCreateClass = async () => {
+    try {
+      const response = await fetch('/api/admin/classes', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Clase creada exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchClasses();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al crear la clase');
+      }
+    } catch (error) {
+      toast.error('Error al crear la clase');
+    }
+  };
+
+  const handleEditClass = async () => {
+    if (!editingClass) return;
+
+    try {
+      const response = await fetch(`/api/admin/classes/${editingClass.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Clase actualizada exitosamente');
+        setIsEditDialogOpen(false);
+        setEditingClass(null);
+        resetForm();
+        fetchClasses();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al actualizar la clase');
+      }
+    } catch (error) {
+      toast.error('Error al actualizar la clase');
+    }
+  };
+
+  const handleDeleteClass = async (classItem: Class) => {
+    if (!confirm(`¿Estás seguro de que deseas eliminar la clase "${classItem.name}"?`)) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/classes/${classItem.id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        toast.success('Clase eliminada exitosamente');
+        fetchClasses();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al eliminar la clase');
+      }
+    } catch (error) {
+      toast.error('Error al eliminar la clase');
+    }
+  };
+
+  const openEditDialog = (classItem: Class) => {
+    setEditingClass(classItem);
+    setFormData({
+      name: classItem.name,
+      code: classItem.code,
+      description: classItem.description || '',
+      periodId: classItem.period.id,
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      code: '',
+      description: '',
+      periodId: '',
+    });
+  };
+
+  const filteredClasses = classes.filter(classItem =>
+    classItem.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    classItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    classItem.period.name.toLowerCase().includes(searchTerm.toLowerCase())
+  );
+
+  if (loading) {
+    return (
+      <MainLayout requiredRole="ADMIN" title="Gestión de Clases">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando clases...</div>
+        </div>
+      </MainLayout>
+    );
+  }
+
+  return (
+    <MainLayout requiredRole="ADMIN" title="Gestión de Clases">
+      <div className="space-y-6">
+        <div className="flex justify-between items-center">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Gestión de Clases</h1>
+            <p className="text-muted-foreground">
+              Administra las clases del sistema educativo
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={resetForm}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nueva Clase
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nueva Clase</DialogTitle>
+                <DialogDescription>
+                  Completa la información para crear una nueva clase.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="name" className="text-right">
+                    Nombre
+                  </Label>
+                  <Input
+                    id="name"
+                    value={formData.name}
+                    onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                    className="col-span-3"
+                    placeholder="Ej: Matemáticas I"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="code" className="text-right">
+                    Código
+                  </Label>
+                  <Input
+                    id="code"
+                    value={formData.code}
+                    onChange={(e) => setFormData({ ...formData, code: e.target.value })}
+                    className="col-span-3"
+                    placeholder="Ej: MAT101"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="description" className="text-right">
+                    Descripción
+                  </Label>
+                  <Input
+                    id="description"
+                    value={formData.description}
+                    onChange={(e) => setFormData({ ...formData, description: e.target.value })}
+                    className="col-span-3"
+                    placeholder="Descripción de la clase"
+                  />
+                </div>
+
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="period" className="text-right">
+                    Periodo
+                  </Label>
+                  <Select
+                    value={formData.periodId}
+                    onValueChange={(value) => setFormData({ ...formData, periodId: value })}
+                  >
+                    <SelectTrigger className="col-span-3">
+                      <SelectValue placeholder="Seleccionar periodo" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {periods.map((period) => (
+                        <SelectItem key={period.id} value={period.id}>
+                          {period.name}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+              </div>
+              <DialogFooter>
+                <Button type="submit" onClick={handleCreateClass}>
+                  Crear Clase
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle>Clases Registradas</CardTitle>
+            <CardDescription>
+              Lista de todas las clases en el sistema
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar por nombre, código o periodo..."
+                value={searchTerm}
+                onChange={(e) => setSearchTerm(e.target.value)}
+                className="max-w-sm"
+              />
+            </div>
+          </CardHeader>
+          <CardContent>
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>Código</TableHead>
+                  <TableHead>Nombre</TableHead>
+                  <TableHead>Periodo</TableHead>
+                  <TableHead>Secciones</TableHead>
+                  <TableHead>Acciones</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {filteredClasses.map((classItem) => (
+                  <TableRow key={classItem.id}>
+                    <TableCell className="font-medium">{classItem.code}</TableCell>
+                    <TableCell>
+                      <div>
+                        <div className="font-medium">{classItem.name}</div>
+                        {classItem.description && (
+                          <div className="text-sm text-muted-foreground">
+                            {classItem.description}
+                          </div>
+                        )}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <Badge variant={classItem.period.isActive ? "default" : "secondary"}>
+                        {classItem.period.name}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center">
+                        <Users className="mr-1 h-4 w-4" />
+                        {classItem._count.sections}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center space-x-2">
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => openEditDialog(classItem)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleDeleteClass(classItem)}
+                          className="text-red-600 hover:text-red-700"
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+            {filteredClasses.length === 0 && (
+              <div className="text-center py-8 text-muted-foreground">
+                No se encontraron clases
+              </div>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Dialog de Edición */}
+        <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+          <DialogContent className="sm:max-w-[425px]">
+            <DialogHeader>
+              <DialogTitle>Editar Clase</DialogTitle>
+              <DialogDescription>
+                Modifica la información de la clase.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-name" className="text-right">
+                  Nombre
+                </Label>
+                <Input
+                  id="edit-name"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-code" className="text-right">
+                  Código
+                </Label>
+                <Input
+                  id="edit-code"
+                  value={formData.code}
+                  onChange={(e) => setFormData({ ...formData, code: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-description" className="text-right">
+                  Descripción
+                </Label>
+                <Input
+                  id="edit-description"
+                  value={formData.description}
+                  onChange={(e) => setFormData({ ...formData, description: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-period" className="text-right">
+                  Periodo
+                </Label>
+                <Select
+                  value={formData.periodId}
+                  onValueChange={(value) => setFormData({ ...formData, periodId: value })}
+                >
+                  <SelectTrigger className="col-span-3">
+                    <SelectValue placeholder="Seleccionar periodo" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+            </div>
+            <DialogFooter>
+              <Button type="submit" onClick={handleEditClass}>
+                Actualizar Clase
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

+ 480 - 0
src/app/admin/periods/page.tsx

@@ -0,0 +1,480 @@
+'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 {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table';
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Switch } from '@/components/ui/switch';
+import { Plus, Pencil, Trash2, Search, Calendar } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+  createdAt: string;
+  updatedAt: string;
+  _count: {
+    classes: number;
+    partials: number;
+  };
+}
+
+interface PeriodFormData {
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+}
+
+export default function PeriodsPage() {
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [selectedPeriod, setSelectedPeriod] = useState<Period | null>(null);
+  const [formData, setFormData] = useState<PeriodFormData>({
+    name: '',
+    startDate: '',
+    endDate: '',
+    isActive: true,
+  });
+
+  useEffect(() => {
+    fetchPeriods();
+  }, []);
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data);
+      } else {
+        toast.error('Error al cargar periodos');
+      }
+    } catch (error) {
+      toast.error('Error al cargar periodos');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleCreatePeriod = async () => {
+    try {
+      const response = await fetch('/api/admin/periods', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Periodo creado exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchPeriods();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al crear periodo');
+      }
+    } catch (error) {
+      toast.error('Error al crear periodo');
+    }
+  };
+
+  const handleUpdatePeriod = async () => {
+    if (!selectedPeriod) return;
+
+    try {
+      const response = await fetch(`/api/admin/periods/${selectedPeriod.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Periodo actualizado exitosamente');
+        setIsEditDialogOpen(false);
+        resetForm();
+        fetchPeriods();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al actualizar periodo');
+      }
+    } catch (error) {
+      toast.error('Error al actualizar periodo');
+    }
+  };
+
+  const handleDeletePeriod = async (periodId: string) => {
+    try {
+      const response = await fetch(`/api/admin/periods/${periodId}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        toast.success('Periodo eliminado exitosamente');
+        fetchPeriods();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al eliminar periodo');
+      }
+    } catch (error) {
+      toast.error('Error al eliminar periodo');
+    }
+  };
+
+  const handleToggleActive = async (periodId: string, isActive: boolean) => {
+    try {
+      const response = await fetch(`/api/admin/periods/${periodId}/toggle`, {
+        method: 'PATCH',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ isActive }),
+      });
+
+      if (response.ok) {
+        toast.success(`Periodo ${isActive ? 'activado' : 'desactivado'} exitosamente`);
+        fetchPeriods();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al cambiar estado del periodo');
+      }
+    } catch (error) {
+      toast.error('Error al cambiar estado del periodo');
+    }
+  };
+
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      startDate: '',
+      endDate: '',
+      isActive: true,
+    });
+    setSelectedPeriod(null);
+  };
+
+  const openEditDialog = (period: Period) => {
+    setSelectedPeriod(period);
+    setFormData({
+      name: period.name,
+      startDate: period.startDate.split('T')[0],
+      endDate: period.endDate.split('T')[0],
+      isActive: period.isActive,
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const filteredPeriods = periods.filter(period =>
+    period.name.toLowerCase().includes(searchTerm.toLowerCase())
+  );
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric',
+    });
+  };
+
+  return (
+    <MainLayout requiredRole="ADMIN" title="Gestión de Periodos Académicos">
+      <div className="space-y-6">
+        <div className="flex items-center justify-between">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Gestión de Periodos Académicos</h1>
+            <p className="text-muted-foreground">
+              Administra los periodos académicos del sistema
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={resetForm}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nuevo Periodo
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nuevo Periodo</DialogTitle>
+                <DialogDescription>
+                  Completa los datos para crear un nuevo periodo académico.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="name" className="text-right">
+                    Nombre
+                  </Label>
+                  <Input
+                    id="name"
+                    value={formData.name}
+                    onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                    className="col-span-3"
+                    placeholder="ej. 2024-1"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="startDate" className="text-right">
+                    Fecha Inicio
+                  </Label>
+                  <Input
+                    id="startDate"
+                    type="date"
+                    value={formData.startDate}
+                    onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
+                    className="col-span-3"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="endDate" className="text-right">
+                    Fecha Fin
+                  </Label>
+                  <Input
+                    id="endDate"
+                    type="date"
+                    value={formData.endDate}
+                    onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
+                    className="col-span-3"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="isActive" className="text-right">
+                    Activo
+                  </Label>
+                  <Switch
+                    id="isActive"
+                    checked={formData.isActive}
+                    onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
+                  />
+                </div>
+              </div>
+              <DialogFooter>
+                <Button type="submit" onClick={handleCreatePeriod}>
+                  Crear Periodo
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle>Periodos Académicos</CardTitle>
+            <CardDescription>
+              Lista de todos los periodos académicos del sistema
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar periodos..."
+                value={searchTerm}
+                onChange={(e) => setSearchTerm(e.target.value)}
+                className="max-w-sm"
+              />
+            </div>
+          </CardHeader>
+          <CardContent>
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando periodos...</div>
+              </div>
+            ) : (
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Nombre</TableHead>
+                    <TableHead>Fecha Inicio</TableHead>
+                    <TableHead>Fecha Fin</TableHead>
+                    <TableHead>Estado</TableHead>
+                    <TableHead>Clases</TableHead>
+                    <TableHead>Parciales</TableHead>
+                    <TableHead>Fecha Creación</TableHead>
+                    <TableHead className="text-right">Acciones</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {filteredPeriods.map((period) => (
+                    <TableRow key={period.id}>
+                      <TableCell className="font-medium">
+                        <div className="flex items-center gap-2">
+                          <Calendar className="h-4 w-4 text-muted-foreground" />
+                          {period.name}
+                        </div>
+                      </TableCell>
+                      <TableCell>{formatDate(period.startDate)}</TableCell>
+                      <TableCell>{formatDate(period.endDate)}</TableCell>
+                      <TableCell>
+                        <div className="flex items-center gap-2">
+                          <Badge variant={period.isActive ? 'default' : 'secondary'}>
+                            {period.isActive ? 'Activo' : 'Inactivo'}
+                          </Badge>
+                          <Switch
+                            checked={period.isActive}
+                            onCheckedChange={(checked) => handleToggleActive(period.id, checked)}
+                          />
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <Badge variant="outline">
+                          {period._count.classes} clases
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        <Badge variant="outline">
+                          {period._count.partials} parciales
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        {new Date(period.createdAt).toLocaleDateString()}
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <div className="flex items-center justify-end space-x-2">
+                          <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => openEditDialog(period)}
+                          >
+                            <Pencil className="h-4 w-4" />
+                          </Button>
+                          <AlertDialog>
+                            <AlertDialogTrigger asChild>
+                              <Button variant="outline" size="sm">
+                                <Trash2 className="h-4 w-4" />
+                              </Button>
+                            </AlertDialogTrigger>
+                            <AlertDialogContent>
+                              <AlertDialogHeader>
+                                <AlertDialogTitle>¿Estás seguro?</AlertDialogTitle>
+                                <AlertDialogDescription>
+                                  Esta acción no se puede deshacer. Esto eliminará permanentemente
+                                  el periodo académico y todos sus datos asociados (clases, parciales, etc.).
+                                </AlertDialogDescription>
+                              </AlertDialogHeader>
+                              <AlertDialogFooter>
+                                <AlertDialogCancel>Cancelar</AlertDialogCancel>
+                                <AlertDialogAction
+                                  onClick={() => handleDeletePeriod(period.id)}
+                                >
+                                  Eliminar
+                                </AlertDialogAction>
+                              </AlertDialogFooter>
+                            </AlertDialogContent>
+                          </AlertDialog>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))}
+                </TableBody>
+              </Table>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Edit Dialog */}
+        <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+          <DialogContent className="sm:max-w-[425px]">
+            <DialogHeader>
+              <DialogTitle>Editar Periodo</DialogTitle>
+              <DialogDescription>
+                Modifica los datos del periodo académico seleccionado.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-name" className="text-right">
+                  Nombre
+                </Label>
+                <Input
+                  id="edit-name"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-startDate" className="text-right">
+                  Fecha Inicio
+                </Label>
+                <Input
+                  id="edit-startDate"
+                  type="date"
+                  value={formData.startDate}
+                  onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-endDate" className="text-right">
+                  Fecha Fin
+                </Label>
+                <Input
+                  id="edit-endDate"
+                  type="date"
+                  value={formData.endDate}
+                  onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-isActive" className="text-right">
+                  Activo
+                </Label>
+                <Switch
+                  id="edit-isActive"
+                  checked={formData.isActive}
+                  onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
+                />
+              </div>
+            </div>
+            <DialogFooter>
+              <Button type="submit" onClick={handleUpdatePeriod}>
+                Actualizar Periodo
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

+ 476 - 0
src/app/admin/sections/page.tsx

@@ -0,0 +1,476 @@
+'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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { toast } from 'sonner';
+import { Plus, Search, Edit, Trash2, Users, Clock, Calendar } from 'lucide-react';
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  period: {
+    id: string;
+    name: string;
+    isActive: boolean;
+  };
+}
+
+interface Section {
+  id: string;
+  name: string;
+  schedule: string;
+  capacity: number;
+  class: Class;
+  _count: {
+    teacherAssignments: number;
+    studentEnrollments: number;
+  };
+}
+
+interface CreateSectionData {
+  name: string;
+  schedule: string;
+  capacity: number;
+  classId: string;
+}
+
+export default function SectionsPage() {
+  const [sections, setSections] = useState<Section[]>([]);
+  const [classes, setClasses] = useState<Class[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [editingSection, setEditingSection] = useState<Section | null>(null);
+  const [formData, setFormData] = useState<CreateSectionData>({
+    name: '',
+    schedule: '',
+    capacity: 30,
+    classId: '',
+  });
+
+  useEffect(() => {
+    fetchSections();
+    fetchClasses();
+  }, []);
+
+  const fetchSections = async () => {
+    try {
+      const response = await fetch('/api/admin/sections');
+      if (response.ok) {
+        const data = await response.json();
+        setSections(data);
+      } else {
+        toast.error('Error al cargar las secciones');
+      }
+    } catch (error) {
+      toast.error('Error al cargar las secciones');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes');
+      if (response.ok) {
+        const data = await response.json();
+        setClasses(data.filter((cls: Class) => cls.period.isActive));
+      }
+    } catch (error) {
+      console.error('Error fetching classes:', error);
+    }
+  };
+
+  const handleCreateSection = async () => {
+    try {
+      const response = await fetch('/api/admin/sections', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Sección creada exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchSections();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al crear la sección');
+      }
+    } catch (error) {
+      toast.error('Error al crear la sección');
+    }
+  };
+
+  const handleEditSection = async () => {
+    if (!editingSection) return;
+
+    try {
+      const response = await fetch(`/api/admin/sections/${editingSection.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Sección actualizada exitosamente');
+        setIsEditDialogOpen(false);
+        setEditingSection(null);
+        resetForm();
+        fetchSections();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al actualizar la sección');
+      }
+    } catch (error) {
+      toast.error('Error al actualizar la sección');
+    }
+  };
+
+  const handleDeleteSection = async (section: Section) => {
+    if (!confirm(`¿Estás seguro de que deseas eliminar la sección "${section.name}"?`)) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/sections/${section.id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        toast.success('Sección eliminada exitosamente');
+        fetchSections();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al eliminar la sección');
+      }
+    } catch (error) {
+      toast.error('Error al eliminar la sección');
+    }
+  };
+
+  const openEditDialog = (section: Section) => {
+    setEditingSection(section);
+    setFormData({
+      name: section.name,
+      schedule: section.schedule,
+      capacity: section.capacity,
+      classId: section.class.id,
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      schedule: '',
+      capacity: 30,
+      classId: '',
+    });
+  };
+
+  const filteredSections = sections.filter(section =>
+    section.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    section.class.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    section.class.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    section.schedule.toLowerCase().includes(searchTerm.toLowerCase())
+  );
+
+  if (loading) {
+    return (
+      <MainLayout requiredRole="ADMIN" title="Gestión de Secciones">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando secciones...</div>
+        </div>
+      </MainLayout>
+    );
+  }
+
+  return (
+    <MainLayout requiredRole="ADMIN" title="Gestión de Secciones">
+      <div className="space-y-6">
+        <div className="flex justify-between items-center">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Gestión de Secciones</h1>
+            <p className="text-muted-foreground">
+              Administra las secciones de las clases
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={resetForm}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nueva Sección
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nueva Sección</DialogTitle>
+                <DialogDescription>
+                  Completa la información para crear una nueva sección.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="class" className="text-right">
+                    Clase
+                  </Label>
+                  <Select
+                    value={formData.classId}
+                    onValueChange={(value) => setFormData({ ...formData, classId: value })}
+                  >
+                    <SelectTrigger className="col-span-3">
+                      <SelectValue placeholder="Seleccionar clase" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {classes.map((cls) => (
+                        <SelectItem key={cls.id} value={cls.id}>
+                          {cls.code} - {cls.name}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="name" className="text-right">
+                    Nombre
+                  </Label>
+                  <Input
+                    id="name"
+                    value={formData.name}
+                    onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                    className="col-span-3"
+                    placeholder="Ej: Sección A"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="schedule" className="text-right">
+                    Horario
+                  </Label>
+                  <Input
+                    id="schedule"
+                    value={formData.schedule}
+                    onChange={(e) => setFormData({ ...formData, schedule: e.target.value })}
+                    className="col-span-3"
+                    placeholder="Ej: Lun-Mie-Vie 8:00-10:00"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="capacity" className="text-right">
+                    Capacidad
+                  </Label>
+                  <Input
+                    id="capacity"
+                    type="number"
+                    min="1"
+                    max="100"
+                    value={formData.capacity}
+                    onChange={(e) => setFormData({ ...formData, capacity: parseInt(e.target.value) || 30 })}
+                    className="col-span-3"
+                  />
+                </div>
+              </div>
+              <DialogFooter>
+                <Button type="submit" onClick={handleCreateSection}>
+                  Crear Sección
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle>Secciones Registradas</CardTitle>
+            <CardDescription>
+              Lista de todas las secciones en el sistema
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar por nombre, clase o horario..."
+                value={searchTerm}
+                onChange={(e) => setSearchTerm(e.target.value)}
+                className="max-w-sm"
+              />
+            </div>
+          </CardHeader>
+          <CardContent>
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>Sección</TableHead>
+                  <TableHead>Clase</TableHead>
+                  <TableHead>Periodo</TableHead>
+                  <TableHead>Horario</TableHead>
+                  <TableHead>Capacidad</TableHead>
+                  <TableHead>Profesores</TableHead>
+                  <TableHead>Estudiantes</TableHead>
+                  <TableHead>Acciones</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {filteredSections.map((section) => (
+                  <TableRow key={section.id}>
+                    <TableCell className="font-medium">{section.name}</TableCell>
+                    <TableCell>
+                      <div>
+                        <div className="font-medium">{section.class.code}</div>
+                        <div className="text-sm text-muted-foreground">
+                          {section.class.name}
+                        </div>
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <Badge variant={section.class.period.isActive ? "default" : "secondary"}>
+                        {section.class.period.name}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center">
+                        <Clock className="mr-1 h-4 w-4" />
+                        {section.schedule}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center">
+                        <Users className="mr-1 h-4 w-4" />
+                        {section._count.studentEnrollments}/{section.capacity}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center">
+                        <Users className="mr-1 h-4 w-4" />
+                        {section._count.teacherAssignments}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center">
+                        <Users className="mr-1 h-4 w-4" />
+                        {section._count.studentEnrollments}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex items-center space-x-2">
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => openEditDialog(section)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleDeleteSection(section)}
+                          className="text-red-600 hover:text-red-700"
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+            {filteredSections.length === 0 && (
+              <div className="text-center py-8 text-muted-foreground">
+                No se encontraron secciones
+              </div>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Dialog de Edición */}
+        <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+          <DialogContent className="sm:max-w-[425px]">
+            <DialogHeader>
+              <DialogTitle>Editar Sección</DialogTitle>
+              <DialogDescription>
+                Modifica la información de la sección.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-class" className="text-right">
+                  Clase
+                </Label>
+                <Select
+                  value={formData.classId}
+                  onValueChange={(value) => setFormData({ ...formData, classId: value })}
+                >
+                  <SelectTrigger className="col-span-3">
+                    <SelectValue placeholder="Seleccionar clase" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {classes.map((cls) => (
+                      <SelectItem key={cls.id} value={cls.id}>
+                        {cls.code} - {cls.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-name" className="text-right">
+                  Nombre
+                </Label>
+                <Input
+                  id="edit-name"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-schedule" className="text-right">
+                  Horario
+                </Label>
+                <Input
+                  id="edit-schedule"
+                  value={formData.schedule}
+                  onChange={(e) => setFormData({ ...formData, schedule: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-capacity" className="text-right">
+                  Capacidad
+                </Label>
+                <Input
+                  id="edit-capacity"
+                  type="number"
+                  min="1"
+                  max="100"
+                  value={formData.capacity}
+                  onChange={(e) => setFormData({ ...formData, capacity: parseInt(e.target.value) || 30 })}
+                  className="col-span-3"
+                />
+              </div>
+            </div>
+            <DialogFooter>
+              <Button type="submit" onClick={handleEditSection}>
+                Actualizar Sección
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

+ 600 - 0
src/app/admin/users/page.tsx

@@ -0,0 +1,600 @@
+'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 {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table';
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select';
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Plus, Pencil, Trash2, Search } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface User {
+  id: string;
+  email: string;
+  role: 'ADMIN' | 'TEACHER' | 'STUDENT';
+  createdAt: string;
+  updatedAt: string;
+  teacher?: {
+    firstName: string;
+    lastName: string;
+    cedula: string;
+    phone: string;
+  };
+  student?: {
+    firstName: string;
+    lastName: string;
+    cedula: string;
+    phone: string;
+    admissionNumber: string;
+  };
+}
+
+interface UserFormData {
+  email: string;
+  password: string;
+  role: 'ADMIN' | 'TEACHER' | 'STUDENT';
+  firstName?: string;
+  lastName?: string;
+  cedula?: string;
+  phone?: string;
+  admissionNumber?: string;
+}
+
+export default function UsersPage() {
+  const [users, setUsers] = useState<User[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [selectedUser, setSelectedUser] = useState<User | null>(null);
+  const [formData, setFormData] = useState<UserFormData>({
+    email: '',
+    password: '',
+    role: 'STUDENT',
+  });
+
+  useEffect(() => {
+    fetchUsers();
+  }, []);
+
+  const fetchUsers = async () => {
+    try {
+      const response = await fetch('/api/admin/users');
+      if (response.ok) {
+        const data = await response.json();
+        setUsers(data);
+      } else {
+        toast.error('Error al cargar usuarios');
+      }
+    } catch (error) {
+      toast.error('Error al cargar usuarios');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleCreateUser = async () => {
+    try {
+      const response = await fetch('/api/admin/users', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Usuario creado exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchUsers();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al crear usuario');
+      }
+    } catch (error) {
+      toast.error('Error al crear usuario');
+    }
+  };
+
+  const handleUpdateUser = async () => {
+    if (!selectedUser) return;
+
+    try {
+      const response = await fetch(`/api/admin/users/${selectedUser.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        toast.success('Usuario actualizado exitosamente');
+        setIsEditDialogOpen(false);
+        resetForm();
+        fetchUsers();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al actualizar usuario');
+      }
+    } catch (error) {
+      toast.error('Error al actualizar usuario');
+    }
+  };
+
+  const handleDeleteUser = async (userId: string) => {
+    try {
+      const response = await fetch(`/api/admin/users/${userId}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        toast.success('Usuario eliminado exitosamente');
+        fetchUsers();
+      } else {
+        const error = await response.json();
+        toast.error(error.message || 'Error al eliminar usuario');
+      }
+    } catch (error) {
+      toast.error('Error al eliminar usuario');
+    }
+  };
+
+  const resetForm = () => {
+    setFormData({
+      email: '',
+      password: '',
+      role: 'STUDENT',
+    });
+    setSelectedUser(null);
+  };
+
+  const openEditDialog = (user: User) => {
+    setSelectedUser(user);
+    setFormData({
+      email: user.email,
+      password: '',
+      role: user.role,
+      firstName: user.teacher?.firstName || user.student?.firstName || '',
+      lastName: user.teacher?.lastName || user.student?.lastName || '',
+      cedula: user.teacher?.cedula || user.student?.cedula || '',
+      phone: user.teacher?.phone || user.student?.phone || '',
+      admissionNumber: user.student?.admissionNumber || '',
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const filteredUsers = users.filter(user =>
+    user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
+    (user.teacher && `${user.teacher.firstName} ${user.teacher.lastName}`.toLowerCase().includes(searchTerm.toLowerCase())) ||
+    (user.student && `${user.student.firstName} ${user.student.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()))
+  );
+
+  const getRoleBadgeVariant = (role: string) => {
+    switch (role) {
+      case 'ADMIN':
+        return 'destructive';
+      case 'TEACHER':
+        return 'default';
+      case 'STUDENT':
+        return 'secondary';
+      default:
+        return 'outline';
+    }
+  };
+
+  const getUserDisplayName = (user: User) => {
+    if (user.teacher) {
+      return `${user.teacher.firstName} ${user.teacher.lastName}`;
+    }
+    if (user.student) {
+      return `${user.student.firstName} ${user.student.lastName}`;
+    }
+    return user.email;
+  };
+
+  return (
+    <MainLayout requiredRole="ADMIN" title="Gestión de Usuarios">
+      <div className="space-y-6">
+        <div className="flex items-center justify-between">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Gestión de Usuarios</h1>
+            <p className="text-muted-foreground">
+              Administra usuarios del sistema
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={resetForm}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nuevo Usuario
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nuevo Usuario</DialogTitle>
+                <DialogDescription>
+                  Completa los datos para crear un nuevo usuario en el sistema.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="email" className="text-right">
+                    Email
+                  </Label>
+                  <Input
+                    id="email"
+                    type="email"
+                    value={formData.email}
+                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                    className="col-span-3"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="password" className="text-right">
+                    Contraseña
+                  </Label>
+                  <Input
+                    id="password"
+                    type="password"
+                    value={formData.password}
+                    onChange={(e) => setFormData({ ...formData, password: e.target.value })}
+                    className="col-span-3"
+                  />
+                </div>
+                <div className="grid grid-cols-4 items-center gap-4">
+                  <Label htmlFor="role" className="text-right">
+                    Rol
+                  </Label>
+                  <Select
+                    value={formData.role}
+                    onValueChange={(value: 'ADMIN' | 'TEACHER' | 'STUDENT') =>
+                      setFormData({ ...formData, role: value })
+                    }
+                  >
+                    <SelectTrigger className="col-span-3">
+                      <SelectValue />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="ADMIN">Administrador</SelectItem>
+                      <SelectItem value="TEACHER">Profesor</SelectItem>
+                      <SelectItem value="STUDENT">Estudiante</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+                {(formData.role === 'TEACHER' || formData.role === 'STUDENT') && (
+                  <>
+                    <div className="grid grid-cols-4 items-center gap-4">
+                      <Label htmlFor="firstName" className="text-right">
+                        Nombres
+                      </Label>
+                      <Input
+                        id="firstName"
+                        value={formData.firstName || ''}
+                        onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
+                        className="col-span-3"
+                      />
+                    </div>
+                    <div className="grid grid-cols-4 items-center gap-4">
+                      <Label htmlFor="lastName" className="text-right">
+                        Apellidos
+                      </Label>
+                      <Input
+                        id="lastName"
+                        value={formData.lastName || ''}
+                        onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
+                        className="col-span-3"
+                      />
+                    </div>
+                    <div className="grid grid-cols-4 items-center gap-4">
+                      <Label htmlFor="cedula" className="text-right">
+                        Cédula
+                      </Label>
+                      <Input
+                        id="cedula"
+                        value={formData.cedula || ''}
+                        onChange={(e) => setFormData({ ...formData, cedula: e.target.value })}
+                        className="col-span-3"
+                      />
+                    </div>
+                    <div className="grid grid-cols-4 items-center gap-4">
+                      <Label htmlFor="phone" className="text-right">
+                        Teléfono
+                      </Label>
+                      <Input
+                        id="phone"
+                        value={formData.phone || ''}
+                        onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                        className="col-span-3"
+                      />
+                    </div>
+                    {formData.role === 'STUDENT' && (
+                      <div className="grid grid-cols-4 items-center gap-4">
+                        <Label htmlFor="admissionNumber" className="text-right">
+                          Matrícula
+                        </Label>
+                        <Input
+                          id="admissionNumber"
+                          value={formData.admissionNumber || ''}
+                          onChange={(e) => setFormData({ ...formData, admissionNumber: e.target.value })}
+                          className="col-span-3"
+                        />
+                      </div>
+                    )}
+                  </>
+                )}
+              </div>
+              <DialogFooter>
+                <Button type="submit" onClick={handleCreateUser}>
+                  Crear Usuario
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle>Usuarios del Sistema</CardTitle>
+            <CardDescription>
+              Lista de todos los usuarios registrados en el sistema
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar usuarios..."
+                value={searchTerm}
+                onChange={(e) => setSearchTerm(e.target.value)}
+                className="max-w-sm"
+              />
+            </div>
+          </CardHeader>
+          <CardContent>
+            {loading ? (
+              <div className="flex items-center justify-center py-8">
+                <div className="text-muted-foreground">Cargando usuarios...</div>
+              </div>
+            ) : (
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Nombre</TableHead>
+                    <TableHead>Email</TableHead>
+                    <TableHead>Rol</TableHead>
+                    <TableHead>Cédula</TableHead>
+                    <TableHead>Teléfono</TableHead>
+                    <TableHead>Fecha Creación</TableHead>
+                    <TableHead className="text-right">Acciones</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {filteredUsers.map((user) => (
+                    <TableRow key={user.id}>
+                      <TableCell className="font-medium">
+                        {getUserDisplayName(user)}
+                      </TableCell>
+                      <TableCell>{user.email}</TableCell>
+                      <TableCell>
+                        <Badge variant={getRoleBadgeVariant(user.role)}>
+                          {user.role === 'ADMIN' ? 'Administrador' :
+                           user.role === 'TEACHER' ? 'Profesor' : 'Estudiante'}
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        {user.teacher?.cedula || user.student?.cedula || '-'}
+                      </TableCell>
+                      <TableCell>
+                        {user.teacher?.phone || user.student?.phone || '-'}
+                      </TableCell>
+                      <TableCell>
+                        {new Date(user.createdAt).toLocaleDateString()}
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <div className="flex items-center justify-end space-x-2">
+                          <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => openEditDialog(user)}
+                          >
+                            <Pencil className="h-4 w-4" />
+                          </Button>
+                          <AlertDialog>
+                            <AlertDialogTrigger asChild>
+                              <Button variant="outline" size="sm">
+                                <Trash2 className="h-4 w-4" />
+                              </Button>
+                            </AlertDialogTrigger>
+                            <AlertDialogContent>
+                              <AlertDialogHeader>
+                                <AlertDialogTitle>¿Estás seguro?</AlertDialogTitle>
+                                <AlertDialogDescription>
+                                  Esta acción no se puede deshacer. Esto eliminará permanentemente
+                                  el usuario y todos sus datos asociados.
+                                </AlertDialogDescription>
+                              </AlertDialogHeader>
+                              <AlertDialogFooter>
+                                <AlertDialogCancel>Cancelar</AlertDialogCancel>
+                                <AlertDialogAction
+                                  onClick={() => handleDeleteUser(user.id)}
+                                >
+                                  Eliminar
+                                </AlertDialogAction>
+                              </AlertDialogFooter>
+                            </AlertDialogContent>
+                          </AlertDialog>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))}
+                </TableBody>
+              </Table>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Edit Dialog */}
+        <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+          <DialogContent className="sm:max-w-[425px]">
+            <DialogHeader>
+              <DialogTitle>Editar Usuario</DialogTitle>
+              <DialogDescription>
+                Modifica los datos del usuario seleccionado.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-email" className="text-right">
+                  Email
+                </Label>
+                <Input
+                  id="edit-email"
+                  type="email"
+                  value={formData.email}
+                  onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                  className="col-span-3"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-password" className="text-right">
+                  Nueva Contraseña
+                </Label>
+                <Input
+                  id="edit-password"
+                  type="password"
+                  value={formData.password}
+                  onChange={(e) => setFormData({ ...formData, password: e.target.value })}
+                  className="col-span-3"
+                  placeholder="Dejar vacío para mantener actual"
+                />
+              </div>
+              <div className="grid grid-cols-4 items-center gap-4">
+                <Label htmlFor="edit-role" className="text-right">
+                  Rol
+                </Label>
+                <Select
+                  value={formData.role}
+                  onValueChange={(value: 'ADMIN' | 'TEACHER' | 'STUDENT') =>
+                    setFormData({ ...formData, role: value })
+                  }
+                >
+                  <SelectTrigger className="col-span-3">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="ADMIN">Administrador</SelectItem>
+                    <SelectItem value="TEACHER">Profesor</SelectItem>
+                    <SelectItem value="STUDENT">Estudiante</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+              {(formData.role === 'TEACHER' || formData.role === 'STUDENT') && (
+                <>
+                  <div className="grid grid-cols-4 items-center gap-4">
+                    <Label htmlFor="edit-firstName" className="text-right">
+                      Nombres
+                    </Label>
+                    <Input
+                      id="edit-firstName"
+                      value={formData.firstName || ''}
+                      onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
+                      className="col-span-3"
+                    />
+                  </div>
+                  <div className="grid grid-cols-4 items-center gap-4">
+                    <Label htmlFor="edit-lastName" className="text-right">
+                      Apellidos
+                    </Label>
+                    <Input
+                      id="edit-lastName"
+                      value={formData.lastName || ''}
+                      onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
+                      className="col-span-3"
+                    />
+                  </div>
+                  <div className="grid grid-cols-4 items-center gap-4">
+                    <Label htmlFor="edit-cedula" className="text-right">
+                      Cédula
+                    </Label>
+                    <Input
+                      id="edit-cedula"
+                      value={formData.cedula || ''}
+                      onChange={(e) => setFormData({ ...formData, cedula: e.target.value })}
+                      className="col-span-3"
+                    />
+                  </div>
+                  <div className="grid grid-cols-4 items-center gap-4">
+                    <Label htmlFor="edit-phone" className="text-right">
+                      Teléfono
+                    </Label>
+                    <Input
+                      id="edit-phone"
+                      value={formData.phone || ''}
+                      onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                      className="col-span-3"
+                    />
+                  </div>
+                  {formData.role === 'STUDENT' && (
+                    <div className="grid grid-cols-4 items-center gap-4">
+                      <Label htmlFor="edit-admissionNumber" className="text-right">
+                        Matrícula
+                      </Label>
+                      <Input
+                        id="edit-admissionNumber"
+                        value={formData.admissionNumber || ''}
+                        onChange={(e) => setFormData({ ...formData, admissionNumber: e.target.value })}
+                        className="col-span-3"
+                      />
+                    </div>
+                  )}
+                </>
+              )}
+            </div>
+            <DialogFooter>
+              <Button type="submit" onClick={handleUpdateUser}>
+                Actualizar Usuario
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

+ 173 - 0
src/app/api/admin/classes/[id]/route.ts

@@ -0,0 +1,173 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+interface RouteParams {
+  params: {
+    id: string;
+  };
+}
+
+// PUT - Actualizar clase
+export async function PUT(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+    const body = await request.json();
+    const { name, code, description, periodId } = body;
+
+    // Verificar si la clase existe
+    const existingClass = await prisma.class.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+    });
+
+    if (!existingClass) {
+      return NextResponse.json(
+        { message: 'Clase no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Validaciones básicas
+    if (!name || !code || !periodId) {
+      return NextResponse.json(
+        { message: 'Nombre, código y periodo son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el periodo existe y está activo
+    const period = await prisma.period.findFirst({
+      where: {
+        id: periodId,
+        deletedAt: null,
+        isActive: true,
+      },
+    });
+
+    if (!period) {
+      return NextResponse.json(
+        { message: 'El periodo seleccionado no existe o no está activo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otra clase con el mismo código en el mismo periodo
+    if (code !== existingClass.code || periodId !== existingClass.periodId) {
+      const duplicateClass = await prisma.class.findFirst({
+        where: {
+          code,
+          periodId,
+          deletedAt: null,
+          id: { not: id },
+        },
+      });
+
+      if (duplicateClass) {
+        return NextResponse.json(
+          { message: 'Ya existe otra clase con este código en el periodo seleccionado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar clase
+    await prisma.class.update({
+      where: { id },
+      data: {
+        name,
+        code,
+        description: description || null,
+        periodId,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Clase actualizada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error updating class:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar clase (soft delete)
+export async function DELETE(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+
+    // Verificar si la clase existe
+    const existingClass = await prisma.class.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+      include: {
+        _count: {
+          select: {
+            sections: true,
+          },
+        },
+      },
+    });
+
+    if (!existingClass) {
+      return NextResponse.json(
+        { message: 'Clase no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si la clase tiene secciones activas
+    if (existingClass._count.sections > 0) {
+      return NextResponse.json(
+        { message: 'No se puede eliminar una clase que tiene secciones activas' },
+        { status: 400 }
+      );
+    }
+
+    // Realizar soft delete
+    await prisma.class.update({
+      where: { id },
+      data: {
+        deletedAt: new Date(),
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Clase eliminada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting class:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 129 - 0
src/app/api/admin/classes/route.ts

@@ -0,0 +1,129 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+// GET - Obtener todas las clases
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const classes = await prisma.class.findMany({
+      where: {
+        deletedAt: null,
+      },
+      include: {
+        period: {
+          select: {
+            id: true,
+            name: true,
+            startDate: true,
+            endDate: true,
+            isActive: true,
+          },
+        },
+        _count: {
+          select: {
+            sections: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: 'desc',
+      },
+    });
+
+    return NextResponse.json(classes);
+  } catch (error) {
+    console.error('Error fetching classes:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nueva clase
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const body = await request.json();
+    const { name, code, description, periodId } = body;
+
+    // Validaciones básicas
+    if (!name || !code || !periodId) {
+      return NextResponse.json(
+        { message: 'Nombre, código y periodo son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el periodo existe y está activo
+    const period = await prisma.period.findFirst({
+      where: {
+        id: periodId,
+        deletedAt: null,
+        isActive: true,
+      },
+    });
+
+    if (!period) {
+      return NextResponse.json(
+        { message: 'El periodo seleccionado no existe o no está activo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una clase con el mismo código en el mismo periodo
+    const existingClass = await prisma.class.findFirst({
+      where: {
+        code,
+        periodId,
+        deletedAt: null,
+      },
+    });
+
+    if (existingClass) {
+      return NextResponse.json(
+        { message: 'Ya existe una clase con este código en el periodo seleccionado' },
+        { status: 400 }
+      );
+    }
+
+    // Crear clase
+    const newClass = await prisma.class.create({
+      data: {
+        name,
+        code,
+        description: description || null,
+        periodId,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Clase creada exitosamente', classId: newClass.id },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating class:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 205 - 0
src/app/api/admin/periods/[id]/route.ts

@@ -0,0 +1,205 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+interface RouteParams {
+  params: {
+    id: string;
+  };
+}
+
+// PUT - Actualizar periodo
+export async function PUT(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+    const body = await request.json();
+    const { name, startDate, endDate, isActive } = body;
+
+    // Verificar si el periodo existe
+    const existingPeriod = await prisma.period.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+    });
+
+    if (!existingPeriod) {
+      return NextResponse.json(
+        { message: 'Periodo no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Validaciones básicas
+    if (!name || !startDate || !endDate) {
+      return NextResponse.json(
+        { message: 'Nombre, fecha de inicio y fecha de fin son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Validar que la fecha de inicio sea anterior a la fecha de fin
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+    
+    if (start >= end) {
+      return NextResponse.json(
+        { message: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otro periodo con el mismo nombre
+    if (name !== existingPeriod.name) {
+      const duplicatePeriod = await prisma.period.findFirst({
+        where: {
+          name,
+          deletedAt: null,
+          id: { not: id },
+        },
+      });
+
+      if (duplicatePeriod) {
+        return NextResponse.json(
+          { message: 'Ya existe otro periodo con este nombre' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar solapamiento de fechas con otros periodos activos (excluyendo el actual)
+    const overlappingPeriod = await prisma.period.findFirst({
+      where: {
+        deletedAt: null,
+        isActive: true,
+        id: { not: id },
+        OR: [
+          {
+            AND: [
+              { startDate: { lte: start } },
+              { endDate: { gte: start } },
+            ],
+          },
+          {
+            AND: [
+              { startDate: { lte: end } },
+              { endDate: { gte: end } },
+            ],
+          },
+          {
+            AND: [
+              { startDate: { gte: start } },
+              { endDate: { lte: end } },
+            ],
+          },
+        ],
+      },
+    });
+
+    if (overlappingPeriod && isActive) {
+      return NextResponse.json(
+        { message: `Las fechas se solapan con el periodo activo "${overlappingPeriod.name}"` },
+        { status: 400 }
+      );
+    }
+
+    // Actualizar periodo
+    await prisma.period.update({
+      where: { id },
+      data: {
+        name,
+        startDate: start,
+        endDate: end,
+        isActive: isActive ?? existingPeriod.isActive,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Periodo actualizado exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error updating period:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar periodo (soft delete)
+export async function DELETE(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+
+    // Verificar si el periodo existe
+    const existingPeriod = await prisma.period.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+      include: {
+        _count: {
+          select: {
+            classes: true,
+            partials: true,
+          },
+        },
+      },
+    });
+
+    if (!existingPeriod) {
+      return NextResponse.json(
+        { message: 'Periodo no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el periodo tiene clases o parciales asociados
+    if (existingPeriod._count.classes > 0 || existingPeriod._count.partials > 0) {
+      return NextResponse.json(
+        { message: 'No se puede eliminar un periodo que tiene clases o parciales asociados' },
+        { status: 400 }
+      );
+    }
+
+    // Realizar soft delete
+    await prisma.period.update({
+      where: { id },
+      data: {
+        deletedAt: new Date(),
+        isActive: false,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Periodo eliminado exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting period:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 107 - 0
src/app/api/admin/periods/[id]/toggle/route.ts

@@ -0,0 +1,107 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+interface RouteParams {
+  params: {
+    id: string;
+  };
+}
+
+// PATCH - Toggle estado activo/inactivo del periodo
+export async function PATCH(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+    const body = await request.json();
+    const { isActive } = body;
+
+    if (typeof isActive !== 'boolean') {
+      return NextResponse.json(
+        { message: 'El estado activo debe ser un valor booleano' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el periodo existe
+    const existingPeriod = await prisma.period.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+    });
+
+    if (!existingPeriod) {
+      return NextResponse.json(
+        { message: 'Periodo no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Si se está activando el periodo, verificar solapamiento con otros periodos activos
+    if (isActive) {
+      const overlappingPeriod = await prisma.period.findFirst({
+        where: {
+          deletedAt: null,
+          isActive: true,
+          id: { not: id },
+          OR: [
+            {
+              AND: [
+                { startDate: { lte: existingPeriod.startDate } },
+                { endDate: { gte: existingPeriod.startDate } },
+              ],
+            },
+            {
+              AND: [
+                { startDate: { lte: existingPeriod.endDate } },
+                { endDate: { gte: existingPeriod.endDate } },
+              ],
+            },
+            {
+              AND: [
+                { startDate: { gte: existingPeriod.startDate } },
+                { endDate: { lte: existingPeriod.endDate } },
+              ],
+            },
+          ],
+        },
+      });
+
+      if (overlappingPeriod) {
+        return NextResponse.json(
+          { message: `No se puede activar el periodo porque se solapa con el periodo activo "${overlappingPeriod.name}"` },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar estado del periodo
+    await prisma.period.update({
+      where: { id },
+      data: {
+        isActive,
+      },
+    });
+
+    return NextResponse.json(
+      { message: `Periodo ${isActive ? 'activado' : 'desactivado'} exitosamente` },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error toggling period status:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

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

@@ -0,0 +1,150 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+// GET - Obtener todos los periodos
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const periods = await prisma.period.findMany({
+      where: {
+        deletedAt: null,
+      },
+      include: {
+        _count: {
+          select: {
+            classes: true,
+            partials: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: 'desc',
+      },
+    });
+
+    return NextResponse.json(periods);
+  } catch (error) {
+    console.error('Error fetching periods:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nuevo periodo
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const body = await request.json();
+    const { name, startDate, endDate, isActive } = body;
+
+    // Validaciones básicas
+    if (!name || !startDate || !endDate) {
+      return NextResponse.json(
+        { message: 'Nombre, fecha de inicio y fecha de fin son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Validar que la fecha de inicio sea anterior a la fecha de fin
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+    
+    if (start >= end) {
+      return NextResponse.json(
+        { message: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe un periodo con el mismo nombre
+    const existingPeriod = await prisma.period.findFirst({
+      where: {
+        name,
+        deletedAt: null,
+      },
+    });
+
+    if (existingPeriod) {
+      return NextResponse.json(
+        { message: 'Ya existe un periodo con este nombre' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar solapamiento de fechas con otros periodos activos
+    const overlappingPeriod = await prisma.period.findFirst({
+      where: {
+        deletedAt: null,
+        isActive: true,
+        OR: [
+          {
+            AND: [
+              { startDate: { lte: start } },
+              { endDate: { gte: start } },
+            ],
+          },
+          {
+            AND: [
+              { startDate: { lte: end } },
+              { endDate: { gte: end } },
+            ],
+          },
+          {
+            AND: [
+              { startDate: { gte: start } },
+              { endDate: { lte: end } },
+            ],
+          },
+        ],
+      },
+    });
+
+    if (overlappingPeriod && isActive) {
+      return NextResponse.json(
+        { message: `Las fechas se solapan con el periodo activo "${overlappingPeriod.name}"` },
+        { status: 400 }
+      );
+    }
+
+    // Crear periodo
+    const period = await prisma.period.create({
+      data: {
+        name,
+        startDate: start,
+        endDate: end,
+        isActive: isActive ?? true,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Periodo creado exitosamente', periodId: period.id },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating period:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 185 - 0
src/app/api/admin/sections/[id]/route.ts

@@ -0,0 +1,185 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+interface RouteParams {
+  params: {
+    id: string;
+  };
+}
+
+// PUT - Actualizar sección
+export async function PUT(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+    const body = await request.json();
+    const { name, classId } = body;
+
+    // Verificar si la sección existe
+    const existingSection = await prisma.section.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+    });
+
+    if (!existingSection) {
+      return NextResponse.json(
+        { message: 'Sección no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Validaciones básicas
+    if (!name || !classId) {
+      return NextResponse.json(
+        { message: 'Nombre y clase son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la clase existe
+    const classExists = await prisma.class.findFirst({
+      where: {
+        id: classId,
+        deletedAt: null,
+      },
+      include: {
+        period: {
+          select: {
+            isActive: true,
+          },
+        },
+      },
+    });
+
+    if (!classExists) {
+      return NextResponse.json(
+        { message: 'La clase seleccionada no existe' },
+        { status: 400 }
+      );
+    }
+
+    if (!classExists.period.isActive) {
+      return NextResponse.json(
+        { message: 'No se puede actualizar una sección para una clase de un periodo inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otra sección con el mismo nombre para la misma clase
+    if (name !== existingSection.name || classId !== existingSection.classId) {
+      const duplicateSection = await prisma.section.findFirst({
+        where: {
+          name,
+          classId,
+          deletedAt: null,
+          id: { not: id },
+        },
+      });
+
+      if (duplicateSection) {
+        return NextResponse.json(
+          { message: 'Ya existe otra sección con este nombre para la clase seleccionada' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar sección
+    await prisma.section.update({
+      where: { id },
+      data: {
+        name,
+        classId,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Sección actualizada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error updating section:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar sección (soft delete)
+export async function DELETE(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+
+    // Verificar si la sección existe
+    const existingSection = await prisma.section.findFirst({
+      where: {
+        id,
+        deletedAt: null,
+      },
+      include: {
+        _count: {
+          select: {
+            teacherAssignments: true,
+            studentEnrollments: true,
+          },
+        },
+      },
+    });
+
+    if (!existingSection) {
+      return NextResponse.json(
+        { message: 'Sección no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si la sección tiene asignaciones de profesores o inscripciones de estudiantes
+    if (existingSection._count.teacherAssignments > 0 || existingSection._count.studentEnrollments > 0) {
+      return NextResponse.json(
+        { message: 'No se puede eliminar una sección que tiene profesores asignados o estudiantes inscritos' },
+        { status: 400 }
+      );
+    }
+
+    // Realizar soft delete
+    await prisma.section.update({
+      where: { id },
+      data: {
+        deletedAt: new Date(),
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Sección eliminada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting section:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 146 - 0
src/app/api/admin/sections/route.ts

@@ -0,0 +1,146 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+// GET - Obtener todas las secciones
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const sections = await prisma.section.findMany({
+      where: {
+        deletedAt: null,
+      },
+      include: {
+        class: {
+          select: {
+            id: true,
+            name: true,
+            code: true,
+            period: {
+              select: {
+                id: true,
+                name: true,
+                isActive: true,
+              },
+            },
+          },
+        },
+        _count: {
+          select: {
+            teacherAssignments: true,
+            studentEnrollments: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: 'desc',
+      },
+    });
+
+    return NextResponse.json(sections);
+  } catch (error) {
+    console.error('Error fetching sections:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nueva sección
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const body = await request.json();
+    const { name, classId } = body;
+
+    // Validaciones básicas
+    if (!name || !classId) {
+      return NextResponse.json(
+        { message: 'Nombre y clase son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la clase existe
+    const classExists = await prisma.class.findFirst({
+      where: {
+        id: classId,
+        deletedAt: null,
+      },
+      include: {
+        period: {
+          select: {
+            isActive: true,
+          },
+        },
+      },
+    });
+
+    if (!classExists) {
+      return NextResponse.json(
+        { message: 'La clase seleccionada no existe' },
+        { status: 400 }
+      );
+    }
+
+    if (!classExists.period.isActive) {
+      return NextResponse.json(
+        { message: 'No se puede crear una sección para una clase de un periodo inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una sección con el mismo nombre para la misma clase
+    const existingSection = await prisma.section.findFirst({
+      where: {
+        name,
+        classId,
+        deletedAt: null,
+      },
+    });
+
+    if (existingSection) {
+      return NextResponse.json(
+        { message: 'Ya existe una sección con este nombre para la clase seleccionada' },
+        { status: 400 }
+      );
+    }
+
+    // Crear sección
+    const newSection = await prisma.section.create({
+      data: {
+        name,
+        classId,
+      },
+    });
+
+    return NextResponse.json(
+      { message: 'Sección creada exitosamente', sectionId: newSection.id },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating section:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 323 - 0
src/app/api/admin/users/[id]/route.ts

@@ -0,0 +1,323 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { UserRole } from '@prisma/client';
+import bcrypt from 'bcryptjs';
+
+interface RouteParams {
+  params: {
+    id: string;
+  };
+}
+
+// PUT - Actualizar usuario
+export async function PUT(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+    const body = await request.json();
+    const {
+      email,
+      password,
+      role,
+      firstName,
+      lastName,
+      cedula,
+      phone,
+      admissionNumber,
+    } = body;
+
+    // Verificar si el usuario existe
+    const existingUser = await prisma.user.findUnique({
+      where: { id },
+      include: {
+        teacher: true,
+        student: true,
+      },
+    });
+
+    if (!existingUser) {
+      return NextResponse.json(
+        { message: 'Usuario no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Validaciones básicas
+    if (!email || !role) {
+      return NextResponse.json(
+        { message: 'Email y rol son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (role !== 'ADMIN' && (!firstName || !lastName || !cedula || !phone)) {
+      return NextResponse.json(
+        { message: 'Nombres, apellidos, cédula y teléfono son requeridos para profesores y estudiantes' },
+        { status: 400 }
+      );
+    }
+
+    if (role === 'STUDENT' && !admissionNumber) {
+      return NextResponse.json(
+        { message: 'Número de matrícula es requerido para estudiantes' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el email ya existe (excluyendo el usuario actual)
+    if (email !== existingUser.email) {
+      const emailExists = await prisma.user.findUnique({
+        where: { email },
+      });
+
+      if (emailExists) {
+        return NextResponse.json(
+          { message: 'El email ya está registrado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar si la cédula ya existe (para profesores y estudiantes, excluyendo el usuario actual)
+    if (role !== 'ADMIN' && cedula) {
+      const existingTeacher = await prisma.teacher.findUnique({
+        where: { cedula },
+      });
+      const existingStudent = await prisma.student.findUnique({
+        where: { cedula },
+      });
+
+      if ((existingTeacher && existingTeacher.userId !== id) || 
+          (existingStudent && existingStudent.userId !== id)) {
+        return NextResponse.json(
+          { message: 'La cédula ya está registrada' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar si el número de matrícula ya existe (para estudiantes, excluyendo el usuario actual)
+    if (role === 'STUDENT' && admissionNumber) {
+      const existingStudent = await prisma.student.findUnique({
+        where: { admissionNumber },
+      });
+
+      if (existingStudent && existingStudent.userId !== id) {
+        return NextResponse.json(
+          { message: 'El número de matrícula ya está registrado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Preparar datos de actualización del usuario
+    const userUpdateData: { email: string; role: UserRole; password?: string } = {
+      email,
+      role: role as UserRole,
+    };
+
+    // Solo actualizar contraseña si se proporciona una nueva
+    if (password && password.trim() !== '') {
+      userUpdateData.password = await bcrypt.hash(password, 12);
+    }
+
+    // Actualizar en una transacción
+    const result = await prisma.$transaction(async (tx) => {
+      // Actualizar usuario
+      const updatedUser = await tx.user.update({
+        where: { id },
+        data: userUpdateData,
+      });
+
+      // Manejar cambios de rol
+      const oldRole = existingUser.role;
+      const newRole = role;
+
+      if (oldRole !== newRole) {
+        // Eliminar perfil anterior si existe
+        if (oldRole === 'TEACHER' && existingUser.teacher) {
+          await tx.teacher.delete({
+            where: { userId: id },
+          });
+        } else if (oldRole === 'STUDENT' && existingUser.student) {
+          await tx.student.delete({
+            where: { userId: id },
+          });
+        }
+
+        // Crear nuevo perfil según el nuevo rol
+        if (newRole === 'TEACHER') {
+          await tx.teacher.create({
+            data: {
+              userId: id,
+              firstName,
+              lastName,
+              cedula,
+              email,
+              phone,
+            },
+          });
+        } else if (newRole === 'STUDENT') {
+          await tx.student.create({
+            data: {
+              userId: id,
+              firstName,
+              lastName,
+              cedula,
+              email,
+              phone,
+              admissionNumber,
+            },
+          });
+        }
+      } else {
+        // Actualizar perfil existente si el rol no cambió
+        if (newRole === 'TEACHER' && existingUser.teacher) {
+          await tx.teacher.update({
+            where: { userId: id },
+            data: {
+              firstName,
+              lastName,
+              cedula,
+              email,
+              phone,
+            },
+          });
+        } else if (newRole === 'STUDENT' && existingUser.student) {
+          await tx.student.update({
+            where: { userId: id },
+            data: {
+              firstName,
+              lastName,
+              cedula,
+              email,
+              phone,
+              admissionNumber,
+            },
+          });
+        }
+      }
+
+      return updatedUser;
+    });
+
+    return NextResponse.json(
+      { message: 'Usuario actualizado exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error updating user:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar usuario
+export async function DELETE(request: NextRequest, { params }: RouteParams) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = params;
+
+    // Verificar si el usuario existe
+    const existingUser = await prisma.user.findUnique({
+      where: { id },
+      include: {
+        teacher: {
+          include: {
+            assignments: true,
+          },
+        },
+        student: {
+          include: {
+            enrollments: true,
+            attendances: true,
+          },
+        },
+      },
+    });
+
+    if (!existingUser) {
+      return NextResponse.json(
+        { message: 'Usuario no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Prevenir eliminación del usuario actual
+    if (existingUser.id === session.user.id) {
+      return NextResponse.json(
+        { message: 'No puedes eliminar tu propio usuario' },
+        { status: 400 }
+      );
+    }
+
+    // Eliminar en una transacción
+    await prisma.$transaction(async (tx) => {
+      // Eliminar registros relacionados según el rol
+      if (existingUser.teacher) {
+        // Eliminar asignaciones de profesor
+        await tx.teacherAssignment.deleteMany({
+          where: { teacherId: existingUser.teacher.id },
+        });
+        
+        // Eliminar perfil de profesor
+        await tx.teacher.delete({
+          where: { userId: id },
+        });
+      }
+
+      if (existingUser.student) {
+        // Eliminar asistencias del estudiante
+        await tx.attendance.deleteMany({
+          where: { studentId: existingUser.student.id },
+        });
+        
+        // Eliminar inscripciones del estudiante
+        await tx.studentEnrollment.deleteMany({
+          where: { studentId: existingUser.student.id },
+        });
+        
+        // Eliminar perfil de estudiante
+        await tx.student.delete({
+          where: { userId: id },
+        });
+      }
+
+      // Eliminar usuario
+      await tx.user.delete({
+        where: { id },
+      });
+    });
+
+    return NextResponse.json(
+      { message: 'Usuario eliminado exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting user:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 204 - 0
src/app/api/admin/users/route.ts

@@ -0,0 +1,204 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import bcrypt from 'bcryptjs';
+
+// GET - Obtener todos los usuarios
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const users = await prisma.user.findMany({
+      select: {
+        id: true,
+        email: true,
+        role: true,
+        createdAt: true,
+        updatedAt: true,
+        teacher: {
+          select: {
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            phone: true,
+            email: true,
+          },
+        },
+        student: {
+          select: {
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            phone: true,
+            email: true,
+            admissionNumber: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: 'desc',
+      },
+    });
+
+    return NextResponse.json(users);
+  } catch (error) {
+    console.error('Error fetching users:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nuevo usuario
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'ADMIN') {
+      return NextResponse.json(
+        { message: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const body = await request.json();
+    const {
+      email,
+      password,
+      role,
+      firstName,
+      lastName,
+      cedula,
+      phone,
+      admissionNumber,
+    } = body;
+
+    // Validaciones básicas
+    if (!email || !password || !role) {
+      return NextResponse.json(
+        { message: 'Email, contraseña y rol son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (role !== 'ADMIN' && (!firstName || !lastName || !cedula || !phone)) {
+      return NextResponse.json(
+        { message: 'Nombres, apellidos, cédula y teléfono son requeridos para profesores y estudiantes' },
+        { status: 400 }
+      );
+    }
+
+    if (role === 'STUDENT' && !admissionNumber) {
+      return NextResponse.json(
+        { message: 'Número de matrícula es requerido para estudiantes' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el email ya existe
+    const existingUser = await prisma.user.findUnique({
+      where: { email },
+    });
+
+    if (existingUser) {
+      return NextResponse.json(
+        { message: 'El email ya está registrado' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la cédula ya existe (para profesores y estudiantes)
+    if (role !== 'ADMIN' && cedula) {
+      const existingTeacher = await prisma.teacher.findUnique({
+        where: { cedula },
+      });
+      const existingStudent = await prisma.student.findUnique({
+        where: { cedula },
+      });
+
+      if (existingTeacher || existingStudent) {
+        return NextResponse.json(
+          { message: 'La cédula ya está registrada' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar si el número de matrícula ya existe (para estudiantes)
+    if (role === 'STUDENT' && admissionNumber) {
+      const existingStudent = await prisma.student.findUnique({
+        where: { admissionNumber },
+      });
+
+      if (existingStudent) {
+        return NextResponse.json(
+          { message: 'El número de matrícula ya está registrado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Encriptar contraseña
+    const hashedPassword = await bcrypt.hash(password, 12);
+
+    // Crear usuario en una transacción
+    const result = await prisma.$transaction(async (tx) => {
+      // Crear usuario
+      const user = await tx.user.create({
+        data: {
+          email,
+          password: hashedPassword,
+          role,
+        },
+      });
+
+      // Crear perfil específico según el rol
+      if (role === 'TEACHER') {
+        await tx.teacher.create({
+          data: {
+            userId: user.id,
+            firstName,
+            lastName,
+            cedula,
+            email,
+            phone,
+          },
+        });
+      } else if (role === 'STUDENT') {
+        await tx.student.create({
+          data: {
+            userId: user.id,
+            firstName,
+            lastName,
+            cedula,
+            email,
+            phone,
+            admissionNumber,
+          },
+        });
+      }
+
+      return user;
+    });
+
+    return NextResponse.json(
+      { message: 'Usuario creado exitosamente', userId: result.id },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating user:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 2 - 0
src/app/layout.tsx

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
 import AuthSessionProvider from "@/components/providers/session-provider";
+import { Toaster } from "sonner";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -31,6 +32,7 @@ export default function RootLayout({
         <AuthSessionProvider>
           {children}
         </AuthSessionProvider>
+        <Toaster richColors position="top-right" />
       </body>
     </html>
   );

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

@@ -21,7 +21,8 @@ import {
   UserCheck,
   BarChart3,
   School,
-  User
+  User,
+  Layers
 } from 'lucide-react'
 
 interface SidebarProps {
@@ -33,6 +34,7 @@ const adminMenuItems = [
   { icon: Users, label: 'Usuarios', href: '/admin/users' },
   { 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: BarChart3, label: 'Reportes', href: '/admin/reports' },