page.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. 'use client'
  2. import { useState, useEffect } from 'react'
  3. import { DashboardLayout } from '@/components/dashboard-layout'
  4. import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
  5. import { Button } from '@/components/ui/button'
  6. import { Input } from '@/components/ui/input'
  7. import { Label } from '@/components/ui/label'
  8. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
  9. import { Badge } from '@/components/ui/badge'
  10. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
  11. import { Calendar, Users, Clock, UserX, UserCheck } from 'lucide-react'
  12. import { toast } from 'sonner'
  13. import { Spinner } from '@/components/ui/spinner'
  14. interface Section {
  15. id: string
  16. name: string
  17. className: string
  18. periodName: string
  19. studentCount: number
  20. isActive: boolean
  21. }
  22. interface AttendanceRecord {
  23. id: string
  24. student: {
  25. id: string
  26. name: string
  27. email: string
  28. }
  29. status: 'present' | 'absent' | 'late'
  30. reason?: string
  31. partial: {
  32. id: string
  33. name: string
  34. } | null
  35. }
  36. interface DayHistory {
  37. date: string
  38. records: AttendanceRecord[]
  39. summary: {
  40. total: number
  41. present: number
  42. absent: number
  43. late: number
  44. }
  45. }
  46. export default function AttendanceHistoryPage() {
  47. const [sections, setSections] = useState<Section[]>([])
  48. const [history, setHistory] = useState<DayHistory[]>([])
  49. const [selectedSection, setSelectedSection] = useState<string>('')
  50. const [startDate, setStartDate] = useState<string>('')
  51. const [endDate, setEndDate] = useState<string>('')
  52. const [loading, setLoading] = useState(false)
  53. useEffect(() => {
  54. fetchSections()
  55. // Set default date range (last 30 days)
  56. const today = new Date()
  57. const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
  58. // Format dates in local timezone to avoid offset issues
  59. const formatLocalDate = (date: Date) => {
  60. const year = date.getFullYear()
  61. const month = String(date.getMonth() + 1).padStart(2, '0')
  62. const day = String(date.getDate()).padStart(2, '0')
  63. return `${year}-${month}-${day}`
  64. }
  65. setEndDate(formatLocalDate(today))
  66. setStartDate(formatLocalDate(thirtyDaysAgo))
  67. }, [])
  68. const fetchSections = async () => {
  69. try {
  70. const response = await fetch('/api/teacher/sections')
  71. if (response.ok) {
  72. const data = await response.json()
  73. setSections(data.filter((s: Section) => s.isActive))
  74. }
  75. } catch (error) {
  76. toast.error('Error al cargar las secciones')
  77. }
  78. }
  79. const fetchHistory = async () => {
  80. if (!selectedSection) {
  81. toast.error('Selecciona una sección')
  82. return
  83. }
  84. setLoading(true)
  85. try {
  86. const params = new URLSearchParams({
  87. sectionId: selectedSection,
  88. ...(startDate && { startDate }),
  89. ...(endDate && { endDate })
  90. })
  91. const response = await fetch(`/api/teacher/attendance-history?${params}`)
  92. if (response.ok) {
  93. const data = await response.json()
  94. setHistory(data)
  95. } else {
  96. toast.error('Error al cargar el historial')
  97. }
  98. } catch (error) {
  99. toast.error('Error al cargar el historial')
  100. } finally {
  101. setLoading(false)
  102. }
  103. }
  104. const getStatusIcon = (status: string) => {
  105. switch (status) {
  106. case 'present':
  107. return <UserCheck className="h-4 w-4 text-green-600" />
  108. case 'late':
  109. return <Clock className="h-4 w-4 text-yellow-600" />
  110. case 'absent':
  111. return <UserX className="h-4 w-4 text-red-600" />
  112. default:
  113. return null
  114. }
  115. }
  116. const getStatusBadge = (status: string) => {
  117. switch (status) {
  118. case 'present':
  119. return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Presente</Badge>
  120. case 'late':
  121. return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">Tardanza</Badge>
  122. case 'absent':
  123. return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Ausente</Badge>
  124. default:
  125. return null
  126. }
  127. }
  128. const formatDate = (dateString: string) => {
  129. // Evitar offset de timezone usando la fecha directamente
  130. const [year, month, day] = dateString.split('-')
  131. const monthNames = [
  132. 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
  133. 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
  134. ]
  135. const dayNames = [
  136. 'domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'
  137. ]
  138. // Crear fecha en zona local para obtener el día de la semana
  139. const localDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
  140. const dayOfWeek = dayNames[localDate.getDay()]
  141. return `${dayOfWeek}, ${parseInt(day)} de ${monthNames[parseInt(month) - 1]} de ${year}`
  142. }
  143. const breadcrumbs = [
  144. { label: 'Dashboard', href: '/teacher' },
  145. { label: 'Historial de Asistencia', href: '/teacher/attendance-history' }
  146. ]
  147. return (
  148. <DashboardLayout breadcrumbs={breadcrumbs}>
  149. <div className="container mx-auto p-6">
  150. <div className="flex items-center gap-2 mb-6">
  151. <Calendar className="h-6 w-6" />
  152. <h1 className="text-2xl font-bold">Historial de Asistencia</h1>
  153. </div>
  154. {/* Filtros */}
  155. <Card className="mb-6">
  156. <CardHeader>
  157. <CardTitle>Filtros</CardTitle>
  158. </CardHeader>
  159. <CardContent>
  160. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
  161. <div className="space-y-2">
  162. <Label htmlFor="section">Sección</Label>
  163. <Select value={selectedSection} onValueChange={setSelectedSection}>
  164. <SelectTrigger className="w-full">
  165. <SelectValue placeholder="Seleccionar sección" />
  166. </SelectTrigger>
  167. <SelectContent className="z-50">
  168. {sections.map((section) => (
  169. <SelectItem key={section.id} value={section.id}>
  170. {section.className} - {section.name} ({section.periodName})
  171. </SelectItem>
  172. ))}
  173. </SelectContent>
  174. </Select>
  175. </div>
  176. <div className="space-y-2">
  177. <Label htmlFor="startDate">Fecha Inicio</Label>
  178. <Input
  179. id="startDate"
  180. type="date"
  181. value={startDate}
  182. onChange={(e) => setStartDate(e.target.value)}
  183. className="w-full"
  184. />
  185. </div>
  186. <div className="space-y-2">
  187. <Label htmlFor="endDate">Fecha Fin</Label>
  188. <Input
  189. id="endDate"
  190. type="date"
  191. value={endDate}
  192. onChange={(e) => setEndDate(e.target.value)}
  193. className="w-full"
  194. />
  195. </div>
  196. <div className="space-y-2">
  197. <Label className="opacity-0">Acción</Label>
  198. <Button
  199. onClick={fetchHistory}
  200. disabled={loading || !selectedSection}
  201. className="w-full"
  202. >
  203. {loading ? (
  204. <div className="flex items-center gap-2">
  205. <Spinner size="sm" />
  206. </div>
  207. ) : (
  208. 'Buscar Historial'
  209. )}
  210. </Button>
  211. </div>
  212. </div>
  213. </CardContent>
  214. </Card>
  215. {/* Resumen General */}
  216. {history.length > 0 && (
  217. <Card className="mb-6">
  218. <CardHeader>
  219. <CardTitle>Resumen del Período</CardTitle>
  220. </CardHeader>
  221. <CardContent>
  222. <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
  223. <div className="text-center">
  224. <div className="text-2xl font-bold text-blue-600">
  225. {history.reduce((acc, day) => acc + day.summary.total, 0)}
  226. </div>
  227. <div className="text-sm">Total Registros</div>
  228. </div>
  229. <div className="text-center">
  230. <div className="text-2xl font-bold text-green-600">
  231. {history.reduce((acc, day) => acc + day.summary.present, 0)}
  232. </div>
  233. <div className="text-sm">Presentes</div>
  234. </div>
  235. <div className="text-center">
  236. <div className="text-2xl font-bold text-yellow-600">
  237. {history.reduce((acc, day) => acc + day.summary.late, 0)}
  238. </div>
  239. <div className="text-sm">Tardanzas</div>
  240. </div>
  241. <div className="text-center">
  242. <div className="text-2xl font-bold text-red-600">
  243. {history.reduce((acc, day) => acc + day.summary.absent, 0)}
  244. </div>
  245. <div className="text-sm">Ausentes</div>
  246. </div>
  247. </div>
  248. </CardContent>
  249. </Card>
  250. )}
  251. {/* Tabla de Historial */}
  252. {history.length > 0 ? (
  253. <Card>
  254. <CardHeader>
  255. <CardTitle>Historial de Asistencia</CardTitle>
  256. </CardHeader>
  257. <CardContent>
  258. <Table>
  259. <TableHeader>
  260. <TableRow>
  261. <TableHead>Fecha</TableHead>
  262. <TableHead>Estudiante</TableHead>
  263. <TableHead>Estado</TableHead>
  264. <TableHead>Parcial</TableHead>
  265. <TableHead>Razón</TableHead>
  266. </TableRow>
  267. </TableHeader>
  268. <TableBody>
  269. {history.map((day) =>
  270. day.records.map((record) => (
  271. <TableRow key={record.id}>
  272. <TableCell className="font-medium">
  273. {day.date.split('-').reverse().join('/')}
  274. </TableCell>
  275. <TableCell>
  276. <div>
  277. <div className="font-medium">{record.student.name}</div>
  278. <div className="text-sm text-gray-500">{record.student.email}</div>
  279. </div>
  280. </TableCell>
  281. <TableCell>
  282. <div className="flex items-center gap-2">
  283. {getStatusIcon(record.status)}
  284. {getStatusBadge(record.status)}
  285. </div>
  286. </TableCell>
  287. <TableCell>
  288. {record.partial ? (
  289. <span className="text-sm">{record.partial.name}</span>
  290. ) : (
  291. <span className="text-sm text-gray-400">-</span>
  292. )}
  293. </TableCell>
  294. <TableCell>
  295. {record.reason ? (
  296. <span className="text-sm italic">{record.reason}</span>
  297. ) : (
  298. <span className="text-sm text-gray-400">-</span>
  299. )}
  300. </TableCell>
  301. </TableRow>
  302. ))
  303. )}
  304. </TableBody>
  305. </Table>
  306. </CardContent>
  307. </Card>
  308. ) : (
  309. !loading && selectedSection && (
  310. <Card>
  311. <CardContent className="text-center py-8">
  312. <Calendar className="h-12 w-12 mx-auto text-gray-400 mb-4" />
  313. <p className="text-gray-500">No se encontraron registros de asistencia para el período seleccionado.</p>
  314. </CardContent>
  315. </Card>
  316. )
  317. )}
  318. {!selectedSection && (
  319. <Card>
  320. <CardContent className="text-center py-8">
  321. <Users className="h-12 w-12 mx-auto mb-4" />
  322. <p className="">Selecciona una sección para ver el historial de asistencia.</p>
  323. </CardContent>
  324. </Card>
  325. )}
  326. </div>
  327. </DashboardLayout>
  328. )
  329. }