Matthew Trejo 4 bulan lalu
induk
melakukan
3c3ece74cf

+ 315 - 0
src/app/admin/enrollment-history/page.tsx

@@ -0,0 +1,315 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
+import { DashboardLayout } from '@/components/dashboard-layout'
+import { Calendar, Users, Download, Filter } from 'lucide-react'
+
+interface Period {
+  id: string
+  name: string
+  startDate: string
+  endDate: string
+  isActive: boolean
+}
+
+interface EnrollmentHistory {
+  id: string
+  studentId: string
+  studentName: string
+  studentEmail: string
+  studentCedula: string
+  studentAdmissionNumber: string
+  classId: string
+  className: string
+  classCode: string
+  sectionId: string
+  sectionName: string
+  periodName: string
+  isActive: boolean
+  createdAt: string
+}
+
+export default function EnrollmentHistoryPage() {
+  const [enrollments, setEnrollments] = useState<EnrollmentHistory[]>([])
+  const [periods, setPeriods] = useState<Period[]>([])
+  const [selectedPeriod, setSelectedPeriod] = useState<string>('all')
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(null)
+
+  useEffect(() => {
+    fetchPeriods()
+    fetchEnrollmentHistory()
+  }, [])
+
+  useEffect(() => {
+    fetchEnrollmentHistory()
+  }, [selectedPeriod])
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods')
+      if (!response.ok) throw new Error('Error al cargar períodos')
+      const data = await response.json()
+      setPeriods(data)
+    } catch (error) {
+      console.error('Error fetching periods:', error)
+    }
+  }
+
+  const fetchEnrollmentHistory = async () => {
+    try {
+      setLoading(true)
+      const url = selectedPeriod === 'all' 
+        ? '/api/admin/student-enrollments?includeInactive=true'
+        : `/api/admin/student-enrollments?includeInactive=true&periodId=${selectedPeriod}`
+      
+      const response = await fetch(url)
+      if (!response.ok) throw new Error('Error al cargar el historial')
+      
+      const data = await response.json()
+      setEnrollments(data)
+    } catch (error) {
+      console.error('Error fetching enrollment history:', error)
+      setError('Error al cargar el historial de inscripciones')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const exportToCSV = () => {
+    const headers = [
+      'Estudiante',
+      'Cédula',
+      'Número de Admisión',
+      'Email',
+      'Clase',
+      'Código de Clase',
+      'Sección',
+      'Período',
+      'Estado',
+      'Fecha de Inscripción'
+    ]
+
+    const csvContent = [
+      headers.join(','),
+      ...enrollments.map(enrollment => [
+        `"${enrollment.studentName}"`,
+        enrollment.studentCedula,
+        enrollment.studentAdmissionNumber,
+        enrollment.studentEmail,
+        `"${enrollment.className}"`,
+        enrollment.classCode,
+        enrollment.sectionName,
+        `"${enrollment.periodName}"`,
+        enrollment.isActive ? 'Activa' : 'Inactiva',
+        new Date(enrollment.createdAt).toLocaleDateString()
+      ].join(','))
+    ].join('\n')
+
+    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
+    const link = document.createElement('a')
+    const url = URL.createObjectURL(blob)
+    link.setAttribute('href', url)
+    link.setAttribute('download', `historial_inscripciones_${selectedPeriod === 'all' ? 'todos' : selectedPeriod}.csv`)
+    link.style.visibility = 'hidden'
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+  }
+
+  const breadcrumbs = [
+    { label: "Dashboard", href: "/admin/dashboard" },
+    { label: "Historial de Inscripciones" }
+  ]
+
+  // Group enrollments by period for statistics
+  const enrollmentsByPeriod = enrollments.reduce((acc, enrollment) => {
+    if (!acc[enrollment.periodName]) {
+      acc[enrollment.periodName] = { total: 0, active: 0, inactive: 0 }
+    }
+    acc[enrollment.periodName].total++
+    if (enrollment.isActive) {
+      acc[enrollment.periodName].active++
+    } else {
+      acc[enrollment.periodName].inactive++
+    }
+    return acc
+  }, {} as Record<string, { total: number; active: number; inactive: number }>)
+
+  if (loading) {
+    return (
+      <DashboardLayout breadcrumbs={breadcrumbs}>
+        <div className="flex items-center justify-center min-h-[400px]">
+          <div className="text-lg">Cargando historial...</div>
+        </div>
+      </DashboardLayout>
+    )
+  }
+
+  return (
+    <DashboardLayout breadcrumbs={breadcrumbs}>
+      <div className="space-y-6">
+        {/* Header */}
+        <div className="flex justify-between items-start">
+          <div>
+            <h1 className="text-3xl font-bold tracking-tight">Historial de Inscripciones</h1>
+            <p className="text-muted-foreground">
+              Visualiza el historial completo de inscripciones de estudiantes por período.
+            </p>
+          </div>
+          <Button onClick={exportToCSV} className="flex items-center gap-2">
+            <Download className="h-4 w-4" />
+            Exportar CSV
+          </Button>
+        </div>
+
+        {/* Filters */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Filter className="h-5 w-5" />
+              Filtros
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="flex gap-4">
+              <div className="flex-1">
+                <label className="text-sm font-medium mb-2 block">Período</label>
+                <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
+                  <SelectTrigger>
+                    <SelectValue placeholder="Seleccionar período" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="all">Todos los períodos</SelectItem>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+            </div>
+          </CardContent>
+        </Card>
+
+        {/* Statistics Cards */}
+        <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Total Inscripciones</CardTitle>
+              <Users className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{enrollments.length}</div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Inscripciones Activas</CardTitle>
+              <Users className="h-4 w-4 text-green-600" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold text-green-600">
+                {enrollments.filter(e => e.isActive).length}
+              </div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Inscripciones Inactivas</CardTitle>
+              <Users className="h-4 w-4 text-red-600" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold text-red-600">
+                {enrollments.filter(e => !e.isActive).length}
+              </div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">Períodos</CardTitle>
+              <Calendar className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">
+                {Object.keys(enrollmentsByPeriod).length}
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Enrollment History Table */}
+        <Card>
+          <CardHeader>
+            <CardTitle>Historial de Inscripciones</CardTitle>
+          </CardHeader>
+          <CardContent>
+            {error ? (
+              <div className="text-center py-8 text-red-600">{error}</div>
+            ) : enrollments.length === 0 ? (
+              <div className="text-center py-8 text-gray-500">
+                No se encontraron inscripciones para el período seleccionado.
+              </div>
+            ) : (
+              <div className="overflow-x-auto">
+                <Table>
+                  <TableHeader>
+                    <TableRow>
+                      <TableHead>Estudiante</TableHead>
+                      <TableHead>Cédula</TableHead>
+                      <TableHead>Admisión</TableHead>
+                      <TableHead>Clase</TableHead>
+                      <TableHead>Sección</TableHead>
+                      <TableHead>Período</TableHead>
+                      <TableHead>Estado</TableHead>
+                      <TableHead>Fecha</TableHead>
+                    </TableRow>
+                  </TableHeader>
+                  <TableBody>
+                    {enrollments.map((enrollment) => (
+                      <TableRow key={enrollment.id}>
+                        <TableCell>
+                          <div>
+                            <div className="font-medium">{enrollment.studentName}</div>
+                            <div className="text-sm text-muted-foreground">{enrollment.studentEmail}</div>
+                          </div>
+                        </TableCell>
+                        <TableCell>{enrollment.studentCedula}</TableCell>
+                        <TableCell>{enrollment.studentAdmissionNumber}</TableCell>
+                        <TableCell>
+                          <div>
+                            <div className="font-medium">{enrollment.classCode}</div>
+                            <div className="text-sm text-muted-foreground">{enrollment.className}</div>
+                          </div>
+                        </TableCell>
+                        <TableCell>{enrollment.sectionName}</TableCell>
+                        <TableCell>{enrollment.periodName}</TableCell>
+                        <TableCell>
+                          <Badge variant={enrollment.isActive ? 'default' : 'secondary'}>
+                            {enrollment.isActive ? 'Activa' : 'Inactiva'}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>
+                          {new Date(enrollment.createdAt).toLocaleDateString()}
+                        </TableCell>
+                      </TableRow>
+                    ))}
+                  </TableBody>
+                </Table>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+      </div>
+    </DashboardLayout>
+  )
+}

+ 10 - 1
src/app/admin/sections/page.tsx

@@ -22,6 +22,7 @@ interface Section {
   createdAt: string;
   teacherName?: string;
   teacherEmail?: string;
+  enrolledStudents: number;
 }
 
 interface Class {
@@ -300,7 +301,15 @@ export default function SectionsPage() {
                         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'}
+                        Estudiantes: <span className={`font-medium ${
+                          section.enrolledStudents >= section.maxStudents 
+                            ? 'text-red-600' 
+                            : section.enrolledStudents >= section.maxStudents * 0.8 
+                            ? 'text-yellow-600' 
+                            : 'text-green-600'
+                        }`}>
+                          {section.enrolledStudents}
+                        </span> / {section.maxStudents} | Estado: {section.isActive ? 'Activa' : 'Inactiva'}
                       </p>
                     </div>
                     <div className="flex gap-2">

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

@@ -0,0 +1,408 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
+import { Badge } from '@/components/ui/badge'
+import { Trash2, Edit, Plus, UserPlus } from 'lucide-react'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
+import { DashboardLayout } from '@/components/dashboard-layout'
+import { toast } from 'sonner'
+
+interface Student {
+  id: string
+  firstName: string
+  lastName: string
+  email: string
+  cedula: string
+  admissionNumber: string
+}
+
+interface Class {
+  id: string
+  name: string
+  code: string
+  periodName: string
+}
+
+interface Section {
+  id: string
+  name: string
+  className: string
+  classCode: string
+  periodName: string
+  maxStudents: number
+}
+
+interface StudentEnrollment {
+  id: string
+  studentId: string
+  studentName: string
+  studentEmail: string
+  studentCedula: string
+  studentAdmissionNumber: string
+  classId: string
+  className: string
+  classCode: string
+  sectionId: string
+  sectionName: string
+  periodName: string
+  isActive: boolean
+  createdAt: string
+}
+
+interface EnrollmentFormData {
+  studentId: string
+  classId: string
+  sectionId: string
+}
+
+export default function StudentEnrollmentsPage() {
+  const [enrollments, setEnrollments] = useState<StudentEnrollment[]>([])
+  const [students, setStudents] = useState<Student[]>([])
+  const [classes, setClasses] = useState<Class[]>([])
+  const [sections, setSections] = useState<Section[]>([])
+  const [filteredSections, setFilteredSections] = useState<Section[]>([])
+  const [isDialogOpen, setIsDialogOpen] = useState(false)
+  const [editingEnrollment, setEditingEnrollment] = useState<StudentEnrollment | null>(null)
+  const [formData, setFormData] = useState<EnrollmentFormData>({
+    studentId: '',
+    classId: '',
+    sectionId: ''
+  })
+  const [loading, setLoading] = useState(false)
+
+  useEffect(() => {
+    fetchEnrollments()
+    fetchStudents()
+    fetchClasses()
+    fetchSections()
+  }, [])
+
+  useEffect(() => {
+    if (formData.classId) {
+      const classSections = sections.filter(section => section.id.includes(formData.classId))
+      setFilteredSections(classSections)
+    } else {
+      setFilteredSections([])
+    }
+  }, [formData.classId, sections])
+
+  const fetchEnrollments = async () => {
+    try {
+      const response = await fetch('/api/admin/student-enrollments')
+      if (response.ok) {
+        const data = await response.json()
+        setEnrollments(data)
+      }
+    } catch (error) {
+      console.error('Error fetching enrollments:', error)
+      toast.error('Error al cargar las inscripciones')
+    }
+  }
+
+  const fetchStudents = async () => {
+    try {
+      const response = await fetch('/api/admin/students')
+      if (response.ok) {
+        const data = await response.json()
+        setStudents(data)
+      }
+    } catch (error) {
+      console.error('Error fetching students:', error)
+    }
+  }
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes')
+      if (response.ok) {
+        const data = await response.json()
+        setClasses(data)
+      }
+    } 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()
+        setFilteredSections(data)
+        setSections(data)
+      }
+    } catch (error) {
+      console.error('Error fetching sections:', error)
+    }
+  }
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+    setLoading(true)
+
+    try {
+      const url = editingEnrollment 
+        ? `/api/admin/student-enrollments/${editingEnrollment.id}`
+        : '/api/admin/student-enrollments'
+      
+      const method = editingEnrollment ? 'PUT' : 'POST'
+      
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      })
+
+      if (response.ok) {
+        toast.success(editingEnrollment ? 'Inscripción actualizada exitosamente' : 'Inscripción creada exitosamente')
+        setIsDialogOpen(false)
+        resetForm()
+        fetchEnrollments()
+      } else {
+        const error = await response.json()
+        toast.error(error.error || 'Error al procesar la inscripción')
+      }
+    } catch (error) {
+      console.error('Error:', error)
+      toast.error('Error al procesar la inscripción')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const handleEdit = (enrollment: StudentEnrollment) => {
+    setEditingEnrollment(enrollment)
+    setFormData({
+      studentId: enrollment.studentId,
+      classId: enrollment.classId,
+      sectionId: enrollment.sectionId
+    })
+    setIsDialogOpen(true)
+  }
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Estás seguro de que quieres eliminar esta inscripción?')) {
+      return
+    }
+
+    try {
+      const response = await fetch(`/api/admin/student-enrollments/${id}`, {
+        method: 'DELETE',
+      })
+
+      if (response.ok) {
+        toast.success('Inscripción eliminada exitosamente')
+        fetchEnrollments()
+      } else {
+        const error = await response.json()
+        toast.error(error.error || 'Error al eliminar la inscripción')
+      }
+    } catch (error) {
+      console.error('Error:', error)
+      toast.error('Error al eliminar la inscripción')
+    }
+  }
+
+  const resetForm = () => {
+    setFormData({
+      studentId: '',
+      classId: '',
+      sectionId: ''
+    })
+    setEditingEnrollment(null)
+  }
+
+  const handleDialogClose = () => {
+    setIsDialogOpen(false)
+    resetForm()
+  }
+
+  const breadcrumbs = [
+    { label: "Dashboard", href: "/admin/dashboard" },
+    { label: "Inscripciones de Estudiantes" }
+  ]
+
+  return (
+    <DashboardLayout breadcrumbs={breadcrumbs}>
+      <div className="space-y-6">
+        <div className="flex justify-between items-center">
+          <div className="flex items-center gap-2">
+            <UserPlus className="h-6 w-6" />
+            <h1 className="text-2xl font-bold">Inscripciones de Estudiantes</h1>
+          </div>
+        <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
+          <DialogTrigger asChild>
+            <Button onClick={() => setIsDialogOpen(true)}>
+              <Plus className="mr-2 h-4 w-4" />
+              Nueva Inscripción
+            </Button>
+          </DialogTrigger>
+          <DialogContent className="sm:max-w-[500px]">
+            <DialogHeader>
+              <DialogTitle>
+                {editingEnrollment ? 'Editar Inscripción' : 'Nueva Inscripción'}
+              </DialogTitle>
+            </DialogHeader>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div className="space-y-2">
+                <Label htmlFor="studentId">Estudiante</Label>
+                <Select
+                  value={formData.studentId}
+                  onValueChange={(value) => setFormData({ ...formData, studentId: value })}
+                  required
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona un estudiante" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {students.map((student) => (
+                      <SelectItem key={student.id} value={student.id}>
+                        {student.firstName} {student.lastName} - {student.admissionNumber}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="space-y-2">
+                <Label htmlFor="classId">Clase</Label>
+                <Select
+                  value={formData.classId}
+                  onValueChange={(value) => setFormData({ ...formData, classId: value, sectionId: '' })}
+                  required
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona una clase" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {classes.map((cls) => (
+                      <SelectItem key={cls.id} value={cls.id}>
+                        {cls.code} - {cls.name} ({cls.periodName})
+                      </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 })}
+                  required
+                  disabled={!formData.classId}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Selecciona una sección" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {filteredSections.map((section) => (
+                      <SelectItem key={section.id} value={section.id}>
+                        {section.name} - {section.className} (Máx: {section.maxStudents})
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="flex justify-end space-x-2">
+                <Button type="button" variant="outline" onClick={handleDialogClose}>
+                  Cancelar
+                </Button>
+                <Button type="submit" disabled={loading}>
+                  {loading ? 'Procesando...' : (editingEnrollment ? 'Actualizar' : 'Crear')}
+                </Button>
+              </div>
+            </form>
+          </DialogContent>
+        </Dialog>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <UserPlus className="h-5 w-5" />
+              Inscripciones Registradas
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>Estudiante</TableHead>
+                  <TableHead>Número de Matrícula</TableHead>
+                  <TableHead>Clase</TableHead>
+                  <TableHead>Sección</TableHead>
+                  <TableHead>Período</TableHead>
+                  <TableHead>Estado</TableHead>
+                  <TableHead>Fecha de Inscripción</TableHead>
+                  <TableHead>Acciones</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {enrollments.map((enrollment) => (
+                  <TableRow key={enrollment.id}>
+                    <TableCell>
+                      <div>
+                        <div className="font-medium">{enrollment.studentName}</div>
+                        <div className="text-sm text-muted-foreground">{enrollment.studentEmail}</div>
+                      </div>
+                    </TableCell>
+                    <TableCell>{enrollment.studentAdmissionNumber}</TableCell>
+                    <TableCell>
+                      <div>
+                        <div className="font-medium">{enrollment.classCode}</div>
+                        <div className="text-sm text-muted-foreground">{enrollment.className}</div>
+                      </div>
+                    </TableCell>
+                    <TableCell>{enrollment.sectionName}</TableCell>
+                    <TableCell>{enrollment.periodName}</TableCell>
+                    <TableCell>
+                      <Badge variant={enrollment.isActive ? 'default' : 'secondary'}>
+                        {enrollment.isActive ? 'Activa' : 'Inactiva'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      {new Date(enrollment.createdAt).toLocaleDateString()}
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex space-x-2">
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleEdit(enrollment)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => handleDelete(enrollment.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+            {enrollments.length === 0 && (
+              <div className="text-center py-8 text-muted-foreground">
+                No hay inscripciones registradas
+              </div>
+            )}
+          </CardContent>
+        </Card>
+      </div>
+    </DashboardLayout>
+  )
+}

+ 20 - 9
src/app/api/admin/sections/route.ts

@@ -2,7 +2,8 @@ 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, teacherAssignments, users, eq, and } from '@/lib/db/schema';
+import { sections, classes, periods, teacherAssignments, users, studentEnrollments } from '@/lib/db/schema';
+import { eq, and, count } from 'drizzle-orm';
 
 export async function GET() {
   try {
@@ -40,15 +41,25 @@ export async function GET() {
       .leftJoin(users, eq(teacherAssignments.teacherId, users.id))
       .orderBy(classes.code, sections.name);
 
-    // 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,
-    }));
+    // Obtener el conteo de estudiantes inscritos para cada sección
+    const sectionsWithStudents = await Promise.all(
+      allSections.map(async (section) => {
+        const enrollmentCount = await db
+          .select({ count: count(studentEnrollments.id) })
+          .from(studentEnrollments)
+          .where(eq(studentEnrollments.sectionId, section.id as string));
 
-    return NextResponse.json(formattedSections);
+        return {
+          ...section,
+          teacherName: section.teacherName && section.teacherLastName 
+            ? `${section.teacherName} ${section.teacherLastName}` 
+            : null,
+          enrolledStudents: enrollmentCount[0]?.count || 0,
+        };
+      })
+    );
+
+    return NextResponse.json(sectionsWithStudents);
   } catch (error) {
     console.error('Error fetching sections:', error);
     return NextResponse.json(

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

@@ -0,0 +1,194 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { db } from '@/lib/db'
+import { studentEnrollments, users, classes, sections } from '@/lib/db/schema'
+import { eq, and, ne } from 'drizzle-orm'
+
+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(
+        { error: 'No autorizado' },
+        { status: 401 }
+      )
+    }
+
+    const { id } = params
+    const { studentId, classId, sectionId } = await request.json()
+
+    // Validate required fields
+    if (!studentId || !classId || !sectionId) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      )
+    }
+
+    // Verify enrollment exists
+    const existingEnrollment = await db
+      .select()
+      .from(studentEnrollments)
+      .where(eq(studentEnrollments.id, id))
+      .limit(1)
+
+    if (existingEnrollment.length === 0) {
+      return NextResponse.json(
+        { error: 'Inscripción no encontrada' },
+        { status: 404 }
+      )
+    }
+
+    // Verify student exists and is active
+    const student = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, studentId), eq(users.role, 'student'), eq(users.isActive, true)))
+      .limit(1)
+
+    if (student.length === 0) {
+      return NextResponse.json(
+        { error: 'Estudiante no encontrado o inactivo' },
+        { status: 404 }
+      )
+    }
+
+    // Verify class exists and is active
+    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 }
+      )
+    }
+
+    // Verify section exists, is active, and belongs to the class
+    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 }
+      )
+    }
+
+    // Check if another student is already enrolled in this section (excluding current enrollment)
+    const conflictingEnrollment = await db
+      .select()
+      .from(studentEnrollments)
+      .where(and(
+        eq(studentEnrollments.studentId, studentId),
+        eq(studentEnrollments.sectionId, sectionId),
+        eq(studentEnrollments.isActive, true),
+        ne(studentEnrollments.id, id)
+      ))
+      .limit(1)
+
+    if (conflictingEnrollment.length > 0) {
+      return NextResponse.json(
+        { error: 'El estudiante ya está inscrito en esta sección' },
+        { status: 409 }
+      )
+    }
+
+    // Check section capacity (excluding current enrollment)
+    const currentEnrollments = await db
+      .select()
+      .from(studentEnrollments)
+      .where(and(
+        eq(studentEnrollments.sectionId, sectionId),
+        eq(studentEnrollments.isActive, true),
+        ne(studentEnrollments.id, id)
+      ))
+
+    if (currentEnrollments.length >= section[0].maxStudents) {
+      return NextResponse.json(
+        { error: 'La sección ha alcanzado su capacidad máxima' },
+        { status: 409 }
+      )
+    }
+
+    // Update the enrollment
+    const updatedEnrollment = await db
+      .update(studentEnrollments)
+      .set({
+        studentId,
+        classId,
+        sectionId,
+      })
+      .where(eq(studentEnrollments.id, id))
+      .returning()
+
+    return NextResponse.json(updatedEnrollment[0])
+  } catch (error) {
+    console.error('Error updating student enrollment:', error)
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    )
+  }
+}
+
+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(
+        { error: 'No autorizado' },
+        { status: 401 }
+      )
+    }
+
+    const { id } = params
+
+    // Verify enrollment exists
+    const existingEnrollment = await db
+      .select()
+      .from(studentEnrollments)
+      .where(eq(studentEnrollments.id, id))
+      .limit(1)
+
+    if (existingEnrollment.length === 0) {
+      return NextResponse.json(
+        { error: 'Inscripción no encontrada' },
+        { status: 404 }
+      )
+    }
+
+    // Soft delete the enrollment by setting isActive to false
+    const deletedEnrollment = await db
+      .update(studentEnrollments)
+      .set({
+        isActive: false,
+      })
+      .where(eq(studentEnrollments.id, id))
+      .returning()
+
+    return NextResponse.json({
+      message: 'Inscripción eliminada exitosamente',
+      enrollment: deletedEnrollment[0]
+    })
+  } catch (error) {
+    console.error('Error deleting student enrollment:', error)
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    )
+  }
+}

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

