|
|
@@ -0,0 +1,322 @@
|
|
|
+'use client'
|
|
|
+
|
|
|
+import { useState, useEffect } from 'react'
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
|
+import { Badge } from '@/components/ui/badge'
|
|
|
+import { CheckCircle, XCircle, Clock, Users } from 'lucide-react'
|
|
|
+import { toast } from 'sonner'
|
|
|
+
|
|
|
+interface Section {
|
|
|
+ id: string
|
|
|
+ name: string
|
|
|
+ className: string
|
|
|
+ periodName: string
|
|
|
+ studentCount: number
|
|
|
+ isActive: boolean
|
|
|
+}
|
|
|
+
|
|
|
+interface Partial {
|
|
|
+ id: string
|
|
|
+ name: string
|
|
|
+ startDate: string
|
|
|
+ endDate: string
|
|
|
+ isActive: boolean
|
|
|
+}
|
|
|
+
|
|
|
+interface Student {
|
|
|
+ id: string
|
|
|
+ name: string
|
|
|
+ email: string
|
|
|
+ attendance?: {
|
|
|
+ id: string
|
|
|
+ status: 'present' | 'absent' | 'late'
|
|
|
+ date: string
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default function AttendancePage() {
|
|
|
+ const [sections, setSections] = useState<Section[]>([])
|
|
|
+ const [partials, setPartials] = useState<Partial[]>([])
|
|
|
+ const [students, setStudents] = useState<Student[]>([])
|
|
|
+ const [selectedSection, setSelectedSection] = useState<string>('')
|
|
|
+ const [selectedPartial, setSelectedPartial] = useState<string>('')
|
|
|
+ const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0])
|
|
|
+ const [loading, setLoading] = useState(false)
|
|
|
+ const [saving, setSaving] = useState(false)
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchSections()
|
|
|
+ fetchPartials()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (selectedSection && selectedPartial && selectedDate) {
|
|
|
+ fetchStudents()
|
|
|
+ }
|
|
|
+ }, [selectedSection, selectedPartial, selectedDate])
|
|
|
+
|
|
|
+ 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 fetchPartials = async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/admin/partials')
|
|
|
+ if (response.ok) {
|
|
|
+ const data = await response.json()
|
|
|
+ setPartials(data.filter((p: Partial) => p.isActive))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('Error al cargar los parciales')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const fetchStudents = async () => {
|
|
|
+ if (!selectedSection || !selectedPartial || !selectedDate) return
|
|
|
+
|
|
|
+ setLoading(true)
|
|
|
+ try {
|
|
|
+ const response = await fetch(
|
|
|
+ `/api/teacher/attendance?sectionId=${selectedSection}&partialId=${selectedPartial}&date=${selectedDate}`
|
|
|
+ )
|
|
|
+ if (response.ok) {
|
|
|
+ const data = await response.json()
|
|
|
+ setStudents(data)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('Error al cargar los estudiantes')
|
|
|
+ } finally {
|
|
|
+ setLoading(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const updateAttendance = (studentId: string, status: 'present' | 'absent' | 'late') => {
|
|
|
+ setStudents(prev => prev.map(student =>
|
|
|
+ student.id === studentId
|
|
|
+ ? {
|
|
|
+ ...student,
|
|
|
+ attendance: {
|
|
|
+ id: student.attendance?.id || '',
|
|
|
+ status,
|
|
|
+ date: selectedDate
|
|
|
+ }
|
|
|
+ }
|
|
|
+ : student
|
|
|
+ ))
|
|
|
+ }
|
|
|
+
|
|
|
+ const saveAttendance = async () => {
|
|
|
+ if (!selectedSection || !selectedPartial || !selectedDate) {
|
|
|
+ toast.error('Selecciona sección, parcial y fecha')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ setSaving(true)
|
|
|
+ try {
|
|
|
+ const attendanceData = students.map(student => ({
|
|
|
+ studentId: student.id,
|
|
|
+ sectionId: selectedSection,
|
|
|
+ partialId: selectedPartial,
|
|
|
+ date: selectedDate,
|
|
|
+ status: student.attendance?.status || 'absent'
|
|
|
+ }))
|
|
|
+
|
|
|
+ const response = await fetch('/api/teacher/attendance', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ attendance: attendanceData })
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ toast.success('Asistencia guardada correctamente')
|
|
|
+ fetchStudents() // Refresh data
|
|
|
+ } else {
|
|
|
+ toast.error('Error al guardar la asistencia')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast.error('Error al guardar la asistencia')
|
|
|
+ } finally {
|
|
|
+ setSaving(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const getStatusIcon = (status: string) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'present':
|
|
|
+ return <CheckCircle className="h-4 w-4 text-green-600" />
|
|
|
+ case 'late':
|
|
|
+ return <Clock className="h-4 w-4 text-yellow-600" />
|
|
|
+ case 'absent':
|
|
|
+ return <XCircle className="h-4 w-4 text-red-600" />
|
|
|
+ default:
|
|
|
+ return <XCircle className="h-4 w-4 text-gray-400" />
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const getStatusBadge = (status: string) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'present':
|
|
|
+ return <Badge className="bg-green-100 text-green-800">Presente</Badge>
|
|
|
+ case 'late':
|
|
|
+ return <Badge className="bg-yellow-100 text-yellow-800">Tardanza</Badge>
|
|
|
+ case 'absent':
|
|
|
+ return <Badge className="bg-red-100 text-red-800">Ausente</Badge>
|
|
|
+ default:
|
|
|
+ return <Badge variant="secondary">Sin marcar</Badge>
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900">Gestión de Asistencia</h1>
|
|
|
+ <p className="text-gray-600">Registra la asistencia de tus estudiantes</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Filters */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ <Users className="h-5 w-5" />
|
|
|
+ Seleccionar Clase
|
|
|
+ </CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium mb-2">Sección</label>
|
|
|
+ <Select value={selectedSection} onValueChange={setSelectedSection}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Seleccionar sección" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ {sections.map((section) => (
|
|
|
+ <SelectItem key={section.id} value={section.id}>
|
|
|
+ {section.className} - {section.name}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium mb-2">Parcial</label>
|
|
|
+ <Select value={selectedPartial} onValueChange={setSelectedPartial}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Seleccionar parcial" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ {partials.map((partial) => (
|
|
|
+ <SelectItem key={partial.id} value={partial.id}>
|
|
|
+ {partial.name}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium mb-2">Fecha</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ value={selectedDate}
|
|
|
+ onChange={(e) => setSelectedDate(e.target.value)}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-end">
|
|
|
+ <Button
|
|
|
+ onClick={fetchStudents}
|
|
|
+ disabled={!selectedSection || !selectedPartial || loading}
|
|
|
+ className="w-full"
|
|
|
+ >
|
|
|
+ {loading ? 'Cargando...' : 'Cargar Estudiantes'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Students List */}
|
|
|
+ {students.length > 0 && (
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <CardTitle>Lista de Estudiantes</CardTitle>
|
|
|
+ <Button onClick={saveAttendance} disabled={saving}>
|
|
|
+ {saving ? 'Guardando...' : 'Guardar Asistencia'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-4">
|
|
|
+ {students.map((student) => (
|
|
|
+ <div
|
|
|
+ key={student.id}
|
|
|
+ className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50"
|
|
|
+ >
|
|
|
+ <div className="flex items-center space-x-3">
|
|
|
+ {getStatusIcon(student.attendance?.status || 'absent')}
|
|
|
+ <div>
|
|
|
+ <h3 className="font-medium">{student.name}</h3>
|
|
|
+ <p className="text-sm text-gray-600">{student.email}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
+ {getStatusBadge(student.attendance?.status || 'absent')}
|
|
|
+
|
|
|
+ <div className="flex space-x-2">
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant={student.attendance?.status === 'present' ? 'default' : 'outline'}
|
|
|
+ onClick={() => updateAttendance(student.id, 'present')}
|
|
|
+ >
|
|
|
+ Presente
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant={student.attendance?.status === 'late' ? 'default' : 'outline'}
|
|
|
+ onClick={() => updateAttendance(student.id, 'late')}
|
|
|
+ >
|
|
|
+ Tardanza
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant={student.attendance?.status === 'absent' ? 'default' : 'outline'}
|
|
|
+ onClick={() => updateAttendance(student.id, 'absent')}
|
|
|
+ >
|
|
|
+ Ausente
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {selectedSection && selectedPartial && students.length === 0 && !loading && (
|
|
|
+ <Card>
|
|
|
+ <CardContent className="text-center py-8">
|
|
|
+ <p className="text-gray-500">No hay estudiantes matriculados en esta sección.</p>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|