|
@@ -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>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|