@@ -0,0 +1,211 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { db } from '@/lib/db'
+import { studentEnrollments, users, classes, sections, periods } from '@/lib/db/schema'
+import { eq, and, count } from 'drizzle-orm'
+
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      )
+    }
+
+    const { searchParams } = new URL(request.url)
+    const includeInactive = searchParams.get('includeInactive') === 'true'
+    const periodId = searchParams.get('periodId')
+
+    // Build the where conditions
+    let whereConditions: any[] = []
+    if (!includeInactive) {
+      whereConditions.push(eq(studentEnrollments.isActive, true))
+    }
+    if (periodId) {
+      whereConditions.push(eq(periods.id, periodId))
+    }
+
+    // Get student enrollments with student, class, section, and period information
+    let query = db
+      .select({
+        id: studentEnrollments.id,
+        studentId: studentEnrollments.studentId,
+        studentFirstName: users.firstName,
+        studentLastName: users.lastName,
+        studentEmail: users.email,
+        studentCedula: users.cedula,
+        studentAdmissionNumber: users.admissionNumber,
+        classId: studentEnrollments.classId,
+        className: classes.name,
+        classCode: classes.code,
+        sectionId: studentEnrollments.sectionId,
+        sectionName: sections.name,
+        periodName: periods.name,
+        isActive: studentEnrollments.isActive,
+        createdAt: studentEnrollments.createdAt,
+      })
+      .from(studentEnrollments)
+      .innerJoin(users, eq(studentEnrollments.studentId, users.id))
+      .innerJoin(classes, eq(studentEnrollments.classId, classes.id))
+      .innerJoin(sections, eq(studentEnrollments.sectionId, sections.id))
+      .innerJoin(periods, eq(classes.periodId, periods.id))
+
+    if (whereConditions.length > 0) {
+      query = query.where(and(...whereConditions)) as any
+    }
+
+    const result = await query.orderBy(studentEnrollments.createdAt)
+
+    // Format the data
+    const formattedEnrollments = result.map(enrollment => ({
+      id: enrollment.id,
+      studentId: enrollment.studentId,
+      studentName: `${enrollment.studentFirstName} ${enrollment.studentLastName}`,
+      studentEmail: enrollment.studentEmail,
+      studentCedula: enrollment.studentCedula,
+      studentAdmissionNumber: enrollment.studentAdmissionNumber,
+      classId: enrollment.classId,
+      className: enrollment.className,
+      classCode: enrollment.classCode,
+      sectionId: enrollment.sectionId,
+      sectionName: enrollment.sectionName,
+      periodName: enrollment.periodName,
+      isActive: enrollment.isActive,
+      createdAt: enrollment.createdAt,
+    }))
+
+    return NextResponse.json(formattedEnrollments)
+  } catch (error) {
+    console.error('Error fetching student enrollments:', error)
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    )
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      )
+    }
+
+    const { studentId, classId, sectionId } = await request.json()
+
+    // Validate required fields
+    if (!studentId || !classId || !sectionId) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      )
+    }
+
+    // Verify student exists and is active
+    const student = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, studentId), eq(users.role, 'student'), eq(users.isActive, true)))
+      .limit(1)
+
+    if (student.length === 0) {
+      return NextResponse.json(
+        { error: 'Estudiante no encontrado o inactivo' },
+        { status: 404 }
+      )
+    }
+
+    // Verify class exists and is active
+    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 }
+      )
+    }
+
+    // Verify section exists, is active, and belongs to the class
+    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 }
+      )
+    }
+
+    // Check if student is already enrolled in this section
+    const existingEnrollment = await db
+      .select()
+      .from(studentEnrollments)
+      .where(and(
+        eq(studentEnrollments.studentId, studentId),
+        eq(studentEnrollments.sectionId, sectionId),
+        eq(studentEnrollments.isActive, true)
+      ))
+      .limit(1)
+
+    if (existingEnrollment.length > 0) {
+      return NextResponse.json(
+        { error: 'El estudiante ya está inscrito en esta sección' },
+        { status: 409 }
+      )
+    }
+
+    // Check section capacity
+    const currentEnrollments = await db
+      .select()
+      .from(studentEnrollments)
+      .where(and(
+        eq(studentEnrollments.sectionId, sectionId),
+        eq(studentEnrollments.isActive, true)
+      ))
+
+    if (currentEnrollments.length >= section[0].maxStudents) {
+      return NextResponse.json(
+        { error: 'La sección ha alcanzado su capacidad máxima' },
+        { status: 409 }
+      )
+    }
+
+    // Create the enrollment
+    const newEnrollment = await db
+      .insert(studentEnrollments)
+      .values({
+        studentId,
+        classId,
+        sectionId,
+        isActive: true,
+      })
+      .returning()
+
+    return NextResponse.json(newEnrollment[0], { status: 201 })
+  } catch (error) {
+    console.error('Error creating student enrollment:', error)
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    )
+  }
+}

