| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- '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)
-
- // Format dates in local timezone to avoid offset issues
- const formatLocalDate = (date: Date) => {
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- return `${year}-${month}-${day}`
- }
-
- setEndDate(formatLocalDate(today))
- setStartDate(formatLocalDate(thirtyDaysAgo))
- }, [])
- 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) => {
- // Evitar offset de timezone usando la fecha directamente
- const [year, month, day] = dateString.split('-')
- const monthNames = [
- 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
- 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
- ]
- const dayNames = [
- 'domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'
- ]
-
- // Crear fecha en zona local para obtener el día de la semana
- const localDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
- const dayOfWeek = dayNames[localDate.getDay()]
-
- return `${dayOfWeek}, ${parseInt(day)} de ${monthNames[parseInt(month) - 1]} de ${year}`
- }
- 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-2 lg:grid-cols-4 gap-4">
- <div className="space-y-2">
- <Label htmlFor="section">Sección</Label>
- <Select value={selectedSection} onValueChange={setSelectedSection}>
- <SelectTrigger className="w-full">
- <SelectValue placeholder="Seleccionar sección" />
- </SelectTrigger>
- <SelectContent className="z-50">
- {sections.map((section) => (
- <SelectItem key={section.id} value={section.id}>
- {section.className} - {section.name} ({section.periodName})
- </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)}
- className="w-full"
- />
- </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)}
- className="w-full"
- />
- </div>
-
- <div className="space-y-2">
- <Label className="opacity-0">Acción</Label>
- <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">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">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">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">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">
- {day.date.split('-').reverse().join('/')}
- </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 mb-4" />
- <p className="">Selecciona una sección para ver el historial de asistencia.</p>
- </CardContent>
- </Card>
- )}
- </div>
- </DashboardLayout>
- )
- }
|