Seth0330 commited on
Commit
a22e2d7
·
verified ·
1 Parent(s): 808185e

Create ClassStudentManager.jsx

Browse files
frontend/src/components/admin/ClassStudentManager.jsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Select,
6
+ SelectTrigger,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectValue,
10
+ } from "@/components/ui/select";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import { Skeleton } from "@/components/ui/skeleton";
13
+ import { Users, X, Plus, Trash2 } from "lucide-react";
14
+ import api from "../../api/client";
15
+
16
+ export default function ClassStudentManager({ classData, onClose }) {
17
+ const [memberships, setMemberships] = useState([]);
18
+ const [enrollments, setEnrollments] = useState([]);
19
+ const [selectedMembershipId, setSelectedMembershipId] = useState("");
20
+ const [loading, setLoading] = useState(true);
21
+ const [saving, setSaving] = useState(false);
22
+
23
+ const loadData = useCallback(async () => {
24
+ if (!classData?.id) return;
25
+ setLoading(true);
26
+ try {
27
+ const [enrollRes, memberRes] = await Promise.all([
28
+ api.get(`/api/admin/classes/${classData.id}/enrollments`),
29
+ api.get("/api/admin/memberships"),
30
+ ]);
31
+
32
+ setEnrollments(enrollRes.data || []);
33
+ setMemberships(memberRes.data?.items || []);
34
+ } catch (err) {
35
+ console.error("Failed to load class students", err);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }, [classData?.id]);
40
+
41
+ useEffect(() => {
42
+ loadData();
43
+ }, [loadData]);
44
+
45
+ const enrolledEmails = new Set(
46
+ enrollments
47
+ .filter((e) => e.status !== "removed")
48
+ .map((e) => e.student_email)
49
+ );
50
+
51
+ const availableMembers = memberships.filter(
52
+ (m) =>
53
+ (m.status === "active" || m.status === "pending") &&
54
+ !enrolledEmails.has(m.user_email)
55
+ );
56
+
57
+ async function handleAddStudent() {
58
+ if (!selectedMembershipId) return;
59
+ setSaving(true);
60
+ try {
61
+ await api.post(
62
+ `/api/admin/classes/${classData.id}/enroll-membership`,
63
+ {
64
+ membership_id: Number(selectedMembershipId),
65
+ }
66
+ );
67
+ setSelectedMembershipId("");
68
+ await loadData();
69
+ } catch (err) {
70
+ console.error("Failed to enroll student", err);
71
+ alert("Could not add student to class. Please try again.");
72
+ } finally {
73
+ setSaving(false);
74
+ }
75
+ }
76
+
77
+ async function handleRemove(enrollmentId) {
78
+ if (!window.confirm("Remove this student from the class?")) return;
79
+ setSaving(true);
80
+ try {
81
+ await api.delete(
82
+ `/api/admin/classes/${classData.id}/enrollments/${enrollmentId}`
83
+ );
84
+ await loadData();
85
+ } catch (err) {
86
+ console.error("Failed to remove enrollment", err);
87
+ alert("Could not remove student from class. Please try again.");
88
+ } finally {
89
+ setSaving(false);
90
+ }
91
+ }
92
+
93
+ return (
94
+ <Card>
95
+ <CardHeader className="flex flex-row items-center justify-between">
96
+ <div className="flex items-center gap-2">
97
+ <Users className="w-5 h-5 text-stone-600" />
98
+ <div>
99
+ <CardTitle className="text-base sm:text-lg">
100
+ Manage Students – {classData?.name}
101
+ </CardTitle>
102
+ <p className="text-xs text-stone-500">
103
+ Add existing members to this class. They will see it in their
104
+ student login.
105
+ </p>
106
+ </div>
107
+ </div>
108
+ <Button
109
+ type="button"
110
+ variant="ghost"
111
+ size="icon"
112
+ onClick={onClose}
113
+ className="text-stone-500"
114
+ >
115
+ <X className="w-4 h-4" />
116
+ </Button>
117
+ </CardHeader>
118
+
119
+ <CardContent className="space-y-4">
120
+ {/* Add student row */}
121
+ <div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
122
+ <Select
123
+ value={selectedMembershipId}
124
+ onValueChange={setSelectedMembershipId}
125
+ >
126
+ <SelectTrigger className="flex-1">
127
+ <SelectValue placeholder="Select a student to add…" />
128
+ </SelectTrigger>
129
+ <SelectContent>
130
+ {availableMembers.length === 0 && (
131
+ <SelectItem value="__none" disabled>
132
+ No available members
133
+ </SelectItem>
134
+ )}
135
+ {availableMembers.map((m) => (
136
+ <SelectItem key={m.id} value={String(m.id)}>
137
+ {m.user_name || m.user_email} • {m.plan_name}
138
+ </SelectItem>
139
+ ))}
140
+ </SelectContent>
141
+ </Select>
142
+
143
+ <Button
144
+ type="button"
145
+ onClick={handleAddStudent}
146
+ disabled={!selectedMembershipId || saving}
147
+ className="gap-2 bg-red-600 hover:bg-red-700"
148
+ >
149
+ <Plus className="w-4 h-4" />
150
+ Add
151
+ </Button>
152
+ </div>
153
+
154
+ {/* Students list */}
155
+ {loading ? (
156
+ <div className="space-y-2 mt-4">
157
+ <Skeleton className="h-10 w-full" />
158
+ <Skeleton className="h-10 w-full" />
159
+ </div>
160
+ ) : enrollments.filter((e) => e.status !== "removed").length === 0 ? (
161
+ <p className="text-sm text-stone-500 mt-4">
162
+ No students enrolled in this class yet.
163
+ </p>
164
+ ) : (
165
+ <div className="mt-4 space-y-2">
166
+ {enrollments
167
+ .filter((e) => e.status !== "removed")
168
+ .map((enrollment) => (
169
+ <div
170
+ key={enrollment.id}
171
+ className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 border rounded-md px-3 py-2"
172
+ >
173
+ <div>
174
+ <div className="font-medium">
175
+ {enrollment.student_name || enrollment.student_email}
176
+ </div>
177
+ <div className="text-xs text-stone-500">
178
+ {enrollment.student_email}
179
+ </div>
180
+ </div>
181
+ <div className="flex items-center gap-3">
182
+ <Badge
183
+ variant={
184
+ enrollment.status === "invited"
185
+ ? "secondary"
186
+ : "default"
187
+ }
188
+ className="text-xs"
189
+ >
190
+ {enrollment.status}
191
+ </Badge>
192
+ <Button
193
+ type="button"
194
+ variant="ghost"
195
+ size="icon"
196
+ className="text-red-600"
197
+ onClick={() => handleRemove(enrollment.id)}
198
+ disabled={saving}
199
+ >
200
+ <Trash2 className="w-4 h-4" />
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ ))}
205
+ </div>
206
+ )}
207
+ </CardContent>
208
+ </Card>
209
+ );
210
+ }