+ 64 - 0
src/app/api/student/sections/route.ts

@@ -0,0 +1,64 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { db } from '@/lib/db'
+import { studentEnrollments, sections, classes, periods } from '@/lib/db/schema'
+import { eq } from 'drizzle-orm'
+
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session || session.user.role !== 'student') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      )
+    }
+
+    const studentId = session.user.id
+
+    // Get enrolled sections with class and period information
+    const result = await db
+      .select({
+        sectionId: sections.id,
+        sectionName: sections.name,
+        className: classes.name,
+        classId: classes.id,
+        classCode: classes.code,
+        periodName: periods.name,
+        periodId: periods.id,
+        maxStudents: sections.maxStudents,
+        isActive: sections.isActive,
+        enrollmentDate: studentEnrollments.createdAt
+      })
+      .from(studentEnrollments)
+      .innerJoin(sections, eq(studentEnrollments.sectionId, sections.id))
+      .innerJoin(classes, eq(studentEnrollments.classId, classes.id))
+      .innerJoin(periods, eq(classes.periodId, periods.id))
+      .where(eq(studentEnrollments.studentId, studentId))
+      .orderBy(classes.code, sections.name)
+
+    // Format the response
+    const formattedSections = result.map(section => ({
+      id: section.sectionId,
+      name: section.sectionName,
+      className: section.className,
+      classId: section.classId,
+      classCode: section.classCode,
+      periodName: section.periodName,
+      periodId: section.periodId,
+      maxStudents: section.maxStudents,
+      isActive: section.isActive,
+      enrollmentDate: section.enrollmentDate
+    }))
+
+    return NextResponse.json(formattedSections)
+  } catch (error) {
+    console.error('Error fetching student sections:', error)
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    )
+  }
+}

