Update frontend/src/components/admin/ClassStudentManager.jsx
Browse files
frontend/src/components/admin/ClassStudentManager.jsx
CHANGED
|
@@ -16,14 +16,13 @@ const fetchEnrollments = async (classId) => {
|
|
| 16 |
};
|
| 17 |
|
| 18 |
/**
|
| 19 |
-
* Fetch all
|
| 20 |
-
* FastAPI route: /api/admin/
|
| 21 |
-
* → Frontend path: /admin/
|
| 22 |
*/
|
| 23 |
-
const
|
| 24 |
-
const res = await client.get("/admin/
|
| 25 |
-
|
| 26 |
-
return payload.items || [];
|
| 27 |
};
|
| 28 |
|
| 29 |
export default function ClassStudentManager({ classData, onClose }) {
|
|
@@ -36,21 +35,34 @@ export default function ClassStudentManager({ classData, onClose }) {
|
|
| 36 |
enabled: !!classId,
|
| 37 |
});
|
| 38 |
|
| 39 |
-
const { data:
|
| 40 |
-
queryKey: ["
|
| 41 |
-
queryFn:
|
| 42 |
});
|
| 43 |
|
| 44 |
-
const [
|
| 45 |
|
| 46 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const enrolledEmails = new Set(
|
| 48 |
enrollments
|
| 49 |
.filter((e) => e.status !== "removed")
|
| 50 |
.map((e) => e.student_email)
|
| 51 |
);
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
const enrollMutation = useMutation({
|
| 56 |
// POST /api/admin/classes/{class_id}/enroll
|
|
@@ -74,12 +86,15 @@ export default function ClassStudentManager({ classData, onClose }) {
|
|
| 74 |
});
|
| 75 |
|
| 76 |
const handleAddStudent = () => {
|
| 77 |
-
if (!
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
const member = memberships.find((m) => m.user_email === selectedEmail);
|
| 80 |
enrollMutation.mutate({
|
| 81 |
-
|
| 82 |
-
|
|
|
|
| 83 |
});
|
| 84 |
};
|
| 85 |
|
|
@@ -95,7 +110,7 @@ export default function ClassStudentManager({ classData, onClose }) {
|
|
| 95 |
Manage Students – {classData?.name}
|
| 96 |
</h2>
|
| 97 |
<p className="text-sm text-stone-500">
|
| 98 |
-
Add students to this class from
|
| 99 |
</p>
|
| 100 |
</div>
|
| 101 |
</div>
|
|
@@ -115,21 +130,24 @@ export default function ClassStudentManager({ classData, onClose }) {
|
|
| 115 |
<div className="flex flex-wrap gap-3 items-center">
|
| 116 |
<select
|
| 117 |
className="flex-1 min-w-[220px] rounded-lg border border-stone-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
| 118 |
-
value={
|
| 119 |
-
onChange={(e) =>
|
| 120 |
-
disabled={
|
| 121 |
>
|
| 122 |
<option value="">Select a student to add...</option>
|
| 123 |
-
{
|
| 124 |
-
<option key={
|
| 125 |
-
{
|
|
|
|
|
|
|
|
|
|
| 126 |
</option>
|
| 127 |
))}
|
| 128 |
</select>
|
| 129 |
<button
|
| 130 |
type="button"
|
| 131 |
onClick={handleAddStudent}
|
| 132 |
-
disabled={!
|
| 133 |
className="inline-flex items-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 134 |
>
|
| 135 |
<span>+</span>
|
|
@@ -156,9 +174,9 @@ export default function ClassStudentManager({ classData, onClose }) {
|
|
| 156 |
>
|
| 157 |
<div>
|
| 158 |
<div className="font-medium text-stone-900 text-sm">
|
| 159 |
-
{enrollment.student_name || enrollment.student_email}
|
| 160 |
</div>
|
| 161 |
-
{enrollment.student_name && (
|
| 162 |
<div className="text-xs text-stone-500">
|
| 163 |
{enrollment.student_email}
|
| 164 |
</div>
|
|
|
|
| 16 |
};
|
| 17 |
|
| 18 |
/**
|
| 19 |
+
* Fetch all students so we can pick individual students.
|
| 20 |
+
* FastAPI route: /api/admin/students
|
| 21 |
+
* → Frontend path: /admin/students
|
| 22 |
*/
|
| 23 |
+
const fetchStudents = async () => {
|
| 24 |
+
const res = await client.get("/admin/students");
|
| 25 |
+
return Array.isArray(res.data) ? res.data : [];
|
|
|
|
| 26 |
};
|
| 27 |
|
| 28 |
export default function ClassStudentManager({ classData, onClose }) {
|
|
|
|
| 35 |
enabled: !!classId,
|
| 36 |
});
|
| 37 |
|
| 38 |
+
const { data: students = [], isLoading: isLoadingStudents } = useQuery({
|
| 39 |
+
queryKey: ["students"],
|
| 40 |
+
queryFn: fetchStudents,
|
| 41 |
});
|
| 42 |
|
| 43 |
+
const [selectedStudentId, setSelectedStudentId] = useState("");
|
| 44 |
|
| 45 |
+
const availableStudents = useMemo(() => {
|
| 46 |
+
const enrolledStudentIds = new Set(
|
| 47 |
+
enrollments
|
| 48 |
+
.filter((e) => e.status !== "removed")
|
| 49 |
+
.map((e) => e.student_id)
|
| 50 |
+
.filter(Boolean) // Remove null/undefined
|
| 51 |
+
);
|
| 52 |
const enrolledEmails = new Set(
|
| 53 |
enrollments
|
| 54 |
.filter((e) => e.status !== "removed")
|
| 55 |
.map((e) => e.student_email)
|
| 56 |
);
|
| 57 |
+
// Filter out students that are already enrolled (by student_id or by email matching student name)
|
| 58 |
+
return students.filter((s) => {
|
| 59 |
+
// Check if student ID is enrolled
|
| 60 |
+
if (enrolledStudentIds.has(s.id)) return false;
|
| 61 |
+
// Check if student name matches any enrolled email (for backward compatibility)
|
| 62 |
+
// This is a fallback for old enrollments that might not have student_id
|
| 63 |
+
return !enrolledEmails.has(s.name);
|
| 64 |
+
});
|
| 65 |
+
}, [students, enrollments]);
|
| 66 |
|
| 67 |
const enrollMutation = useMutation({
|
| 68 |
// POST /api/admin/classes/{class_id}/enroll
|
|
|
|
| 86 |
});
|
| 87 |
|
| 88 |
const handleAddStudent = () => {
|
| 89 |
+
if (!selectedStudentId) return;
|
| 90 |
+
|
| 91 |
+
const student = students.find((s) => s.id === parseInt(selectedStudentId, 10));
|
| 92 |
+
if (!student) return;
|
| 93 |
|
|
|
|
| 94 |
enrollMutation.mutate({
|
| 95 |
+
student_id: student.id,
|
| 96 |
+
student_email: student.membership_email || "",
|
| 97 |
+
student_name: student.name,
|
| 98 |
});
|
| 99 |
};
|
| 100 |
|
|
|
|
| 110 |
Manage Students – {classData?.name}
|
| 111 |
</h2>
|
| 112 |
<p className="text-sm text-stone-500">
|
| 113 |
+
Add individual students to this class (students can be from the same or different memberships)
|
| 114 |
</p>
|
| 115 |
</div>
|
| 116 |
</div>
|
|
|
|
| 130 |
<div className="flex flex-wrap gap-3 items-center">
|
| 131 |
<select
|
| 132 |
className="flex-1 min-w-[220px] rounded-lg border border-stone-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
| 133 |
+
value={selectedStudentId}
|
| 134 |
+
onChange={(e) => setSelectedStudentId(e.target.value)}
|
| 135 |
+
disabled={isLoadingStudents || enrollMutation.isPending}
|
| 136 |
>
|
| 137 |
<option value="">Select a student to add...</option>
|
| 138 |
+
{availableStudents.map((student) => (
|
| 139 |
+
<option key={student.id} value={student.id}>
|
| 140 |
+
{student.name}
|
| 141 |
+
{student.belt_level ? ` (${student.belt_level})` : ""}
|
| 142 |
+
{student.membership_plan ? ` - ${student.membership_plan}` : ""}
|
| 143 |
+
{student.membership_email ? ` [${student.membership_email}]` : ""}
|
| 144 |
</option>
|
| 145 |
))}
|
| 146 |
</select>
|
| 147 |
<button
|
| 148 |
type="button"
|
| 149 |
onClick={handleAddStudent}
|
| 150 |
+
disabled={!selectedStudentId || enrollMutation.isPending || isLoadingStudents}
|
| 151 |
className="inline-flex items-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 152 |
>
|
| 153 |
<span>+</span>
|
|
|
|
| 174 |
>
|
| 175 |
<div>
|
| 176 |
<div className="font-medium text-stone-900 text-sm">
|
| 177 |
+
{enrollment.student_name || enrollment.student_email || "Unknown Student"}
|
| 178 |
</div>
|
| 179 |
+
{enrollment.student_email && enrollment.student_name && (
|
| 180 |
<div className="text-xs text-stone-500">
|
| 181 |
{enrollment.student_email}
|
| 182 |
</div>
|