Seth0330 commited on
Commit
336a91b
·
verified ·
1 Parent(s): d1e333b

Update frontend/src/components/admin/ClassStudentManager.jsx

Browse files
frontend/src/components/admin/ClassStudentManager.jsx CHANGED
@@ -1,5 +1,6 @@
1
  // frontend/src/components/admin/ClassStudentManager.jsx
2
  import React, { useState, useMemo } from "react";
 
3
  import client from "../../api/client";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
 
@@ -25,13 +26,14 @@ const fetchMemberships = async () => {
25
  return payload.items || [];
26
  };
27
 
28
- export default function ClassStudentManager({ classData }) {
29
  const queryClient = useQueryClient();
30
- const classId = classData.id;
31
 
32
  const { data: enrollments = [], isLoading: isLoadingEnrollments } = useQuery({
33
  queryKey: ["class-enrollments", classId],
34
  queryFn: () => fetchEnrollments(classId),
 
35
  });
36
 
37
  const { data: memberships = [], isLoading: isLoadingMembers } = useQuery({
@@ -50,10 +52,10 @@ export default function ClassStudentManager({ classData }) {
50
  return memberships.filter((m) => !enrolledEmails.has(m.user_email));
51
  }, [memberships, enrollments]);
52
 
53
- const inviteMutation = useMutation({
54
- // POST /api/admin/classes/{class_id}/invite
55
  mutationFn: (payload) =>
56
- client.post(`/admin/classes/${classId}/invite`, payload),
57
  onSuccess: () => {
58
  queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] });
59
  setSelectedEmail("");
@@ -75,80 +77,106 @@ export default function ClassStudentManager({ classData }) {
75
  if (!selectedEmail) return;
76
 
77
  const member = memberships.find((m) => m.user_email === selectedEmail);
78
- inviteMutation.mutate({
79
  student_email: selectedEmail,
80
  student_name: member?.user_name || "",
81
  });
82
  };
83
 
 
 
84
  return (
85
- <div className="space-y-4">
86
- {/* Add student row */}
87
- <div className="flex flex-wrap gap-3 items-center">
88
- <select
89
- className="border border-stone-300 rounded-md px-3 py-2 text-sm min-w-[220px]"
90
- value={selectedEmail}
91
- onChange={(e) => setSelectedEmail(e.target.value)}
92
- disabled={isLoadingMembers}
93
- >
94
- <option value="">Select a student to add…</option>
95
- {availableMembers.map((m) => (
96
- <option key={m.id} value={m.user_email}>
97
- {m.user_name || m.user_email} ({m.user_email})
98
- </option>
99
- ))}
100
- </select>
101
- <button
102
- type="button"
103
- onClick={handleAddStudent}
104
- disabled={!selectedEmail || inviteMutation.isPending}
105
- className="inline-flex items-center px-4 py-2 rounded-md bg-red-600 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
106
- >
107
- Add
108
- </button>
109
  </div>
110
 
111
- {/* Enrolled students */}
112
- <div className="border border-stone-200 rounded-md">
113
- <div className="px-4 py-2 border-b border-stone-200 text-sm font-medium text-stone-700">
114
- Enrolled Students
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  </div>
116
- <div className="divide-y divide-stone-100">
117
- {isLoadingEnrollments ? (
118
- <div className="px-4 py-4 text-sm text-stone-500">
119
- Loading students…
120
- </div>
121
- ) : enrollments.length === 0 ? (
122
- <div className="px-4 py-4 text-sm text-stone-500">
123
- No students enrolled in this class yet.
124
- </div>
125
- ) : (
126
- enrollments
127
- .filter((e) => e.status !== "removed")
128
- .map((enrollment) => (
 
129
  <div
130
  key={enrollment.id}
131
- className="px-4 py-3 flex items-center justify-between text-sm"
132
  >
133
  <div>
134
- <div className="font-medium">
135
  {enrollment.student_name || enrollment.student_email}
136
  </div>
137
- <div className="text-stone-500 text-xs">
138
- {enrollment.student_email}
139
- </div>
 
 
140
  </div>
141
  <button
142
  type="button"
143
  onClick={() => removeMutation.mutate(enrollment.id)}
144
- className="text-xs text-red-600 hover:underline"
 
145
  >
146
  Remove
147
  </button>
148
  </div>
149
- ))
150
- )}
151
- </div>
 
152
  </div>
153
  </div>
154
  );
 
1
  // frontend/src/components/admin/ClassStudentManager.jsx
2
  import React, { useState, useMemo } from "react";
3
+ import { Users2, X } from "lucide-react";
4
  import client from "../../api/client";
5
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
6
 
 
26
  return payload.items || [];
27
  };
28
 
29
+ export default function ClassStudentManager({ classData, onClose }) {
30
  const queryClient = useQueryClient();
31
+ const classId = classData?.id;
32
 
33
  const { data: enrollments = [], isLoading: isLoadingEnrollments } = useQuery({
34
  queryKey: ["class-enrollments", classId],
35
  queryFn: () => fetchEnrollments(classId),
36
+ enabled: !!classId,
37
  });
38
 
39
  const { data: memberships = [], isLoading: isLoadingMembers } = useQuery({
 
52
  return memberships.filter((m) => !enrolledEmails.has(m.user_email));
53
  }, [memberships, enrollments]);
54
 
55
+ const enrollMutation = useMutation({
56
+ // POST /api/admin/classes/{class_id}/enroll
57
  mutationFn: (payload) =>
58
+ client.post(`/admin/classes/${classId}/enroll`, payload),
59
  onSuccess: () => {
60
  queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] });
61
  setSelectedEmail("");
 
77
  if (!selectedEmail) return;
78
 
79
  const member = memberships.find((m) => m.user_email === selectedEmail);
80
+ enrollMutation.mutate({
81
  student_email: selectedEmail,
82
  student_name: member?.user_name || "",
83
  });
84
  };
85
 
86
+ const enrolledStudents = enrollments.filter((e) => e.status !== "removed");
87
+
88
  return (
89
+ <div className="mb-6 bg-white border border-stone-200 rounded-2xl shadow-sm">
90
+ <div className="px-4 sm:px-6 py-4 border-b border-stone-100 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
91
+ <div className="flex items-center gap-2">
92
+ <Users2 className="w-5 h-5 text-stone-700" />
93
+ <div>
94
+ <h2 className="text-base font-semibold text-stone-900">
95
+ Manage Students – {classData?.name}
96
+ </h2>
97
+ <p className="text-sm text-stone-500">
98
+ Add students to this class from your membership list
99
+ </p>
100
+ </div>
101
+ </div>
102
+ {onClose && (
103
+ <button
104
+ type="button"
105
+ onClick={onClose}
106
+ className="inline-flex items-center justify-center rounded-lg border border-stone-200 px-2.5 py-1.5 text-sm text-stone-600 hover:bg-stone-50"
107
+ >
108
+ <X className="w-4 h-4" />
109
+ </button>
110
+ )}
 
 
111
  </div>
112
 
113
+ <div className="px-4 sm:px-6 py-4 space-y-4">
114
+ {/* Add student row */}
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={selectedEmail}
119
+ onChange={(e) => setSelectedEmail(e.target.value)}
120
+ disabled={isLoadingMembers || enrollMutation.isPending}
121
+ >
122
+ <option value="">Select a student to add...</option>
123
+ {availableMembers.map((m) => (
124
+ <option key={m.id} value={m.user_email}>
125
+ {m.user_name || m.user_email} ({m.user_email})
126
+ </option>
127
+ ))}
128
+ </select>
129
+ <button
130
+ type="button"
131
+ onClick={handleAddStudent}
132
+ disabled={!selectedEmail || enrollMutation.isPending || isLoadingMembers}
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>
136
+ {enrollMutation.isPending ? "Adding..." : "Add"}
137
+ </button>
138
  </div>
139
+
140
+ {/* Enrolled students list */}
141
+ {isLoadingEnrollments ? (
142
+ <div className="text-center py-8 text-sm text-stone-500">
143
+ Loading students...
144
+ </div>
145
+ ) : enrolledStudents.length === 0 ? (
146
+ <div className="text-center py-8 text-sm text-stone-500">
147
+ No students enrolled in this class yet.
148
+ </div>
149
+ ) : (
150
+ <div className="border-t border-stone-100 pt-4">
151
+ <div className="space-y-2">
152
+ {enrolledStudents.map((enrollment) => (
153
  <div
154
  key={enrollment.id}
155
+ className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-stone-50"
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>
165
+ )}
166
  </div>
167
  <button
168
  type="button"
169
  onClick={() => removeMutation.mutate(enrollment.id)}
170
+ disabled={removeMutation.isPending}
171
+ className="text-xs font-medium text-red-600 hover:text-red-700 disabled:opacity-50"
172
  >
173
  Remove
174
  </button>
175
  </div>
176
+ ))}
177
+ </div>
178
+ </div>
179
+ )}
180
  </div>
181
  </div>
182
  );