| | |
| | import React, { useState } from "react"; |
| | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
| | import api from "../../api/client"; |
| |
|
| | export default function ClassEnrollments({ classData, onClose }) { |
| | const queryClient = useQueryClient(); |
| | const [email, setEmail] = useState(""); |
| |
|
| | const classId = classData?.id; |
| |
|
| | const { |
| | data: enrollments = [], |
| | isLoading, |
| | } = useQuery({ |
| | queryKey: ["class-enrollments", classId], |
| | enabled: !!classId, |
| | queryFn: async () => { |
| | const res = await api.get(`/admin/classes/${classId}/enrollments`); |
| | return Array.isArray(res.data) ? res.data : []; |
| | }, |
| | initialData: [], |
| | }); |
| |
|
| | const inviteMutation = useMutation({ |
| | mutationFn: async (payload) => { |
| | const res = await api.post( |
| | `/admin/classes/${classId}/invite`, |
| | payload |
| | ); |
| | return res.data; |
| | }, |
| | onSuccess: () => { |
| | setEmail(""); |
| | queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] }); |
| | }, |
| | }); |
| |
|
| | const removeMutation = useMutation({ |
| | mutationFn: async (enrollmentId) => { |
| | await api.delete( |
| | `/admin/classes/${classId}/enrollments/${enrollmentId}` |
| | ); |
| | }, |
| | onSuccess: () => { |
| | queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] }); |
| | }, |
| | }); |
| |
|
| | function handleInvite(e) { |
| | e.preventDefault(); |
| | if (!email.trim()) return; |
| | inviteMutation.mutate({ student_email: email.trim() }); |
| | } |
| |
|
| | return ( |
| | <div className="fixed inset-0 z-40 flex items-center justify-center bg-black/30"> |
| | <div className="w-full max-w-xl bg-white rounded-2xl shadow-xl border border-stone-200 p-5"> |
| | <div className="flex justify-between items-center mb-4"> |
| | <div> |
| | <h2 className="text-sm font-semibold text-stone-900"> |
| | Enrollments – {classData?.name} |
| | </h2> |
| | <p className="text-xs text-stone-500"> |
| | Invite students by email and manage their enrollment. |
| | </p> |
| | </div> |
| | <button |
| | type="button" |
| | onClick={onClose} |
| | className="text-xs text-stone-500 hover:text-stone-800" |
| | > |
| | Close |
| | </button> |
| | </div> |
| | |
| | {/* Invite form */} |
| | <form onSubmit={handleInvite} className="mb-4 flex gap-3"> |
| | <input |
| | type="email" |
| | placeholder="student@example.com" |
| | className="flex-1 rounded-lg border border-stone-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-red-500" |
| | value={email} |
| | onChange={(e) => setEmail(e.target.value)} |
| | /> |
| | <button |
| | type="submit" |
| | disabled={inviteMutation.isPending} |
| | className="rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 disabled:opacity-60" |
| | > |
| | {inviteMutation.isPending ? "Sending..." : "Invite"} |
| | </button> |
| | </form> |
| | |
| | {/* Enrollments list */} |
| | <div className="border-t border-stone-100 pt-3 max-h-64 overflow-y-auto"> |
| | {isLoading ? ( |
| | <div className="py-3 text-sm text-stone-500"> |
| | Loading enrollments... |
| | </div> |
| | ) : enrollments.length === 0 ? ( |
| | <div className="py-3 text-sm text-stone-500"> |
| | No enrollments yet. |
| | </div> |
| | ) : ( |
| | <ul className="divide-y divide-stone-100 text-sm"> |
| | {enrollments.map((en) => ( |
| | <li key={en.id} className="py-2 flex items-center justify-between"> |
| | <div> |
| | <div className="font-medium text-stone-900"> |
| | {en.student_email} |
| | </div> |
| | <div className="text-xs text-stone-500"> |
| | Status: {en.status || "invited"} |
| | </div> |
| | </div> |
| | <button |
| | type="button" |
| | onClick={() => removeMutation.mutate(en.id)} |
| | className="text-xs font-medium text-red-600 hover:text-red-700" |
| | > |
| | Remove |
| | </button> |
| | </li> |
| | ))} |
| | </ul> |
| | )} |
| | </div> |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|