Added routes
This commit is contained in:
117
apps/web/src/features/class-attendance/AttendanceProvider.tsx
Normal file
117
apps/web/src/features/class-attendance/AttendanceProvider.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user