Matthew Trejo 4 місяців тому
батько
коміт
da67a3ab74

+ 0 - 13
ROADMAP.md

@@ -118,19 +118,6 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
   - ✅ Gestión de clases y materias (`/admin/classes`) - CRUD completo
   - ✅ Gestión de secciones (`/admin/sections`) - CRUD completo
 - **Pendiente**:
-  - Gestión de inscripciones de estudiantes (`/admin/student-enrollments`)
-  - Sistema de reportes (`/admin/reports`)
-  - Configuración del sistema (`/admin/settings`)
-
----
-
-## 🚧 En Progreso
-
-### Funcionalidades Administrativas Avanzadas 🔄
-- **Estado**: Pendiente
-- **Descripción**: Funcionalidades administrativas complementarias
-- **Por implementar**:
-  - Gestión de inscripciones de estudiantes (`/admin/student-enrollments`)
   - Sistema de reportes (`/admin/reports`)
   - Configuración del sistema (`/admin/settings`)
 

+ 559 - 0
src/app/admin/student-enrollments/page.tsx

@@ -0,0 +1,559 @@
+'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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Switch } from '@/components/ui/switch';
+import { toast } from 'sonner';
+import { Plus, Search, Edit, Trash2, Users, GraduationCap, BookOpen, UserCheck } from 'lucide-react';
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  cedula: string;
+  email: string;
+  phone: string;
+  admissionNumber: string;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  class: {
+    id: string;
+    name: string;
+    code: string;
+    period: {
+      id: string;
+      name: string;
+      isActive: boolean;
+    };
+  };
+}
+
+interface StudentEnrollment {
+  id: string;
+  studentId: string;
+  sectionId: string;
+  isActive: boolean;
+  createdAt: string;
+  updatedAt: string;
+  student: Student;
+  section: Section;
+}
+
+interface CreateEnrollmentData {
+  studentId: string;
+  sectionId: string;
+}
+
+export default function StudentEnrollmentsPage() {
+  const [enrollments, setEnrollments] = useState<StudentEnrollment[]>([]);
+  const [students, setStudents] = useState<Student[]>([]);
+  const [sections, setSections] = useState<Section[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [editingEnrollment, setEditingEnrollment] = useState<StudentEnrollment | null>(null);
+  const [formData, setFormData] = useState<CreateEnrollmentData>({
+    studentId: '',
+    sectionId: '',
+  });
+
+  useEffect(() => {
+    fetchEnrollments();
+    fetchStudents();
+    fetchSections();
+  }, []);
+
+  const fetchEnrollments = async () => {
+    try {
+      const response = await fetch('/api/admin/student-enrollments');
+      if (response.ok) {
+        const data = await response.json();
+        setEnrollments(data);
+      } else {
+        toast.error('Error al cargar las inscripciones');
+      }
+    } catch (error) {
+      console.error('Error fetching enrollments:', error);
+      toast.error('Error al cargar las inscripciones');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchStudents = async () => {
+    try {
+      const response = await fetch('/api/admin/users');
+      if (response.ok) {
+        const users = await response.json();
+        const studentUsers = users.filter((user: any) => user.role === 'STUDENT' && user.student);
+        const studentsData = studentUsers.map((user: any) => ({
+          id: user.student.id,
+          firstName: user.student.firstName,
+          lastName: user.student.lastName,
+          cedula: user.student.cedula,
+          email: user.student.email,
+          phone: user.student.phone,
+          admissionNumber: user.student.admissionNumber,
+        }));
+        setStudents(studentsData);
+      }
+    } catch (error) {
+      console.error('Error fetching students:', error);
+    }
+  };
+
+  const fetchSections = async () => {
+    try {
+      const response = await fetch('/api/admin/sections');
+      if (response.ok) {
+        const data = await response.json();
+        setSections(data);
+      }
+    } catch (error) {
+      console.error('Error fetching sections:', error);
+    }
+  };
+
+  const handleCreateEnrollment = async () => {
+    if (!validateForm()) return;
+
+    try {
+      const response = await fetch('/api/admin/student-enrollments', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success('Inscripción creada exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchEnrollments();
+      } else {
+        toast.error(result.message || 'Error al crear la inscripción');
+      }
+    } catch (error) {
+      console.error('Error creating enrollment:', error);
+      toast.error('Error al crear la inscripción');
+    }
+  };
+
+  const handleUpdateEnrollment = async () => {
+    if (!editingEnrollment || !validateForm()) return;
+
+    try {
+      const response = await fetch(`/api/admin/student-enrollments/${editingEnrollment.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          ...formData,
+          isActive: editingEnrollment.isActive,
+        }),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success('Inscripción actualizada exitosamente');
+        setIsEditDialogOpen(false);
+        setEditingEnrollment(null);
+        resetForm();
+        fetchEnrollments();
+      } else {
+        toast.error(result.message || 'Error al actualizar la inscripción');
+      }
+    } catch (error) {
+      console.error('Error updating enrollment:', error);
+      toast.error('Error al actualizar la inscripción');
+    }
+  };
+
+  const handleDeleteEnrollment = async (enrollmentId: string) => {
+    try {
+      const response = await fetch(`/api/admin/student-enrollments/${enrollmentId}`, {
+        method: 'DELETE',
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success(result.message || 'Inscripción eliminada exitosamente');
+        fetchEnrollments();
+      } else {
+        toast.error(result.message || 'Error al eliminar la inscripción');
+      }
+    } catch (error) {
+      console.error('Error deleting enrollment:', error);
+      toast.error('Error al eliminar la inscripción');
+    }
+  };
+
+  const handleToggleActive = async (enrollment: StudentEnrollment) => {
+    try {
+      const response = await fetch(`/api/admin/student-enrollments/${enrollment.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          studentId: enrollment.studentId,
+          sectionId: enrollment.sectionId,
+          isActive: !enrollment.isActive,
+        }),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success(`Inscripción ${!enrollment.isActive ? 'activada' : 'desactivada'} exitosamente`);
+        fetchEnrollments();
+      } else {
+        toast.error(result.message || 'Error al cambiar el estado de la inscripción');
+      }
+    } catch (error) {
+      console.error('Error toggling enrollment status:', error);
+      toast.error('Error al cambiar el estado de la inscripción');
+    }
+  };
+
+  const validateForm = (): boolean => {
+    if (!formData.studentId) {
+      toast.error('Por favor selecciona un estudiante');
+      return false;
+    }
+    if (!formData.sectionId) {
+      toast.error('Por favor selecciona una sección');
+      return false;
+    }
+    return true;
+  };
+
+  const resetForm = () => {
+    setFormData({
+      studentId: '',
+      sectionId: '',
+    });
+  };
+
+  const openEditDialog = (enrollment: StudentEnrollment) => {
+    setEditingEnrollment(enrollment);
+    setFormData({
+      studentId: enrollment.studentId,
+      sectionId: enrollment.sectionId,
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const filteredEnrollments = enrollments.filter(enrollment => {
+    const searchLower = searchTerm.toLowerCase();
+    const studentName = `${enrollment.student.firstName} ${enrollment.student.lastName}`.toLowerCase();
+    const studentCedula = enrollment.student.cedula.toLowerCase();
+    const admissionNumber = enrollment.student.admissionNumber.toLowerCase();
+    const sectionName = enrollment.section.name.toLowerCase();
+    const className = enrollment.section.class.name.toLowerCase();
+    const classCode = enrollment.section.class.code.toLowerCase();
+    
+    return studentName.includes(searchLower) ||
+           studentCedula.includes(searchLower) ||
+           admissionNumber.includes(searchLower) ||
+           sectionName.includes(searchLower) ||
+           className.includes(searchLower) ||
+           classCode.includes(searchLower);
+  });
+
+  const getStatusBadgeVariant = (isActive: boolean) => {
+    return isActive ? 'default' : 'secondary';
+  };
+
+  const getStudentName = (student: Student) => {
+    return `${student.firstName} ${student.lastName}`;
+  };
+
+  const getSectionDisplay = (section: Section) => {
+    return `${section.class.code} - ${section.class.name} (${section.name})`;
+  };
+
+  return (
+    <MainLayout>
+      <div className="space-y-6">
+        <div className="flex items-center justify-between">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Inscripciones de Estudiantes</h1>
+            <p className="text-muted-foreground">
+              Gestiona las inscripciones de estudiantes a secciones
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={() => resetForm()}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nueva Inscripción
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nueva Inscripción</DialogTitle>
+                <DialogDescription>
+                  Inscribe un estudiante a una sección específica.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid gap-2">
+                  <Label htmlFor="student">Estudiante</Label>
+                  <Select
+                    value={formData.studentId}
+                    onValueChange={(value) => setFormData({ ...formData, studentId: value })}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="Selecciona un estudiante" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {students.map((student) => (
+                        <SelectItem key={student.id} value={student.id}>
+                          {getStudentName(student)} - {student.admissionNumber}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+                <div className="grid gap-2">
+                  <Label htmlFor="section">Sección</Label>
+                  <Select
+                    value={formData.sectionId}
+                    onValueChange={(value) => setFormData({ ...formData, sectionId: value })}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="Selecciona una sección" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {sections
+                        .filter(section => section.class.period.isActive)
+                        .map((section) => (
+                        <SelectItem key={section.id} value={section.id}>
+                          {getSectionDisplay(section)}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+              </div>
+              <DialogFooter>
+                <Button type="submit" onClick={handleCreateEnrollment}>
+                  Crear Inscripción
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <UserCheck className="h-5 w-5" />
+              Inscripciones del Sistema
+            </CardTitle>
+            <CardDescription>
+              Lista de todas las inscripciones de estudiantes a secciones
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar por estudiante, cédula, matrícula o sección..."
+                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 inscripciones...</div>
+              </div>
+            ) : (
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Estudiante</TableHead>
+                    <TableHead>Cédula</TableHead>
+                    <TableHead>Matrícula</TableHead>
+                    <TableHead>Materia</TableHead>
+                    <TableHead>Sección</TableHead>
+                    <TableHead>Periodo</TableHead>
+                    <TableHead>Estado</TableHead>
+                    <TableHead>Fecha Inscripción</TableHead>
+                    <TableHead className="text-right">Acciones</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {filteredEnrollments.length === 0 ? (
+                    <TableRow>
+                      <TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
+                        {searchTerm ? 'No se encontraron inscripciones que coincidan con la búsqueda' : 'No hay inscripciones registradas'}
+                      </TableCell>
+                    </TableRow>
+                  ) : (
+                    filteredEnrollments.map((enrollment) => (
+                      <TableRow key={enrollment.id}>
+                        <TableCell className="font-medium">
+                          {getStudentName(enrollment.student)}
+                        </TableCell>
+                        <TableCell>{enrollment.student.cedula}</TableCell>
+                        <TableCell>
+                          <Badge variant="outline">
+                            {enrollment.student.admissionNumber}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>
+                          <div className="flex items-center gap-2">
+                            <BookOpen className="h-4 w-4 text-muted-foreground" />
+                            <div>
+                              <div className="font-medium">{enrollment.section.class.name}</div>
+                              <div className="text-sm text-muted-foreground">{enrollment.section.class.code}</div>
+                            </div>
+                          </div>
+                        </TableCell>
+                        <TableCell>{enrollment.section.name}</TableCell>
+                        <TableCell>
+                          <Badge variant={enrollment.section.class.period.isActive ? 'default' : 'secondary'}>
+                            {enrollment.section.class.period.name}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>
+                          <div className="flex items-center gap-2">
+                            <Switch
+                              checked={enrollment.isActive}
+                              onCheckedChange={() => handleToggleActive(enrollment)}
+                            />
+                            <Badge variant={getStatusBadgeVariant(enrollment.isActive)}>
+                              {enrollment.isActive ? 'Activa' : 'Inactiva'}
+                            </Badge>
+                          </div>
+                        </TableCell>
+                        <TableCell>
+                          {new Date(enrollment.createdAt).toLocaleDateString('es-ES')}
+                        </TableCell>
+                        <TableCell className="text-right">
+                          <div className="flex items-center justify-end gap-2">
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              onClick={() => openEditDialog(enrollment)}
+                            >
+                              <Edit 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 eliminará la inscripción de {getStudentName(enrollment.student)} en la sección {enrollment.section.name}.
+                                    {enrollment.isActive && ' Si existen registros de asistencia, la inscripción será desactivada en lugar de eliminada.'}
+                                  </AlertDialogDescription>
+                                </AlertDialogHeader>
+                                <AlertDialogFooter>
+                                  <AlertDialogCancel>Cancelar</AlertDialogCancel>
+                                  <AlertDialogAction
+                                    onClick={() => handleDeleteEnrollment(enrollment.id)}
+                                    className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+                                  >
+                                    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 Inscripción</DialogTitle>
+              <DialogDescription>
+                Modifica los detalles de la inscripción.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid gap-2">
+                <Label htmlFor="edit-student">Estudiante</Label>
+                <Select
+                  value={formData.studentId}
+                  onValueChange={(value) => setFormData({ ...formData, studentId: value })}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona un estudiante" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {students.map((student) => (
+                      <SelectItem key={student.id} value={student.id}>
+                        {getStudentName(student)} - {student.admissionNumber}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+              <div className="grid gap-2">
+                <Label htmlFor="edit-section">Sección</Label>
+                <Select
+                  value={formData.sectionId}
+                  onValueChange={(value) => setFormData({ ...formData, sectionId: value })}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona una sección" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {sections
+                      .filter(section => section.class.period.isActive)
+                      .map((section) => (
+                      <SelectItem key={section.id} value={section.id}>
+                        {getSectionDisplay(section)}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+            </div>
+            <DialogFooter>
+              <Button type="submit" onClick={handleUpdateEnrollment}>
+                Actualizar Inscripción
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

+ 231 - 0
src/app/api/admin/student-enrollments/[id]/route.ts

@@ -0,0 +1,231 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+// PUT - Actualizar inscripción de estudiante
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: { id: string } }
+) {
+  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 { studentId, sectionId, isActive } = body;
+
+    // Validaciones básicas
+    if (!studentId || !sectionId) {
+      return NextResponse.json(
+        { message: 'Estudiante y sección son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la inscripción existe
+    const existingEnrollment = await prisma.studentEnrollment.findUnique({
+      where: { id },
+    });
+
+    if (!existingEnrollment) {
+      return NextResponse.json(
+        { message: 'Inscripción no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el estudiante existe y está activo
+    const student = await prisma.student.findFirst({
+      where: {
+        id: studentId,
+        isActive: true,
+        deletedAt: null,
+      },
+    });
+
+    if (!student) {
+      return NextResponse.json(
+        { message: 'El estudiante seleccionado no existe o no está activo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la sección existe y está activa
+    const section = await prisma.section.findFirst({
+      where: {
+        id: sectionId,
+        isActive: true,
+        deletedAt: null,
+        class: {
+          isActive: true,
+          deletedAt: null,
+          period: {
+            isActive: true,
+            deletedAt: null,
+          },
+        },
+      },
+      include: {
+        class: {
+          include: {
+            period: true,
+          },
+        },
+      },
+    });
+
+    if (!section) {
+      return NextResponse.json(
+        { message: 'La sección seleccionada no existe, no está activa o pertenece a un período inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Si se está cambiando el estudiante o la sección, verificar duplicados
+    if (studentId !== existingEnrollment.studentId || sectionId !== existingEnrollment.sectionId) {
+      const duplicateEnrollment = await prisma.studentEnrollment.findFirst({
+        where: {
+          studentId,
+          sectionId,
+          isActive: true,
+          id: { not: id },
+        },
+      });
+
+      if (duplicateEnrollment) {
+        return NextResponse.json(
+          { message: 'El estudiante ya está inscrito en esta sección' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar la inscripción
+    const updatedEnrollment = await prisma.studentEnrollment.update({
+      where: { id },
+      data: {
+        studentId,
+        sectionId,
+        isActive: isActive !== undefined ? isActive : existingEnrollment.isActive,
+      },
+      include: {
+        student: {
+          select: {
+            id: true,
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            email: true,
+            phone: true,
+            admissionNumber: true,
+          },
+        },
+        section: {
+          select: {
+            id: true,
+            name: true,
+            class: {
+              select: {
+                id: true,
+                name: true,
+                code: true,
+                period: {
+                  select: {
+                    id: true,
+                    name: true,
+                    isActive: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return NextResponse.json(updatedEnrollment);
+  } catch (error) {
+    console.error('Error updating student enrollment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar inscripción de estudiante
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: { id: string } }
+) {
+  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 inscripción existe
+    const enrollment = await prisma.studentEnrollment.findUnique({
+      where: { id },
+    });
+
+    if (!enrollment) {
+      return NextResponse.json(
+        { message: 'Inscripción no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si existen registros de asistencia asociados
+    const attendanceCount = await prisma.attendance.count({
+      where: {
+        studentId: enrollment.studentId,
+        section: {
+          id: enrollment.sectionId,
+        },
+      },
+    });
+
+    if (attendanceCount > 0) {
+      // Si existen registros de asistencia, desactivar en lugar de eliminar
+      const deactivatedEnrollment = await prisma.studentEnrollment.update({
+        where: { id },
+        data: { isActive: false },
+      });
+
+      return NextResponse.json({
+        message: 'La inscripción ha sido desactivada debido a registros de asistencia existentes',
+        enrollment: deactivatedEnrollment,
+      });
+    } else {
+      // Si no hay registros de asistencia, eliminar completamente
+      await prisma.studentEnrollment.delete({
+        where: { id },
+      });
+
+      return NextResponse.json({
+        message: 'Inscripción eliminada exitosamente',
+      });
+    }
+  } catch (error) {
+    console.error('Error deleting student enrollment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 203 - 0
src/app/api/admin/student-enrollments/route.ts

@@ -0,0 +1,203 @@
+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 inscripciones de estudiantes
+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 enrollments = await prisma.studentEnrollment.findMany({
+      include: {
+        student: {
+          select: {
+            id: true,
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            email: true,
+            phone: true,
+            admissionNumber: true,
+          },
+        },
+        section: {
+          select: {
+            id: true,
+            name: true,
+            class: {
+              select: {
+                id: true,
+                name: true,
+                code: true,
+                period: {
+                  select: {
+                    id: true,
+                    name: true,
+                    isActive: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      orderBy: {
+        createdAt: 'desc',
+      },
+    });
+
+    return NextResponse.json(enrollments);
+  } catch (error) {
+    console.error('Error fetching student enrollments:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nueva inscripción de estudiante
+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 { studentId, sectionId } = body;
+
+    // Validaciones básicas
+    if (!studentId || !sectionId) {
+      return NextResponse.json(
+        { message: 'Estudiante y sección son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el estudiante existe y está activo
+    const student = await prisma.student.findFirst({
+      where: {
+        id: studentId,
+        isActive: true,
+        deletedAt: null,
+      },
+    });
+
+    if (!student) {
+      return NextResponse.json(
+        { message: 'El estudiante seleccionado no existe o no está activo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la sección existe y está activa
+    const section = await prisma.section.findFirst({
+      where: {
+        id: sectionId,
+        isActive: true,
+        deletedAt: null,
+        class: {
+          isActive: true,
+          deletedAt: null,
+          period: {
+            isActive: true,
+            deletedAt: null,
+          },
+        },
+      },
+      include: {
+        class: {
+          include: {
+            period: true,
+          },
+        },
+      },
+    });
+
+    if (!section) {
+      return NextResponse.json(
+        { message: 'La sección seleccionada no existe, no está activa o pertenece a un período inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una inscripción activa para este estudiante en esta sección
+    const existingEnrollment = await prisma.studentEnrollment.findFirst({
+      where: {
+        studentId,
+        sectionId,
+        isActive: true,
+      },
+    });
+
+    if (existingEnrollment) {
+      return NextResponse.json(
+        { message: 'El estudiante ya está inscrito en esta sección' },
+        { status: 400 }
+      );
+    }
+
+    // Crear la nueva inscripción
+    const enrollment = await prisma.studentEnrollment.create({
+      data: {
+        studentId,
+        sectionId,
+        isActive: true,
+      },
+      include: {
+        student: {
+          select: {
+            id: true,
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            email: true,
+            phone: true,
+            admissionNumber: true,
+          },
+        },
+        section: {
+          select: {
+            id: true,
+            name: true,
+            class: {
+              select: {
+                id: true,
+                name: true,
+                code: true,
+                period: {
+                  select: {
+                    id: true,
+                    name: true,
+                    isActive: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return NextResponse.json(enrollment, { status: 201 });
+  } catch (error) {
+    console.error('Error creating student enrollment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

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

@@ -35,6 +35,7 @@ export async function GET() {
         },
         student: {
           select: {
+            id: true,
             firstName: true,
             lastName: true,
             cedula: true,