Skip to content

Commit 77c2d62

Browse files
authored
273 - Session Absent Student Updates (#283)
* init * fixed * lint
1 parent ae23c43 commit 77c2d62

4 files changed

Lines changed: 114 additions & 16 deletions

File tree

frontend/src/app/sessions/[id]/page.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { Avatar } from "@/components/ui/avatar";
44
import { Button } from "@/components/ui/button";
55
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
6+
import type { StudentAttendance, StudentTuple} from '@/contexts/sessionContext';
7+
import { useSessionContext } from '@/contexts/sessionContext';
68
import { formatRecurrence, useSession, useSessions } from "@/hooks/useSessions";
79
import {
810
useSessionStudents,
@@ -31,7 +33,7 @@ import {
3133
} from "lucide-react";
3234
import Link from "next/link";
3335
import { useSearchParams } from "next/navigation";
34-
import { use, useState } from "react";
36+
import { use, useEffect, useState } from "react";
3537

3638
interface PageProps {
3739
params: Promise<{
@@ -79,6 +81,43 @@ export default function SessionPage({ params }: PageProps) {
7981
const [deleteType, setDeleteType] = useState<"single" | "recurring" | null>(
8082
null
8183
);
84+
const { attendance, setAttendance, setStudents, students: contextStudents, attendance: contextAttendance } = useSessionContext();
85+
86+
useEffect(() => {
87+
if (!studentsLoading && sessionStudents.length > 0) {
88+
// 1. Calculate the NEW student tuples based on current session students
89+
const studentTuples: StudentTuple[] = sessionStudents
90+
.filter(s => s.session_student_id)
91+
.map(s => ({
92+
studentId: s.id,
93+
sessionStudentId: s.session_student_id!,
94+
}));
95+
96+
// 2. Calculate the NEW attendance map
97+
const initialAttendance: StudentAttendance = sessionStudents.reduce((acc, student) => {
98+
if (student.session_student_id) {
99+
acc[student.session_student_id] = student.present ?? true;
100+
}
101+
return acc;
102+
}, {} as StudentAttendance);
103+
104+
// --- CRITICAL CHECK: Prevent infinite loop by comparing objects/arrays ---
105+
106+
// Check if the student list length has changed (simplest check)
107+
const studentsChanged = studentTuples.length !== contextStudents.length;
108+
109+
// Check if the attendance map contents have changed (more robust)
110+
const attendanceJson = JSON.stringify(initialAttendance);
111+
const contextAttendanceJson = JSON.stringify(contextAttendance);
112+
const attendanceChanged = attendanceJson !== contextAttendanceJson;
113+
114+
if (studentsChanged || attendanceChanged) {
115+
// Only update if there's a difference
116+
setStudents(studentTuples);
117+
setAttendance(initialAttendance);
118+
}
119+
}
120+
}, [sessionStudents, studentsLoading, setAttendance, setStudents, contextStudents, contextAttendance]); // Add context state dependencies
82121

83122
if (sessionLoading || studentsLoading) {
84123
return (
@@ -139,12 +178,25 @@ export default function SessionPage({ params }: PageProps) {
139178
});
140179
};
141180

142-
const handleToggleAttendance = (studentId: string, present: boolean) => {
181+
const handleToggleAttendance = (
182+
studentId: string,
183+
sessionStudentId: number | undefined,
184+
present: boolean
185+
) => {
186+
// 1. Update the backend/API
143187
updateSessionStudent({
144188
session_id: id,
145189
student_id: studentId,
146190
present,
147191
});
192+
193+
// 2. Update the Session Context for immediate UI filtering in other views
194+
if (sessionStudentId) {
195+
setAttendance({
196+
...(attendance ?? {}),
197+
[sessionStudentId]: present,
198+
});
199+
}
148200
};
149201

150202
const handleEditClick = () => {
@@ -526,7 +578,7 @@ export default function SessionPage({ params }: PageProps) {
526578
<Button
527579
onClick={(e) => {
528580
e.preventDefault();
529-
handleToggleAttendance(student.id, true);
581+
handleToggleAttendance(student.id, student.session_student_id, true);
530582
}}
531583
variant={student.present ? "default" : "outline"}
532584
size="sm"
@@ -536,7 +588,7 @@ export default function SessionPage({ params }: PageProps) {
536588
<Button
537589
onClick={(e) => {
538590
e.preventDefault();
539-
handleToggleAttendance(student.id, false);
591+
handleToggleAttendance(student.id, student.session_student_id, false);
540592
}}
541593
variant={!student.present ? "default" : "outline"}
542594
size="sm"

frontend/src/components/games/StudentSelector.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ export function StudentSelector({
1818
onStudentsSelected,
1919
gameTitle
2020
}: StudentSelectorProps) {
21-
const { students: sessionStudents } = useSessionContext()
21+
const { students: sessionStudents, attendance } = useSessionContext() // <-- Get attendance
2222
const [selectedStudentIds, setSelectedStudentIds] = useState<string[]>([])
2323
const { students: allStudents, isLoading } = useStudents()
2424

25-
// Get full student details for session students
26-
const studentsInSession = sessionStudents
25+
// 1. FILTER: Only keep students who are marked as present
26+
const presentSessionStudents = sessionStudents.filter(s => attendance[s.sessionStudentId] !== false)
27+
28+
// 2. Get full student details for *present* session students
29+
const studentsInSession = presentSessionStudents // <-- Use filtered list
2730
.map(({ studentId, sessionStudentId }) => {
2831
const student = allStudents?.find(s => s.id === studentId)
2932
return student ? {
@@ -44,6 +47,8 @@ export function StudentSelector({
4447

4548
const handleStartGame = () => {
4649
if (selectedStudentIds.length > 0) {
50+
// NOTE: This component passes back the sessionStudentId (as a string),
51+
// which is correctly the unique identifier needed for tracking within the session.
4752
onStudentsSelected(selectedStudentIds)
4853
}
4954
}
@@ -80,7 +85,7 @@ export function StudentSelector({
8085

8186
{studentsInSession.length === 0 ? (
8287
<div className="text-center py-8">
83-
<p className="text-secondary mb-4">No students in this session</p>
88+
<p className="text-secondary mb-4">No students are marked as Present for this session.</p>
8489
<button
8590
onClick={onBack}
8691
className="px-6 py-2 bg-blue text-white rounded-lg hover:bg-blue-hover transition-colors"
@@ -91,7 +96,8 @@ export function StudentSelector({
9196
) : (
9297
<>
9398
<div className="space-y-3 mb-6">
94-
{studentsInSession.map((student) => (
99+
{/* This map automatically only includes PRESENT students */}
100+
{studentsInSession.map((student) => (
95101
<label
96102
key={student.sessionStudentId}
97103
className={`flex items-center gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all ${

frontend/src/components/rate/StudentSelector.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// --- RateStudentSelector.tsx (Updated) ---
2+
13
'use client'
24

35
import {
@@ -10,6 +12,7 @@ import { Avatar } from '@/components/ui/avatar'
1012
import { useRouter } from 'next/navigation'
1113
import { useStudents } from '@/hooks/useStudents'
1214
import type { StudentTuple } from '@/contexts/sessionContext'
15+
import { useSessionContext } from '@/contexts/sessionContext' // <-- Import context
1316
import { getAvatarName, getAvatarVariant } from '@/lib/avatarUtils'
1417

1518
interface RateStudentSelectorProps {
@@ -24,16 +27,27 @@ export default function RateStudentSelector({
2427
sessionId,
2528
}: RateStudentSelectorProps) {
2629
const router = useRouter()
30+
const { attendance } = useSessionContext()
31+
32+
const presentStudents = students.filter(s => attendance[s.sessionStudentId] !== false)
2733

28-
// Fetch full student data for all students in this session
29-
const { students: studentData } = useStudents({
30-
ids: students.map(s => s.studentId)
34+
// Fetch full student data for all *present* students in this session
35+
// Extract the isLoading state
36+
const { students: studentData, isLoading } = useStudents({ // <-- Get isLoading
37+
ids: presentStudents.map(s => s.studentId)
3138
})
39+
40+
// 1. ADD LOADING CHECK HERE
41+
if (isLoading) {
42+
// Return a simple loading state or null while the data fetches
43+
return <div className="w-[280px] h-14 bg-gray-100 animate-pulse rounded-full mt-3.5" />;
44+
}
3245

33-
// Map student tuples to full student objects
46+
// Map present student tuples to full student objects
3447
const studentMap = new Map(studentData?.map(s => [s.id, s]) ?? [])
3548

36-
const enrichedStudents = students
49+
// ... (rest of enrichedStudents calculation remains the same)
50+
const enrichedStudents = presentStudents
3751
.map(s => ({
3852
sessionStudentId: s.sessionStudentId,
3953
student: studentMap.get(s.studentId),
@@ -46,11 +60,14 @@ export default function RateStudentSelector({
4660
router.push(`/sessions/${sessionId}/rate/${value}`)
4761
}
4862

63+
// Ensure the current student is still available (they should be present if they are being rated)
4964
const currentStudent = enrichedStudents.find(
5065
(s) => s.sessionStudentId === currentSessionStudentId
5166
)?.student
5267

5368
if (!currentStudent) {
69+
// This case should ideally not happen if the URL is pointing to a present student
70+
// but we can fall back or return null if the current student is filtered out.
5471
return null
5572
}
5673

@@ -71,7 +88,8 @@ export default function RateStudentSelector({
7188
</div>
7289
</SelectTrigger>
7390
<SelectContent className="bg-white border border-border">
74-
{enrichedStudents.map((s) => (
91+
{/* Only present students are mapped into enrichedStudents */}
92+
{enrichedStudents.map((s) => (
7593
<SelectItem
7694
key={s.sessionStudentId}
7795
value={s.sessionStudentId.toString()}
@@ -90,4 +108,4 @@ export default function RateStudentSelector({
90108
</SelectContent>
91109
</Select>
92110
)
93-
}
111+
}

frontend/src/contexts/sessionContext.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
'use client'
23
import type { Session } from '@/lib/api/theSpecialStandardAPI.schemas'
34
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
@@ -7,15 +8,20 @@ export interface StudentTuple {
78
sessionStudentId: number
89
}
910

11+
// Type for attendance tracking (Map of sessionStudentId to isPresent)
12+
export type StudentAttendance = Record<number, boolean>
13+
1014
interface SessionContextType {
1115
session: Session | null
1216
students: StudentTuple[]
17+
attendance: StudentAttendance
1318
currentWeek: number
1419
currentMonth: number
1520
currentYear: number
1621
currentLevel: number | null
1722
setSession: (session: Session) => void
1823
setStudents: (students: StudentTuple[]) => void
24+
setAttendance: (attendance: StudentAttendance) => void
1925
setCurrentWeek: (week: number) => void
2026
setCurrentMonth: (month: number) => void
2127
setCurrentYear: (year: number) => void
@@ -28,6 +34,7 @@ const SessionContext = createContext<SessionContextType | undefined>(undefined)
2834
export function SessionProvider({ children }: { children: React.ReactNode }) {
2935
const [session, setSessionState] = useState<Session | null>(null)
3036
const [students, setStudentsState] = useState<StudentTuple[]>([])
37+
const [attendance, setAttendanceState] = useState<StudentAttendance>({})
3138
const [currentWeek, setCurrentWeek] = useState<number>(1)
3239
const now = new Date()
3340
const [currentMonth, setCurrentMonth] = useState<number>(now.getMonth())
@@ -39,13 +46,15 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
3946
try {
4047
const savedSession = localStorage.getItem('session')
4148
const savedStudents = localStorage.getItem('students')
49+
const savedAttendance = localStorage.getItem('attendance')
4250
const savedCurrentWeek = localStorage.getItem('currentWeek')
4351
const savedCurrentMonth = localStorage.getItem('currentMonth')
4452
const savedCurrentYear = localStorage.getItem('currentYear')
4553
const savedCurrentLevel = localStorage.getItem('currentLevel')
4654

4755
if (savedSession) setSessionState(JSON.parse(savedSession))
4856
if (savedStudents) setStudentsState(JSON.parse(savedStudents))
57+
if (savedAttendance) setAttendanceState(JSON.parse(savedAttendance))
4958
if (savedCurrentWeek) setCurrentWeek(Number(savedCurrentWeek))
5059
if (savedCurrentMonth) setCurrentMonth(Number(savedCurrentMonth))
5160
if (savedCurrentYear) setCurrentYear(Number(savedCurrentYear))
@@ -64,6 +73,10 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
6473
localStorage.setItem('students', JSON.stringify(students))
6574
}, [students])
6675

76+
useEffect(() => {
77+
localStorage.setItem('attendance', JSON.stringify(attendance))
78+
}, [attendance])
79+
6780
useEffect(() => {
6881
localStorage.setItem('currentWeek', String(currentWeek))
6982
}, [currentWeek])
@@ -88,9 +101,15 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
88101
setStudentsState(newStudents)
89102
}, [])
90103

104+
const setAttendance = useCallback((newAttendance: StudentAttendance) => {
105+
106+
setAttendanceState(newAttendance)
107+
}, [])
108+
91109
const clearSession = useCallback(() => {
92110
setSessionState(null)
93111
setStudentsState([])
112+
setAttendanceState({})
94113
setCurrentWeek(1)
95114
const now = new Date()
96115
setCurrentMonth(now.getMonth())
@@ -100,6 +119,7 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
100119
// Clear localStorage
101120
localStorage.removeItem('session')
102121
localStorage.removeItem('students')
122+
localStorage.removeItem('attendance')
103123
localStorage.removeItem('currentWeek')
104124
localStorage.removeItem('currentMonth')
105125
localStorage.removeItem('currentYear')
@@ -111,12 +131,14 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
111131
value={{
112132
session,
113133
students,
134+
attendance,
114135
currentWeek,
115136
currentMonth,
116137
currentYear,
117138
currentLevel,
118139
setSession,
119140
setStudents,
141+
setAttendance,
120142
setCurrentWeek,
121143
setCurrentMonth,
122144
setCurrentYear,

0 commit comments

Comments
 (0)