page.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. setEndDate(today.toISOString().split('T')[0])
  59. setStartDate(thirtyDaysAgo.toISOString().split('T')[0])
  60. }, [])
  61. const fetchSections = async () => {
  62. try {
  63. const response = await fetch('/api/teacher/sections')
  64. if (response.ok) {
  65. const data = await response.json()
  66. setSections(data.filter((s: Section) => s.isActive))
  67. }
  68. } catch (error) {
  69. toast.error('Error al cargar las secciones')
  70. }
  71. }
  72. const fetchHistory = async () => {
  73. if (!selectedSection) {
  74. toast.error('Selecciona una sección')
  75. return
  76. }
  77. setLoading(true)
  78. try {
  79. const params = new URLSearchParams({
  80. sectionId: selectedSection,
  81. ...(startDate && { startDate }),
  82. ...(endDate && { endDate })
  83. })
  84. const response = await fetch(`/api/teacher/attendance-history?${params}`)
  85. if (response.ok) {
  86. const data = await response.json()
  87. setHistory(data)
  88. } else {
  89. toast.error('Error al cargar el historial')
  90. }
  91. } catch (error) {
  92. toast.error('Error al cargar el historial')
  93. } finally {
  94. setLoading(false)
  95. }
  96. }
  97. const getStatusIcon = (status: string) => {
  98. switch (status) {
  99. case 'present':
  100. return <UserCheck className="h-4 w-4 text-green-600" />
  101. case 'late':
  102. return <Clock className="h-4 w-4 text-yellow-600" />
  103. case 'absent':
  104. return <UserX className="h-4 w-4 text-red-600" />
  105. default:
  106. return null
  107. }
  108. }
  109. const getStatusBadge = (status: string) => {
  110. switch (status) {
  111. case 'present':
  112. return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Presente</Badge>
  113. case 'late':
  114. return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">Tardanza</Badge>
  115. case 'absent':
  116. return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Ausente</Badge>
  117. default:
  118. return null
  119. }
  120. }
  121. const formatDate = (dateString: string) => {
  122. return new Date(dateString).toLocaleDateString('es-ES', {
  123. weekday: 'long',
  124. year: 'numeric',
  125. month: 'long',
  126. day: 'numeric'
  127. })
  128. }
  129. const breadcrumbs = [
  130. { label: 'Dashboard', href: '/teacher' },
  131. { label: 'Historial de Asistencia', href: '/teacher/attendance-history' }
  132. ]
  133. return (
  134. <DashboardLayout breadcrumbs={breadcrumbs}>
  135. <div className="container mx-auto p-6">
  136. <div className="flex items-center gap-2 mb-6">
  137. <Calendar className="h-6 w-6" />
  138. <h1 className="text-2xl font-bold">Historial de Asistencia</h1>
  139. </div>
  140. {/* Filtros */}
  141. <Card className="mb-6">
  142. <CardHeader>
  143. <CardTitle>Filtros</CardTitle>
  144. </CardHeader>
  145. <CardContent>
  146. <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
  147. <div>
  148. <Label htmlFor="section">Sección</Label>
  149. <Select value={selectedSection} onValueChange={setSelectedSection}>
  150. <SelectTrigger>
  151. <SelectValue placeholder="Seleccionar sección" />
  152. </SelectTrigger>
  153. <SelectContent>
  154. {sections.map((section) => (
  155. <SelectItem key={section.id} value={section.id}>
  156. {section.className} - {section.name} ({section.periodName})
  157. </SelectItem>
  158. ))}
  159. </SelectContent>
  160. </Select>
  161. </div>
  162. <div>
  163. <Label htmlFor="startDate">Fecha Inicio</Label>
  164. <Input
  165. id="startDate"
  166. type="date"
  167. value={startDate}
  168. onChange={(e) => setStartDate(e.target.value)}
  169. />
  170. </div>
  171. <div>
  172. <Label htmlFor="endDate">Fecha Fin</Label>
  173. <Input
  174. id="endDate"
  175. type="date"
  176. value={endDate}
  177. onChange={(e) => setEndDate(e.target.value)}
  178. />
  179. </div>
  180. <div className="flex items-end">
  181. <Button
  182. onClick={fetchHistory}
  183. disabled={loading || !selectedSection}
  184. className="w-full"
  185. >
  186. {loading ? (
  187. <div className="flex items-center gap-2">
  188. <Spinner size="sm" />
  189. </div>
  190. ) : (
  191. 'Buscar Historial'
  192. )}
  193. </Button>
  194. </div>
  195. </div>
  196. </CardContent>
  197. </Card>
  198. {/* Resumen General */}
  199. {history.length > 0 && (
  200. <Card className="mb-6">
  201. <CardHeader>
  202. <CardTitle>Resumen del Período</CardTitle>
  203. </CardHeader>
  204. <CardContent>
  205. <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
  206. <div className="text-center">
  207. <div className="text-2xl font-bold text-blue-600">
  208. {history.reduce((acc, day) => acc + day.summary.total, 0)}
  209. </div>
  210. <div className="text-sm text-gray-600">Total Registros</div>
  211. </div>
  212. <div className="text-center">
  213. <div className="text-2xl font-bold text-green-600">
  214. {history.reduce((acc, day) => acc + day.summary.present, 0)}
  215. </div>
  216. <div className="text-sm text-gray-600">Presentes</div>
  217. </div>
  218. <div className="text-center">
  219. <div className="text-2xl font-bold text-yellow-600">
  220. {history.reduce((acc, day) => acc + day.summary.late, 0)}
  221. </div>
  222. <div className="text-sm text-gray-600">Tardanzas</div>
  223. </div>
  224. <div className="text-center">
  225. <div className="text-2xl font-bold text-red-600">
  226. {history.reduce((acc, day) => acc + day.summary.absent, 0)}
  227. </div>
  228. <div className="text-sm text-gray-600">Ausentes</div>
  229. </div>
  230. </div>
  231. </CardContent>
  232. </Card>
  233. )}
  234. {/* Tabla de Historial */}
  235. {history.length > 0 ? (
  236. <Card>
  237. <CardHeader>
  238. <CardTitle>Historial de Asistencia</CardTitle>
  239. </CardHeader>
  240. <CardContent>
  241. <Table>
  242. <TableHeader>
  243. <TableRow>
  244. <TableHead>Fecha</TableHead>
  245. <TableHead>Estudiante</TableHead>
  246. <TableHead>Estado</TableHead>
  247. <TableHead>Parcial</TableHead>
  248. <TableHead>Razón</TableHead>
  249. </TableRow>
  250. </TableHeader>
  251. <TableBody>
  252. {history.map((day) =>
  253. day.records.map((record) => (
  254. <TableRow key={record.id}>
  255. <TableCell className="font-medium">
  256. {new Date(day.date).toLocaleDateString('es-ES', {
  257. day: '2-digit',
  258. month: '2-digit',
  259. year: 'numeric'
  260. })}
  261. </TableCell>
  262. <TableCell>
  263. <div>
  264. <div className="font-medium">{record.student.name}</div>
  265. <div className="text-sm text-gray-500">{record.student.email}</div>
  266. </div>
  267. </TableCell>
  268. <TableCell>
  269. <div className="flex items-center gap-2">
  270. {getStatusIcon(record.status)}
  271. {getStatusBadge(record.status)}
  272. </div>
  273. </TableCell>
  274. <TableCell>
  275. {record.partial ? (
  276. <span className="text-sm">{record.partial.name}</span>
  277. ) : (
  278. <span className="text-sm text-gray-400">-</span>
  279. )}
  280. </TableCell>
  281. <TableCell>
  282. {record.reason ? (
  283. <span className="text-sm italic">{record.reason}</span>
  284. ) : (
  285. <span className="text-sm text-gray-400">-</span>
  286. )}
  287. </TableCell>
  288. </TableRow>
  289. ))
  290. )}
  291. </TableBody>
  292. </Table>
  293. </CardContent>
  294. </Card>
  295. ) : (
  296. !loading && selectedSection && (
  297. <Card>
  298. <CardContent className="text-center py-8">
  299. <Calendar className="h-12 w-12 mx-auto text-gray-400 mb-4" />
  300. <p className="text-gray-500">No se encontraron registros de asistencia para el período seleccionado.</p>
  301. </CardContent>
  302. </Card>
  303. )
  304. )}
  305. {!selectedSection && (
  306. <Card>
  307. <CardContent className="text-center py-8">
  308. <Users className="h-12 w-12 mx-auto text-gray-400 mb-4" />
  309. <p className="text-gray-500">Selecciona una sección para ver el historial de asistencia.</p>
  310. </CardContent>
  311. </Card>
  312. )}
  313. </div>
  314. </DashboardLayout>
  315. )
  316. }