|
@@ -1,133 +1,153 @@
|
|
|
'use client'
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
+import { useEffect, useState } from 'react'
|
|
|
import { MainLayout } from '@/components/layout/main-layout'
|
|
import { MainLayout } from '@/components/layout/main-layout'
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
|
-import { BookOpen, Calendar, ClipboardList, TrendingUp, Clock, CheckCircle, XCircle } from 'lucide-react'
|
|
|
|
|
|
|
+import { BookOpen, Calendar, ClipboardList, TrendingUp, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
|
|
|
|
+import { toast } from 'sonner'
|
|
|
|
|
|
|
|
-const stats = [
|
|
|
|
|
- {
|
|
|
|
|
- title: 'Mis Clases',
|
|
|
|
|
- value: '3',
|
|
|
|
|
- description: 'Clases matriculadas',
|
|
|
|
|
- icon: BookOpen,
|
|
|
|
|
- color: 'text-blue-600',
|
|
|
|
|
- bgColor: 'bg-blue-100'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: 'Asistencia General',
|
|
|
|
|
- value: '88%',
|
|
|
|
|
- description: 'Promedio del semestre',
|
|
|
|
|
- icon: TrendingUp,
|
|
|
|
|
- color: 'text-green-600',
|
|
|
|
|
- bgColor: 'bg-green-100'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: 'Clases Hoy',
|
|
|
|
|
- value: '2',
|
|
|
|
|
- description: 'Clases programadas',
|
|
|
|
|
- icon: Calendar,
|
|
|
|
|
- color: 'text-purple-600',
|
|
|
|
|
- bgColor: 'bg-purple-100'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: 'Faltas Este Mes',
|
|
|
|
|
- value: '3',
|
|
|
|
|
- description: 'Inasistencias registradas',
|
|
|
|
|
- icon: XCircle,
|
|
|
|
|
- color: 'text-red-600',
|
|
|
|
|
- bgColor: 'bg-red-100'
|
|
|
|
|
- }
|
|
|
|
|
-]
|
|
|
|
|
|
|
+interface Student {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ firstName: string;
|
|
|
|
|
+ lastName: string;
|
|
|
|
|
+ admissionNumber: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Stats {
|
|
|
|
|
+ totalClasses: number;
|
|
|
|
|
+ attendanceRate: number;
|
|
|
|
|
+ todayClasses: number;
|
|
|
|
|
+ monthlyAbsences: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface TodayScheduleItem {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ section: string;
|
|
|
|
|
+ time: string;
|
|
|
|
|
+ room: string;
|
|
|
|
|
+ professor: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface AttendanceHistoryItem {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ class: string;
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ status: string;
|
|
|
|
|
+ time: string;
|
|
|
|
|
+ statusColor: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ClassOverviewItem {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ section: string;
|
|
|
|
|
+ professor: string;
|
|
|
|
|
+ attendance: number;
|
|
|
|
|
+ totalClasses: number;
|
|
|
|
|
+ attendedClasses: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DashboardData {
|
|
|
|
|
+ student: Student;
|
|
|
|
|
+ stats: Stats;
|
|
|
|
|
+ todaySchedule: TodayScheduleItem[];
|
|
|
|
|
+ recentAttendances: AttendanceHistoryItem[];
|
|
|
|
|
+ classesOverview: ClassOverviewItem[];
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-const todaySchedule = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- name: 'Matemáticas I',
|
|
|
|
|
- section: 'Sección A',
|
|
|
|
|
- time: '08:00 - 10:00',
|
|
|
|
|
- room: 'Aula 101',
|
|
|
|
|
- professor: 'Juan Pérez'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- name: 'Programación I',
|
|
|
|
|
- section: 'Sección B',
|
|
|
|
|
- time: '14:00 - 16:00',
|
|
|
|
|
- room: 'Lab 201',
|
|
|
|
|
- professor: 'Juan Pérez'
|
|
|
|
|
|
|
+export default function StudentDashboard() {
|
|
|
|
|
+ const [data, setData] = useState<DashboardData | null>(null)
|
|
|
|
|
+ const [loading, setLoading] = useState(true)
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchDashboardData()
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
|
|
+ const fetchDashboardData = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true)
|
|
|
|
|
+ const response = await fetch('/api/student/dashboard')
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Error al cargar los datos del dashboard')
|
|
|
|
|
+ }
|
|
|
|
|
+ const dashboardData: DashboardData = await response.json()
|
|
|
|
|
+ setData(dashboardData)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error:', error)
|
|
|
|
|
+ toast.error('Error al cargar los datos del dashboard')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-]
|
|
|
|
|
|
|
|
|
|
-const attendanceHistory = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- class: 'Matemáticas I',
|
|
|
|
|
- date: '2024-01-20',
|
|
|
|
|
- status: 'Presente',
|
|
|
|
|
- time: '08:00',
|
|
|
|
|
- statusColor: 'text-green-600'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- class: 'Programación I',
|
|
|
|
|
- date: '2024-01-19',
|
|
|
|
|
- status: 'Presente',
|
|
|
|
|
- time: '14:00',
|
|
|
|
|
- statusColor: 'text-green-600'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3,
|
|
|
|
|
- class: 'Física I',
|
|
|
|
|
- date: '2024-01-18',
|
|
|
|
|
- status: 'Ausente',
|
|
|
|
|
- time: '10:00',
|
|
|
|
|
- statusColor: 'text-red-600'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 4,
|
|
|
|
|
- class: 'Matemáticas I',
|
|
|
|
|
- date: '2024-01-17',
|
|
|
|
|
- status: 'Presente',
|
|
|
|
|
- time: '08:00',
|
|
|
|
|
- statusColor: 'text-green-600'
|
|
|
|
|
|
|
+ if (loading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <MainLayout
|
|
|
|
|
+ title="Dashboard Estudiante"
|
|
|
|
|
+ subtitle="Mi progreso académico y asistencia"
|
|
|
|
|
+ requiredRole="STUDENT"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex items-center justify-center h-64">
|
|
|
|
|
+ <Loader2 className="h-8 w-8 animate-spin" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </MainLayout>
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
-]
|
|
|
|
|
|
|
|
|
|
-const classesOverview = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- name: 'Matemáticas I',
|
|
|
|
|
- section: 'Sección A',
|
|
|
|
|
- professor: 'Juan Pérez',
|
|
|
|
|
- attendance: 92,
|
|
|
|
|
- totalClasses: 25,
|
|
|
|
|
- attendedClasses: 23
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- name: 'Programación I',
|
|
|
|
|
- section: 'Sección B',
|
|
|
|
|
- professor: 'Juan Pérez',
|
|
|
|
|
- attendance: 85,
|
|
|
|
|
- totalClasses: 20,
|
|
|
|
|
- attendedClasses: 17
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3,
|
|
|
|
|
- name: 'Física I',
|
|
|
|
|
- section: 'Sección A',
|
|
|
|
|
- professor: 'Juan Pérez',
|
|
|
|
|
- attendance: 87,
|
|
|
|
|
- totalClasses: 23,
|
|
|
|
|
- attendedClasses: 20
|
|
|
|
|
|
|
+ if (!data) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <MainLayout
|
|
|
|
|
+ title="Dashboard Estudiante"
|
|
|
|
|
+ subtitle="Mi progreso académico y asistencia"
|
|
|
|
|
+ requiredRole="STUDENT"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="text-center py-8">
|
|
|
|
|
+ <p className="text-muted-foreground">No se pudieron cargar los datos del dashboard.</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </MainLayout>
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
-]
|
|
|
|
|
|
|
|
|
|
-export default function StudentDashboard() {
|
|
|
|
|
|
|
+ const stats = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Mis Clases',
|
|
|
|
|
+ value: data.stats.totalClasses.toString(),
|
|
|
|
|
+ description: 'Clases matriculadas',
|
|
|
|
|
+ icon: BookOpen,
|
|
|
|
|
+ color: 'text-blue-600',
|
|
|
|
|
+ bgColor: 'bg-blue-100'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Asistencia General',
|
|
|
|
|
+ value: `${data.stats.attendanceRate}%`,
|
|
|
|
|
+ description: 'Promedio del semestre',
|
|
|
|
|
+ icon: TrendingUp,
|
|
|
|
|
+ color: 'text-green-600',
|
|
|
|
|
+ bgColor: 'bg-green-100'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Clases Hoy',
|
|
|
|
|
+ value: data.stats.todayClasses.toString(),
|
|
|
|
|
+ description: 'Clases programadas',
|
|
|
|
|
+ icon: Calendar,
|
|
|
|
|
+ color: 'text-purple-600',
|
|
|
|
|
+ bgColor: 'bg-purple-100'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Faltas Este Mes',
|
|
|
|
|
+ value: data.stats.monthlyAbsences.toString(),
|
|
|
|
|
+ description: 'Inasistencias registradas',
|
|
|
|
|
+ icon: XCircle,
|
|
|
|
|
+ color: 'text-red-600',
|
|
|
|
|
+ bgColor: 'bg-red-100'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<MainLayout
|
|
<MainLayout
|
|
|
title="Dashboard Estudiante"
|
|
title="Dashboard Estudiante"
|
|
|
- subtitle="Mi progreso académico y asistencia"
|
|
|
|
|
|
|
+ subtitle={`Bienvenido/a ${data.student.firstName} ${data.student.lastName}`}
|
|
|
requiredRole="STUDENT"
|
|
requiredRole="STUDENT"
|
|
|
>
|
|
>
|
|
|
<div className="space-y-6">
|
|
<div className="space-y-6">
|
|
@@ -170,19 +190,25 @@ export default function StudentDashboard() {
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- {todaySchedule.map((classItem) => (
|
|
|
|
|
- <div key={classItem.id} className="p-3 border rounded-lg">
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- <p className="text-sm font-medium">{classItem.name}</p>
|
|
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
|
|
- {classItem.section} • {classItem.room}
|
|
|
|
|
- </p>
|
|
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
|
|
- {classItem.time} • Prof. {classItem.professor}
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ {data.todaySchedule.length > 0 ? (
|
|
|
|
|
+ data.todaySchedule.map((classItem) => (
|
|
|
|
|
+ <div key={classItem.id} className="p-3 border rounded-lg">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <p className="text-sm font-medium">{classItem.name}</p>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
|
|
+ {classItem.section} • {classItem.room}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
|
|
+ {classItem.time} • Prof. {classItem.professor}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-sm text-muted-foreground text-center py-4">
|
|
|
|
|
+ No hay clases programadas para hoy
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
@@ -200,26 +226,32 @@ export default function StudentDashboard() {
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
<div className="space-y-3">
|
|
<div className="space-y-3">
|
|
|
- {attendanceHistory.map((record) => (
|
|
|
|
|
- <div key={record.id} className="flex items-center justify-between">
|
|
|
|
|
- <div className="space-y-1">
|
|
|
|
|
- <p className="text-sm font-medium">{record.class}</p>
|
|
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
|
|
- {record.date} • {record.time}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- {record.status === 'Presente' ? (
|
|
|
|
|
- <CheckCircle className="h-4 w-4 text-green-600" />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <XCircle className="h-4 w-4 text-red-600" />
|
|
|
|
|
- )}
|
|
|
|
|
- <span className={`text-xs font-medium ${record.statusColor}`}>
|
|
|
|
|
- {record.status}
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ {data.recentAttendances.length > 0 ? (
|
|
|
|
|
+ data.recentAttendances.map((record) => (
|
|
|
|
|
+ <div key={record.id} className="flex items-center justify-between">
|
|
|
|
|
+ <div className="space-y-1">
|
|
|
|
|
+ <p className="text-sm font-medium">{record.class}</p>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
|
|
+ {record.date} • {record.time}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ {record.status === 'Presente' || record.status === 'Justificado' ? (
|
|
|
|
|
+ <CheckCircle className="h-4 w-4 text-green-600" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <XCircle className="h-4 w-4 text-red-600" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ <span className={`text-xs font-medium ${record.statusColor}`}>
|
|
|
|
|
+ {record.status}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-sm text-muted-foreground text-center py-4">
|
|
|
|
|
+ No hay registros de asistencia
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
@@ -237,42 +269,48 @@ export default function StudentDashboard() {
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- {classesOverview.map((classItem) => (
|
|
|
|
|
- <div key={classItem.id} className="space-y-2">
|
|
|
|
|
- <div className="flex justify-between items-start">
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm font-medium">{classItem.name}</p>
|
|
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
|
|
- {classItem.section} • Prof. {classItem.professor}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <span className={`text-xs font-medium ${
|
|
|
|
|
- classItem.attendance >= 90
|
|
|
|
|
- ? 'text-green-600'
|
|
|
|
|
- : classItem.attendance >= 80
|
|
|
|
|
- ? 'text-yellow-600'
|
|
|
|
|
- : 'text-red-600'
|
|
|
|
|
- }`}>
|
|
|
|
|
- {classItem.attendance}%
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="w-full bg-gray-200 rounded-full h-2">
|
|
|
|
|
- <div
|
|
|
|
|
- className={`h-2 rounded-full ${
|
|
|
|
|
|
|
+ {data.classesOverview.length > 0 ? (
|
|
|
|
|
+ data.classesOverview.map((classItem) => (
|
|
|
|
|
+ <div key={classItem.id} className="space-y-2">
|
|
|
|
|
+ <div className="flex justify-between items-start">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm font-medium">{classItem.name}</p>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
|
|
+ {classItem.section} • Prof. {classItem.professor}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span className={`text-xs font-medium ${
|
|
|
classItem.attendance >= 90
|
|
classItem.attendance >= 90
|
|
|
- ? 'bg-green-600'
|
|
|
|
|
|
|
+ ? 'text-green-600'
|
|
|
: classItem.attendance >= 80
|
|
: classItem.attendance >= 80
|
|
|
- ? 'bg-yellow-600'
|
|
|
|
|
- : 'bg-red-600'
|
|
|
|
|
- }`}
|
|
|
|
|
- style={{ width: `${classItem.attendance}%` }}
|
|
|
|
|
- ></div>
|
|
|
|
|
|
|
+ ? 'text-yellow-600'
|
|
|
|
|
+ : 'text-red-600'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {classItem.attendance}%
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="w-full bg-gray-200 rounded-full h-2">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`h-2 rounded-full ${
|
|
|
|
|
+ classItem.attendance >= 90
|
|
|
|
|
+ ? 'bg-green-600'
|
|
|
|
|
+ : classItem.attendance >= 80
|
|
|
|
|
+ ? 'bg-yellow-600'
|
|
|
|
|
+ : 'bg-red-600'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ style={{ width: `${classItem.attendance}%` }}
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
|
|
+ {classItem.attendedClasses}/{classItem.totalClasses} clases asistidas
|
|
|
|
|
+ </p>
|
|
|
</div>
|
|
</div>
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
|
|
- {classItem.attendedClasses}/{classItem.totalClasses} clases asistidas
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-sm text-muted-foreground text-center py-4">
|
|
|
|
|
+ No hay clases matriculadas
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|