Просмотр исходного кода

teacher knows the way now less goo

Matthew Trejo 4 месяцев назад
Родитель
Сommit
6716b95e17

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

@@ -20,6 +20,8 @@ interface Section {
   maxStudents: number;
   isActive: boolean;
   createdAt: string;
+  teacherName?: string;
+  teacherEmail?: string;
 }
 
 interface Class {
@@ -294,6 +296,9 @@ export default function SectionsPage() {
                       <p className="text-sm text-gray-600 mt-1">
                         {section.className} - {section.periodName}
                       </p>
+                      <p className="text-xs text-gray-500 mt-1">
+                        Profesor: {section.teacherName ? `${section.teacherName} (${section.teacherEmail})` : 'Sin asignar'}
+                      </p>
                       <p className="text-xs text-gray-500 mt-1">
                         Máx. estudiantes: {section.maxStudents} | Estado: {section.isActive ? 'Activa' : 'Inactiva'}
                       </p>

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

@@ -0,0 +1,378 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Trash2, Edit, Plus, UserCheck } from 'lucide-react';
+import { DashboardLayout } from '@/components/dashboard-layout';
+
+interface TeacherAssignment {
+  id: string;
+  teacherId: string;
+  teacherName: string;
+  teacherEmail: string;
+  classId: string;
+  className: string;
+  classCode: string;
+  sectionId: string;
+  sectionName: string;
+  periodName: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface Teacher {
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+  isActive?: boolean;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  periodId?: string;
+  isActive?: boolean;
+}
+
+interface Section {
+  id: string;
+  name: string;
+  classId: string;
+  className: string;
+  isActive?: boolean;
+}
+
+interface FormData {
+  teacherId: string;
+  classId: string;
+  sectionId: string;
+}
+
+export default function TeacherAssignmentsPage() {
+  const [assignments, setAssignments] = useState<TeacherAssignment[]>([]);
+  const [teachers, setTeachers] = useState<Teacher[]>([]);
+  const [classes, setClasses] = useState<Class[]>([]);
+  const [sections, setSections] = useState<Section[]>([]);
+  const [filteredSections, setFilteredSections] = useState<Section[]>([]);
+  const [formData, setFormData] = useState<FormData>({
+    teacherId: '',
+    classId: '',
+    sectionId: ''
+  });
+  const [editingId, setEditingId] = useState<string | null>(null);
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    fetchAssignments();
+    fetchTeachers();
+    fetchClasses();
+    fetchSections();
+  }, []);
+
+  useEffect(() => {
+    if (formData.classId) {
+      const classSections = sections.filter(s => s.classId === formData.classId && s.isActive !== false);
+      setFilteredSections(classSections);
+      setFormData(prev => ({ ...prev, sectionId: '' }));
+    } else {
+      setFilteredSections([]);
+    }
+  }, [formData.classId, sections]);
+
+  const fetchAssignments = async () => {
+    try {
+      const response = await fetch('/api/admin/teacher-assignments');
+      if (response.ok) {
+        const data = await response.json();
+        setAssignments(data);
+      }
+    } catch (error) {
+      console.error('Error fetching assignments:', error);
+    }
+  };
+
+  const fetchTeachers = async () => {
+    try {
+      const response = await fetch('/api/admin/teachers');
+      if (response.ok) {
+        const data = await response.json();
+        setTeachers(data.filter((t: Teacher) => t.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching teachers:', error);
+    }
+  };
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes');
+      if (response.ok) {
+        const data = await response.json();
+        setClasses(data.filter((c: Class) => c.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching classes:', error);
+    }
+  };
+
+  const fetchSections = async () => {
+    try {
+      const response = await fetch('/api/admin/sections');
+      if (response.ok) {
+        const data = await response.json();
+        setSections(data.filter((s: Section) => s.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching sections:', error);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+    setSuccess('');
+
+    try {
+      const body = {
+        teacherId: formData.teacherId,
+        classId: formData.classId,
+        sectionId: formData.sectionId
+      };
+
+      const url = editingId 
+        ? `/api/admin/teacher-assignments/${editingId}`
+        : '/api/admin/teacher-assignments';
+      
+      const method = editingId ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(body),
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        setSuccess(editingId ? 'Asignación actualizada exitosamente' : 'Asignación creada exitosamente');
+        setFormData({ teacherId: '', classId: '', sectionId: '' });
+        setEditingId(null);
+        fetchAssignments();
+      } else {
+        setError(result.error || 'Error al procesar la asignación');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleEdit = (assignment: TeacherAssignment) => {
+    setFormData({
+      teacherId: assignment.teacherId,
+      classId: assignment.classId,
+      sectionId: assignment.sectionId
+    });
+    setEditingId(assignment.id);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleCancel = () => {
+    setFormData({ teacherId: '', classId: '', sectionId: '' });
+    setEditingId(null);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar esta asignación?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/teacher-assignments/${id}`, {
+        method: 'DELETE',
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        setSuccess('Asignación eliminada exitosamente');
+        fetchAssignments();
+      } else {
+        setError(result.error || 'Error al eliminar la asignación');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  return (
+    <DashboardLayout>
+      <div className="space-y-6">
+        <div className="flex items-center justify-between">
+          <h1 className="text-3xl font-bold tracking-tight">Asignaciones de Profesores</h1>
+        </div>
+
+        {error && (
+          <Alert variant="destructive">
+            <AlertDescription>{error}</AlertDescription>
+          </Alert>
+        )}
+
+        {success && (
+          <Alert>
+            <AlertDescription>{success}</AlertDescription>
+          </Alert>
+        )}
+
+        <div className="grid gap-6 md:grid-cols-2">
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <Plus className="h-5 w-5" />
+                {editingId ? 'Editar Asignación' : 'Nueva Asignación'}
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <form onSubmit={handleSubmit} className="space-y-4">
+                <div className="space-y-2">
+                  <Label htmlFor="teacherId">Profesor</Label>
+                  <Select
+                    value={formData.teacherId}
+                    onValueChange={(value) => setFormData({ ...formData, teacherId: value })}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="Seleccionar profesor" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {teachers.map((teacher) => (
+                        <SelectItem key={teacher.id} value={teacher.id}>
+                          {teacher.firstName} {teacher.lastName} ({teacher.email})
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                <div className="space-y-2">
+                  <Label htmlFor="classId">Clase</Label>
+                  <Select
+                    value={formData.classId}
+                    onValueChange={(value) => setFormData({ ...formData, classId: value })}
+                  >
+                    <SelectTrigger>
+                      <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="space-y-2">
+                  <Label htmlFor="sectionId">Sección</Label>
+                  <Select
+                    value={formData.sectionId}
+                    onValueChange={(value) => setFormData({ ...formData, sectionId: value })}
+                    disabled={!formData.classId}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder={formData.classId ? "Seleccionar sección" : "Primero selecciona una clase"} />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {filteredSections.map((section) => (
+                        <SelectItem key={section.id} value={section.id}>
+                          {section.name}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                <div className="flex gap-2">
+                  <Button type="submit" disabled={loading}>
+                    {loading ? 'Procesando...' : (editingId ? 'Actualizar' : 'Crear')}
+                  </Button>
+                  {editingId && (
+                    <Button type="button" variant="outline" onClick={handleCancel}>
+                      Cancelar
+                    </Button>
+                  )}
+                </div>
+              </form>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <UserCheck className="h-5 w-5" />
+                Asignaciones Registradas
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <div className="space-y-4">
+                {assignments.length === 0 ? (
+                  <p className="text-muted-foreground text-center py-4">
+                    No hay asignaciones registradas
+                  </p>
+                ) : (
+                  assignments.map((assignment) => (
+                    <div key={assignment.id} className="flex items-center justify-between p-3 border rounded-lg">
+                      <div className="flex-1">
+                        <div className="font-medium">
+                          {assignment.teacherName}
+                        </div>
+                        <div className="text-sm text-muted-foreground">
+                          {assignment.classCode} - {assignment.className}
+                        </div>
+                        <div className="text-sm text-muted-foreground">
+                          Sección: {assignment.sectionName} | Período: {assignment.periodName}
+                        </div>
+                      </div>
+                      <div className="flex gap-2">
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleEdit(assignment)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleDelete(assignment.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </div>
+                  ))
+                )}
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    </DashboardLayout>
+  );
+}

+ 18 - 2
src/app/api/admin/sections/route.ts

@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
 import { getServerSession } from 'next-auth';
 import { authOptions } from '@/lib/auth';
 import { db } from '@/lib/db';
-import { sections, classes, periods, eq, and } from '@/lib/db/schema';
+import { sections, classes, periods, teacherAssignments, users, eq, and } from '@/lib/db/schema';
 
 export async function GET() {
   try {
@@ -26,13 +26,29 @@ export async function GET() {
         maxStudents: sections.maxStudents,
         isActive: sections.isActive,
         createdAt: sections.createdAt,
+        teacherName: users.firstName,
+        teacherLastName: users.lastName,
+        teacherEmail: users.email,
       })
       .from(sections)
       .leftJoin(classes, eq(sections.classId, classes.id))
       .leftJoin(periods, eq(classes.periodId, periods.id))
+      .leftJoin(teacherAssignments, and(
+        eq(teacherAssignments.sectionId, sections.id),
+        eq(teacherAssignments.isActive, true)
+      ))
+      .leftJoin(users, eq(teacherAssignments.teacherId, users.id))
       .orderBy(classes.code, sections.name);
 
-    return NextResponse.json(allSections);
+    // Formatear los datos para incluir el nombre completo del profesor
+    const formattedSections = allSections.map(section => ({
+      ...section,
+      teacherName: section.teacherName && section.teacherLastName 
+        ? `${section.teacherName} ${section.teacherLastName}` 
+        : null,
+    }));
+
+    return NextResponse.json(formattedSections);
   } catch (error) {
     console.error('Error fetching sections:', error);
     return NextResponse.json(

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

@@ -0,0 +1,166 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { db } from '@/lib/db';
+import { teacherAssignments, users, classes, sections } from '@/lib/db/schema';
+import { eq, and, ne } from 'drizzle-orm';
+
+// PUT - Actualizar asignación de profesor
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: { id: string } }
+) {
+  try {
+    const { id } = params;
+    const body = await request.json();
+    const { teacherId, classId, sectionId } = body;
+
+    // Validar campos requeridos
+    if (!teacherId || !classId || !sectionId) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar que la asignación existe
+    const existingAssignment = await db
+      .select()
+      .from(teacherAssignments)
+      .where(eq(teacherAssignments.id, id))
+      .limit(1);
+
+    if (existingAssignment.length === 0) {
+      return NextResponse.json(
+        { error: 'Asignación no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que el profesor existe y es activo
+    const teacher = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, teacherId), eq(users.role, 'teacher'), eq(users.isActive, true)))
+      .limit(1);
+
+    if (teacher.length === 0) {
+      return NextResponse.json(
+        { error: 'Profesor no encontrado o inactivo' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que la clase existe y es activa
+    const classExists = await db
+      .select()
+      .from(classes)
+      .where(and(eq(classes.id, classId), eq(classes.isActive, true)))
+      .limit(1);
+
+    if (classExists.length === 0) {
+      return NextResponse.json(
+        { error: 'Clase no encontrada o inactiva' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que la sección existe, es activa y pertenece a la clase
+    const section = await db
+      .select()
+      .from(sections)
+      .where(and(
+        eq(sections.id, sectionId),
+        eq(sections.classId, classId),
+        eq(sections.isActive, true)
+      ))
+      .limit(1);
+
+    if (section.length === 0) {
+      return NextResponse.json(
+        { error: 'Sección no encontrada, inactiva o no pertenece a la clase seleccionada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que no existe ya una asignación activa para esta sección (excluyendo la actual)
+    const conflictingAssignment = await db
+      .select()
+      .from(teacherAssignments)
+      .where(and(
+        eq(teacherAssignments.sectionId, sectionId),
+        eq(teacherAssignments.isActive, true),
+        ne(teacherAssignments.id, id)
+      ))
+      .limit(1);
+
+    if (conflictingAssignment.length > 0) {
+      return NextResponse.json(
+        { error: 'Esta sección ya tiene otro profesor asignado' },
+        { status: 409 }
+      );
+    }
+
+    // Actualizar la asignación
+    const updatedAssignment = await db
+      .update(teacherAssignments)
+      .set({
+        teacherId,
+        classId,
+        sectionId,
+      })
+      .where(eq(teacherAssignments.id, id))
+      .returning();
+
+    return NextResponse.json({
+      message: 'Asignación actualizada exitosamente',
+      assignment: updatedAssignment[0],
+    });
+  } catch (error) {
+    console.error('Error updating teacher assignment:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar asignación de profesor
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: { id: string } }
+) {
+  try {
+    const { id } = params;
+
+    // Verificar que la asignación existe
+    const existingAssignment = await db
+      .select()
+      .from(teacherAssignments)
+      .where(eq(teacherAssignments.id, id))
+      .limit(1);
+
+    if (existingAssignment.length === 0) {
+      return NextResponse.json(
+        { error: 'Asignación no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Marcar la asignación como inactiva (soft delete)
+    await db
+      .update(teacherAssignments)
+      .set({
+        isActive: false,
+      })
+      .where(eq(teacherAssignments.id, id));
+
+    return NextResponse.json({
+      message: 'Asignación eliminada exitosamente',
+    });
+  } catch (error) {
+    console.error('Error deleting teacher assignment:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

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

@@ -0,0 +1,148 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { db } from '@/lib/db';
+import { teacherAssignments, users, classes, sections, periods } from '@/lib/db/schema';
+import { eq, and } from 'drizzle-orm';
+
+// GET - Obtener todas las asignaciones de profesores
+export async function GET() {
+  try {
+    const assignments = await db
+      .select({
+        id: teacherAssignments.id,
+        teacherId: teacherAssignments.teacherId,
+        teacherName: users.firstName,
+        teacherLastName: users.lastName,
+        teacherEmail: users.email,
+        classId: teacherAssignments.classId,
+        className: classes.name,
+        classCode: classes.code,
+        sectionId: teacherAssignments.sectionId,
+        sectionName: sections.name,
+        periodName: periods.name,
+        isActive: teacherAssignments.isActive,
+        createdAt: teacherAssignments.createdAt,
+      })
+      .from(teacherAssignments)
+      .innerJoin(users, eq(teacherAssignments.teacherId, users.id))
+      .innerJoin(classes, eq(teacherAssignments.classId, classes.id))
+      .innerJoin(sections, eq(teacherAssignments.sectionId, sections.id))
+      .innerJoin(periods, eq(classes.periodId, periods.id))
+      .where(eq(teacherAssignments.isActive, true))
+      .orderBy(teacherAssignments.createdAt);
+
+    // Formatear los datos para el frontend
+    const formattedAssignments = assignments.map(assignment => ({
+      ...assignment,
+      teacherName: `${assignment.teacherName} ${assignment.teacherLastName}`,
+    }));
+
+    return NextResponse.json(formattedAssignments);
+  } catch (error) {
+    console.error('Error fetching teacher assignments:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nueva asignación de profesor
+export async function POST(request: NextRequest) {
+  try {
+    const body = await request.json();
+    const { teacherId, classId, sectionId } = body;
+
+    // Validar campos requeridos
+    if (!teacherId || !classId || !sectionId) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar que el profesor existe y es activo
+    const teacher = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, teacherId), eq(users.role, 'teacher'), eq(users.isActive, true)))
+      .limit(1);
+
+    if (teacher.length === 0) {
+      return NextResponse.json(
+        { error: 'Profesor no encontrado o inactivo' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que la clase existe y es activa
+    const classExists = await db
+      .select()
+      .from(classes)
+      .where(and(eq(classes.id, classId), eq(classes.isActive, true)))
+      .limit(1);
+
+    if (classExists.length === 0) {
+      return NextResponse.json(
+        { error: 'Clase no encontrada o inactiva' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que la sección existe, es activa y pertenece a la clase
+    const section = await db
+      .select()
+      .from(sections)
+      .where(and(
+        eq(sections.id, sectionId),
+        eq(sections.classId, classId),
+        eq(sections.isActive, true)
+      ))
+      .limit(1);
+
+    if (section.length === 0) {
+      return NextResponse.json(
+        { error: 'Sección no encontrada, inactiva o no pertenece a la clase seleccionada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar que no existe ya una asignación activa para esta sección
+    const existingAssignment = await db
+      .select()
+      .from(teacherAssignments)
+      .where(and(
+        eq(teacherAssignments.sectionId, sectionId),
+        eq(teacherAssignments.isActive, true)
+      ))
+      .limit(1);
+
+    if (existingAssignment.length > 0) {
+      return NextResponse.json(
+        { error: 'Esta sección ya tiene un profesor asignado' },
+        { status: 409 }
+      );
+    }
+
+    // Crear la asignación
+    const newAssignment = await db
+      .insert(teacherAssignments)
+      .values({
+        teacherId,
+        classId,
+        sectionId,
+        isActive: true,
+      })
+      .returning();
+
+    return NextResponse.json(
+      { message: 'Asignación creada exitosamente', assignment: newAssignment[0] },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error creating teacher assignment:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 5 - 0
src/components/app-sidebar.tsx

@@ -65,6 +65,11 @@ const adminMenuItems = [
     url: "/admin/sections",
     icon: School,
   },
+  {
+    title: "Asignaciones de Profesores",
+    url: "/admin/teacher-assignments",
+    icon: UserCheck,
+  },
   {
     title: "Parciales",
     url: "/admin/partials",