// frontend/src/components/admin/ClassStudentManager.jsx import React, { useState, useMemo, useRef, useEffect } from "react"; import { Link } from "react-router-dom"; import { Users2, X, ChevronDown, Search, User } from "lucide-react"; import client from "../../api/client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; /** * Fetch enrollments for a specific class. * FastAPI route: /api/admin/classes/{class_id}/enrollments * Axios baseURL: /api * → Frontend path: /admin/classes/{class_id}/enrollments */ const fetchEnrollments = async (classId) => { const res = await client.get(`/admin/classes/${classId}/enrollments`); return res.data || []; }; /** * Fetch all students so we can pick individual students. * FastAPI route: /api/admin/students * → Frontend path: /admin/students */ const fetchStudents = async () => { const res = await client.get("/admin/students"); return Array.isArray(res.data) ? res.data : []; }; export default function ClassStudentManager({ classData, onClose }) { const queryClient = useQueryClient(); const classId = classData?.id; const { data: enrollments = [], isLoading: isLoadingEnrollments } = useQuery({ queryKey: ["class-enrollments", classId], queryFn: () => fetchEnrollments(classId), enabled: !!classId, }); const { data: students = [], isLoading: isLoadingStudents } = useQuery({ queryKey: ["students"], queryFn: fetchStudents, }); const [selectedStudentId, setSelectedStudentId] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const dropdownRef = useRef(null); const inputRef = useRef(null); const availableStudents = useMemo(() => { const enrolledStudentIds = new Set( enrollments .filter((e) => e.status !== "removed") .map((e) => e.student_id) .filter(Boolean) // Remove null/undefined ); const enrolledEmails = new Set( enrollments .filter((e) => e.status !== "removed") .map((e) => e.student_email) ); // Filter out students that are already enrolled (by student_id or by email matching student name) return students.filter((s) => { // Check if student ID is enrolled if (enrolledStudentIds.has(s.id)) return false; // Check if student name matches any enrolled email (for backward compatibility) // This is a fallback for old enrollments that might not have student_id return !enrolledEmails.has(s.name); }); }, [students, enrollments]); // Filter students based on search query const filteredStudents = useMemo(() => { if (!searchQuery.trim()) return availableStudents; const query = searchQuery.toLowerCase(); return availableStudents.filter((student) => { const fullName = `${student.first_name} ${student.last_name}`.toLowerCase(); const email = (student.membership_email || "").toLowerCase(); const planName = (student.membership_plan_name || "").toLowerCase(); return ( fullName.includes(query) || email.includes(query) || planName.includes(query) || (student.gender && student.gender.toLowerCase().includes(query)) ); }); }, [availableStudents, searchQuery]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setIsDropdownOpen(false); setHighlightedIndex(-1); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); // Handle keyboard navigation const handleKeyDown = (e) => { if (!isDropdownOpen && (e.key === "ArrowDown" || e.key === "Enter")) { setIsDropdownOpen(true); return; } if (e.key === "ArrowDown") { e.preventDefault(); setHighlightedIndex((prev) => prev < filteredStudents.length - 1 ? prev + 1 : prev ); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1)); } else if (e.key === "Enter" && highlightedIndex >= 0) { e.preventDefault(); const student = filteredStudents[highlightedIndex]; if (student) { handleSelectStudent(student); } } else if (e.key === "Escape") { setIsDropdownOpen(false); setHighlightedIndex(-1); } }; const handleSelectStudent = (student) => { setSelectedStudentId(student.id.toString()); setSearchQuery(`${student.first_name} ${student.last_name}`); setIsDropdownOpen(false); setHighlightedIndex(-1); }; const handleInputChange = (e) => { setSearchQuery(e.target.value); setIsDropdownOpen(true); setHighlightedIndex(-1); if (!e.target.value) { setSelectedStudentId(""); } }; const handleInputFocus = () => { setIsDropdownOpen(true); }; const enrollMutation = useMutation({ // POST /api/admin/classes/{class_id}/enroll mutationFn: (payload) => client.post(`/admin/classes/${classId}/enroll`, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] }); setSelectedStudentId(""); setSearchQuery(""); setIsDropdownOpen(false); }, }); const removeMutation = useMutation({ // DELETE /api/admin/classes/{class_id}/enrollments/{enrollment_id} mutationFn: (enrollmentId) => client.delete( `/admin/classes/${classId}/enrollments/${enrollmentId}` ), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] }); }, }); const handleAddStudent = () => { if (!selectedStudentId) return; const student = students.find((s) => s.id === parseInt(selectedStudentId, 10)); if (!student) return; enrollMutation.mutate({ student_id: student.id, student_email: student.membership_email || "", student_name: `${student.first_name} ${student.last_name}`, }); }; const enrolledStudents = enrollments.filter((e) => e.status !== "removed"); return (

Manage Students – {classData?.name}

Add individual students to this class (students can be from the same or different memberships)

{onClose && ( )}
{/* Add student row */}
{/* Dropdown list */} {isDropdownOpen && filteredStudents.length > 0 && (
{filteredStudents.map((student, index) => { const fullName = `${student.first_name} ${student.last_name}`; const isHighlighted = index === highlightedIndex; return ( ); })}
)} {/* No results message */} {isDropdownOpen && searchQuery && filteredStudents.length === 0 && (
No students found matching "{searchQuery}"
)} {/* Empty state */} {isDropdownOpen && !searchQuery && availableStudents.length === 0 && (
No available students to add
)}
{/* Enrolled students list */} {isLoadingEnrollments ? (
Loading students...
) : enrolledStudents.length === 0 ? (
No students enrolled in this class yet.
) : (
{enrolledStudents.map((enrollment) => { // Try to find the student by student_id or by matching email/name const student = enrollment.student_id ? students.find((s) => s.id === enrollment.student_id) : students.find( (s) => s.membership_email === enrollment.student_email || `${s.first_name} ${s.last_name}` === enrollment.student_name ); const studentId = student?.id || enrollment.student_id; return (
{enrollment.student_name || enrollment.student_email || "Unknown Student"}
{enrollment.student_email && enrollment.student_name && (
{enrollment.student_email}
)}
{studentId && ( e.stopPropagation()} > )}
); })}
)}
); }