Added routes

This commit is contained in:
Jose Selesan
2026-09-25 10:05:56 -03:00
parent 336e1af523
commit 94c353f4f2
100 changed files with 6126 additions and 183 deletions

View 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
}