| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- 'use client'
- import { useState, useEffect } from 'react'
- import { DashboardLayout } from '@/components/dashboard-layout'
- 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 { Badge } from '@/components/ui/badge'
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
- import { Calendar, Users, Clock, UserX, UserCheck } from 'lucide-react'
- import { toast } from 'sonner'
- import { Spinner } from '@/components/ui/spinner'
- interface Section {
- id: string
- name: string
- className: string
- periodName: string
- studentCount: number
- isActive: boolean
- }
- interface AttendanceRecord {
- id: string
- student: {
- id: string
- name: string
- email: string
- }
- status: 'present' | 'absent' | 'late'
- reason?: string
- partial: {
- id: string
- name: string
- } | null
- }
- interface DayHistory {
- date: string
- records: AttendanceRecord[]
- summary: {
- total: number
- present: number
- absent: number
- late: number
- }
- }
- export default function AttendanceHistoryPage() {
- const [sections, setSections] = useState<Section[]>([])
- const [history, setHistory] = useState<DayHistory[]>([])
- const [selectedSection, setSelectedSection] = useState<string>('')
- const [startDate, setStartDate] = useState<string>('')
- const [endDate, setEndDate] = useState<string>('')
- const [loading, setLoading] = useState(false)
- useEffect(() => {
- fetchSections()
- // Set default date range (last 30 days)
- const today = new Date()
- const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
- setEndDate(today.toISOString().split('T')[0])
- setStartDate(thirtyDaysAgo.toISOString().split('T')[0])
- }, [])
- const fetchSections = async () => {
- try {
- const response = await fetch('/api/teacher/sections')
- if (response.ok) {
- const data = await response.json()
- setSections(data.filter((s: Section) => s.isActive))
- }
- } catch (error) {
- toast.error('Error al cargar las secciones')
- }
- }
- const fetchHistory = async () => {
- if (!selectedSection) {
- toast.error('Selecciona una sección')
- return
- }
- setLoading(true)
- try {
- const params = new URLSearchParams({
- sectionId: selectedSection,
- ...(startDate && { startDate }),
- ...(endDate && { endDate })
- })
- const response = await fetch(`/api/teacher/attendance-history?${params}`)
- if (response.ok) {
- const data = await response.json()
- setHistory(data)
- } else {
- toast.error('Error al cargar el historial')
- }
- } catch (error) {
- toast.error('Error al cargar el historial')
- } finally {
- setLoading(false)
- }
- }
- const getStatusIcon = (status: string) => {
- switch (status) {
- case 'present':
- return <UserCheck className="h-4 w-4 text-green-600" />
- case 'late':
- return <Clock className="h-4 w-4 text-yellow-600" />
- case 'absent':
- return <UserX className="h-4 w-4 text-red-600" />
- default:
- return null
- }
- }
- const getStatusBadge = (status: string) => {
- switch (status) {
- case 'present':
- return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Presente</Badge>
- case 'late':
- return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">Tardanza</Badge>
- case 'absent':
- return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Ausente</Badge>
- default:
- return null
- }
- }
- const formatDate = (dateString: string) => {
- return new Date(dateString).toLocaleDateString('es-ES', {
- weekday: 'long',
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })
- }
- const breadcrumbs = [
- { label: 'Dashboard', href: '/teacher' },
- { label: 'Historial de Asistencia', href: '/teacher/attendance-history' }
- ]
- return (
- <DashboardLayout breadcrumbs={breadcrumbs}>
- <div className="container mx-auto p-6">
- <div className="flex items-center gap-2 mb-6">
- <Calendar className="h-6 w-6" />
- <h1 className="text-2xl font-bold">Historial de Asistencia</h1>
- </div>
- {/* Filtros */}
- <Card className="mb-6">
- <CardHeader>
- <CardTitle>Filtros</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
- <div>
- <Label htmlFor="section">Sección</Label>
- <Select value={selectedSection} onValueChange={setSelectedSection}>
- <SelectTrigger>
- <SelectValue placeholder="Seleccionar sección" />
- </SelectTrigger>
- <SelectContent>
- {sections.map((section) => (
- <SelectItem key={section.id} value={section.id}>
- {section.className} - {section.name} ({section.periodName})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div>
- <Label htmlFor="startDate">Fecha Inicio</Label>
- <Input
- id="startDate"
- type="date"
- value={startDate}
- onChange={(e) => setStartDate(e.target.value)}
- />
- </div>
-
- <div>
- <Label htmlFor="endDate">Fecha Fin</Label>
- <Input
- id="endDate"
- type="date"
- value={endDate}
- onChange={(e) => setEndDate(e.target.value)}
- />
- </div>
-
- <div className="flex items-end">
- <Button
- onClick={fetchHistory}
- disabled={loading || !selectedSection}
- className="w-full"
- >
- {loading ? (
- <div className="flex items-center gap-2">
- <Spinner size="sm" />
- </div>
- ) : (
- 'Buscar Historial'
- )}
- </Button>
- </div>
- </div>
- </CardContent>
- </Card>
- {/* Resumen General */}
- {history.length > 0 && (
- <Card className="mb-6">
- <CardHeader>
- <CardTitle>Resumen del Período</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">
- {history.reduce((acc, day) => acc + day.summary.total, 0)}
- </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">
- {history.reduce((acc, day) => acc + day.summary.present, 0)}
- </div>
- <div className="text-sm text-gray-600">Presentes</div>
- </div>
- <div className="text-center">
- <div className="text-2xl font-bold text-yellow-600">
- {history.reduce((acc, day) => acc + day.summary.late, 0)}
- </div>
- <div className="text-sm text-gray-600">Tardanzas</div>
- </div>
- <div className="text-center">
- <div className="text-2xl font-bold text-red-600">
- {history.reduce((acc, day) => acc + day.summary.absent, 0)}
- </div>
- <div className="text-sm text-gray-600">Ausentes</div>
- </div>
- </div>
- </CardContent>
- </Card>
- )}
- {/* Tabla de Historial */}
- {history.length > 0 ? (
- <Card>
- <CardHeader>
- <CardTitle>Historial de Asistencia</CardTitle>
- </CardHeader>
- <CardContent>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>Fecha</TableHead>
- <TableHead>Estudiante</TableHead>
- <TableHead>Estado</TableHead>
- <TableHead>Parcial</TableHead>
- <TableHead>Razón</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {history.map((day) =>
- day.records.map((record) => (
- <TableRow key={record.id}>
- <TableCell className="font-medium">
- {new Date(day.date).toLocaleDateString('es-ES', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric'
- })}
- </TableCell>
- <TableCell>
- <div>
- <div className="font-medium">{record.student.name}</div>
- <div className="text-sm text-gray-500">{record.student.email}</div>
- </div>
- </TableCell>
- <TableCell>
- <div className="flex items-center gap-2">
- {getStatusIcon(record.status)}
- {getStatusBadge(record.status)}
- </div>
- </TableCell>
- <TableCell>
- {record.partial ? (
- <span className="text-sm">{record.partial.name}</span>
- ) : (
- <span className="text-sm text-gray-400">-</span>
- )}
- </TableCell>
- <TableCell>
- {record.reason ? (
- <span className="text-sm italic">{record.reason}</span>
- ) : (
- <span className="text-sm text-gray-400">-</span>
- )}
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </CardContent>
- </Card>
- ) : (
- !loading && selectedSection && (
- <Card>
- <CardContent className="text-center py-8">
- <Calendar className="h-12 w-12 mx-auto text-gray-400 mb-4" />
- <p className="text-gray-500">No se encontraron registros de asistencia para el período seleccionado.</p>
- </CardContent>
- </Card>
- )
- )}
- {!selectedSection && (
- <Card>
- <CardContent className="text-center py-8">
- <Users className="h-12 w-12 mx-auto text-gray-400 mb-4" />
- <p className="text-gray-500">Selecciona una sección para ver el historial de asistencia.</p>
- </CardContent>
- </Card>
- )}
- </div>
- </DashboardLayout>
- )
- }
|