|
|
@@ -0,0 +1,895 @@
|
|
|
+'use client';
|
|
|
+
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
+import { MainLayout } from '@/components/layout/main-layout';
|
|
|
+import { Button } from '@/components/ui/button';
|
|
|
+import { Input } from '@/components/ui/input';
|
|
|
+import { Label } from '@/components/ui/label';
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
+import { Badge } from '@/components/ui/badge';
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
|
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import {
|
|
|
+ BarChart3,
|
|
|
+ Users,
|
|
|
+ GraduationCap,
|
|
|
+ BookOpen,
|
|
|
+ UserCheck,
|
|
|
+ Calendar,
|
|
|
+ Download,
|
|
|
+ TrendingUp,
|
|
|
+ PieChart,
|
|
|
+ Activity,
|
|
|
+ FileText
|
|
|
+} from 'lucide-react';
|
|
|
+
|
|
|
+interface Period {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ startDate: string;
|
|
|
+ endDate: string;
|
|
|
+ isActive: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface OverviewData {
|
|
|
+ overview: {
|
|
|
+ totalUsers: number;
|
|
|
+ totalStudents: number;
|
|
|
+ totalTeachers: number;
|
|
|
+ totalClasses: number;
|
|
|
+ totalSections: number;
|
|
|
+ activePeriods: number;
|
|
|
+ recentEnrollments: number;
|
|
|
+ recentAttendance: number;
|
|
|
+ };
|
|
|
+ usersByRole: Array<{
|
|
|
+ role: string;
|
|
|
+ count: number;
|
|
|
+ }>;
|
|
|
+}
|
|
|
+
|
|
|
+interface StudentReportData {
|
|
|
+ students: Array<{
|
|
|
+ id: string;
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ cedula: string;
|
|
|
+ email: string;
|
|
|
+ admissionNumber: string;
|
|
|
+ enrollmentsCount: number;
|
|
|
+ attendanceRate: number;
|
|
|
+ totalAttendances: number;
|
|
|
+ presentAttendances: number;
|
|
|
+ enrollments: Array<{
|
|
|
+ sectionName: string;
|
|
|
+ className: string;
|
|
|
+ classCode: string;
|
|
|
+ periodName: string;
|
|
|
+ }>;
|
|
|
+ }>;
|
|
|
+ summary: {
|
|
|
+ totalStudents: number;
|
|
|
+ averageEnrollments: number;
|
|
|
+ averageAttendanceRate: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface TeacherReportData {
|
|
|
+ teachers: Array<{
|
|
|
+ id: string;
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ cedula: string;
|
|
|
+ email: string;
|
|
|
+ phone: string;
|
|
|
+ assignmentsCount: number;
|
|
|
+ totalStudents: number;
|
|
|
+ assignments: Array<{
|
|
|
+ sectionName: string;
|
|
|
+ className: string;
|
|
|
+ classCode: string;
|
|
|
+ periodName: string;
|
|
|
+ studentsCount: number;
|
|
|
+ }>;
|
|
|
+ }>;
|
|
|
+ summary: {
|
|
|
+ totalTeachers: number;
|
|
|
+ averageAssignments: number;
|
|
|
+ totalStudentsManaged: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface AttendanceReportData {
|
|
|
+ attendances: Array<{
|
|
|
+ id: string;
|
|
|
+ date: string;
|
|
|
+ status: string;
|
|
|
+ reason: string | null;
|
|
|
+ student: {
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ cedula: string;
|
|
|
+ admissionNumber: string;
|
|
|
+ };
|
|
|
+ section: {
|
|
|
+ name: string;
|
|
|
+ className: string;
|
|
|
+ classCode: string;
|
|
|
+ periodName: string;
|
|
|
+ };
|
|
|
+ }>;
|
|
|
+ summary: {
|
|
|
+ totalRecords: number;
|
|
|
+ byStatus: Array<{
|
|
|
+ status: string;
|
|
|
+ count: number;
|
|
|
+ }>;
|
|
|
+ byDate: Array<{
|
|
|
+ date: string;
|
|
|
+ count: number;
|
|
|
+ }>;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface EnrollmentReportData {
|
|
|
+ enrollments: Array<{
|
|
|
+ id: string;
|
|
|
+ isActive: boolean;
|
|
|
+ createdAt: string;
|
|
|
+ student: {
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ cedula: string;
|
|
|
+ admissionNumber: string;
|
|
|
+ };
|
|
|
+ section: {
|
|
|
+ name: string;
|
|
|
+ className: string;
|
|
|
+ classCode: string;
|
|
|
+ periodName: string;
|
|
|
+ };
|
|
|
+ }>;
|
|
|
+ summary: {
|
|
|
+ totalEnrollments: number;
|
|
|
+ byStatus: Array<{
|
|
|
+ isActive: boolean;
|
|
|
+ count: number;
|
|
|
+ }>;
|
|
|
+ bySectionCount: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface ClassReportData {
|
|
|
+ classes: Array<{
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ code: string;
|
|
|
+ description: string | null;
|
|
|
+ isActive: boolean;
|
|
|
+ period: {
|
|
|
+ name: string;
|
|
|
+ isActive: boolean;
|
|
|
+ };
|
|
|
+ totalSections: number;
|
|
|
+ totalStudents: number;
|
|
|
+ totalTeachers: number;
|
|
|
+ sections: Array<{
|
|
|
+ name: string;
|
|
|
+ studentsCount: number;
|
|
|
+ teachersCount: number;
|
|
|
+ teachers: Array<{
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ }>;
|
|
|
+ }>;
|
|
|
+ }>;
|
|
|
+ summary: {
|
|
|
+ totalClasses: number;
|
|
|
+ totalSections: number;
|
|
|
+ totalStudents: number;
|
|
|
+ averageStudentsPerClass: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export default function ReportsPage() {
|
|
|
+ const [periods, setPeriods] = useState<Period[]>([]);
|
|
|
+ const [selectedPeriod, setSelectedPeriod] = useState<string>('');
|
|
|
+ const [startDate, setStartDate] = useState<string>('');
|
|
|
+ const [endDate, setEndDate] = useState<string>('');
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [activeTab, setActiveTab] = useState('overview');
|
|
|
+
|
|
|
+ // Data states
|
|
|
+ const [overviewData, setOverviewData] = useState<OverviewData | null>(null);
|
|
|
+ const [studentsData, setStudentsData] = useState<StudentReportData | null>(null);
|
|
|
+ const [teachersData, setTeachersData] = useState<TeacherReportData | null>(null);
|
|
|
+ const [attendanceData, setAttendanceData] = useState<AttendanceReportData | null>(null);
|
|
|
+ const [enrollmentsData, setEnrollmentsData] = useState<EnrollmentReportData | null>(null);
|
|
|
+ const [classesData, setClassesData] = useState<ClassReportData | null>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchPeriods();
|
|
|
+ fetchReport('overview');
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const fetchPeriods = async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/admin/reports/periods');
|
|
|
+ if (response.ok) {
|
|
|
+ const data = await response.json();
|
|
|
+ setPeriods(data);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching periods:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchReport = async (reportType: string) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const params = new URLSearchParams({
|
|
|
+ type: reportType,
|
|
|
+ ...(selectedPeriod && selectedPeriod !== 'all' && { periodId: selectedPeriod }),
|
|
|
+ ...(startDate && { startDate }),
|
|
|
+ ...(endDate && { endDate })
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await fetch(`/api/admin/reports?${params}`);
|
|
|
+ if (response.ok) {
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ switch (reportType) {
|
|
|
+ case 'overview':
|
|
|
+ setOverviewData(data);
|
|
|
+ break;
|
|
|
+ case 'students':
|
|
|
+ setStudentsData(data);
|
|
|
+ break;
|
|
|
+ case 'teachers':
|
|
|
+ setTeachersData(data);
|
|
|
+ break;
|
|
|
+ case 'attendance':
|
|
|
+ setAttendanceData(data);
|
|
|
+ break;
|
|
|
+ case 'enrollments':
|
|
|
+ setEnrollmentsData(data);
|
|
|
+ break;
|
|
|
+ case 'classes':
|
|
|
+ setClassesData(data);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ toast.error('Error al cargar el reporte');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching report:', error);
|
|
|
+ toast.error('Error al cargar el reporte');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTabChange = (value: string) => {
|
|
|
+ setActiveTab(value);
|
|
|
+ fetchReport(value);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFilterChange = () => {
|
|
|
+ fetchReport(activeTab);
|
|
|
+ };
|
|
|
+
|
|
|
+ const exportToCSV = (data: any[], filename: string) => {
|
|
|
+ if (!data || data.length === 0) {
|
|
|
+ toast.error('No hay datos para exportar');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const headers = Object.keys(data[0]);
|
|
|
+ const csvContent = [
|
|
|
+ headers.join(','),
|
|
|
+ ...data.map(row =>
|
|
|
+ headers.map(header => {
|
|
|
+ const value = row[header];
|
|
|
+ if (typeof value === 'object' && value !== null) {
|
|
|
+ return JSON.stringify(value).replace(/"/g, '""');
|
|
|
+ }
|
|
|
+ return `"${String(value).replace(/"/g, '""')}"`;
|
|
|
+ }).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', `${filename}_${new Date().toISOString().split('T')[0]}.csv`);
|
|
|
+ link.style.visibility = 'hidden';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+
|
|
|
+ toast.success('Reporte exportado exitosamente');
|
|
|
+ };
|
|
|
+
|
|
|
+ const getAttendanceStatusBadge = (status: string) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'PRESENT':
|
|
|
+ return <Badge variant="default">Presente</Badge>;
|
|
|
+ case 'ABSENT':
|
|
|
+ return <Badge variant="destructive">Ausente</Badge>;
|
|
|
+ case 'JUSTIFIED':
|
|
|
+ return <Badge variant="secondary">Justificado</Badge>;
|
|
|
+ default:
|
|
|
+ return <Badge variant="outline">{status}</Badge>;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatDate = (dateString: string) => {
|
|
|
+ return new Date(dateString).toLocaleDateString('es-ES');
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatPercentage = (value: number) => {
|
|
|
+ return `${value.toFixed(1)}%`;
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <MainLayout>
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-3xl font-bold tracking-tight">Sistema de Reportes</h1>
|
|
|
+ <p className="text-muted-foreground">
|
|
|
+ Genera y visualiza reportes detallados del sistema educativo
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Filtros */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <Activity className="h-5 w-5" />
|
|
|
+ Filtros de Reporte
|
|
|
+ </CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Periodo Académico</Label>
|
|
|
+ <Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Todos los periodos" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="all">Todos los periodos</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>Fecha Inicio</Label>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ value={startDate}
|
|
|
+ onChange={(e) => setStartDate(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Fecha Fin</Label>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ value={endDate}
|
|
|
+ onChange={(e) => setEndDate(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="flex items-end">
|
|
|
+ <Button onClick={handleFilterChange} className="w-full">
|
|
|
+ Aplicar Filtros
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Tabs de Reportes */}
|
|
|
+ <Tabs value={activeTab} onValueChange={handleTabChange}>
|
|
|
+ <TabsList className="grid w-full grid-cols-6">
|
|
|
+ <TabsTrigger value="overview">Resumen</TabsTrigger>
|
|
|
+ <TabsTrigger value="students">Estudiantes</TabsTrigger>
|
|
|
+ <TabsTrigger value="teachers">Profesores</TabsTrigger>
|
|
|
+ <TabsTrigger value="attendance">Asistencia</TabsTrigger>
|
|
|
+ <TabsTrigger value="enrollments">Inscripciones</TabsTrigger>
|
|
|
+ <TabsTrigger value="classes">Clases</TabsTrigger>
|
|
|
+ </TabsList>
|
|
|
+
|
|
|
+ {/* Reporte de Resumen */}
|
|
|
+ <TabsContent value="overview">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : overviewData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <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 Usuarios</CardTitle>
|
|
|
+ <Users className="h-4 w-4 text-muted-foreground" />
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="text-2xl font-bold">{overviewData.overview.totalUsers}</div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
|
+ <CardTitle className="text-sm font-medium">Estudiantes Activos</CardTitle>
|
|
|
+ <GraduationCap className="h-4 w-4 text-muted-foreground" />
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="text-2xl font-bold">{overviewData.overview.totalStudents}</div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
|
+ <CardTitle className="text-sm font-medium">Profesores Activos</CardTitle>
|
|
|
+ <Users className="h-4 w-4 text-muted-foreground" />
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="text-2xl font-bold">{overviewData.overview.totalTeachers}</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 Activas</CardTitle>
|
|
|
+ <BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="text-2xl font-bold">{overviewData.overview.totalClasses}</div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Usuarios por Rol</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-2">
|
|
|
+ {overviewData.usersByRole.map((item) => (
|
|
|
+ <div key={item.role} className="flex justify-between items-center">
|
|
|
+ <span className="capitalize">{item.role.toLowerCase()}</span>
|
|
|
+ <Badge variant="outline">{item.count}</Badge>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Actividad Reciente</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <span>Inscripciones (últimos 30 días)</span>
|
|
|
+ <Badge variant="default">{overviewData.overview.recentEnrollments}</Badge>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <span>Registros de asistencia (últimos 7 días)</span>
|
|
|
+ <Badge variant="default">{overviewData.overview.recentAttendance}</Badge>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte de Estudiantes */}
|
|
|
+ <TabsContent value="students">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : studentsData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div className="grid grid-cols-3 gap-4">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{studentsData.summary.totalStudents}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Estudiantes</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{studentsData.summary.averageEnrollments.toFixed(1)}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Promedio Inscripciones</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{formatPercentage(studentsData.summary.averageAttendanceRate)}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Asistencia Promedio</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ <Button onClick={() => exportToCSV(studentsData.students, 'reporte_estudiantes')}>
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Detalle de Estudiantes</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Estudiante</TableHead>
|
|
|
+ <TableHead>Cédula</TableHead>
|
|
|
+ <TableHead>Matrícula</TableHead>
|
|
|
+ <TableHead>Inscripciones</TableHead>
|
|
|
+ <TableHead>Asistencia</TableHead>
|
|
|
+ <TableHead>Tasa Asistencia</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {studentsData.students.map((student) => (
|
|
|
+ <TableRow key={student.id}>
|
|
|
+ <TableCell className="font-medium">
|
|
|
+ {student.firstName} {student.lastName}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{student.cedula}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{student.admissionNumber}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{student.enrollmentsCount}</TableCell>
|
|
|
+ <TableCell>{student.presentAttendances}/{student.totalAttendances}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant={student.attendanceRate >= 80 ? 'default' : 'destructive'}>
|
|
|
+ {formatPercentage(student.attendanceRate)}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte de Profesores */}
|
|
|
+ <TabsContent value="teachers">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : teachersData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div className="grid grid-cols-3 gap-4">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{teachersData.summary.totalTeachers}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Profesores</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{teachersData.summary.averageAssignments.toFixed(1)}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Promedio Asignaciones</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{teachersData.summary.totalStudentsManaged}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Estudiantes Gestionados</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ <Button onClick={() => exportToCSV(teachersData.teachers, 'reporte_profesores')}>
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Detalle de Profesores</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Profesor</TableHead>
|
|
|
+ <TableHead>Cédula</TableHead>
|
|
|
+ <TableHead>Email</TableHead>
|
|
|
+ <TableHead>Asignaciones</TableHead>
|
|
|
+ <TableHead>Total Estudiantes</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {teachersData.teachers.map((teacher) => (
|
|
|
+ <TableRow key={teacher.id}>
|
|
|
+ <TableCell className="font-medium">
|
|
|
+ {teacher.firstName} {teacher.lastName}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{teacher.cedula}</TableCell>
|
|
|
+ <TableCell>{teacher.email}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{teacher.assignmentsCount}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{teacher.totalStudents}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte de Asistencia */}
|
|
|
+ <TabsContent value="attendance">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : attendanceData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div className="grid grid-cols-4 gap-4">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{attendanceData.summary.totalRecords}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Registros</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ {attendanceData.summary.byStatus.map((status) => (
|
|
|
+ <Card key={status.status}>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{status.count}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ {status.status === 'PRESENT' ? 'Presentes' :
|
|
|
+ status.status === 'ABSENT' ? 'Ausentes' : 'Justificados'}
|
|
|
+ </p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ <Button onClick={() => exportToCSV(attendanceData.attendances, 'reporte_asistencia')}>
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Registros de Asistencia</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Fecha</TableHead>
|
|
|
+ <TableHead>Estudiante</TableHead>
|
|
|
+ <TableHead>Matrícula</TableHead>
|
|
|
+ <TableHead>Sección</TableHead>
|
|
|
+ <TableHead>Estado</TableHead>
|
|
|
+ <TableHead>Razón</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {attendanceData.attendances.slice(0, 100).map((attendance) => (
|
|
|
+ <TableRow key={attendance.id}>
|
|
|
+ <TableCell>{formatDate(attendance.date)}</TableCell>
|
|
|
+ <TableCell className="font-medium">
|
|
|
+ {attendance.student.firstName} {attendance.student.lastName}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{attendance.student.admissionNumber}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {attendance.section.classCode} - {attendance.section.name}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{getAttendanceStatusBadge(attendance.status)}</TableCell>
|
|
|
+ <TableCell>{attendance.reason || '-'}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ {attendanceData.attendances.length > 100 && (
|
|
|
+ <div className="text-center py-4 text-muted-foreground">
|
|
|
+ Mostrando los primeros 100 registros de {attendanceData.attendances.length} total
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte de Inscripciones */}
|
|
|
+ <TabsContent value="enrollments">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : enrollmentsData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div className="grid grid-cols-3 gap-4">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{enrollmentsData.summary.totalEnrollments}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Inscripciones</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ {enrollmentsData.summary.byStatus.map((status) => (
|
|
|
+ <Card key={String(status.isActive)}>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{status.count}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ {status.isActive ? 'Activas' : 'Inactivas'}
|
|
|
+ </p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ <Button onClick={() => exportToCSV(enrollmentsData.enrollments, 'reporte_inscripciones')}>
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Detalle de Inscripciones</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Estudiante</TableHead>
|
|
|
+ <TableHead>Matrícula</TableHead>
|
|
|
+ <TableHead>Clase</TableHead>
|
|
|
+ <TableHead>Sección</TableHead>
|
|
|
+ <TableHead>Estado</TableHead>
|
|
|
+ <TableHead>Fecha Inscripción</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {enrollmentsData.enrollments.slice(0, 100).map((enrollment) => (
|
|
|
+ <TableRow key={enrollment.id}>
|
|
|
+ <TableCell className="font-medium">
|
|
|
+ {enrollment.student.firstName} {enrollment.student.lastName}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{enrollment.student.admissionNumber}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {enrollment.section.classCode} - {enrollment.section.className}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{enrollment.section.name}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant={enrollment.isActive ? 'default' : 'secondary'}>
|
|
|
+ {enrollment.isActive ? 'Activa' : 'Inactiva'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{formatDate(enrollment.createdAt)}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ {enrollmentsData.enrollments.length > 100 && (
|
|
|
+ <div className="text-center py-4 text-muted-foreground">
|
|
|
+ Mostrando las primeras 100 inscripciones de {enrollmentsData.enrollments.length} total
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte de Clases */}
|
|
|
+ <TabsContent value="classes">
|
|
|
+ {loading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-muted-foreground">Cargando reporte...</div>
|
|
|
+ </div>
|
|
|
+ ) : classesData ? (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div className="grid grid-cols-4 gap-4">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{classesData.summary.totalClasses}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Clases</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{classesData.summary.totalSections}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Secciones</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{classesData.summary.totalStudents}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Total Estudiantes</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-2xl font-bold">{classesData.summary.averageStudentsPerClass.toFixed(1)}</div>
|
|
|
+ <p className="text-xs text-muted-foreground">Promedio por Clase</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ <Button onClick={() => exportToCSV(classesData.classes, 'reporte_clases')}>
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Detalle de Clases</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Clase</TableHead>
|
|
|
+ <TableHead>Código</TableHead>
|
|
|
+ <TableHead>Periodo</TableHead>
|
|
|
+ <TableHead>Secciones</TableHead>
|
|
|
+ <TableHead>Estudiantes</TableHead>
|
|
|
+ <TableHead>Profesores</TableHead>
|
|
|
+ <TableHead>Estado</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {classesData.classes.map((classItem) => (
|
|
|
+ <TableRow key={classItem.id}>
|
|
|
+ <TableCell className="font-medium">{classItem.name}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{classItem.code}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant={classItem.period.isActive ? 'default' : 'secondary'}>
|
|
|
+ {classItem.period.name}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{classItem.totalSections}</TableCell>
|
|
|
+ <TableCell>{classItem.totalStudents}</TableCell>
|
|
|
+ <TableCell>{classItem.totalTeachers}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant={classItem.isActive ? 'default' : 'secondary'}>
|
|
|
+ {classItem.isActive ? 'Activa' : 'Inactiva'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </TabsContent>
|
|
|
+ </Tabs>
|
|
|
+ </div>
|
|
|
+ </MainLayout>
|
|
|
+ );
|
|
|
+}
|