Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions frontend/src/app/sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { Avatar } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import type { StudentAttendance, StudentTuple} from '@/contexts/sessionContext';
import { useSessionContext } from '@/contexts/sessionContext';
import { formatRecurrence, useSession, useSessions } from "@/hooks/useSessions";
import {
useSessionStudents,
Expand Down Expand Up @@ -31,7 +33,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { use, useState } from "react";
import { use, useEffect, useState } from "react";

interface PageProps {
params: Promise<{
Expand Down Expand Up @@ -79,6 +81,43 @@ export default function SessionPage({ params }: PageProps) {
const [deleteType, setDeleteType] = useState<"single" | "recurring" | null>(
null
);
const { attendance, setAttendance, setStudents, students: contextStudents, attendance: contextAttendance } = useSessionContext();

useEffect(() => {
if (!studentsLoading && sessionStudents.length > 0) {
// 1. Calculate the NEW student tuples based on current session students
const studentTuples: StudentTuple[] = sessionStudents
.filter(s => s.session_student_id)
.map(s => ({
studentId: s.id,
sessionStudentId: s.session_student_id!,
}));

// 2. Calculate the NEW attendance map
const initialAttendance: StudentAttendance = sessionStudents.reduce((acc, student) => {
if (student.session_student_id) {
acc[student.session_student_id] = student.present ?? true;
}
return acc;
}, {} as StudentAttendance);

// --- CRITICAL CHECK: Prevent infinite loop by comparing objects/arrays ---

// Check if the student list length has changed (simplest check)
const studentsChanged = studentTuples.length !== contextStudents.length;

// Check if the attendance map contents have changed (more robust)
const attendanceJson = JSON.stringify(initialAttendance);
const contextAttendanceJson = JSON.stringify(contextAttendance);
const attendanceChanged = attendanceJson !== contextAttendanceJson;

if (studentsChanged || attendanceChanged) {
// Only update if there's a difference
setStudents(studentTuples);
setAttendance(initialAttendance);
}
}
}, [sessionStudents, studentsLoading, setAttendance, setStudents, contextStudents, contextAttendance]); // Add context state dependencies

if (sessionLoading || studentsLoading) {
return (
Expand Down Expand Up @@ -139,12 +178,25 @@ export default function SessionPage({ params }: PageProps) {
});
};

const handleToggleAttendance = (studentId: string, present: boolean) => {
const handleToggleAttendance = (
studentId: string,
sessionStudentId: number | undefined,
present: boolean
) => {
// 1. Update the backend/API
updateSessionStudent({
session_id: id,
student_id: studentId,
present,
});

// 2. Update the Session Context for immediate UI filtering in other views
if (sessionStudentId) {
setAttendance({
...(attendance ?? {}),
[sessionStudentId]: present,
});
}
};

const handleEditClick = () => {
Expand Down Expand Up @@ -526,7 +578,7 @@ export default function SessionPage({ params }: PageProps) {
<Button
onClick={(e) => {
e.preventDefault();
handleToggleAttendance(student.id, true);
handleToggleAttendance(student.id, student.session_student_id, true);
}}
variant={student.present ? "default" : "outline"}
size="sm"
Expand All @@ -536,7 +588,7 @@ export default function SessionPage({ params }: PageProps) {
<Button
onClick={(e) => {
e.preventDefault();
handleToggleAttendance(student.id, false);
handleToggleAttendance(student.id, student.session_student_id, false);
}}
variant={!student.present ? "default" : "outline"}
size="sm"
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/components/games/StudentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ export function StudentSelector({
onStudentsSelected,
gameTitle
}: StudentSelectorProps) {
const { students: sessionStudents } = useSessionContext()
const { students: sessionStudents, attendance } = useSessionContext() // <-- Get attendance
const [selectedStudentIds, setSelectedStudentIds] = useState<string[]>([])
const { students: allStudents, isLoading } = useStudents()

// Get full student details for session students
const studentsInSession = sessionStudents
// 1. FILTER: Only keep students who are marked as present
const presentSessionStudents = sessionStudents.filter(s => attendance[s.sessionStudentId] !== false)

// 2. Get full student details for *present* session students
const studentsInSession = presentSessionStudents // <-- Use filtered list
.map(({ studentId, sessionStudentId }) => {
const student = allStudents?.find(s => s.id === studentId)
return student ? {
Expand All @@ -44,6 +47,8 @@ export function StudentSelector({

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

{studentsInSession.length === 0 ? (
<div className="text-center py-8">
<p className="text-secondary mb-4">No students in this session</p>
<p className="text-secondary mb-4">No students are marked as Present for this session.</p>
<button
onClick={onBack}
className="px-6 py-2 bg-blue text-white rounded-lg hover:bg-blue-hover transition-colors"
Expand All @@ -91,7 +96,8 @@ export function StudentSelector({
) : (
<>
<div className="space-y-3 mb-6">
{studentsInSession.map((student) => (
{/* This map automatically only includes PRESENT students */}
{studentsInSession.map((student) => (
<label
key={student.sessionStudentId}
className={`flex items-center gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all ${
Expand Down
32 changes: 25 additions & 7 deletions frontend/src/components/rate/StudentSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// --- RateStudentSelector.tsx (Updated) ---

'use client'

import {
Expand All @@ -10,6 +12,7 @@ import { Avatar } from '@/components/ui/avatar'
import { useRouter } from 'next/navigation'
import { useStudents } from '@/hooks/useStudents'
import type { StudentTuple } from '@/contexts/sessionContext'
import { useSessionContext } from '@/contexts/sessionContext' // <-- Import context
import { getAvatarName, getAvatarVariant } from '@/lib/avatarUtils'

interface RateStudentSelectorProps {
Expand All @@ -24,16 +27,27 @@ export default function RateStudentSelector({
sessionId,
}: RateStudentSelectorProps) {
const router = useRouter()
const { attendance } = useSessionContext()

const presentStudents = students.filter(s => attendance[s.sessionStudentId] !== false)

// Fetch full student data for all students in this session
const { students: studentData } = useStudents({
ids: students.map(s => s.studentId)
// Fetch full student data for all *present* students in this session
// Extract the isLoading state
const { students: studentData, isLoading } = useStudents({ // <-- Get isLoading
ids: presentStudents.map(s => s.studentId)
})

// 1. ADD LOADING CHECK HERE
if (isLoading) {
// Return a simple loading state or null while the data fetches
return <div className="w-[280px] h-14 bg-gray-100 animate-pulse rounded-full mt-3.5" />;
}

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

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

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

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

Expand All @@ -71,7 +88,8 @@ export default function RateStudentSelector({
</div>
</SelectTrigger>
<SelectContent className="bg-white border border-border">
{enrichedStudents.map((s) => (
{/* Only present students are mapped into enrichedStudents */}
{enrichedStudents.map((s) => (
<SelectItem
key={s.sessionStudentId}
value={s.sessionStudentId.toString()}
Expand All @@ -90,4 +108,4 @@ export default function RateStudentSelector({
</SelectContent>
</Select>
)
}
}
22 changes: 22 additions & 0 deletions frontend/src/contexts/sessionContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

'use client'
import type { Session } from '@/lib/api/theSpecialStandardAPI.schemas'
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
Expand All @@ -7,15 +8,20 @@ export interface StudentTuple {
sessionStudentId: number
}

// Type for attendance tracking (Map of sessionStudentId to isPresent)
export type StudentAttendance = Record<number, boolean>

interface SessionContextType {
session: Session | null
students: StudentTuple[]
attendance: StudentAttendance
currentWeek: number
currentMonth: number
currentYear: number
currentLevel: number | null
setSession: (session: Session) => void
setStudents: (students: StudentTuple[]) => void
setAttendance: (attendance: StudentAttendance) => void
setCurrentWeek: (week: number) => void
setCurrentMonth: (month: number) => void
setCurrentYear: (year: number) => void
Expand All @@ -28,6 +34,7 @@ const SessionContext = createContext<SessionContextType | undefined>(undefined)
export function SessionProvider({ children }: { children: React.ReactNode }) {
const [session, setSessionState] = useState<Session | null>(null)
const [students, setStudentsState] = useState<StudentTuple[]>([])
const [attendance, setAttendanceState] = useState<StudentAttendance>({})
const [currentWeek, setCurrentWeek] = useState<number>(1)
const now = new Date()
const [currentMonth, setCurrentMonth] = useState<number>(now.getMonth())
Expand All @@ -39,13 +46,15 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
try {
const savedSession = localStorage.getItem('session')
const savedStudents = localStorage.getItem('students')
const savedAttendance = localStorage.getItem('attendance')
const savedCurrentWeek = localStorage.getItem('currentWeek')
const savedCurrentMonth = localStorage.getItem('currentMonth')
const savedCurrentYear = localStorage.getItem('currentYear')
const savedCurrentLevel = localStorage.getItem('currentLevel')

if (savedSession) setSessionState(JSON.parse(savedSession))
if (savedStudents) setStudentsState(JSON.parse(savedStudents))
if (savedAttendance) setAttendanceState(JSON.parse(savedAttendance))
if (savedCurrentWeek) setCurrentWeek(Number(savedCurrentWeek))
if (savedCurrentMonth) setCurrentMonth(Number(savedCurrentMonth))
if (savedCurrentYear) setCurrentYear(Number(savedCurrentYear))
Expand All @@ -64,6 +73,10 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
localStorage.setItem('students', JSON.stringify(students))
}, [students])

useEffect(() => {
localStorage.setItem('attendance', JSON.stringify(attendance))
}, [attendance])

useEffect(() => {
localStorage.setItem('currentWeek', String(currentWeek))
}, [currentWeek])
Expand All @@ -88,9 +101,15 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
setStudentsState(newStudents)
}, [])

const setAttendance = useCallback((newAttendance: StudentAttendance) => {

setAttendanceState(newAttendance)
}, [])

const clearSession = useCallback(() => {
setSessionState(null)
setStudentsState([])
setAttendanceState({})
setCurrentWeek(1)
const now = new Date()
setCurrentMonth(now.getMonth())
Expand All @@ -100,6 +119,7 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
// Clear localStorage
localStorage.removeItem('session')
localStorage.removeItem('students')
localStorage.removeItem('attendance')
localStorage.removeItem('currentWeek')
localStorage.removeItem('currentMonth')
localStorage.removeItem('currentYear')
Expand All @@ -111,12 +131,14 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
value={{
session,
students,
attendance,
currentWeek,
currentMonth,
currentYear,
currentLevel,
setSession,
setStudents,
setAttendance,
setCurrentWeek,
setCurrentMonth,
setCurrentYear,
Expand Down