|
|
@@ -0,0 +1,778 @@
|
|
|
+'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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
|
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
+import {
|
|
|
+ BarChart3,
|
|
|
+ FileText,
|
|
|
+ Download,
|
|
|
+ Calendar,
|
|
|
+ Users,
|
|
|
+ TrendingUp,
|
|
|
+ CheckCircle,
|
|
|
+ XCircle,
|
|
|
+ AlertCircle,
|
|
|
+ BookOpen,
|
|
|
+ Clock
|
|
|
+} from 'lucide-react';
|
|
|
+import { toast } from 'sonner';
|
|
|
+
|
|
|
+interface Student {
|
|
|
+ id: string;
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ cedula: string;
|
|
|
+ email: string;
|
|
|
+ admissionNumber: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface Period {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ startDate: string;
|
|
|
+ endDate: string;
|
|
|
+ isActive: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface Class {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ code: string;
|
|
|
+ period: Period;
|
|
|
+}
|
|
|
+
|
|
|
+interface Section {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ class: Class;
|
|
|
+ studentCount?: number;
|
|
|
+ students?: Student[];
|
|
|
+}
|
|
|
+
|
|
|
+interface Assignment {
|
|
|
+ id: string;
|
|
|
+ section: Section;
|
|
|
+}
|
|
|
+
|
|
|
+interface AttendanceRecord {
|
|
|
+ id: string;
|
|
|
+ studentId: string;
|
|
|
+ sectionId: string;
|
|
|
+ date: string;
|
|
|
+ status: 'PRESENT' | 'ABSENT' | 'JUSTIFIED';
|
|
|
+ reason: string | null;
|
|
|
+ student: Student;
|
|
|
+ section: Section;
|
|
|
+}
|
|
|
+
|
|
|
+interface SummaryData {
|
|
|
+ section: Section;
|
|
|
+ totalRecords: number;
|
|
|
+ present: number;
|
|
|
+ absent: number;
|
|
|
+ justified: number;
|
|
|
+ attendanceRate: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface StatisticsData {
|
|
|
+ overall: {
|
|
|
+ totalRecords: number;
|
|
|
+ present: number;
|
|
|
+ absent: number;
|
|
|
+ justified: number;
|
|
|
+ attendanceRate: number;
|
|
|
+ };
|
|
|
+ byPeriod: Array<{
|
|
|
+ period: { id: string; name: string };
|
|
|
+ total: number;
|
|
|
+ present: number;
|
|
|
+ absent: number;
|
|
|
+ justified: number;
|
|
|
+ rate: number;
|
|
|
+ }>;
|
|
|
+ byClass: Array<{
|
|
|
+ class: { id: string; name: string; code: string };
|
|
|
+ total: number;
|
|
|
+ present: number;
|
|
|
+ absent: number;
|
|
|
+ justified: number;
|
|
|
+ rate: number;
|
|
|
+ }>;
|
|
|
+}
|
|
|
+
|
|
|
+interface StudentReportData {
|
|
|
+ student: Student;
|
|
|
+ statistics: {
|
|
|
+ totalRecords: number;
|
|
|
+ present: number;
|
|
|
+ absent: number;
|
|
|
+ justified: number;
|
|
|
+ attendanceRate: number;
|
|
|
+ };
|
|
|
+ data: AttendanceRecord[];
|
|
|
+}
|
|
|
+
|
|
|
+interface BaseReportResponse {
|
|
|
+ reportType: string;
|
|
|
+ teacher: {
|
|
|
+ id: string;
|
|
|
+ firstName: string;
|
|
|
+ lastName: string;
|
|
|
+ };
|
|
|
+ generatedAt: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface SummaryReportResponse extends BaseReportResponse {
|
|
|
+ reportType: 'summary';
|
|
|
+ data: SummaryData[];
|
|
|
+ totalRecords: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface DetailedReportResponse extends BaseReportResponse {
|
|
|
+ reportType: 'detailed';
|
|
|
+ data: AttendanceRecord[];
|
|
|
+ totalRecords: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface StatisticsReportResponse extends BaseReportResponse {
|
|
|
+ reportType: 'statistics';
|
|
|
+ data: StatisticsData;
|
|
|
+ overall: StatisticsData['overall'];
|
|
|
+ byPeriod: StatisticsData['byPeriod'];
|
|
|
+ byClass: StatisticsData['byClass'];
|
|
|
+}
|
|
|
+
|
|
|
+interface StudentReportResponse extends BaseReportResponse {
|
|
|
+ reportType: 'student';
|
|
|
+ data: AttendanceRecord[];
|
|
|
+ student: Student;
|
|
|
+ statistics: StudentReportData['statistics'];
|
|
|
+}
|
|
|
+
|
|
|
+type ReportResponse = SummaryReportResponse | DetailedReportResponse | StatisticsReportResponse | StudentReportResponse;
|
|
|
+
|
|
|
+export default function ReportsPage() {
|
|
|
+ const [assignments, setAssignments] = useState<Assignment[]>([]);
|
|
|
+ const [students, setStudents] = useState<Student[]>([]);
|
|
|
+ const [reportData, setReportData] = useState<ReportResponse | null>(null);
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [activeTab, setActiveTab] = useState('summary');
|
|
|
+
|
|
|
+ // Filtros
|
|
|
+ const [selectedSectionId, setSelectedSectionId] = useState<string>('all');
|
|
|
+ const [selectedStudentId, setSelectedStudentId] = useState<string>('all');
|
|
|
+ const [startDate, setStartDate] = useState<string>('');
|
|
|
+ const [endDate, setEndDate] = useState<string>('');
|
|
|
+
|
|
|
+ // Cargar asignaciones del profesor
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchAssignments = async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/teacher/assignments');
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Error al cargar asignaciones');
|
|
|
+ }
|
|
|
+ const data: { assignments: Assignment[] } = await response.json();
|
|
|
+ setAssignments(data.assignments || []);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ toast.error('Error al cargar las asignaciones');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchAssignments();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // Cargar estudiantes cuando se selecciona una sección
|
|
|
+ useEffect(() => {
|
|
|
+ if (selectedSectionId && selectedSectionId !== 'all') {
|
|
|
+ const selectedAssignment = assignments.find(a => a.section.id === selectedSectionId);
|
|
|
+ if (selectedAssignment?.section.students) {
|
|
|
+ // Los estudiantes ya están incluidos en la respuesta de assignments
|
|
|
+ const sectionStudents = selectedAssignment.section.students;
|
|
|
+ setStudents(sectionStudents);
|
|
|
+ } else {
|
|
|
+ setStudents([]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ setStudents([]);
|
|
|
+ }
|
|
|
+ setSelectedStudentId('all');
|
|
|
+ }, [selectedSectionId, assignments]);
|
|
|
+
|
|
|
+ const generateReport = async (reportType: string) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const params = new URLSearchParams({
|
|
|
+ type: reportType
|
|
|
+ });
|
|
|
+
|
|
|
+ if (selectedSectionId && selectedSectionId !== 'all') {
|
|
|
+ params.append('sectionId', selectedSectionId);
|
|
|
+ }
|
|
|
+ if (startDate) {
|
|
|
+ params.append('startDate', startDate);
|
|
|
+ }
|
|
|
+ if (endDate) {
|
|
|
+ params.append('endDate', endDate);
|
|
|
+ }
|
|
|
+ if (selectedStudentId && selectedStudentId !== 'all' && reportType === 'student') {
|
|
|
+ params.append('studentId', selectedStudentId);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(`/api/teacher/reports?${params.toString()}`);
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Error al generar reporte');
|
|
|
+ }
|
|
|
+
|
|
|
+ const data: ReportResponse = await response.json();
|
|
|
+ setReportData(data);
|
|
|
+ setActiveTab(reportType);
|
|
|
+ toast.success('Reporte generado exitosamente');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ toast.error('Error al generar el reporte');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const exportToCSV = (data: SummaryData[] | AttendanceRecord[], 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 as keyof typeof row];
|
|
|
+ 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 getStatusBadge = (status: 'PRESENT' | 'ABSENT' | 'JUSTIFIED') => {
|
|
|
+ switch (status) {
|
|
|
+ case 'PRESENT':
|
|
|
+ return <Badge variant="default" className="bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1" />Presente</Badge>;
|
|
|
+ case 'ABSENT':
|
|
|
+ return <Badge variant="destructive"><XCircle className="w-3 h-3 mr-1" />Ausente</Badge>;
|
|
|
+ case 'JUSTIFIED':
|
|
|
+ return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800"><AlertCircle className="w-3 h-3 mr-1" />Justificado</Badge>;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatDate = (dateString: string) => {
|
|
|
+ return new Date(dateString).toLocaleDateString('es-ES', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: 'short',
|
|
|
+ day: 'numeric'
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatPercentage = (value: number) => {
|
|
|
+ return `${value.toFixed(1)}%`;
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <MainLayout title="Reportes de Asistencia">
|
|
|
+
|
|
|
+ <div className="container mx-auto p-6 space-y-6">
|
|
|
+ {/* Header */}
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-3xl font-bold text-gray-900">Reportes de Asistencia</h1>
|
|
|
+ <p className="text-gray-600 mt-1">Genera y visualiza reportes detallados de asistencia por clase</p>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <BarChart3 className="h-8 w-8 text-blue-600" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Filtros */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <Calendar className="h-5 w-5" />
|
|
|
+ Filtros de Reporte
|
|
|
+ </CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ Configura los parámetros para generar reportes personalizados
|
|
|
+ </CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="section">Sección</Label>
|
|
|
+ <Select value={selectedSectionId} onValueChange={setSelectedSectionId}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Todas las secciones" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="all">Todas las secciones</SelectItem>
|
|
|
+ {assignments.map((assignment) => (
|
|
|
+ <SelectItem key={assignment.section.id} value={assignment.section.id}>
|
|
|
+ {assignment.section.class.code} - {assignment.section.name}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="startDate">Fecha Inicio</Label>
|
|
|
+ <Input
|
|
|
+ id="startDate"
|
|
|
+ type="date"
|
|
|
+ value={startDate}
|
|
|
+ onChange={(e) => setStartDate(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="endDate">Fecha Fin</Label>
|
|
|
+ <Input
|
|
|
+ id="endDate"
|
|
|
+ type="date"
|
|
|
+ value={endDate}
|
|
|
+ onChange={(e) => setEndDate(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="student">Estudiante (para reporte individual)</Label>
|
|
|
+ <Select value={selectedStudentId} onValueChange={setSelectedStudentId}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Seleccionar estudiante" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="all">Todos los estudiantes</SelectItem>
|
|
|
+ {students.map((student) => (
|
|
|
+ <SelectItem key={student.id} value={student.id}>
|
|
|
+ {student.lastName}, {student.firstName}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex flex-wrap gap-2 mt-4">
|
|
|
+ <Button
|
|
|
+ onClick={() => generateReport('summary')}
|
|
|
+ disabled={loading}
|
|
|
+ variant="default"
|
|
|
+ >
|
|
|
+ <BarChart3 className="w-4 h-4 mr-2" />
|
|
|
+ Resumen por Sección
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={() => generateReport('detailed')}
|
|
|
+ disabled={loading}
|
|
|
+ variant="outline"
|
|
|
+ >
|
|
|
+ <FileText className="w-4 h-4 mr-2" />
|
|
|
+ Reporte Detallado
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={() => generateReport('statistics')}
|
|
|
+ disabled={loading}
|
|
|
+ variant="outline"
|
|
|
+ >
|
|
|
+ <TrendingUp className="w-4 h-4 mr-2" />
|
|
|
+ Estadísticas Generales
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={() => generateReport('student')}
|
|
|
+ disabled={loading || selectedStudentId === 'all'}
|
|
|
+ variant="outline"
|
|
|
+ >
|
|
|
+ <Users className="w-4 h-4 mr-2" />
|
|
|
+ Reporte por Estudiante
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Resultados */}
|
|
|
+ {reportData && (
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <FileText className="h-5 w-5" />
|
|
|
+ Resultados del Reporte
|
|
|
+ </CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ Generado el {formatDate(reportData.generatedAt)} por {reportData.teacher.firstName} {reportData.teacher.lastName}
|
|
|
+ </CardDescription>
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ onClick={() => {
|
|
|
+ if (reportData.reportType === 'summary') {
|
|
|
+ exportToCSV(reportData.data, 'reporte_resumen');
|
|
|
+ } else if (reportData.reportType === 'detailed') {
|
|
|
+ exportToCSV(reportData.data, 'reporte_detallado');
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ disabled={reportData.reportType !== 'summary' && reportData.reportType !== 'detailed'}
|
|
|
+ >
|
|
|
+ <Download className="w-4 h-4 mr-2" />
|
|
|
+ Exportar CSV
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
|
+ <TabsList className="grid w-full grid-cols-4">
|
|
|
+ <TabsTrigger value="summary">Resumen</TabsTrigger>
|
|
|
+ <TabsTrigger value="detailed">Detallado</TabsTrigger>
|
|
|
+ <TabsTrigger value="statistics">Estadísticas</TabsTrigger>
|
|
|
+ <TabsTrigger value="student">Por Estudiante</TabsTrigger>
|
|
|
+ </TabsList>
|
|
|
+
|
|
|
+ {/* Reporte Resumen */}
|
|
|
+ <TabsContent value="summary" className="space-y-4">
|
|
|
+ {reportData.reportType === 'summary' && (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
+ {reportData.data.map((sectionData) => (
|
|
|
+ <Card key={sectionData.section.id}>
|
|
|
+ <CardHeader className="pb-2">
|
|
|
+ <CardTitle className="text-lg flex items-center gap-2">
|
|
|
+ <BookOpen className="h-4 w-4" />
|
|
|
+ {sectionData.section.class.code} - {sectionData.section.name}
|
|
|
+ </CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ {sectionData.section.class.name}
|
|
|
+ </CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-sm text-gray-600">Total Registros:</span>
|
|
|
+ <span className="font-medium">{sectionData.totalRecords}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-sm text-green-600">Presentes:</span>
|
|
|
+ <span className="font-medium text-green-600">{sectionData.present}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-sm text-red-600">Ausentes:</span>
|
|
|
+ <span className="font-medium text-red-600">{sectionData.absent}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-sm text-yellow-600">Justificados:</span>
|
|
|
+ <span className="font-medium text-yellow-600">{sectionData.justified}</span>
|
|
|
+ </div>
|
|
|
+ <div className="border-t pt-2 mt-2">
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-sm font-medium">Tasa de Asistencia:</span>
|
|
|
+ <span className="font-bold text-blue-600">
|
|
|
+ {formatPercentage(sectionData.attendanceRate)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte Detallado */}
|
|
|
+ <TabsContent value="detailed" className="space-y-4">
|
|
|
+ {reportData.reportType === 'detailed' && (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <h3 className="text-lg font-semibold">Registros Detallados</h3>
|
|
|
+ <Badge variant="outline">{reportData.totalRecords} registros</Badge>
|
|
|
+ </div>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Fecha</TableHead>
|
|
|
+ <TableHead>Estudiante</TableHead>
|
|
|
+ <TableHead>Cédula</TableHead>
|
|
|
+ <TableHead>Clase</TableHead>
|
|
|
+ <TableHead>Sección</TableHead>
|
|
|
+ <TableHead>Estado</TableHead>
|
|
|
+ <TableHead>Observación</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {reportData.data.map((record) => (
|
|
|
+ <TableRow key={record.id}>
|
|
|
+ <TableCell>{formatDate(record.date)}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {record.student.lastName}, {record.student.firstName}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{record.student.cedula}</TableCell>
|
|
|
+ <TableCell>{record.section.class.code}</TableCell>
|
|
|
+ <TableCell>{record.section.name}</TableCell>
|
|
|
+ <TableCell>{getStatusBadge(record.status)}</TableCell>
|
|
|
+ <TableCell>{record.reason || '-'}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Estadísticas */}
|
|
|
+ <TabsContent value="statistics" className="space-y-4">
|
|
|
+ {reportData.reportType === 'statistics' && (
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Estadísticas Generales */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <TrendingUp className="h-5 w-5" />
|
|
|
+ Estadísticas Generales
|
|
|
+ </CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-blue-600">{reportData.overall.totalRecords}</div>
|
|
|
+ <div className="text-sm text-gray-600">Total Registros</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-green-600">{reportData.overall.present}</div>
|
|
|
+ <div className="text-sm text-gray-600">Presentes</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-red-600">{reportData.overall.absent}</div>
|
|
|
+ <div className="text-sm text-gray-600">Ausentes</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-yellow-600">{reportData.overall.justified}</div>
|
|
|
+ <div className="text-sm text-gray-600">Justificados</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="mt-4 text-center">
|
|
|
+ <div className="text-3xl font-bold text-blue-600">
|
|
|
+ {formatPercentage(reportData.overall.attendanceRate)}
|
|
|
+ </div>
|
|
|
+ <div className="text-sm text-gray-600">Tasa de Asistencia General</div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Por Período */}
|
|
|
+ {reportData.byPeriod && reportData.byPeriod.length > 0 && (
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <Clock className="h-5 w-5" />
|
|
|
+ Estadísticas por Período
|
|
|
+ </CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Período</TableHead>
|
|
|
+ <TableHead>Total</TableHead>
|
|
|
+ <TableHead>Presentes</TableHead>
|
|
|
+ <TableHead>Ausentes</TableHead>
|
|
|
+ <TableHead>Justificados</TableHead>
|
|
|
+ <TableHead>Tasa</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {reportData.byPeriod.map((period) => (
|
|
|
+ <TableRow key={period.period.id}>
|
|
|
+ <TableCell className="font-medium">{period.period.name}</TableCell>
|
|
|
+ <TableCell>{period.total}</TableCell>
|
|
|
+ <TableCell className="text-green-600">{period.present}</TableCell>
|
|
|
+ <TableCell className="text-red-600">{period.absent}</TableCell>
|
|
|
+ <TableCell className="text-yellow-600">{period.justified}</TableCell>
|
|
|
+ <TableCell className="font-medium">{formatPercentage(period.rate)}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Por Clase */}
|
|
|
+ {reportData.byClass && reportData.byClass.length > 0 && (
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <BookOpen className="h-5 w-5" />
|
|
|
+ Estadísticas por Clase
|
|
|
+ </CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Código</TableHead>
|
|
|
+ <TableHead>Clase</TableHead>
|
|
|
+ <TableHead>Total</TableHead>
|
|
|
+ <TableHead>Presentes</TableHead>
|
|
|
+ <TableHead>Ausentes</TableHead>
|
|
|
+ <TableHead>Justificados</TableHead>
|
|
|
+ <TableHead>Tasa</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {reportData.byClass.map((classData) => (
|
|
|
+ <TableRow key={classData.class.id}>
|
|
|
+ <TableCell className="font-medium">{classData.class.code}</TableCell>
|
|
|
+ <TableCell>{classData.class.name}</TableCell>
|
|
|
+ <TableCell>{classData.total}</TableCell>
|
|
|
+ <TableCell className="text-green-600">{classData.present}</TableCell>
|
|
|
+ <TableCell className="text-red-600">{classData.absent}</TableCell>
|
|
|
+ <TableCell className="text-yellow-600">{classData.justified}</TableCell>
|
|
|
+ <TableCell className="font-medium">{formatPercentage(classData.rate)}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ {/* Reporte por Estudiante */}
|
|
|
+ <TabsContent value="student" className="space-y-4">
|
|
|
+ {reportData.reportType === 'student' && (
|
|
|
+ <div className="space-y-4">
|
|
|
+ {/* Información del Estudiante */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <Users className="h-5 w-5" />
|
|
|
+ {reportData.student.lastName}, {reportData.student.firstName}
|
|
|
+ </CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ Cédula: {reportData.student.cedula} | Matrícula: {reportData.student.admissionNumber}
|
|
|
+ </CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-blue-600">{reportData.statistics.totalRecords}</div>
|
|
|
+ <div className="text-sm text-gray-600">Total Registros</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-green-600">{reportData.statistics.present}</div>
|
|
|
+ <div className="text-sm text-gray-600">Presentes</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-red-600">{reportData.statistics.absent}</div>
|
|
|
+ <div className="text-sm text-gray-600">Ausentes</div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="text-2xl font-bold text-yellow-600">{reportData.statistics.justified}</div>
|
|
|
+ <div className="text-sm text-gray-600">Justificados</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="mt-4 text-center">
|
|
|
+ <div className="text-3xl font-bold text-blue-600">
|
|
|
+ {formatPercentage(reportData.statistics.attendanceRate)}
|
|
|
+ </div>
|
|
|
+ <div className="text-sm text-gray-600">Tasa de Asistencia</div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Historial de Asistencia */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Historial de Asistencia</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Fecha</TableHead>
|
|
|
+ <TableHead>Clase</TableHead>
|
|
|
+ <TableHead>Sección</TableHead>
|
|
|
+ <TableHead>Estado</TableHead>
|
|
|
+ <TableHead>Observación</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {reportData.data.map((record) => (
|
|
|
+ <TableRow key={record.id}>
|
|
|
+ <TableCell>{formatDate(record.date)}</TableCell>
|
|
|
+ <TableCell>{record.section.class.code}</TableCell>
|
|
|
+ <TableCell>{record.section.name}</TableCell>
|
|
|
+ <TableCell>{getStatusBadge(record.status)}</TableCell>
|
|
|
+ <TableCell>{record.reason || '-'}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+ </Tabs>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {loading && (
|
|
|
+ <Card>
|
|
|
+ <CardContent className="flex items-center justify-center py-8">
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
|
+ <p className="text-gray-600">Generando reporte...</p>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </MainLayout>
|
|
|
+ );
|
|
|
+}
|