117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
import type { ReactNode } from 'react'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import type { SessionStudents } from '@gruperly/shared'
|
|
import { useToast } from '../../components/ui'
|
|
import { getSessionStudents, markAttendance } from '../../lib/api'
|
|
import { isAttendanceLocked } from './utils'
|
|
import type { AttendanceChoice } from './utils'
|
|
|
|
type AttendanceContextValue = {
|
|
sessionId: string
|
|
sessionInfo: SessionStudents | undefined
|
|
studentsPending: boolean
|
|
studentsFailed: boolean
|
|
retryStudents: () => void
|
|
startTime: string | null
|
|
presentCount: number
|
|
choices: Record<string, AttendanceChoice>
|
|
choiceFor: (attendeeId: string) => AttendanceChoice
|
|
toggleStudent: (attendeeId: string) => void
|
|
saveAttendance: () => void
|
|
isSaving: boolean
|
|
}
|
|
|
|
const AttendanceContext = createContext<AttendanceContextValue | null>(null)
|
|
|
|
export function AttendanceProvider({ sessionId, children }: { sessionId: string; children: ReactNode }) {
|
|
const queryClient = useQueryClient()
|
|
const toast = useToast()
|
|
|
|
const studentsQuery = useQuery({
|
|
queryKey: ['class-students', sessionId],
|
|
queryFn: () => getSessionStudents(sessionId),
|
|
staleTime: 0,
|
|
})
|
|
|
|
const sessionInfo = studentsQuery.data
|
|
const [choices, setChoices] = useState<Record<string, AttendanceChoice>>({})
|
|
|
|
useEffect(() => {
|
|
if (!sessionInfo) return
|
|
const initial: Record<string, AttendanceChoice> = {}
|
|
for (const student of sessionInfo.students) {
|
|
if (isAttendanceLocked(student)) continue
|
|
initial[student.attendeeId] = student.attendanceStatus === 'ABSENT' ? 'ABSENT' : 'PRESENT'
|
|
}
|
|
setChoices(initial)
|
|
}, [sessionInfo])
|
|
|
|
const presentCount = useMemo(
|
|
() =>
|
|
(sessionInfo?.students ?? []).filter(
|
|
(student) => !isAttendanceLocked(student) && choices[student.attendeeId] === 'PRESENT',
|
|
).length,
|
|
[sessionInfo, choices],
|
|
)
|
|
|
|
const toggleStudent = (attendeeId: string) => {
|
|
setChoices((prev) => ({
|
|
...prev,
|
|
[attendeeId]: prev[attendeeId] === 'ABSENT' ? 'PRESENT' : 'ABSENT',
|
|
}))
|
|
}
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: () => {
|
|
const records = (sessionInfo?.students ?? [])
|
|
.filter((student) => !isAttendanceLocked(student))
|
|
.map((student) => ({
|
|
attendeeId: student.attendeeId,
|
|
status: choices[student.attendeeId] ?? 'PRESENT',
|
|
}))
|
|
return markAttendance(sessionId, { classSessionId: sessionId, records })
|
|
},
|
|
onSuccess: () => {
|
|
toast.success('Asistencia guardada correctamente.', 'Listo')
|
|
void queryClient.invalidateQueries({ queryKey: ['classes-today'] })
|
|
void queryClient.invalidateQueries({ queryKey: ['class-students', sessionId] })
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || 'No pudimos guardar la asistencia.')
|
|
},
|
|
})
|
|
|
|
const startTime = sessionInfo
|
|
? new Date(sessionInfo.startsAt).toLocaleTimeString('es-MX', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
})
|
|
: null
|
|
|
|
const value: AttendanceContextValue = {
|
|
sessionId,
|
|
sessionInfo,
|
|
studentsPending: studentsQuery.isPending,
|
|
studentsFailed: studentsQuery.isError,
|
|
retryStudents: () => void studentsQuery.refetch(),
|
|
startTime,
|
|
presentCount,
|
|
choices,
|
|
choiceFor: (attendeeId) => choices[attendeeId] ?? 'PRESENT',
|
|
toggleStudent,
|
|
saveAttendance: () => saveMutation.mutate(),
|
|
isSaving: saveMutation.isPending,
|
|
}
|
|
|
|
return <AttendanceContext.Provider value={value}>{children}</AttendanceContext.Provider>
|
|
}
|
|
|
|
export function useAttendance() {
|
|
const context = useContext(AttendanceContext)
|
|
if (!context) {
|
|
throw new Error('useAttendance debe usarse dentro de <AttendanceProvider>')
|
|
}
|
|
return context
|
|
} |