page.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. 'use client'
  2. import { useState, useEffect } from 'react'
  3. import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
  4. import { Button } from '@/components/ui/button'
  5. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
  6. import { Badge } from '@/components/ui/badge'
  7. import { CheckCircle, XCircle, Clock, Users } from 'lucide-react'
  8. import { toast } from 'sonner'
  9. interface Section {
  10. id: string
  11. name: string
  12. className: string
  13. periodName: string
  14. studentCount: number
  15. isActive: boolean
  16. }
  17. interface Partial {
  18. id: string
  19. name: string
  20. startDate: string
  21. endDate: string
  22. isActive: boolean
  23. }
  24. interface Student {
  25. id: string
  26. name: string
  27. email: string
  28. attendance?: {
  29. id: string
  30. status: 'present' | 'absent' | 'late'
  31. date: string
  32. }
  33. }
  34. export default function AttendancePage() {
  35. const [sections, setSections] = useState<Section[]>([])
  36. const [partials, setPartials] = useState<Partial[]>([])
  37. const [students, setStudents] = useState<Student[]>([])
  38. const [selectedSection, setSelectedSection] = useState<string>('')
  39. const [selectedPartial, setSelectedPartial] = useState<string>('')
  40. const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0])
  41. const [loading, setLoading] = useState(false)
  42. const [saving, setSaving] = useState(false)
  43. useEffect(() => {
  44. fetchSections()
  45. fetchPartials()
  46. }, [])
  47. useEffect(() => {
  48. if (selectedSection && selectedPartial && selectedDate) {
  49. fetchStudents()
  50. }
  51. }, [selectedSection, selectedPartial, selectedDate])
  52. const fetchSections = async () => {
  53. try {
  54. const response = await fetch('/api/teacher/sections')
  55. if (response.ok) {
  56. const data = await response.json()
  57. setSections(data.filter((s: Section) => s.isActive))
  58. }
  59. } catch (error) {
  60. toast.error('Error al cargar las secciones')
  61. }
  62. }
  63. const fetchPartials = async () => {
  64. try {
  65. const response = await fetch('/api/admin/partials')
  66. if (response.ok) {
  67. const data = await response.json()
  68. setPartials(data.filter((p: Partial) => p.isActive))
  69. }
  70. } catch (error) {
  71. toast.error('Error al cargar los parciales')
  72. }
  73. }
  74. const fetchStudents = async () => {
  75. if (!selectedSection || !selectedPartial || !selectedDate) return
  76. setLoading(true)
  77. try {
  78. const response = await fetch(
  79. `/api/teacher/attendance?sectionId=${selectedSection}&partialId=${selectedPartial}&date=${selectedDate}`
  80. )
  81. if (response.ok) {
  82. const data = await response.json()
  83. setStudents(data)
  84. }
  85. } catch (error) {
  86. toast.error('Error al cargar los estudiantes')
  87. } finally {
  88. setLoading(false)
  89. }
  90. }
  91. const updateAttendance = (studentId: string, status: 'present' | 'absent' | 'late') => {
  92. setStudents(prev => prev.map(student =>
  93. student.id === studentId
  94. ? {
  95. ...student,
  96. attendance: {
  97. id: student.attendance?.id || '',
  98. status,
  99. date: selectedDate
  100. }
  101. }
  102. : student
  103. ))
  104. }
  105. const saveAttendance = async () => {
  106. if (!selectedSection || !selectedPartial || !selectedDate) {
  107. toast.error('Selecciona sección, parcial y fecha')
  108. return
  109. }
  110. setSaving(true)
  111. try {
  112. const attendanceData = students.map(student => ({
  113. studentId: student.id,
  114. sectionId: selectedSection,
  115. partialId: selectedPartial,
  116. date: selectedDate,
  117. status: student.attendance?.status || 'absent'
  118. }))
  119. const response = await fetch('/api/teacher/attendance', {
  120. method: 'POST',
  121. headers: {
  122. 'Content-Type': 'application/json'
  123. },
  124. body: JSON.stringify({ attendance: attendanceData })
  125. })
  126. if (response.ok) {
  127. toast.success('Asistencia guardada correctamente')
  128. fetchStudents() // Refresh data
  129. } else {
  130. toast.error('Error al guardar la asistencia')
  131. }
  132. } catch (error) {
  133. toast.error('Error al guardar la asistencia')
  134. } finally {
  135. setSaving(false)
  136. }
  137. }
  138. const getStatusIcon = (status: string) => {
  139. switch (status) {
  140. case 'present':
  141. return <CheckCircle className="h-4 w-4 text-green-600" />
  142. case 'late':
  143. return <Clock className="h-4 w-4 text-yellow-600" />
  144. case 'absent':
  145. return <XCircle className="h-4 w-4 text-red-600" />
  146. default:
  147. return <XCircle className="h-4 w-4 text-gray-400" />
  148. }
  149. }
  150. const getStatusBadge = (status: string) => {
  151. switch (status) {
  152. case 'present':
  153. return <Badge className="bg-green-100 text-green-800">Presente</Badge>
  154. case 'late':
  155. return <Badge className="bg-yellow-100 text-yellow-800">Tardanza</Badge>
  156. case 'absent':
  157. return <Badge className="bg-red-100 text-red-800">Ausente</Badge>
  158. default:
  159. return <Badge variant="secondary">Sin marcar</Badge>
  160. }
  161. }
  162. return (
  163. <div className="space-y-6">
  164. <div>
  165. <h1 className="text-2xl font-bold text-gray-900">Gestión de Asistencia</h1>
  166. <p className="text-gray-600">Registra la asistencia de tus estudiantes</p>
  167. </div>
  168. {/* Filters */}
  169. <Card>
  170. <CardHeader>
  171. <CardTitle className="flex items-center gap-2">
  172. <Users className="h-5 w-5" />
  173. Seleccionar Clase
  174. </CardTitle>
  175. </CardHeader>
  176. <CardContent>
  177. <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
  178. <div>
  179. <label className="block text-sm font-medium mb-2">Sección</label>
  180. <Select value={selectedSection} onValueChange={setSelectedSection}>
  181. <SelectTrigger>
  182. <SelectValue placeholder="Seleccionar sección" />
  183. </SelectTrigger>
  184. <SelectContent>
  185. {sections.map((section) => (
  186. <SelectItem key={section.id} value={section.id}>
  187. {section.className} - {section.name}
  188. </SelectItem>
  189. ))}
  190. </SelectContent>
  191. </Select>
  192. </div>
  193. <div>
  194. <label className="block text-sm font-medium mb-2">Parcial</label>
  195. <Select value={selectedPartial} onValueChange={setSelectedPartial}>
  196. <SelectTrigger>
  197. <SelectValue placeholder="Seleccionar parcial" />
  198. </SelectTrigger>
  199. <SelectContent>
  200. {partials.map((partial) => (
  201. <SelectItem key={partial.id} value={partial.id}>
  202. {partial.name}
  203. </SelectItem>
  204. ))}
  205. </SelectContent>
  206. </Select>
  207. </div>
  208. <div>
  209. <label className="block text-sm font-medium mb-2">Fecha</label>
  210. <input
  211. type="date"
  212. value={selectedDate}
  213. onChange={(e) => setSelectedDate(e.target.value)}
  214. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
  215. />
  216. </div>
  217. <div className="flex items-end">
  218. <Button
  219. onClick={fetchStudents}
  220. disabled={!selectedSection || !selectedPartial || loading}
  221. className="w-full"
  222. >
  223. {loading ? 'Cargando...' : 'Cargar Estudiantes'}
  224. </Button>
  225. </div>
  226. </div>
  227. </CardContent>
  228. </Card>
  229. {/* Students List */}
  230. {students.length > 0 && (
  231. <Card>
  232. <CardHeader>
  233. <div className="flex justify-between items-center">
  234. <CardTitle>Lista de Estudiantes</CardTitle>
  235. <Button onClick={saveAttendance} disabled={saving}>
  236. {saving ? 'Guardando...' : 'Guardar Asistencia'}
  237. </Button>
  238. </div>
  239. </CardHeader>
  240. <CardContent>
  241. <div className="space-y-4">
  242. {students.map((student) => (
  243. <div
  244. key={student.id}
  245. className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50"
  246. >
  247. <div className="flex items-center space-x-3">
  248. {getStatusIcon(student.attendance?.status || 'absent')}
  249. <div>
  250. <h3 className="font-medium">{student.name}</h3>
  251. <p className="text-sm text-gray-600">{student.email}</p>
  252. </div>
  253. </div>
  254. <div className="flex items-center space-x-4">
  255. {getStatusBadge(student.attendance?.status || 'absent')}
  256. <div className="flex space-x-2">
  257. <Button
  258. size="sm"
  259. variant={student.attendance?.status === 'present' ? 'default' : 'outline'}
  260. onClick={() => updateAttendance(student.id, 'present')}
  261. >
  262. Presente
  263. </Button>
  264. <Button
  265. size="sm"
  266. variant={student.attendance?.status === 'late' ? 'default' : 'outline'}
  267. onClick={() => updateAttendance(student.id, 'late')}
  268. >
  269. Tardanza
  270. </Button>
  271. <Button
  272. size="sm"
  273. variant={student.attendance?.status === 'absent' ? 'default' : 'outline'}
  274. onClick={() => updateAttendance(student.id, 'absent')}
  275. >
  276. Ausente
  277. </Button>
  278. </div>
  279. </div>
  280. </div>
  281. ))}
  282. </div>
  283. </CardContent>
  284. </Card>
  285. )}
  286. {selectedSection && selectedPartial && students.length === 0 && !loading && (
  287. <Card>
  288. <CardContent className="text-center py-8">
  289. <p className="text-gray-500">No hay estudiantes matriculados en esta sección.</p>
  290. </CardContent>
  291. </Card>
  292. )}
  293. </div>
  294. )
  295. }