| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { MainLayout } from '@/components/layout/main-layout';
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
- import { Badge } from '@/components/ui/badge';
- 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 { Progress } from '@/components/ui/progress';
- import {
- BookOpen,
- Users,
- Calendar,
- Clock,
- TrendingUp,
- Search,
- Filter,
- CheckCircle,
- XCircle,
- AlertCircle,
- User,
- GraduationCap
- } from 'lucide-react';
- import { toast } from 'sonner';
- import { LoadingSpinner } from '@/components/ui/spinner';
- interface Teacher {
- id: string;
- firstName: string;
- lastName: string;
- email: string;
- }
- interface Period {
- id: string;
- name: string;
- startDate: string;
- endDate: string;
- isActive: boolean;
- }
- interface Class {
- id: string;
- name: string;
- code: string;
- description: string | null;
- period: Period;
- }
- interface Section {
- id: string;
- name: string;
- studentCount: number;
- class: Class;
- teachers: Teacher[];
- }
- interface AttendanceStats {
- totalRecords: number;
- present: number;
- absent: number;
- justified: number;
- attendanceRate: number;
- }
- interface EnrolledClass {
- id: string;
- enrolledAt: string;
- section: Section;
- attendanceStats: AttendanceStats;
- }
- interface Student {
- id: string;
- firstName: string;
- lastName: string;
- admissionNumber: string;
- }
- interface ClassesResponse {
- classes: EnrolledClass[];
- student: Student;
- }
- export default function StudentClassesPage() {
- const [classes, setClasses] = useState<EnrolledClass[]>([]);
- const [student, setStudent] = useState<Student | null>(null);
- const [loading, setLoading] = useState(true);
- const [searchTerm, setSearchTerm] = useState('');
- const [selectedPeriod, setSelectedPeriod] = useState<string>('all');
- const [selectedStatus, setSelectedStatus] = useState<string>('all');
- useEffect(() => {
- fetchClasses();
- }, []);
- const fetchClasses = async () => {
- try {
- setLoading(true);
- const response = await fetch('/api/student/classes');
- if (!response.ok) {
- throw new Error('Error al cargar las clases');
- }
- const data: ClassesResponse = await response.json();
- setClasses(data.classes);
- setStudent(data.student);
- } catch (error) {
- console.error('Error:', error);
- toast.error('Error al cargar las clases matriculadas');
- } finally {
- setLoading(false);
- }
- };
- const filteredClasses = classes.filter(enrolledClass => {
- const matchesSearch =
- enrolledClass.section.class.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- enrolledClass.section.class.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
- enrolledClass.section.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- enrolledClass.section.class.period.name.toLowerCase().includes(searchTerm.toLowerCase());
-
- const matchesPeriod = selectedPeriod === 'all' || enrolledClass.section.class.period.id === selectedPeriod;
- const matchesStatus = selectedStatus === 'all' ||
- (selectedStatus === 'active' && enrolledClass.section.class.period.isActive) ||
- (selectedStatus === 'inactive' && !enrolledClass.section.class.period.isActive);
-
- return matchesSearch && matchesPeriod && matchesStatus;
- });
- const periods = Array.from(new Set(classes.map(c => c.section.class.period)))
- .filter((period, index, self) => self.findIndex(p => p.id === period.id) === index);
- const activeClasses = filteredClasses.filter(c => c.section.class.period.isActive);
- const totalAttendanceRecords = filteredClasses.reduce((sum, c) => sum + c.attendanceStats.totalRecords, 0);
- const averageAttendanceRate = filteredClasses.length > 0
- ? Math.round(filteredClasses.reduce((sum, c) => sum + c.attendanceStats.attendanceRate, 0) / filteredClasses.length)
- : 0;
- const getAttendanceColor = (rate: number) => {
- if (rate >= 90) return 'text-green-600';
- if (rate >= 75) return 'text-yellow-600';
- return 'text-red-600';
- };
- const getAttendanceBadgeVariant = (rate: number): 'default' | 'secondary' | 'destructive' => {
- if (rate >= 90) return 'default';
- if (rate >= 75) return 'secondary';
- return 'destructive';
- };
- const formatDate = (dateString: string) => {
- return new Date(dateString).toLocaleDateString('es-ES', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- });
- };
- const getTeacherNames = (teachers: Teacher[]) => {
- return teachers.map(teacher => `${teacher.firstName} ${teacher.lastName}`).join(', ');
- };
- if (loading) {
- return (
- <MainLayout requiredRole="STUDENT" title="Mis Clases">
- <div className="flex items-center justify-center h-64">
- <LoadingSpinner size="lg" />
- </div>
- </MainLayout>
- );
- }
- return (
- <MainLayout requiredRole="STUDENT" title="Mis Clases">
- <div className="space-y-6">
- {/* Header */}
- <div className="flex justify-between items-center">
- <div>
- <h1 className="text-3xl font-bold tracking-tight">Mis Clases Matriculadas</h1>
- <p className="text-muted-foreground">
- {student && `${student.firstName} ${student.lastName} - ${student.admissionNumber}`}
- </p>
- </div>
- </div>
- {/* Estadísticas Generales */}
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">Total de Clases</CardTitle>
- <BookOpen className="h-4 w-4 text-muted-foreground" />
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{filteredClasses.length}</div>
- <p className="text-xs text-muted-foreground">
- {activeClasses.length} activas
- </p>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">Registros de Asistencia</CardTitle>
- <Calendar className="h-4 w-4 text-muted-foreground" />
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{totalAttendanceRecords}</div>
- <p className="text-xs text-muted-foreground">
- Total acumulado
- </p>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">Asistencia Promedio</CardTitle>
- <TrendingUp className="h-4 w-4 text-muted-foreground" />
- </CardHeader>
- <CardContent>
- <div className={`text-2xl font-bold ${getAttendanceColor(averageAttendanceRate)}`}>
- {averageAttendanceRate}%
- </div>
- <p className="text-xs text-muted-foreground">
- Todas las clases
- </p>
- </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 Únicos</CardTitle>
- <Clock className="h-4 w-4 text-muted-foreground" />
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{periods.length}</div>
- <p className="text-xs text-muted-foreground">
- Histórico
- </p>
- </CardContent>
- </Card>
- </div>
- {/* Filtros */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Filter className="h-5 w-5" />
- Filtros
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="space-y-2">
- <Label htmlFor="search">Buscar</Label>
- <div className="relative">
- <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
- <Input
- id="search"
- placeholder="Buscar por clase, código o sección..."
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- className="pl-10"
- />
- </div>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="period">Período</Label>
- <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
- <SelectTrigger>
- <SelectValue placeholder="Filtrar por período" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">Todos los períodos</SelectItem>
- {periods.map((period) => (
- <SelectItem key={period.id} value={period.id}>
- {period.name} {period.isActive && '(Activo)'}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="status">Estado</Label>
- <Select value={selectedStatus} onValueChange={setSelectedStatus}>
- <SelectTrigger>
- <SelectValue placeholder="Filtrar por estado" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">Todos los estados</SelectItem>
- <SelectItem value="active">Períodos activos</SelectItem>
- <SelectItem value="inactive">Períodos inactivos</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- </CardContent>
- </Card>
- {/* Lista de Clases */}
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
- {filteredClasses.length > 0 ? (
- filteredClasses.map((enrolledClass) => (
- <Card key={enrolledClass.id} className="hover:shadow-md transition-shadow">
- <CardHeader>
- <div className="flex justify-between items-start">
- <div className="space-y-1">
- <CardTitle className="flex items-center gap-2">
- <GraduationCap className="h-5 w-5" />
- {enrolledClass.section.class.name}
- </CardTitle>
- <CardDescription>
- {enrolledClass.section.class.code} - {enrolledClass.section.name}
- </CardDescription>
- </div>
- <Badge variant={enrolledClass.section.class.period.isActive ? 'default' : 'secondary'}>
- {enrolledClass.section.class.period.name}
- </Badge>
- </div>
- </CardHeader>
-
- <CardContent className="space-y-4">
- {/* Información de la clase */}
- <div className="space-y-2">
- {enrolledClass.section.class.description && (
- <p className="text-sm text-muted-foreground">
- {enrolledClass.section.class.description}
- </p>
- )}
-
- <div className="flex items-center gap-4 text-sm text-muted-foreground">
- <div className="flex items-center gap-1">
- <Users className="h-4 w-4" />
- {enrolledClass.section.studentCount} estudiantes
- </div>
- <div className="flex items-center gap-1">
- <User className="h-4 w-4" />
- {getTeacherNames(enrolledClass.section.teachers) || 'Sin profesor asignado'}
- </div>
- </div>
-
- <div className="text-sm text-muted-foreground">
- <strong>Matriculado:</strong> {formatDate(enrolledClass.enrolledAt)}
- </div>
-
- <div className="text-sm text-muted-foreground">
- <strong>Período:</strong> {formatDate(enrolledClass.section.class.period.startDate)} - {formatDate(enrolledClass.section.class.period.endDate)}
- </div>
- </div>
- {/* Estadísticas de Asistencia */}
- <div className="space-y-3">
- <div className="flex justify-between items-center">
- <h4 className="text-sm font-medium">Asistencia</h4>
- <Badge variant={getAttendanceBadgeVariant(enrolledClass.attendanceStats.attendanceRate)}>
- {enrolledClass.attendanceStats.attendanceRate}%
- </Badge>
- </div>
-
- <Progress
- value={enrolledClass.attendanceStats.attendanceRate}
- className="h-2"
- />
-
- <div className="grid grid-cols-4 gap-2 text-xs">
- <div className="text-center">
- <div className="flex items-center justify-center gap-1">
- <CheckCircle className="h-3 w-3 text-green-600" />
- <span className="font-medium">{enrolledClass.attendanceStats.present}</span>
- </div>
- <div className="text-muted-foreground">Presente</div>
- </div>
-
- <div className="text-center">
- <div className="flex items-center justify-center gap-1">
- <XCircle className="h-3 w-3 text-red-600" />
- <span className="font-medium">{enrolledClass.attendanceStats.absent}</span>
- </div>
- <div className="text-muted-foreground">Ausente</div>
- </div>
-
- <div className="text-center">
- <div className="flex items-center justify-center gap-1">
- <AlertCircle className="h-3 w-3 text-yellow-600" />
- <span className="font-medium">{enrolledClass.attendanceStats.justified}</span>
- </div>
- <div className="text-muted-foreground">Justificado</div>
- </div>
-
- <div className="text-center">
- <div className="flex items-center justify-center gap-1">
- <Calendar className="h-3 w-3 text-blue-600" />
- <span className="font-medium">{enrolledClass.attendanceStats.totalRecords}</span>
- </div>
- <div className="text-muted-foreground">Total</div>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- ))
- ) : (
- <div className="col-span-full">
- <Card>
- <CardContent className="flex flex-col items-center justify-center py-12">
- <BookOpen className="h-12 w-12 text-muted-foreground mb-4" />
- <h3 className="text-lg font-medium mb-2">No se encontraron clases</h3>
- <p className="text-muted-foreground text-center">
- {searchTerm || selectedPeriod !== 'all' || selectedStatus !== 'all'
- ? 'No hay clases que coincidan con los filtros aplicados.'
- : 'No tienes clases matriculadas en este momento.'}
- </p>
- {(searchTerm || selectedPeriod !== 'all' || selectedStatus !== 'all') && (
- <Button
- variant="outline"
- className="mt-4"
- onClick={() => {
- setSearchTerm('');
- setSelectedPeriod('all');
- setSelectedStatus('all');
- }}
- >
- Limpiar filtros
- </Button>
- )}
- </CardContent>
- </Card>
- </div>
- )}
- </div>
- </div>
- </MainLayout>
- );
- }
|