Jelajahi Sumber

teacher asigments

Matthew Trejo 4 bulan lalu
induk
melakukan
a53d50689c

+ 31 - 14
ROADMAP.md

@@ -108,18 +108,29 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
 
 ---
 
-## 🚧 En Progreso
-
-### Funcionalidades del Rol Administrador 🔄
-- **Estado**: En progreso
+### 8. Funcionalidades del Rol Administrador ✅
+- **Estado**: Mayormente completado
 - **Descripción**: Implementación de funcionalidades administrativas
 - **Dashboard**: ✅ Completado
+- **Implementado**:
+  - ✅ Gestión de usuarios (`/admin/users`) - CRUD completo con validaciones
+  - ✅ Gestión de periodos académicos (`/admin/periods`) - CRUD completo con toggle activo/inactivo
+  - ✅ Gestión de clases y materias (`/admin/classes`) - CRUD completo
+  - ✅ Gestión de secciones (`/admin/sections`) - CRUD completo
 - **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`)
+  - 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`)
 
@@ -166,9 +177,9 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
 ## 🎯 Próximos Pasos
 
 ### Prioridad Alta
-1. **Completar funcionalidades de Administrador**
-   - Gestión CRUD de usuarios
-   - Gestión de periodos y clases
+1. **Completar funcionalidades administrativas avanzadas**
+   - Sistema de asignaciones de profesores
+   - Sistema de inscripciones de estudiantes
    - Sistema de reportes básico
 
 2. **Implementar funcionalidades de Profesor**
@@ -200,9 +211,15 @@ TAPIR es un sistema integral de gestión de asistencia estudiantil desarrollado
 
 ## 📊 Progreso General
 
-- **Completado**: 62.5% (5/8 tareas principales)
+- **Completado**: 87.5% (7/8 tareas principales)
 - **En progreso**: 12.5% (1/8 tareas principales)
-- **Pendiente**: 25% (2/8 tareas principales)
+- **Pendiente**: 0% (0/8 tareas principales)
+
+### Desglose de Funcionalidades Administrativas
+- **Funcionalidades básicas**: ✅ 100% completado (4/4)
+  - Gestión de usuarios, periodos, clases y secciones
+- **Funcionalidades avanzadas**: 🔄 0% completado (0/4)
+  - Asignaciones, inscripciones, reportes y configuración
 
 ### Arquitectura Técnica ✅
 - ✅ Next.js 15 con App Router

+ 547 - 0
src/app/admin/teacher-assignments/page.tsx

