Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
11 changes: 11 additions & 0 deletions frontend/src/app/account-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

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

export default function AccountDemoPage() {
return (
<div className="container mx-auto p-6">
<AccountTable />
</div>
);
}
4 changes: 1 addition & 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,6 @@ 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",
};
}
return null;
Expand Down Expand Up @@ -96,4 +94,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/location-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

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

export default function LocationDemoPage() {
return (
<div className="container mx-auto p-6">
<LocationTable />
</div>
);
}
11 changes: 11 additions & 0 deletions frontend/src/app/party-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

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

export default function PartyDemoPage() {
return (
<div className="container mx-auto p-6">
<PartyTable />
</div>
);
}
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>
);
}
181 changes: 181 additions & 0 deletions frontend/src/components/AccountTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client";

import { Button } from "@/components/ui/button";
import { AccountService } from "@/services/accountService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
import { useState } from "react";
import * as z from "zod";
import AccountTableCreateEditForm, {
AccountCreateEditValues as AccountCreateEditSchema,
} from "./AccountTableCreateEdit";
import { TableTemplate } from "./TableTemplate";

import type { Account, AccountRole } from "@/services/accountService";

type AccountCreateEditValues = z.infer<typeof AccountCreateEditSchema>;

const accountService = new AccountService();

export const AccountTable = () => {
const queryClient = useQueryClient();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarMode, setSidebarMode] = useState<"create" | "edit">("create");
const [editingAccount, setEditingAccount] = useState<Account | null>(null);

const accountsQuery = useQuery({
queryKey: ["accounts"],
queryFn: () => accountService.listAccounts(),
retry: 1,
});

const accounts = (accountsQuery.data ?? []).slice().sort((a, b) =>
a.last_name.localeCompare(b.last_name) ||
a.first_name.localeCompare(b.first_name)
);

const createMutation = useMutation({
mutationFn: (data: AccountCreateEditValues) =>
accountService.createAccount({
email: data.email,
first_name: data.firstName,
last_name: data.lastName,
pid: data.pid,
role: data.role as AccountRole,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
setSidebarOpen(false);
setEditingAccount(null);
},
onError: (error: Error) => {
console.error("Failed to create account:", error);
},
});

const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: AccountCreateEditValues }) =>
accountService.updateAccount(id, {
email: data.email,
first_name: data.firstName,
last_name: data.lastName,
pid: data.pid,
role: data.role as AccountRole,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
setSidebarOpen(false);
setEditingAccount(null);
},
onError: (error: Error) => {
console.error("Failed to update account:", error);
},
});

const deleteMutation = useMutation({
mutationFn: (id: number) => accountService.deleteAccount(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
},
onError: (error: Error) => {
console.error("Failed to delete account:", error);
},
});

const handleEdit = (account: Account) => {
setEditingAccount(account);
setSidebarMode("edit");
setSidebarOpen(true);
};

const handleDelete = (account: Account) => {
deleteMutation.mutate(account.id);
};

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

const handleFormSubmit = async (data: AccountCreateEditValues) => {
if (sidebarMode === "edit" && editingAccount) {
updateMutation.mutate({ id: editingAccount.id, data });
} else if (sidebarMode === "create") {
createMutation.mutate(data);
}
};

const columns: ColumnDef<Account>[] = [
{
accessorKey: "email",
header: "Email",
enableColumnFilter: true,
},
{
accessorKey: "first_name",
header: "First Name",
enableColumnFilter: true,
},
{
accessorKey: "last_name",
header: "Last Name",
enableColumnFilter: true,
},
{
accessorKey: "role",
header: "Admin Type",
enableColumnFilter: true,
},
];

return (
<div className="space-y-4">
<TableTemplate
data={accounts}
columns={columns}
resourceName="Account"
onEdit={handleEdit}
onDelete={handleDelete}
onCreateNew={handleCreate}
isLoading={accountsQuery.isLoading}
error={accountsQuery.error as Error | null}
getDeleteDescription={(account: Account) =>
`Are you sure you want to delete account ${account.email}? This action cannot be undone.`
}
isDeleting={deleteMutation.isPending}
/>

{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 Account" : "Edit Account"}
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(false)}
>
Close
</Button>
</div>
<AccountTableCreateEditForm
onSubmit={handleFormSubmit}
editData={
editingAccount
? {
email: editingAccount.email,
firstName: editingAccount.first_name,
lastName: editingAccount.last_name,
role: editingAccount.role,
pid: editingAccount.pid ?? "",
}
: undefined
}
/>
</div>
)}
</div>
);
}
61 changes: 61 additions & 0 deletions frontend/src/components/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"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) {
const handleConfirm = () => {
onConfirm();
onOpenChange(false);
};

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={handleConfirm}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Loading