From be6541a65307505dd53552c4bcf8cadc32406ee4 Mon Sep 17 00:00:00 2001 From: Guneev Pannu Date: Tue, 8 Sep 2026 21:56:20 -0400 Subject: [PATCH 01/29] Expanded backend endpoints to accomodate mutating professors of a course and updated old docstring --- .../api/courses/[courseId]/students/route.ts | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/app/api/courses/[courseId]/students/route.ts b/src/app/api/courses/[courseId]/students/route.ts index b1932e8..cf8e757 100644 --- a/src/app/api/courses/[courseId]/students/route.ts +++ b/src/app/api/courses/[courseId]/students/route.ts @@ -12,11 +12,11 @@ interface RouteParams { // --------------------------------------------------------------------------- /** - * Returns all enrolled students and TAs for a course. + * Returns enrolled students, staff (TAs), and professors for a course. * - * Returns: { students: [{name, utorid}], tas: [{name, utorid}] } + * Returns: { students, tas, professors } each [{name, utorid}] * - * Only the professor enrolled in the course may call this. + * Only a professor enrolled in the course may call this. */ export async function GET(_request: NextRequest, { params }: RouteParams) { try { @@ -39,20 +39,21 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { } const enrollments = await prisma.courseEnrollment.findMany({ - where: { courseId, role: { in: ["STUDENT", "TA"] } }, + where: { courseId, role: { in: ["STUDENT", "TA", "PROFESSOR"] } }, include: { user: { select: { name: true, utorid: true } } }, orderBy: { user: { name: "asc" } }, }); - const students = enrollments - .filter((e) => e.role === "STUDENT") - .map((e) => ({ name: e.user.name, utorid: e.user.utorid })); - - const tas = enrollments - .filter((e) => e.role === "TA") - .map((e) => ({ name: e.user.name, utorid: e.user.utorid })); + const mapEntry = (e: (typeof enrollments)[number]) => ({ + name: e.user.name, + utorid: e.user.utorid, + }); - return NextResponse.json({ students, tas }); + return NextResponse.json({ + students: enrollments.filter((e) => e.role === "STUDENT").map(mapEntry), + tas: enrollments.filter((e) => e.role === "TA").map(mapEntry), + professors: enrollments.filter((e) => e.role === "PROFESSOR").map(mapEntry), + }); } catch (error) { console.error("[Courses API] Failed to fetch roster:", error); return NextResponse.json({ error: "An error occurred." }, { status: 500 }); @@ -119,8 +120,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: "utorids must be a non-empty array." }, { status: 400 }); } - // Only STUDENT and TA are valid enrollment roles for this endpoint - const enrollmentRole: "STUDENT" | "TA" = roleParam === "TA" ? "TA" : "STUDENT"; + const enrollmentRole: "STUDENT" | "TA" | "PROFESSOR" = + roleParam === "TA" ? "TA" : roleParam === "PROFESSOR" ? "PROFESSOR" : "STUDENT"; const added: string[] = []; const alreadyEnrolled: string[] = []; @@ -186,7 +187,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { * * - Students in the new list but not enrolled → added as STUDENT * - Students currently enrolled as STUDENT but not in the new list → removed - * - TAs and the PROFESSOR are never touched. + * - TAs and professors are never touched. * * Returns: { added: string[], removed: string[], unchanged: number } * @@ -310,11 +311,17 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { // --------------------------------------------------------------------------- /** - * Removes a student or TA enrollment from a course by UTORid. + * Demotes a staff (TA) or co-professor enrollment to STUDENT by UTORid. + * Students are demoted the same way (no-op if already STUDENT — still "removed" + * from the instructional list by role). Actually students get role STUDENT update. * * Request body: { utorid: string } * - * Only the professor enrolled in the course may call this. + * Guards for PROFESSOR targets: + * - Cannot remove yourself + * - Cannot remove the last professor on the course + * + * Only a professor enrolled in the course may call this. */ export async function DELETE(request: NextRequest, { params }: RouteParams) { try { @@ -348,8 +355,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: "utorid is required." }, { status: 400 }); } + const normalizedUtorid = utorid.trim().toLowerCase(); const target = await prisma.user.findUnique({ - where: { utorid: utorid.trim().toLowerCase() }, + where: { utorid: normalizedUtorid }, select: { id: true }, }); if (!target) { @@ -362,18 +370,32 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (!targetEnrollment) { return NextResponse.json({ error: "User is not enrolled in this course." }, { status: 404 }); } + if (targetEnrollment.role === "PROFESSOR") { - return NextResponse.json({ error: "Cannot remove the course professor." }, { status: 403 }); + if (target.id === user.userId) { + return NextResponse.json( + { error: "You cannot remove yourself as a professor." }, + { status: 403 } + ); + } + const professorCount = await prisma.courseEnrollment.count({ + where: { courseId, role: "PROFESSOR" }, + }); + if (professorCount <= 1) { + return NextResponse.json( + { error: "Cannot remove the last professor from the course." }, + { status: 403 } + ); + } } - // Demote TA back to STUDENT rather than removing them from the course entirely. - // This preserves their enrollment so they stay in any active session. + // Demote to STUDENT rather than deleting the row — keeps them in live sessions. await prisma.courseEnrollment.update({ where: { userId_courseId: { userId: target.id, courseId } }, data: { role: "STUDENT" }, }); - return NextResponse.json({ removed: utorid.trim().toLowerCase() }); + return NextResponse.json({ removed: normalizedUtorid }); } catch (error) { console.error("[Courses API] Failed to remove student:", error); return NextResponse.json({ error: "An error occurred." }, { status: 500 }); From 33d5aa20e49fefcd2b07ffeda7331ab25f05b754 Mon Sep 17 00:00:00 2001 From: Guneev Pannu Date: Tue, 8 Sep 2026 22:06:01 -0400 Subject: [PATCH 02/29] Updated ManageClassModal internal states to support modifying professors. And renamed TAs tab to Staff --- src/app/classes/ManageClassModal.tsx | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/app/classes/ManageClassModal.tsx b/src/app/classes/ManageClassModal.tsx index a4cc2d0..702b0cb 100644 --- a/src/app/classes/ManageClassModal.tsx +++ b/src/app/classes/ManageClassModal.tsx @@ -32,7 +32,7 @@ interface ManageClassModalProps { onDeleted: (courseId: string) => void; } -type Tab = "students" | "tas" | "rename" | "delete"; +type Tab = "students" | "staff" | "rename" | "delete"; interface RosterEntry { name: string; @@ -193,12 +193,14 @@ export default function ManageClassModal({ // ---- Roster state ---- const [rosterStudents, setRosterStudents] = useState([]); const [rosterTas, setRosterTas] = useState([]); + const [rosterProfessors, setRosterProfessors] = useState([]); const [rosterLoading, setRosterLoading] = useState(true); const [rosterError, setRosterError] = useState(null); // ---- Search state ---- const [studentSearch, setStudentSearch] = useState(""); const [taSearch, setTaSearch] = useState(""); + const [professorSearch, setProfessorSearch] = useState(""); // ---- Remove state ---- const [removingUtorid, setRemovingUtorid] = useState(null); @@ -224,6 +226,16 @@ export default function ManageClassModal({ } | null>(null); const [taAddError, setTaAddError] = useState(null); + // ---- Add professors state ---- + const [professorUtoridsInput, setProfessorUtoridsInput] = useState(""); + const [addingProfessors, setAddingProfessors] = useState(false); + const [professorAddResult, setProfessorAddResult] = useState<{ + added: string[]; + alreadyEnrolled: string[]; + invalid: string[]; + } | null>(null); + const [professorAddError, setProfessorAddError] = useState(null); + // ---- CSV sync state ---- const csvInputRef = useRef(null); const [csvParsing, setCsvParsing] = useState(false); @@ -271,6 +283,7 @@ export default function ManageClassModal({ } setRosterStudents(data.students ?? []); setRosterTas(data.tas ?? []); + setRosterProfessors(data.professors ?? []); } catch { setRosterError("Failed to load roster."); } finally { @@ -315,6 +328,7 @@ export default function ManageClassModal({ // Optimistically remove from local state setRosterStudents((prev) => prev.filter((e) => e.utorid !== utorid)); setRosterTas((prev) => prev.filter((e) => e.utorid !== utorid)); + setRosterProfessors((prev) => prev.filter((e) => e.utorid !== utorid)); } catch { setRemoveError("Failed to remove. Please try again."); } finally { @@ -386,6 +400,38 @@ export default function ManageClassModal({ } } + async function handleAddProfessors() { + const utorids = professorUtoridsInput + .split(/[\n,\s]+/) + .map((s) => s.trim()) + .filter(Boolean); + if (utorids.length === 0) return; + + setAddingProfessors(true); + setProfessorAddError(null); + setProfessorAddResult(null); + + try { + const res = await fetch(`/api/courses/${course.id}/students`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ utorids, role: "PROFESSOR" }), + }); + const data = await res.json(); + if (!res.ok) { + setProfessorAddError(data.error ?? "Failed to add professors."); + return; + } + setProfessorAddResult(data); + setProfessorUtoridsInput(""); + await fetchRoster(); + } catch { + setProfessorAddError("Failed to add professors. Please try again."); + } finally { + setAddingProfessors(false); + } + } + async function handleCsvSelect(file: File) { setCsvError(null); setCsvPreview(null); @@ -522,7 +568,7 @@ export default function ManageClassModal({ {( [ { id: "students", label: "Students", icon: UserPlus }, - { id: "tas", label: "TAs", icon: GraduationCap }, + { id: "staff", label: "Staff", icon: GraduationCap }, { id: "rename", label: "Rename", icon: Pencil }, { id: "delete", label: "Delete", icon: Trash2 }, ] as { id: Tab; label: string; icon: React.ElementType }[] From 3dbbc2185ba094f860e4a2b5952884b91bd6cafa Mon Sep 17 00:00:00 2001 From: Guneev Pannu Date: Tue, 8 Sep 2026 22:09:00 -0400 Subject: [PATCH 03/29] Modified ManageClassModal UI to allow the updating of professors --- src/app/classes/ManageClassModal.tsx | 177 ++++++++++++++++++--------- 1 file changed, 122 insertions(+), 55 deletions(-) diff --git a/src/app/classes/ManageClassModal.tsx b/src/app/classes/ManageClassModal.tsx index 702b0cb..9bd9640 100644 --- a/src/app/classes/ManageClassModal.tsx +++ b/src/app/classes/ManageClassModal.tsx @@ -610,7 +610,8 @@ export default function ManageClassModal({

Sync Roster from CSV

Upload your updated class list CSV. Students added to the file will be enrolled; - students removed from the file will be unenrolled. TAs are not affected. + students removed from the file will be unenrolled. TAs and professors are not + affected.

{/* File picker */} @@ -734,61 +735,127 @@ export default function ManageClassModal({ )} - {/* ---- TAs ---- */} - {activeTab === "tas" && ( -
- -
-

Add TAs

-

- Enter one or more UTORids — separated by commas, spaces, or new lines. TAs can see - all questions and answer in restricted mode. -

-