+ 199 - 0
src/app/student/page.tsx

@@ -0,0 +1,199 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { DashboardLayout } from '@/components/dashboard-layout'
+import { BookOpen, Calendar, Users, GraduationCap } from 'lucide-react'
+
+interface EnrolledSection {
+  id: string
+  name: string
+  className: string
+  classId: string
+  classCode: string
+  periodName: string
+  periodId: string
+  maxStudents: number
+  isActive: boolean
+  enrollmentDate: string
+}
+
+export default function StudentPage() {
+  const [enrolledSections, setEnrolledSections] = useState<EnrolledSection[]>([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(null)
+
+  useEffect(() => {
+    fetchEnrolledSections()
+  }, [])
+
+  const fetchEnrolledSections = async () => {
+    try {
+      setLoading(true)
+      const response = await fetch('/api/student/sections')
+      
+      if (!response.ok) {
+        throw new Error('Error al cargar las secciones')
+      }
+      
+      const data = await response.json()
+      setEnrolledSections(data)
+    } catch (error) {
+      console.error('Error fetching enrolled sections:', error)
+      setError('Error al cargar las secciones inscritas')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const breadcrumbs = [
+    { label: "Dashboard Estudiante" }
+  ]
+
+  if (loading) {
+    return (
+      <DashboardLayout breadcrumbs={breadcrumbs}>
+        <div className="flex items-center justify-center min-h-[400px]">
+          <div className="text-lg">Cargando secciones...</div>
+        </div>
+      </DashboardLayout>
+    )
+  }
+
+  if (error) {
+    return (
+      <DashboardLayout breadcrumbs={breadcrumbs}>
+        <div className="flex items-center justify-center min-h-[400px]">
+          <div className="text-lg text-red-600">{error}</div>
+        </div>
+      </DashboardLayout>
+    )
+  }
+
+  // Group sections by period
+  const sectionsByPeriod = enrolledSections.reduce((acc, section) => {
+    if (!acc[section.periodName]) {
+      acc[section.periodName] = []
+    }
+    acc[section.periodName].push(section)
+    return acc
+  }, {} as Record<string, EnrolledSection[]>)
+
+  return (
+    <DashboardLayout breadcrumbs={breadcrumbs}>
+      <div className="space-y-6">
+        {/* Header */}
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">Dashboard Estudiante</h1>
+          <p className="text-muted-foreground">
+            Bienvenido a tu panel de estudiante. Aquí puedes ver tus secciones inscritas.
+          </p>
+        </div>
+
+        {/* Stats Cards */}
+        <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Total Secciones
+              </CardTitle>
+              <BookOpen className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{enrolledSections.length}</div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Clases Diferentes
+              </CardTitle>
+              <GraduationCap className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">
+                {new Set(enrolledSections.map(s => s.classId)).size}
+              </div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Períodos Activos
+              </CardTitle>
+              <Calendar className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">
+                {Object.keys(sectionsByPeriod).length}
+              </div>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Secciones Activas
+              </CardTitle>
+              <Users className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">
+                {enrolledSections.filter(s => s.isActive).length}
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Enrolled Sections by Period */}
+        {Object.keys(sectionsByPeriod).length === 0 ? (
+          <Card>
+            <CardContent className="text-center py-8">
+              <p className="text-gray-500">No tienes secciones inscritas actualmente.</p>
+            </CardContent>
+          </Card>
+        ) : (
+          Object.entries(sectionsByPeriod).map(([periodName, sections]) => (
+            <Card key={periodName}>
+              <CardHeader>
+                <CardTitle className="flex items-center gap-2">
+                  <Calendar className="h-5 w-5" />
+                  Período: {periodName}
+                </CardTitle>
+              </CardHeader>
+              <CardContent>
+                <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+                  {sections.map((section) => (
+                    <div
+                      key={section.id}
+                      className="border rounded-lg p-4 hover:shadow-md transition-shadow"
+                    >
+                      <div className="flex justify-between items-start mb-2">
+                        <div>
+                          <h3 className="font-semibold">{section.name}</h3>
+                          <p className="text-sm text-muted-foreground">
+                            {section.classCode} - {section.className}
+                          </p>
+                        </div>
+                        <Badge variant={section.isActive ? 'default' : 'secondary'}>
+                          {section.isActive ? 'Activa' : 'Inactiva'}
+                        </Badge>
+                      </div>
+                      <div className="space-y-1 text-sm text-muted-foreground">
+                        <p>Capacidad máxima: {section.maxStudents} estudiantes</p>
+                        <p>Fecha de inscripción: {new Date(section.enrollmentDate).toLocaleDateString()}</p>
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              </CardContent>
+            </Card>
+          ))
+        )}
+      </div>
+    </DashboardLayout>
+  )
+}

+ 24 - 3
src/components/app-sidebar.tsx

@@ -12,6 +12,7 @@ import {
   BarChart3,
   ClipboardList,
   UserCheck,
+  UserPlus,
   School,
 } from "lucide-react"
 
@@ -70,6 +71,16 @@ const adminMenuItems = [
     url: "/admin/teacher-assignments",
     icon: UserCheck,
   },
+  {
+    title: "Inscripciones de Estudiantes",
+    url: "/admin/student-enrollments",
+    icon: UserPlus,
+  },
+  {
+    title: "Historial de Inscripciones",
+    url: "/admin/enrollment-history",
+    icon: BarChart3,
+  },
   {
     title: "Parciales",
     url: "/admin/partials",
@@ -96,6 +107,15 @@ const teacherMenuItems = [
   },
 ]
 
+// Menú para Estudiante
+const studentMenuItems = [
+  {
+    title: "Dashboard",
+    url: "/student",
+    icon: Home,
+  },
+]
+
 export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
   const { data: session, status } = useSession()
   
@@ -109,7 +129,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
 
   const isAdmin = session.user.role === "admin"
   const isTeacher = session.user.role === "teacher"
-  const menuItems = isAdmin ? adminMenuItems : teacherMenuItems
+  const isStudent = session.user.role === "student"
+  const menuItems = isAdmin ? adminMenuItems : isTeacher ? teacherMenuItems : studentMenuItems
   const userInitials = session.user.firstName?.[0] + (session.user.lastName?.[0] || "")
 
   return (
@@ -118,14 +139,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
         <SidebarMenu>
           <SidebarMenuItem>
             <SidebarMenuButton size="lg" asChild>
-              <Link href={isAdmin ? "/admin" : "/teacher"}>
+              <Link href={isAdmin ? "/admin" : isTeacher ? "/teacher" : "/student"}>
                 <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                   <School className="size-4" />
                 </div>
                 <div className="grid flex-1 text-left text-sm leading-tight">
                   <span className="truncate font-semibold">Sistema de Asistencia</span>
                   <span className="truncate text-xs">
-                    {isAdmin ? "Administrador" : "Profesor"}
+                    {isAdmin ? "Administrador" : isTeacher ? "Profesor" : "Estudiante"}
                   </span>
                 </div>
               </Link>

+ 143 - 0
src/components/ui/dialog.tsx

@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />
+}
+
+function DialogTrigger({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
+}
+
+function DialogPortal({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
+}
+
+function DialogClose({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogContent({
+  className,
+  children,
+  showCloseButton = true,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Content> & {
+  showCloseButton?: boolean
+}) {
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        {showCloseButton && (
+          <DialogPrimitive.Close
+            data-slot="dialog-close"
+            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
+          >
+            <XIcon />
+            <span className="sr-only">Close</span>
+          </DialogPrimitive.Close>
+        )}
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn("text-lg leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+}

+ 116 - 0
src/components/ui/table.tsx

@@ -0,0 +1,116 @@
+"use client"
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Table({ className, ...props }: React.ComponentProps<"table">) {
+  return (
+    <div
+      data-slot="table-container"
+      className="relative w-full overflow-x-auto"
+    >
+      <table
+        data-slot="table"
+        className={cn("w-full caption-bottom text-sm", className)}
+        {...props}
+      />
+    </div>
+  )
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+  return (
+    <thead
+      data-slot="table-header"
+      className={cn("[&_tr]:border-b", className)}
+      {...props}
+    />
+  )
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+  return (
+    <tbody
+      data-slot="table-body"
+      className={cn("[&_tr:last-child]:border-0", className)}
+      {...props}
+    />
+  )
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+  return (
+    <tfoot
+      data-slot="table-footer"
+      className={cn(
+        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+  return (
+    <tr
+      data-slot="table-row"
+      className={cn(
+        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+  return (
+    <th
+      data-slot="table-head"
+      className={cn(
+        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+  return (
+    <td
+      data-slot="table-cell"
+      className={cn(
+        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function TableCaption({
+  className,
+  ...props
+}: React.ComponentProps<"caption">) {
+  return (
+    <caption
+      data-slot="table-caption"
+      className={cn("text-muted-foreground mt-4 text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Table,
+  TableHeader,
+  TableBody,
+  TableFooter,
+  TableHead,
+  TableRow,
+  TableCell,
+  TableCaption,
+}