page.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. 'use client'
  2. import { useState, useEffect } from 'react'
  3. import { DashboardLayout } from '@/components/dashboard-layout'
  4. import { Card, CardContent, CardDescription, 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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb'
  10. import { Download, FileText, Calendar, Users } from 'lucide-react'
  11. import { format } from 'date-fns'
  12. import { es } from 'date-fns/locale'
  13. import { toast } from 'sonner'
  14. interface Section {
  15. id: number
  16. name: string
  17. className: string
  18. classCode: string
  19. }
  20. export default function ExportReportsPage() {
  21. const [sections, setSections] = useState<Section[]>([])
  22. const [selectedDate, setSelectedDate] = useState('')
  23. const [selectedSection, setSelectedSection] = useState('all')
  24. const [isExporting, setIsExporting] = useState(false)
  25. const [loading, setLoading] = useState(true)
  26. useEffect(() => {
  27. fetchSections()
  28. // Establecer fecha actual por defecto
  29. const today = new Date()
  30. setSelectedDate(format(today, 'yyyy-MM-dd'))
  31. }, [])
  32. const fetchSections = async () => {
  33. try {
  34. const response = await fetch('/api/teacher/sections')
  35. if (response.ok) {
  36. const data = await response.json()
  37. setSections(data)
  38. } else {
  39. toast.error('Error al cargar las secciones')
  40. }
  41. } catch (error) {
  42. console.error('Error:', error)
  43. toast.error('Error al cargar las secciones')
  44. } finally {
  45. setLoading(false)
  46. }
  47. }
  48. const handleExport = async () => {
  49. if (!selectedDate) {
  50. toast.error('Por favor selecciona una fecha')
  51. return
  52. }
  53. setIsExporting(true)
  54. try {
  55. const params = new URLSearchParams({
  56. date: selectedDate,
  57. sectionId: selectedSection
  58. })
  59. const response = await fetch(`/api/teacher/export-attendance?${params}`)
  60. if (!response.ok) {
  61. const errorData = await response.json()
  62. throw new Error(errorData.error || 'Error al exportar')
  63. }
  64. // Obtener el blob del archivo CSV
  65. const blob = await response.blob()
  66. // Crear URL para descarga
  67. const url = window.URL.createObjectURL(blob)
  68. const link = document.createElement('a')
  69. link.href = url
  70. // Obtener nombre del archivo desde los headers o generar uno
  71. const contentDisposition = response.headers.get('Content-Disposition')
  72. let filename = `asistencia_${selectedDate}.csv`
  73. if (contentDisposition) {
  74. const filenameMatch = contentDisposition.match(/filename="(.+)"/)
  75. if (filenameMatch) {
  76. filename = filenameMatch[1]
  77. }
  78. }
  79. link.download = filename
  80. document.body.appendChild(link)
  81. link.click()
  82. // Limpiar
  83. document.body.removeChild(link)
  84. window.URL.revokeObjectURL(url)
  85. toast.success('Reporte exportado exitosamente')
  86. } catch (error) {
  87. console.error('Error al exportar:', error)
  88. toast.error(error instanceof Error ? error.message : 'Error al exportar el reporte')
  89. } finally {
  90. setIsExporting(false)
  91. }
  92. }
  93. const getSelectedSectionName = () => {
  94. if (selectedSection === 'all') return 'Todas las secciones'
  95. const section = sections.find(s => s.id.toString() === selectedSection)
  96. return section ? `${section.className} - ${section.name}` : 'Sección desconocida'
  97. }
  98. const formatSelectedDate = () => {
  99. if (!selectedDate) return ''
  100. return format(new Date(selectedDate), 'dd \\de MMMM \\de yyyy', { locale: es })
  101. }
  102. if (loading) {
  103. return (
  104. <div className="flex items-center justify-center min-h-[400px]">
  105. <div className="text-center">
  106. <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
  107. <p className="text-muted-foreground">Cargando...</p>
  108. </div>
  109. </div>
  110. )
  111. }
  112. return (
  113. <DashboardLayout>
  114. <div className="space-y-6">
  115. {/* Breadcrumb */}
  116. <Breadcrumb>
  117. <BreadcrumbList>
  118. <BreadcrumbItem>
  119. <BreadcrumbLink href="/teacher">Profesor</BreadcrumbLink>
  120. </BreadcrumbItem>
  121. <BreadcrumbSeparator />
  122. <BreadcrumbItem>
  123. <BreadcrumbPage>Exportar Reportes</BreadcrumbPage>
  124. </BreadcrumbItem>
  125. </BreadcrumbList>
  126. </Breadcrumb>
  127. {/* Header */}
  128. <div className="flex items-center gap-3">
  129. <div className="p-2 bg-primary/10 rounded-lg">
  130. <Download className="h-6 w-6 text-primary" />
  131. </div>
  132. <div>
  133. <h1 className="text-2xl font-bold">Exportar Reportes de Asistencia</h1>
  134. <p className="text-muted-foreground">
  135. Genera y descarga reportes de asistencia en formato CSV
  136. </p>
  137. </div>
  138. </div>
  139. <div className="grid gap-6 md:grid-cols-2">
  140. {/* Configuración de Exportación */}
  141. <Card>
  142. <CardHeader>
  143. <CardTitle className="flex items-center gap-2">
  144. <Calendar className="h-5 w-5" />
  145. Configuración del Reporte
  146. </CardTitle>
  147. <CardDescription>
  148. Selecciona la fecha y sección para exportar
  149. </CardDescription>
  150. </CardHeader>
  151. <CardContent className="space-y-4">
  152. {/* Selector de Fecha */}
  153. <div className="space-y-2">
  154. <Label htmlFor="date">Fecha</Label>
  155. <Input
  156. id="date"
  157. type="date"
  158. value={selectedDate}
  159. onChange={(e) => setSelectedDate(e.target.value)}
  160. max={format(new Date(), 'yyyy-MM-dd')}
  161. />
  162. </div>
  163. {/* Selector de Sección */}
  164. <div className="space-y-2">
  165. <Label htmlFor="section">Sección</Label>
  166. <Select value={selectedSection} onValueChange={setSelectedSection}>
  167. <SelectTrigger>
  168. <SelectValue placeholder="Selecciona una sección" />
  169. </SelectTrigger>
  170. <SelectContent>
  171. <SelectItem value="all">Todas las secciones</SelectItem>
  172. {sections.map((section) => (
  173. <SelectItem key={section.id} value={section.id.toString()}>
  174. {section.className} - {section.name}
  175. </SelectItem>
  176. ))}
  177. </SelectContent>
  178. </Select>
  179. </div>
  180. {/* Botón de Exportación */}
  181. <Button
  182. onClick={handleExport}
  183. disabled={!selectedDate || isExporting}
  184. className="w-full"
  185. >
  186. {isExporting ? (
  187. <>
  188. <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
  189. Exportando...
  190. </>
  191. ) : (
  192. <>
  193. <Download className="h-4 w-4 mr-2" />
  194. Exportar CSV
  195. </>
  196. )}
  197. </Button>
  198. </CardContent>
  199. </Card>
  200. {/* Vista Previa de Configuración */}
  201. <Card>
  202. <CardHeader>
  203. <CardTitle className="flex items-center gap-2">
  204. <FileText className="h-5 w-5" />
  205. Vista Previa del Reporte
  206. </CardTitle>
  207. <CardDescription>
  208. Información que se incluirá en el reporte
  209. </CardDescription>
  210. </CardHeader>
  211. <CardContent className="space-y-4">
  212. {/* Información del Reporte */}
  213. <div className="space-y-3">
  214. <div className="flex justify-between items-center py-2 border-b">
  215. <span className="font-medium">Fecha:</span>
  216. <span className="text-muted-foreground">
  217. {selectedDate ? formatSelectedDate() : 'No seleccionada'}
  218. </span>
  219. </div>
  220. <div className="flex justify-between items-center py-2 border-b">
  221. <span className="font-medium">Sección:</span>
  222. <span className="text-muted-foreground">
  223. {getSelectedSectionName()}
  224. </span>
  225. </div>
  226. </div>
  227. {/* Columnas del CSV */}
  228. <div className="space-y-2">
  229. <h4 className="font-medium text-sm">Columnas incluidas:</h4>
  230. <div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
  231. <div>• Fecha</div>
  232. <div>• Estudiante</div>
  233. <div>• Email</div>
  234. <div>• Clase</div>
  235. <div>• Código de Clase</div>
  236. <div>• Sección</div>
  237. <div>• Estado</div>
  238. <div>• Parcial</div>
  239. <div>• Razón</div>
  240. </div>
  241. </div>
  242. {/* Información adicional */}
  243. <div className="bg-muted/50 p-3 rounded-lg">
  244. <div className="flex items-start gap-2">
  245. <Users className="h-4 w-4 mt-0.5 text-muted-foreground" />
  246. <div className="text-sm text-muted-foreground">
  247. <p className="font-medium mb-1">Formato CSV</p>
  248. <p>El archivo incluirá todos los registros de asistencia del día seleccionado, ordenados por fecha y nombre del estudiante.</p>
  249. </div>
  250. </div>
  251. </div>
  252. </CardContent>
  253. </Card>
  254. </div>
  255. </div>
  256. </DashboardLayout>
  257. )
  258. }