Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions frontend/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const authOptions: NextAuthOptions = {
id: "1",
name: "Admin User",
email: "[email protected]",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverting back to what it is in main

Suggested change
email: "[email protected]",
email: "[email protected]",
accessToken: "fake-access-token-for-dev",
refreshToken: "fake-refresh-token-for-dev",

accessToken: "fake-access-token-for-dev",
refreshToken: "fake-refresh-token-for-dev",
accessToken: "admin", // Backend mock auth expects "admin", "staff", or "student"
refreshToken: "admin",
};
}
return null;
Expand Down Expand Up @@ -96,4 +96,4 @@ const authOptions: NextAuthOptions = {

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST, authOptions };
export { authOptions, handler as GET, handler as POST };
11 changes: 11 additions & 0 deletions frontend/src/app/student-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { StudentTable } from "@/components/StudentTable";

export default function StudentDemoPage() {
return (
<div className="container mx-auto p-6">
<StudentTable />
</div>
);
}
56 changes: 56 additions & 0 deletions frontend/src/components/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";

interface DeleteConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
title?: string;
description?: string;
isDeleting?: boolean;
}

export function DeleteConfirmDialog({
open,
onOpenChange,
onConfirm,
title = "Delete Item",
description = "Are you sure you want to delete this item? This action cannot be undone.",
isDeleting = false,
}: DeleteConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirm}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
260 changes: 241 additions & 19 deletions frontend/src/components/StudentTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,186 @@
"use client";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { AccountService } from "@/services/accountService";
import { StudentService } from "@/services/studentService";
import { Student } from "@/types/api/student";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
import { useState } from "react";
import StudentTableCreateEditForm from "./StudentTableCreateEdit";
import { TableTemplate } from "./TableTemplate";

export const StudentTable = ({ data }: { data: Student[] }) => {
const [tableData, setTableData] = useState<Student[]>(data);
const studentService = new StudentService();
const accountService = new AccountService();

export const StudentTable = () => {
const queryClient = useQueryClient();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarMode, setSidebarMode] = useState<"create" | "edit">("create");
const [editingStudent, setEditingStudent] = useState<Student | null>(null);

// Fetch students
const studentsQuery = useQuery({
queryKey: ["students"],
queryFn: () => studentService.listStudents(),
retry: 1, // Only retry once
});

// Update student mutation (for checkbox and edit)
const updateMutation = useMutation({
mutationFn: ({
id,
data,
}: {
id: number;
data: {
firstName: string;
lastName: string;
phoneNumber: string;
contactPreference: "call" | "text";
lastRegistered: Date | null;
};
}) => studentService.updateStudent(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["students"] });
setSidebarOpen(false);
setEditingStudent(null);
},
onError: (error: Error) => {
console.error("Failed to update student:", error);
},
});

// Create account mutation
const createAccountMutation = useMutation({
mutationFn: (data: {
email: string;
firstName: string;
lastName: string;
pid: string;
}) =>
accountService.createAccount({
email: data.email,
first_name: data.firstName,
last_name: data.lastName,
pid: data.pid,
role: "student",
}),
onError: (error: Error) => {
console.error("Failed to create account:", error);
},
});

// Create student mutation
const createStudentMutation = useMutation({
mutationFn: ({
accountId,
data,
}: {
accountId: number;
data: {
firstName: string;
lastName: string;
phoneNumber: string;
contactPreference: "call" | "text";
lastRegistered: Date | null;
};
}) =>
studentService.createStudent({
account_id: accountId,
data: {
first_name: data.firstName,
last_name: data.lastName,
phone_number: data.phoneNumber,
contact_preference: data.contactPreference,
last_registered: data.lastRegistered
? data.lastRegistered.toISOString()
: null,
},
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["students"] });
setSidebarOpen(false);
setEditingStudent(null);
},
onError: (error: Error) => {
console.error("Failed to create student:", error);
},
});

// Delete student mutation
const deleteMutation = useMutation({
mutationFn: (id: number) => studentService.deleteStudent(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["students"] });
},
onError: (error: Error) => {
console.error("Failed to delete student:", error);
},
});

const handleEdit = (student: Student) => {
setEditingStudent(student);
setSidebarMode("edit");
setSidebarOpen(true);
};

const handleDelete = (student: Student) => {
deleteMutation.mutate(student.id);
};

const handleCreate = () => {
setEditingStudent(null);
setSidebarMode("create");
setSidebarOpen(true);
};

