Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 31 additions & 31 deletions frontend/src/app/staff/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
// import { AccountTable } from "@/components/AccountTable";
// import { LocationTable } from "@/components/LocationTable";
// import { PartyTable } from "@/components/PartyTable";
// import { StudentTable } from "@/components/StudentTable";
// import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AccountTable } from "@/components/AccountTable";
import { LocationTable } from "@/components/LocationTable";
import { PartyTable } from "@/components/PartyTable";
import { StudentTable } from "@/components/StudentTable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

// export default function StaffPage() {
// return (
// <div className="container mx-auto p-6">
// <Tabs>
// <TabsList defaultValue={"party"}>
// <TabsTrigger value="party">Parties</TabsTrigger>
// <TabsTrigger value="student">Students</TabsTrigger>
// <TabsTrigger value="location">Locations</TabsTrigger>
// <TabsTrigger value="account">Accounts</TabsTrigger>
// </TabsList>
// <TabsContent value="party">
// <PartyTable />
// </TabsContent>
// <TabsContent value="student">
// <StudentTable />
// </TabsContent>
// <TabsContent value="location">
// <LocationTable />
// </TabsContent>
// <TabsContent value="account">
// <AccountTable />
// </TabsContent>
// </Tabs>
// </div>
// );
// }
export default function StaffPage() {
return (
<div className="container mx-auto p-6">
<Tabs defaultValue="party">
<TabsList>
<TabsTrigger value="party">Parties</TabsTrigger>
<TabsTrigger value="student">Students</TabsTrigger>
<TabsTrigger value="location">Locations</TabsTrigger>
<TabsTrigger value="account">Accounts</TabsTrigger>
</TabsList>
<TabsContent value="party">
<PartyTable />
</TabsContent>
<TabsContent value="student">
<StudentTable />
</TabsContent>
<TabsContent value="location">
<LocationTable />
</TabsContent>
<TabsContent value="account">
<AccountTable />
</TabsContent>
</Tabs>
</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>
);
}
205 changes: 205 additions & 0 deletions frontend/src/components/AccountTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"use client";

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 { useSidebar } from "./SidebarContext";

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

type AccountCreateEditValues = z.infer<typeof AccountCreateEditSchema>;

const accountService = new AccountService();

export const AccountTable = () => {
const queryClient = useQueryClient();
const { openSidebar, closeSidebar } = useSidebar();
const [sidebarMode, setSidebarMode] = useState<"create" | "edit">("create");
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
const [submissionError, setSubmissionError] = useState<string | null>(null);

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

const accounts = (accountsQuery.data ?? [])
.filter((a) => a.role === "admin" || a.role === "staff")
.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,
}),
onError: (error: Error) => {
if (isAxiosError(error) && error.response) {
setSubmissionError(`${error.response.data.message}`);
} else {
setSubmissionError(`Failed to create account: ${error.message}`);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
setSidebarOpen(false);
setEditingAccount(null);
setSubmissionError(null);
},
});

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,
}),
onError: (error: Error) => {
console.error("Failed to update account:", error);
setSubmissionError(`Failed to update account: ${error.message}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
closeSidebar();
setEditingAccount(null);
setSubmissionError(null);
},
});

const deleteMutation = useMutation({
mutationFn: (id: number) => accountService.deleteAccount(id),
// Optimistically remove the account from the cache.
onMutate: async (id: number) => {
await queryClient.cancelQueries({ queryKey: ["accounts"] });

const previous = queryClient.getQueryData<Account[]>(["accounts"]);

queryClient.setQueryData<Account[] | undefined>(["accounts"], (old) =>
old?.filter((a) => a.id !== id)
);

return { previous };
},
onError: (error: Error, _vars, context) => {
console.error("Failed to delete account:", error);
if (context?.previous) {
queryClient.setQueryData(["accounts"], context.previous);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
},
});

const handleEdit = (account: Account) => {
setEditingAccount(account);
setSidebarMode("edit");
setSubmissionError(null);
openSidebar(
`edit-account-${account.id}`,
"Edit Account",
"Update account information",
<AccountTableCreateEditForm
title="Edit Account"
onSubmit={handleFormSubmit}
submissionError={submissionError}
editData={{
email: account.email,
firstName: account.first_name,
lastName: account.last_name,
role: account.role,
pid: account.pid ?? "",
}}
/>
);
};

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

const handleCreate = () => {
setEditingAccount(null);
setSidebarMode("create");
setSubmissionError(null);
openSidebar(
"create-account",
"New Account",
"Add a new account to the system",
<AccountTableCreateEditForm
title="New Account"
onSubmit={handleFormSubmit}
submissionError={submissionError}
/>
);
};

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}
/>
</div>
);
};
Loading