Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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>
);
}
190 changes: 171 additions & 19 deletions frontend/src/components/StudentTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,106 @@
"use client";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
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 { Plus } from "lucide-react";
import { useState } from "react";
import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
import StudentTableCreateEditForm from "./StudentTableCreateEdit";
import { TableTemplate } from "./TableTemplate";

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

export const StudentTable = () => {
const queryClient = useQueryClient();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [studentToDelete, setStudentToDelete] = useState<Student | null>(null);
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: Partial<Student> }) =>
studentService.updateStudent(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["students"] });
setSidebarOpen(false);
setEditingStudent(null);
},
onError: (error: Error) => {
console.error("Failed to update student:", error);
},
});

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

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

const handleDelete = (student: Student) => {
setStudentToDelete(student);
setDeleteDialogOpen(true);
};

const confirmDelete = () => {
if (studentToDelete) {
deleteMutation.mutate(studentToDelete.id);
}
};

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

const handleFormSubmit = (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,
},
});
}
// Note: Create functionality will need account_id, which requires additional work
};

const columns: ColumnDef<Student>[] = [
{
Expand Down Expand Up @@ -54,35 +149,92 @@ 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: {
lastRegistered: checked ? new Date() : null,
},
});
}}
disabled={updateMutation.isPending}
/>
);
},
},
];

return (
<TableTemplate data={data} columns={columns} details="Student table" />
<div className="space-y-4">
{/* New Student Button */}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Students</h2>
<Button onClick={handleCreate}>
<Plus className="mr-2 h-4 w-4" />
New Student
</Button>
</div>

{/* Table */}
<TableTemplate
data={studentsQuery.data?.items ?? []}
columns={columns}
details="Student table"
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={studentsQuery.isLoading}
error={studentsQuery.error}
/>

{/* 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>
)}

{/* Delete Confirmation Dialog */}
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
title="Delete Student"
description={`Are you sure you want to delete ${studentToDelete?.firstName} ${studentToDelete?.lastName}? This action cannot be undone.`}
isDeleting={deleteMutation.isPending}
/>
</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