const handleFormSubmit = async (data: {
pid: string;
firstName: string;
lastName: string;
phoneNumber: string;
contactPreference: "call" | "text";
lastRegistered: Date | null;
}) => {
if (sidebarMode === "edit" && editingStudent) {
updateMutation.mutate({
id: editingStudent.id,
data: {
firstName: data.firstName,
lastName: data.lastName,
phoneNumber: data.phoneNumber,
contactPreference: data.contactPreference,
lastRegistered: data.lastRegistered,
},
});
} else if (sidebarMode === "create") {
// First create account, then create student with that account_id
try {
const account = await createAccountMutation.mutateAsync({
email: `${data.pid}@unc.edu`, // Generate email from PID
firstName: data.firstName,
lastName: data.lastName,
pid: data.pid,
});

// Then create student with the new account ID
createStudentMutation.mutate({
accountId: account.id,
data: {
firstName: data.firstName,
lastName: data.lastName,
phoneNumber: data.phoneNumber,
contactPreference: data.contactPreference,
lastRegistered: data.lastRegistered,
},
});
} catch (error) {
console.error("Failed to create student:", error);
}
}
};

const columns: ColumnDef<Student>[] = [
{
Expand Down Expand Up @@ -54,35 +229,82 @@ export const StudentTable = ({ data }: { data: Student[] }) => {
{
accessorKey: "lastRegistered",
header: "Is Registered",
enableColumnFilter: false, // disable filtering for checkbox column
enableColumnFilter: false,
cell: ({ row }) => {
const pid = row.getValue("pid") as string;
const student = tableData.find((s) => s.pid === pid);
const isRegistered = !!student?.lastRegistered;
const student = row.original;
const isRegistered = !!student.lastRegistered;
return (
<Checkbox
checked={isRegistered}
onCheckedChange={(checked: boolean) => {
setTableData((prev) =>
prev.map((student) =>
student.pid === pid
? {
...student,
lastRegistered: checked
? new Date()
: null,
}
: student
)
);
updateMutation.mutate({
id: student.id,
data: {
firstName: student.firstName,
lastName: student.lastName,
phoneNumber: student.phoneNumber,
contactPreference: student.contactPreference,
lastRegistered: checked ? new Date() : null,
},
});
}}
disabled={updateMutation.isPending}
/>
);
},
},
];

return (
<TableTemplate data={data} columns={columns} details="Student table" />
<div className="space-y-4">
{/* Table */}
<TableTemplate
data={studentsQuery.data?.items ?? []}
columns={columns}
resourceName="Student"
onEdit={handleEdit}
onDelete={handleDelete}
onCreateNew={handleCreate}
isLoading={studentsQuery.isLoading}
error={studentsQuery.error}
getDeleteDescription={(student: Student) =>
`Are you sure you want to delete ${student.firstName} ${student.lastName}? This action cannot be undone.`
}
isDeleting={deleteMutation.isPending}
/>

{/* Sidebar for Create/Edit */}
{sidebarOpen && (
<div className="fixed right-0 top-0 h-full w-96 bg-white shadow-lg p-6 overflow-y-auto z-50">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">
{sidebarMode === "create" ? "New Student" : "Edit Student"}
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(false)}
>
</Button>
</div>
<StudentTableCreateEditForm
onSubmit={handleFormSubmit}
editData={
editingStudent
? {
pid: editingStudent.pid,
firstName: editingStudent.firstName,
lastName: editingStudent.lastName,
phoneNumber: editingStudent.phoneNumber,
contactPreference: editingStudent.contactPreference,
lastRegistered: editingStudent.lastRegistered,
}
: undefined
}
/>
</div>
)}
</div>
);
};
4 changes: 2 additions & 2 deletions frontend/src/components/StudentTableCreateEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const StudentCreateEditValues = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Second name is required"),
phoneNumber: z.string().min(1, "Phone number is required"),
contactPreference: z.string(),
contactPreference: z.enum(["call", "text"]),
lastRegistered: z.date().nullable(),
pid: z.string().length(9, "Please input a valid PID")
});
Expand All @@ -49,7 +49,7 @@ export default function StudentTableCreateEditForm({ onSubmit, editData }: Stude
firstName: editData?.firstName ?? "",
lastName: editData?.lastName ?? "",
phoneNumber: editData?.phoneNumber ?? "",
contactPreference: editData?.contactPreference ?? "",
contactPreference: editData?.contactPreference ?? undefined,
lastRegistered: editData?.lastRegistered ?? null
});
const [errors, setErrors] = useState<Record<string, string>>({});
Expand Down
Loading