@@ -0,0 +1,547 @@
+'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 } from 'lucide-react';
+
+interface Teacher {
+  id: string;
+  firstName: string;
+  lastName: string;
+  cedula: string;
+  email: string;
+  phone: string;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  class: {
+    id: string;
+    name: string;
+    code: string;
+    period: {
+      id: string;
+      name: string;
+      isActive: boolean;
+    };
+  };
+}
+
+interface TeacherAssignment {
+  id: string;
+  teacherId: string;
+  sectionId: string;
+  isActive: boolean;
+  createdAt: string;
+  updatedAt: string;
+  teacher: Teacher;
+  section: Section;
+}
+
+interface CreateAssignmentData {
+  teacherId: string;
+  sectionId: string;
+}
+
+export default function TeacherAssignmentsPage() {
+  const [assignments, setAssignments] = useState<TeacherAssignment[]>([]);
+  const [teachers, setTeachers] = useState<Teacher[]>([]);
+  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 [editingAssignment, setEditingAssignment] = useState<TeacherAssignment | null>(null);
+  const [formData, setFormData] = useState<CreateAssignmentData>({
+    teacherId: '',
+    sectionId: '',
+  });
+
+  useEffect(() => {
+    fetchAssignments();
+    fetchTeachers();
+    fetchSections();
+  }, []);
+
+  const fetchAssignments = async () => {
+    try {
+      const response = await fetch('/api/admin/teacher-assignments');
+      if (response.ok) {
+        const data = await response.json();
+        setAssignments(data);
+      } else {
+        toast.error('Error al cargar las asignaciones');
+      }
+    } catch (error) {
+      console.error('Error fetching assignments:', error);
+      toast.error('Error al cargar las asignaciones');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchTeachers = async () => {
+    try {
+      const response = await fetch('/api/admin/users');
+      if (response.ok) {
+        const users = await response.json();
+        const teacherUsers = users.filter((user: any) => user.role === 'TEACHER' && user.teacher);
+        const teachersData = teacherUsers.map((user: any) => ({
+          id: user.teacher.id,
+          firstName: user.teacher.firstName,
+          lastName: user.teacher.lastName,
+          cedula: user.teacher.cedula,
+          email: user.teacher.email,
+          phone: user.teacher.phone,
+        }));
+        setTeachers(teachersData);
+      }
+    } catch (error) {
+      console.error('Error fetching teachers:', 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 handleCreateAssignment = async () => {
+    if (!validateForm()) return;
+
+    try {
+      const response = await fetch('/api/admin/teacher-assignments', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success('Asignación creada exitosamente');
+        setIsCreateDialogOpen(false);
+        resetForm();
+        fetchAssignments();
+      } else {
+        toast.error(result.message || 'Error al crear la asignación');
+      }
+    } catch (error) {
+      console.error('Error creating assignment:', error);
+      toast.error('Error al crear la asignación');
+    }
+  };
+
+  const handleUpdateAssignment = async () => {
+    if (!editingAssignment || !validateForm()) return;
+
+    try {
+      const response = await fetch(`/api/admin/teacher-assignments/${editingAssignment.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          ...formData,
+          isActive: editingAssignment.isActive,
+        }),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success('Asignación actualizada exitosamente');
+        setIsEditDialogOpen(false);
+        setEditingAssignment(null);
+        resetForm();
+        fetchAssignments();
+      } else {
+        toast.error(result.message || 'Error al actualizar la asignación');
+      }
+    } catch (error) {
+      console.error('Error updating assignment:', error);
+      toast.error('Error al actualizar la asignación');
+    }
+  };
+
+  const handleDeleteAssignment = async (assignmentId: string) => {
+    try {
+      const response = await fetch(`/api/admin/teacher-assignments/${assignmentId}`, {
+        method: 'DELETE',
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success(result.message || 'Asignación eliminada exitosamente');
+        fetchAssignments();
+      } else {
+        toast.error(result.message || 'Error al eliminar la asignación');
+      }
+    } catch (error) {
+      console.error('Error deleting assignment:', error);
+      toast.error('Error al eliminar la asignación');
+    }
+  };
+
+  const handleToggleActive = async (assignment: TeacherAssignment) => {
+    try {
+      const response = await fetch(`/api/admin/teacher-assignments/${assignment.id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          teacherId: assignment.teacherId,
+          sectionId: assignment.sectionId,
+          isActive: !assignment.isActive,
+        }),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        toast.success(`Asignación ${!assignment.isActive ? 'activada' : 'desactivada'} exitosamente`);
+        fetchAssignments();
+      } else {
+        toast.error(result.message || 'Error al cambiar el estado de la asignación');
+      }
+    } catch (error) {
+      console.error('Error toggling assignment status:', error);
+      toast.error('Error al cambiar el estado de la asignación');
+    }
+  };
+
+  const validateForm = (): boolean => {
+    if (!formData.teacherId) {
+      toast.error('Por favor selecciona un profesor');
+      return false;
+    }
+    if (!formData.sectionId) {
+      toast.error('Por favor selecciona una sección');
+      return false;
+    }
+    return true;
+  };
+
+  const resetForm = () => {
+    setFormData({
+      teacherId: '',
+      sectionId: '',
+    });
+  };
+
+  const openEditDialog = (assignment: TeacherAssignment) => {
+    setEditingAssignment(assignment);
+    setFormData({
+      teacherId: assignment.teacherId,
+      sectionId: assignment.sectionId,
+    });
+    setIsEditDialogOpen(true);
+  };
+
+  const filteredAssignments = assignments.filter(assignment => {
+    const searchLower = searchTerm.toLowerCase();
+    const teacherName = `${assignment.teacher.firstName} ${assignment.teacher.lastName}`.toLowerCase();
+    const sectionName = assignment.section.name.toLowerCase();
+    const className = assignment.section.class.name.toLowerCase();
+    const classCode = assignment.section.class.code.toLowerCase();
+    
+    return teacherName.includes(searchLower) ||
+           sectionName.includes(searchLower) ||
+           className.includes(searchLower) ||
+           classCode.includes(searchLower);
+  });
+
+  const getStatusBadgeVariant = (isActive: boolean) => {
+    return isActive ? 'default' : 'secondary';
+  };
+
+  const getTeacherName = (teacher: Teacher) => {
+    return `${teacher.firstName} ${teacher.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">Asignaciones de Profesores</h1>
+            <p className="text-muted-foreground">
+              Gestiona las asignaciones de profesores a secciones
+            </p>
+          </div>
+          <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+            <DialogTrigger asChild>
+              <Button onClick={() => resetForm()}>
+                <Plus className="mr-2 h-4 w-4" />
+                Nueva Asignación
+              </Button>
+            </DialogTrigger>
+            <DialogContent className="sm:max-w-[425px]">
+              <DialogHeader>
+                <DialogTitle>Crear Nueva Asignación</DialogTitle>
+                <DialogDescription>
+                  Asigna un profesor a una sección específica.
+                </DialogDescription>
+              </DialogHeader>
+              <div className="grid gap-4 py-4">
+                <div className="grid gap-2">
+                  <Label htmlFor="teacher">Profesor</Label>
+                  <Select
+                    value={formData.teacherId}
+                    onValueChange={(value) => setFormData({ ...formData, teacherId: value })}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="Selecciona un profesor" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {teachers.map((teacher) => (
+                        <SelectItem key={teacher.id} value={teacher.id}>
+                          {getTeacherName(teacher)} - {teacher.cedula}
+                        </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={handleCreateAssignment}>
+                  Crear Asignación
+                </Button>
+              </DialogFooter>
+            </DialogContent>
+          </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <GraduationCap className="h-5 w-5" />
+              Asignaciones del Sistema
+            </CardTitle>
+            <CardDescription>
+              Lista de todas las asignaciones de profesores a secciones
+            </CardDescription>
+            <div className="flex items-center space-x-2">
+              <Search className="h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Buscar por profesor, sección o materia..."
+                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 asignaciones...</div>
+              </div>
+            ) : (
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Profesor</TableHead>
+                    <TableHead>Cédula</TableHead>
+                    <TableHead>Materia</TableHead>
+                    <TableHead>Sección</TableHead>
+                    <TableHead>Periodo</TableHead>
+                    <TableHead>Estado</TableHead>
+                    <TableHead>Fecha Asignación</TableHead>
+                    <TableHead className="text-right">Acciones</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {filteredAssignments.length === 0 ? (
+                    <TableRow>
+                      <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
+                        {searchTerm ? 'No se encontraron asignaciones que coincidan con la búsqueda' : 'No hay asignaciones registradas'}
+                      </TableCell>
+                    </TableRow>
+                  ) : (
+                    filteredAssignments.map((assignment) => (
+                      <TableRow key={assignment.id}>
+                        <TableCell className="font-medium">
+                          {getTeacherName(assignment.teacher)}
+                        </TableCell>
+                        <TableCell>{assignment.teacher.cedula}</TableCell>
+                        <TableCell>
+                          <div className="flex items-center gap-2">
+                            <BookOpen className="h-4 w-4 text-muted-foreground" />
+                            <div>
+                              <div className="font-medium">{assignment.section.class.name}</div>
+                              <div className="text-sm text-muted-foreground">{assignment.section.class.code}</div>
+                            </div>
+                          </div>
+                        </TableCell>
+                        <TableCell>{assignment.section.name}</TableCell>
+                        <TableCell>
+                          <Badge variant={assignment.section.class.period.isActive ? 'default' : 'secondary'}>
+                            {assignment.section.class.period.name}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>
+                          <div className="flex items-center gap-2">
+                            <Switch
+                              checked={assignment.isActive}
+                              onCheckedChange={() => handleToggleActive(assignment)}
+                            />
+                            <Badge variant={getStatusBadgeVariant(assignment.isActive)}>
+                              {assignment.isActive ? 'Activa' : 'Inactiva'}
+                            </Badge>
+                          </div>
+                        </TableCell>
+                        <TableCell>
+                          {new Date(assignment.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(assignment)}
+                            >
+                              <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 asignación de {getTeacherName(assignment.teacher)} a la sección {assignment.section.name}.
+                                    {assignment.isActive && ' Si existen registros de asistencia, la asignación será desactivada en lugar de eliminada.'}
+                                  </AlertDialogDescription>
+                                </AlertDialogHeader>
+                                <AlertDialogFooter>
+                                  <AlertDialogCancel>Cancelar</AlertDialogCancel>
+                                  <AlertDialogAction
+                                    onClick={() => handleDeleteAssignment(assignment.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 Asignación</DialogTitle>
+              <DialogDescription>
+                Modifica los detalles de la asignación.
+              </DialogDescription>
+            </DialogHeader>
+            <div className="grid gap-4 py-4">
+              <div className="grid gap-2">
+                <Label htmlFor="edit-teacher">Profesor</Label>
+                <Select
+                  value={formData.teacherId}
+                  onValueChange={(value) => setFormData({ ...formData, teacherId: value })}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona un profesor" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {teachers.map((teacher) => (
+                      <SelectItem key={teacher.id} value={teacher.id}>
+                        {getTeacherName(teacher)} - {teacher.cedula}
+                      </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={handleUpdateAssignment}>
+                Actualizar Asignación
+              </Button>
+            </DialogFooter>
+          </DialogContent>
+        </Dialog>
+      </div>
+    </MainLayout>
+  );
+}

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

@@ -110,7 +110,55 @@ export default function UsersPage() {
     }
   };
 
+  const validateForm = (): boolean => {
+    if (!formData.email.trim()) {
+      toast.error('El email es requerido');
+      return false;
+    }
+    
+    if (!formData.email.includes('@')) {
+      toast.error('El email debe tener un formato válido');
+      return false;
+    }
+    
+    if (!formData.password.trim()) {
+      toast.error('La contraseña es requerida');
+      return false;
+    }
+    
+    if (formData.password.length < 6) {
+      toast.error('La contraseña debe tener al menos 6 caracteres');
+      return false;
+    }
+    
+    if (formData.role === 'TEACHER' || formData.role === 'STUDENT') {
+      if (!formData.firstName?.trim()) {
+        toast.error('Los nombres son requeridos');
+        return false;
+      }
+      
+      if (!formData.lastName?.trim()) {
+        toast.error('Los apellidos son requeridos');
+        return false;
+      }
+      
+      if (!formData.cedula?.trim()) {
+        toast.error('La cédula es requerida');
+        return false;
+      }
+      
+      if (formData.role === 'STUDENT' && !formData.admissionNumber?.trim()) {
+        toast.error('El número de matrícula es requerido para estudiantes');
+        return false;
+      }
+    }
+    
+    return true;
+  };
+
   const handleCreateUser = async () => {
+    if (!validateForm()) return;
+    
     try {
       const response = await fetch('/api/admin/users', {
         method: 'POST',
@@ -134,8 +182,50 @@ export default function UsersPage() {
     }
   };
 
+  const validateUpdateForm = (): boolean => {
+    if (!formData.email.trim()) {
+      toast.error('El email es requerido');
+      return false;
+    }
+    
+    if (!formData.email.includes('@')) {
+      toast.error('El email debe tener un formato válido');
+      return false;
+    }
+    
+    if (formData.password && formData.password.length < 6) {
+      toast.error('La contraseña debe tener al menos 6 caracteres');
+      return false;
+    }
+    
+    if (formData.role === 'TEACHER' || formData.role === 'STUDENT') {
+      if (!formData.firstName?.trim()) {
+        toast.error('Los nombres son requeridos');
+        return false;
+      }
+      
+      if (!formData.lastName?.trim()) {
+        toast.error('Los apellidos son requeridos');
+        return false;
+      }
+      
+      if (!formData.cedula?.trim()) {
+        toast.error('La cédula es requerida');
+        return false;
+      }
+      
+      if (formData.role === 'STUDENT' && !formData.admissionNumber?.trim()) {
+        toast.error('El número de matrícula es requerido para estudiantes');
+        return false;
+      }
+    }
+    
+    return true;
+  };
+
   const handleUpdateUser = async () => {
     if (!selectedUser) return;
+    if (!validateUpdateForm()) return;
 
     try {
       const response = await fetch(`/api/admin/users/${selectedUser.id}`, {

+ 220 - 0
src/app/api/admin/teacher-assignments/[id]/route.ts

@@ -0,0 +1,220 @@
+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 asignación de profesor
+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 { teacherId, sectionId, isActive } = body;
+
+    // Verificar si la asignación existe
+    const existingAssignment = await prisma.teacherAssignment.findUnique({
+      where: { id },
+    });
+
+    if (!existingAssignment) {
+      return NextResponse.json(
+        { message: 'Asignación no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Validaciones básicas
+    if (!teacherId || !sectionId) {
+      return NextResponse.json(
+        { message: 'Profesor y sección son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el profesor existe y está activo
+    const teacher = await prisma.teacher.findFirst({
+      where: {
+        id: teacherId,
+        isActive: true,
+        deletedAt: null,
+      },
+    });
+
+    if (!teacher) {
+      return NextResponse.json(
+        { message: 'El profesor 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,
+      },
+      include: {
+        class: {
+          include: {
+            period: {
+              select: {
+                isActive: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    if (!section) {
+      return NextResponse.json(
+        { message: 'La sección seleccionada no existe o no está activa' },
+        { status: 400 }
+      );
+    }
+
+    if (!section.class.period.isActive) {
+      return NextResponse.json(
+        { message: 'No se puede asignar un profesor a una sección de un periodo inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otra asignación activa para este profesor y sección
+    if (teacherId !== existingAssignment.teacherId || sectionId !== existingAssignment.sectionId) {
+      const duplicateAssignment = await prisma.teacherAssignment.findFirst({
+        where: {
+          teacherId,
+          sectionId,
+          isActive: true,
+          id: { not: id },
+        },
+      });
+
+      if (duplicateAssignment) {
+        return NextResponse.json(
+          { message: 'Ya existe una asignación activa para este profesor y sección' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar la asignación
+    const updatedAssignment = await prisma.teacherAssignment.update({
+      where: { id },
+      data: {
+        teacherId,
+        sectionId,
+        isActive: isActive !== undefined ? isActive : existingAssignment.isActive,
+      },
+      include: {
+        teacher: {
+          select: {
+            firstName: true,
+            lastName: true,
+            email: true,
+          },
+        },
+        section: {
+          select: {
+            name: true,
+            class: {
+              select: {
+                name: true,
+                code: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return NextResponse.json({
+      message: 'Asignación actualizada exitosamente',
+      assignment: updatedAssignment,
+    });
+  } catch (error) {
+    console.error('Error updating teacher assignment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar asignación de profesor
+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 asignación existe
+    const existingAssignment = await prisma.teacherAssignment.findUnique({
+      where: { id },
+    });
+
+    if (!existingAssignment) {
+      return NextResponse.json(
+        { message: 'Asignación no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si hay registros de asistencia asociados
+    const attendanceCount = await prisma.attendance.count({
+      where: {
+        sectionId: existingAssignment.sectionId,
+      },
+    });
+
+    if (attendanceCount > 0) {
+      // Si hay registros de asistencia, solo desactivar la asignación
+      await prisma.teacherAssignment.update({
+        where: { id },
+        data: { isActive: false },
+      });
+
+      return NextResponse.json({
+        message: 'Asignación desactivada exitosamente (existen registros de asistencia asociados)',
+      });
+    } else {
+      // Si no hay registros de asistencia, eliminar completamente
+      await prisma.teacherAssignment.delete({
+        where: { id },
+      });
+
+      return NextResponse.json({
+        message: 'Asignación eliminada exitosamente',
+      });
+    }
+  } catch (error) {
+    console.error('Error deleting teacher assignment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

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

@@ -0,0 +1,197 @@
+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 asignaciones de profesores
+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 assignments = await prisma.teacherAssignment.findMany({
+      include: {
+        teacher: {
+          select: {
+            id: true,
+            firstName: true,
+            lastName: true,
+            cedula: true,
+            email: true,
+            phone: 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(assignments);
+  } catch (error) {
+    console.error('Error fetching teacher assignments:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nueva asignación de profesor
+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 { teacherId, sectionId } = body;
+
+    // Validaciones básicas
+    if (!teacherId || !sectionId) {
+      return NextResponse.json(
+        { message: 'Profesor y sección son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el profesor existe y está activo
+    const teacher = await prisma.teacher.findFirst({
+      where: {
+        id: teacherId,
+        isActive: true,
+        deletedAt: null,
+      },
+    });
+
+    if (!teacher) {
+      return NextResponse.json(
+        { message: 'El profesor 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,
+      },
+      include: {
+        class: {
+          include: {
+            period: {
+              select: {
+                isActive: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    if (!section) {
+      return NextResponse.json(
+        { message: 'La sección seleccionada no existe o no está activa' },
+        { status: 400 }
+      );
+    }
+
+    if (!section.class.period.isActive) {
+      return NextResponse.json(
+        { message: 'No se puede asignar un profesor a una sección de un periodo inactivo' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una asignación activa para este profesor y sección
+    const existingAssignment = await prisma.teacherAssignment.findFirst({
+      where: {
+        teacherId,
+        sectionId,
+        isActive: true,
+      },
+    });
+
+    if (existingAssignment) {
+      return NextResponse.json(
+        { message: 'Ya existe una asignación activa para este profesor y sección' },
+        { status: 400 }
+      );
+    }
+
+    // Crear la nueva asignación
+    const newAssignment = await prisma.teacherAssignment.create({
+      data: {
+        teacherId,
+        sectionId,
+      },
+      include: {
+        teacher: {
+          select: {
+            firstName: true,
+            lastName: true,
+            email: true,
+          },
+        },
+        section: {
+          select: {
+            name: true,
+            class: {
+              select: {
+                name: true,
+                code: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return NextResponse.json(
+      { 
+        message: 'Asignación creada exitosamente', 
+        assignmentId: newAssignment.id 
+      },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating teacher assignment:', error);
+    return NextResponse.json(
+      { message: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

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

@@ -25,6 +25,7 @@ export async function GET() {
         updatedAt: true,
         teacher: {
           select: {
+            id: true,
             firstName: true,
             lastName: true,
             cedula: true,