From 8409a943e6e9487ddbfea5a2768d0ad13b52e5af Mon Sep 17 00:00:00 2001 From: David Katrinka Date: Tue, 6 Jan 2026 19:20:32 +0100 Subject: [PATCH] user CRUD, index CRUD, category CRUD --- src/MainLayout/MainLayout.tsx | 2 + src/api/categories.ts | 31 ++++ src/api/indexes.ts | 41 +++++ src/api/publications.ts | 0 src/api/users.ts | 42 ++++- src/components/AdminSidebar/AdminSidebar.tsx | 81 +++++++++ src/components/shared/types/AuthTypes.ts | 2 +- src/hooks/categories/useCategories.ts | 9 + src/hooks/categories/useCreateCategory.ts | 17 ++ src/hooks/categories/useDeleteCategory.ts | 17 ++ src/hooks/categories/useGetCategoryById.ts | 10 ++ src/hooks/categories/useUpdateCategory.ts | 18 ++ src/hooks/indexes/useCreateIndex.ts | 25 +++ src/hooks/indexes/useDeleteIndex.ts | 18 ++ src/hooks/indexes/useGetIndexById.ts | 10 ++ src/hooks/indexes/useIndexes.ts | 8 + src/hooks/indexes/useUpdateIndex.ts | 26 +++ src/hooks/users/useCreateUser.ts | 17 ++ src/hooks/users/useDeleteUser.ts | 18 ++ src/hooks/users/useUpdateUser.ts | 19 +++ src/hooks/users/useUserById.ts | 10 ++ src/pages/CategoriesPage/CategoriesPage.tsx | 127 ++++++++++++++ src/pages/CategoryForm/CategoryForm.tsx | 76 +++++++++ src/pages/IndexForm/IndexForm.tsx | 79 +++++++++ src/pages/IndexesPage/IndexesPage.tsx | 137 +++++++++++++++ src/pages/ProfilePage/ProfilePage.tsx | 2 +- src/pages/UserForm/UserForm.tsx | 167 +++++++++++++++++++ src/pages/UsersPage/UsersPage.tsx | 140 ++++++++++++++++ src/router/router.tsx | 66 ++++++-- 29 files changed, 1195 insertions(+), 20 deletions(-) create mode 100644 src/api/categories.ts create mode 100644 src/api/indexes.ts create mode 100644 src/api/publications.ts create mode 100644 src/components/AdminSidebar/AdminSidebar.tsx create mode 100644 src/hooks/categories/useCategories.ts create mode 100644 src/hooks/categories/useCreateCategory.ts create mode 100644 src/hooks/categories/useDeleteCategory.ts create mode 100644 src/hooks/categories/useGetCategoryById.ts create mode 100644 src/hooks/categories/useUpdateCategory.ts create mode 100644 src/hooks/indexes/useCreateIndex.ts create mode 100644 src/hooks/indexes/useDeleteIndex.ts create mode 100644 src/hooks/indexes/useGetIndexById.ts create mode 100644 src/hooks/indexes/useIndexes.ts create mode 100644 src/hooks/indexes/useUpdateIndex.ts create mode 100644 src/hooks/users/useCreateUser.ts create mode 100644 src/hooks/users/useDeleteUser.ts create mode 100644 src/hooks/users/useUpdateUser.ts create mode 100644 src/pages/CategoriesPage/CategoriesPage.tsx create mode 100644 src/pages/CategoryForm/CategoryForm.tsx create mode 100644 src/pages/IndexForm/IndexForm.tsx create mode 100644 src/pages/IndexesPage/IndexesPage.tsx create mode 100644 src/pages/UserForm/UserForm.tsx create mode 100644 src/pages/UsersPage/UsersPage.tsx diff --git a/src/MainLayout/MainLayout.tsx b/src/MainLayout/MainLayout.tsx index f449b30..4af7691 100644 --- a/src/MainLayout/MainLayout.tsx +++ b/src/MainLayout/MainLayout.tsx @@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom"; import { LayoutWrapper } from "./Layout.styles"; import { StyledDistance } from "../components/shared/StyledDistance"; import Header from "../components/Header/Header"; +import { AdminSidebar } from "../components/AdminSidebar/AdminSidebar"; const MainLayout = () => { @@ -10,6 +11,7 @@ const MainLayout = () => { <>
+ diff --git a/src/api/categories.ts b/src/api/categories.ts new file mode 100644 index 0000000..57538d1 --- /dev/null +++ b/src/api/categories.ts @@ -0,0 +1,31 @@ +import axiosInstance from "./axiosInstance"; + +export type CategoryType = { + pk: number; + name: string; +}; + +export const getCategories = async () => { + const res = await axiosInstance.get("/api/categories"); + return res.data; +}; + +export const getCategoryById = async (id: number) => { + const res = await axiosInstance.get(`/api/categories/${id}`); + return res.data; +}; + +export const createCategory = async (name: string) => { + const res = await axiosInstance.post(`/api/categories/`, { name }); + return res.data; +}; + +export const updateCategory = async (id: number, name: string) => { + const res = await axiosInstance.put(`/api/categories/${id}`, { name }); + return res.data; +}; + +export const deleteCategory = async (id: number) => { + const res = await axiosInstance.delete(`/api/categories/${id}`); + return res.data; +}; diff --git a/src/api/indexes.ts b/src/api/indexes.ts new file mode 100644 index 0000000..e51a4e0 --- /dev/null +++ b/src/api/indexes.ts @@ -0,0 +1,41 @@ +import axiosInstance from "./axiosInstance"; + +export interface IndexResponse { + count: number; + page_size: number; + page: number; + total_pages: number; + results: IndexType[]; +} + +export interface IndexType { + id: number; + school_index: string; +} + +export const getIndexes = async ( page?: number, search?:string ) => { + const res = await axiosInstance.get("/api/admin/schools", { + params: { page, search }, + }); + return res.data; +}; + +export const getIndexById = async (id: number) => { + const res = await axiosInstance.get(`/api/admin/schools/${id}`); + return res.data; +}; + +export const createIndex = async (name: string) => { + const res = await axiosInstance.post(`/api/admin/schools/`, { school_index: name }); + return res.data; +}; + +export const updateIndex = async (id: number, name: string) => { + const res = await axiosInstance.put(`/api/admin/schools/${id}`, { school_index: name }); + return res.data; +}; + +export const deleteIndex = async (id: number) => { + const res = await axiosInstance.delete(`/api/admin/schools/${id}`); + return res.data; +}; diff --git a/src/api/publications.ts b/src/api/publications.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/api/users.ts b/src/api/users.ts index 33adcc3..b68b079 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,13 +1,51 @@ +import type { User } from "../components/shared/types/AuthTypes"; import axiosInstance from "./axiosInstance"; +export type UserPayload = { + username: string; + school_index: number; + password?: string; + email: string; + role: string; + is_superuser: boolean; +}; + export const getAllUsers = async (page: number) => { - const res = await axiosInstance.get("/api/users", { + const res = await axiosInstance.get("/api/admin/users", { params: { page }, }); return res.data; }; export const getUserById = async (id: number) => { - const res = await axiosInstance.get(`api/users/${id}`); + const res = await axiosInstance.get(`api/admin/users/${id}`); + return res.data; +}; + +export const deleteUser = async (id: number) => { + const res = await axiosInstance.delete(`/api/admin/users/${id}`); + return res.data; +}; + +export const createUser = async (payload: UserPayload) => { + const res = await axiosInstance.post("/api/admin/users", { + username: payload.username, + school_index: payload.school_index, + email: payload.email, + password: payload.password, + role: payload.role, + is_superuser: payload.is_superuser, + }); + return res.data; +}; + +export const updateUser = async (id: number, payload: UserPayload) => { + const res = await axiosInstance.patch(`/api/admin/users/${id}`, { + username: payload.username, + school_index: payload.school_index, + email: payload.email, + role: payload.role, + is_superuser: payload.is_superuser, + }); return res.data; }; diff --git a/src/components/AdminSidebar/AdminSidebar.tsx b/src/components/AdminSidebar/AdminSidebar.tsx new file mode 100644 index 0000000..01835ca --- /dev/null +++ b/src/components/AdminSidebar/AdminSidebar.tsx @@ -0,0 +1,81 @@ +import { + Drawer, + List, + ListItemButton, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import { NavLink } from "react-router-dom"; + +import PeopleIcon from "@mui/icons-material/People"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import QuizIcon from "@mui/icons-material/Quiz"; +import CategoryIcon from "@mui/icons-material/Category"; +import SchoolIcon from '@mui/icons-material/School'; +import { useAuth } from "../../context/AuthContext"; +import PublishIcon from '@mui/icons-material/Publish'; +type MenuItem = { + label: string; + icon: React.ReactNode; + to: string; +}; + +const drawerWidth = 240; + +const adminMenu: MenuItem[] = [ + { label: "Users", icon: , to: "/users" }, + { label: "School Index", icon: , to: "/indexes" }, + { label: "Publications", icon: , to: "/publications" }, + { label: "Categories", icon: , to: "/categories" }, +]; + +const profMenu: MenuItem[] = [ + { label: "School Index", icon: , to: "/indexes" }, + { label: "Publications", icon: , to: "/publications" }, +]; + +export const AdminSidebar = () => { + const { user } = useAuth(); + + const menuItems = + user?.is_superuser === true + ? adminMenu + : user?.role === "prof" + ? profMenu + : []; + return ( + + + {menuItems.map((item) => ( + + {item.icon} + + + ))} + + + ); +}; diff --git a/src/components/shared/types/AuthTypes.ts b/src/components/shared/types/AuthTypes.ts index 53ed3f2..77ad5c5 100644 --- a/src/components/shared/types/AuthTypes.ts +++ b/src/components/shared/types/AuthTypes.ts @@ -6,7 +6,7 @@ export interface school_index_object { export interface User { id: number; username: string; - index: school_index_object; + school_index_detail: school_index_object; school_index: number; email: string; role: string; diff --git a/src/hooks/categories/useCategories.ts b/src/hooks/categories/useCategories.ts new file mode 100644 index 0000000..0ab3a66 --- /dev/null +++ b/src/hooks/categories/useCategories.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getCategories } from "../../api/categories"; + + +export const useCategories = () => + useQuery({ + queryKey: ["categories"], + queryFn: getCategories, + }); diff --git a/src/hooks/categories/useCreateCategory.ts b/src/hooks/categories/useCreateCategory.ts new file mode 100644 index 0000000..d68172c --- /dev/null +++ b/src/hooks/categories/useCreateCategory.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createCategory } from "../../api/categories"; +import { toast } from "react-toastify"; + +export const useCreateCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => createCategory(name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + toast.success("Category Created"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/categories/useDeleteCategory.ts b/src/hooks/categories/useDeleteCategory.ts new file mode 100644 index 0000000..78b1bf8 --- /dev/null +++ b/src/hooks/categories/useDeleteCategory.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteCategory } from "../../api/categories"; +import { toast } from "react-toastify"; + +export const useDeleteCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteCategory(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + toast.success("Category Deleted"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/categories/useGetCategoryById.ts b/src/hooks/categories/useGetCategoryById.ts new file mode 100644 index 0000000..caa1270 --- /dev/null +++ b/src/hooks/categories/useGetCategoryById.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { getCategoryById } from "../../api/categories"; + +export const useCategoryById = (id?: number) => { + return useQuery({ + queryKey: ["categories", id], + queryFn: () => getCategoryById(id!), + enabled: !!id, + }); +}; \ No newline at end of file diff --git a/src/hooks/categories/useUpdateCategory.ts b/src/hooks/categories/useUpdateCategory.ts new file mode 100644 index 0000000..9403428 --- /dev/null +++ b/src/hooks/categories/useUpdateCategory.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { updateCategory } from "../../api/categories"; +import { toast } from "react-toastify"; + +export const useUpdateCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, name }: { id: number; name: string }) => + updateCategory(id, name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + toast.success("Category Updated"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/indexes/useCreateIndex.ts b/src/hooks/indexes/useCreateIndex.ts new file mode 100644 index 0000000..2c7e780 --- /dev/null +++ b/src/hooks/indexes/useCreateIndex.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "react-toastify"; +import { createIndex } from "../../api/indexes"; + +export const useCreateIndex = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => createIndex(name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["indexes"] }); + toast.success("Index Created"); + }, + onError: (error: any) => { + let message = "Failed to create index"; + if (error?.response?.data) { + message = Object.values(error.response.data).flat().join("\n"); + } else if (error?.message) { + message = error.message; + } + + toast.error(message); + }, + }); +}; diff --git a/src/hooks/indexes/useDeleteIndex.ts b/src/hooks/indexes/useDeleteIndex.ts new file mode 100644 index 0000000..6325056 --- /dev/null +++ b/src/hooks/indexes/useDeleteIndex.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "react-toastify"; +import { deleteIndex } from "../../api/indexes"; + +export const useDeleteIndex = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteIndex(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["indexes"] }); + toast.success("Index Deleted"); + }, + onError: () => { + toast.error("Can not delete index assigned to user."); + }, + }); +}; diff --git a/src/hooks/indexes/useGetIndexById.ts b/src/hooks/indexes/useGetIndexById.ts new file mode 100644 index 0000000..777723a --- /dev/null +++ b/src/hooks/indexes/useGetIndexById.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { getIndexById } from "../../api/indexes"; + +export const useGetIndexById = (id?: number) => { + return useQuery({ + queryKey: ["indexes", id], + queryFn: () => getIndexById(id!), + enabled: !!id, + }); +}; diff --git a/src/hooks/indexes/useIndexes.ts b/src/hooks/indexes/useIndexes.ts new file mode 100644 index 0000000..c2432af --- /dev/null +++ b/src/hooks/indexes/useIndexes.ts @@ -0,0 +1,8 @@ +import { useQuery } from "@tanstack/react-query"; +import { getIndexes } from "../../api/indexes"; + +export const useIndexes = (page?: number, search?: string) => + useQuery({ + queryKey: ["indexes", page, search], + queryFn: () => getIndexes(page, search), + }); diff --git a/src/hooks/indexes/useUpdateIndex.ts b/src/hooks/indexes/useUpdateIndex.ts new file mode 100644 index 0000000..ea7426b --- /dev/null +++ b/src/hooks/indexes/useUpdateIndex.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "react-toastify"; +import { updateIndex } from "../../api/indexes"; + +export const useUpdateIndex = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, name }: { id: number; name: string }) => + updateIndex(id, name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["indexes"] }); + toast.success("Index Updated"); + }, + onError: (error: any) => { + let message = "Failed to update index"; + if (error?.response?.data) { + message = Object.values(error.response.data).flat().join("\n"); + } else if (error?.message) { + message = error.message; + } + + toast.error(message); + }, + }); +}; diff --git a/src/hooks/users/useCreateUser.ts b/src/hooks/users/useCreateUser.ts new file mode 100644 index 0000000..828ff6a --- /dev/null +++ b/src/hooks/users/useCreateUser.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createUser, type UserPayload } from "../../api/users"; +import { toast } from "react-toastify"; + +export const useCreateUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (payload: UserPayload) => createUser(payload), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User Created"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/users/useDeleteUser.ts b/src/hooks/users/useDeleteUser.ts new file mode 100644 index 0000000..59b2ff5 --- /dev/null +++ b/src/hooks/users/useDeleteUser.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "react-toastify"; +import { deleteUser } from "../../api/users"; + +export const useDeleteUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User Deleted!"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/users/useUpdateUser.ts b/src/hooks/users/useUpdateUser.ts new file mode 100644 index 0000000..b16eb77 --- /dev/null +++ b/src/hooks/users/useUpdateUser.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "react-toastify"; +import { updateUser, type UserPayload } from "../../api/users"; + +export const useUpdateUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, payload }: { id: number; payload: UserPayload }) => + updateUser(id, payload), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User Updated"); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/src/hooks/users/useUserById.ts b/src/hooks/users/useUserById.ts index e69de29..dc48e5c 100644 --- a/src/hooks/users/useUserById.ts +++ b/src/hooks/users/useUserById.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUserById } from "../../api/users"; + +export const useUserById = (id?: number) => { + return useQuery({ + queryKey: ["user", id], + queryFn: () => getUserById(id!), + enabled: !!id, + }); +}; diff --git a/src/pages/CategoriesPage/CategoriesPage.tsx b/src/pages/CategoriesPage/CategoriesPage.tsx new file mode 100644 index 0000000..efe817c --- /dev/null +++ b/src/pages/CategoriesPage/CategoriesPage.tsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { + Box, + Button, + CircularProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Container, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +import { useCategories } from "../../hooks/categories/useCategories"; +import { useDeleteCategory } from "../../hooks/categories/useDeleteCategory"; + +const CategoriesPage = () => { + const [deleteCategoryId, setDeleteCategoryId] = useState(null); + + const { data, isLoading, isError } = useCategories(); + const deleteMutation = useDeleteCategory(); + const navigate = useNavigate(); + + const handleCreateCategory = () => { + navigate("/categories/create"); + }; + + const handleUpdateCategory = (id: number) => { + navigate(`/categories/${id}/update`); + }; + + const handleDeleteCategory = () => { + if (deleteCategoryId !== null) { + deleteMutation.mutate(deleteCategoryId, { + onSuccess: () => setDeleteCategoryId(null), + }); + } + }; + + if (isLoading) return ; + if (isError) + return Error loading categories.; + + return ( + + + Categories + + + + + + + + ID + Index + Actions + + + + + {data?.map((category) => ( + + {category.pk} + {category.name} + + + + + + ))} + +
+
+ + setDeleteCategoryId(null)} + > + Confirm Delete + + Are you sure you want to delete this category? + + + + + + +
+ ); +}; + +export default CategoriesPage; diff --git a/src/pages/CategoryForm/CategoryForm.tsx b/src/pages/CategoryForm/CategoryForm.tsx new file mode 100644 index 0000000..afe187b --- /dev/null +++ b/src/pages/CategoryForm/CategoryForm.tsx @@ -0,0 +1,76 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Box, + Button, + CircularProgress, + TextField, + Typography, +} from "@mui/material"; + +import { useCreateCategory } from "../../hooks/categories/useCreateCategory"; +import { useUpdateCategory } from "../../hooks/categories/useUpdateCategory"; +import { useCategoryById } from "../../hooks/categories/useGetCategoryById"; + +const CategoryForm = () => { + const { id } = useParams<{ id: string }>(); + const isUpdate = Boolean(id); + const navigate = useNavigate(); + + const [name, setName] = useState(""); + + const { data: category, isLoading } = useCategoryById( + id ? Number(id) : undefined + ); + + const createCategoryMutation = useCreateCategory(); + const updateCategoryMutation = useUpdateCategory(); + + useEffect(() => { + if (category) { + setName(category.name); + } + }, [category]); + + if (isUpdate && isLoading) return ; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (isUpdate && id) { + updateCategoryMutation.mutate( + { id: Number(id), name }, + { onSuccess: () => navigate("/categories") } + ); + } else { + createCategoryMutation.mutate(name, { + onSuccess: () => navigate("/categories"), + }); + } + }; + + return ( + + + {isUpdate ? "Update Category" : "Create Category"} + + +
+ setName(e.target.value)} + fullWidth + required + sx={{ mb: 3 }} + /> + + + +
+ ); +}; + +export default CategoryForm; diff --git a/src/pages/IndexForm/IndexForm.tsx b/src/pages/IndexForm/IndexForm.tsx new file mode 100644 index 0000000..49b2bda --- /dev/null +++ b/src/pages/IndexForm/IndexForm.tsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Box, + Button, + CircularProgress, + TextField, + Typography, +} from "@mui/material"; +import { useGetIndexById } from "../../hooks/indexes/useGetIndexById"; +import { useCreateIndex } from "../../hooks/indexes/useCreateIndex"; +import { useUpdateIndex } from "../../hooks/indexes/useUpdateIndex"; + + + +const IndexForm = () => { + const { id } = useParams<{ id: string }>(); + const isUpdate = Boolean(id); + const navigate = useNavigate(); + + const [indexText, setIndexText] = useState(""); + + const { data: index, isLoading } = useGetIndexById( + id ? Number(id) : undefined + ); + + const createMutation = useCreateIndex(); + const updateMutation = useUpdateIndex(); + + + useEffect(() => { + if (index) { + setIndexText(index.school_index); + } + }, [index]); + + if (isUpdate && isLoading) return ; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (isUpdate && id) { + updateMutation.mutate( + { id: Number(id), name: indexText }, + { onSuccess: () => navigate("/indexes") } + ); + } else { + createMutation.mutate(indexText, { + onSuccess: () => navigate("/indexes"), + }); + } + }; + + return ( + + + {isUpdate ? "Update Index" : "Create Index"} + + +
+ setIndexText(e.target.value)} + fullWidth + required + sx={{ mb: 3 }} + /> + + + +
+ ); +}; + +export default IndexForm; + diff --git a/src/pages/IndexesPage/IndexesPage.tsx b/src/pages/IndexesPage/IndexesPage.tsx new file mode 100644 index 0000000..9e99d13 --- /dev/null +++ b/src/pages/IndexesPage/IndexesPage.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { + Box, + Button, + CircularProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Pagination, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import Container from "../../components/shared/Container/Container"; +import { useIndexes } from "../../hooks/indexes/useIndexes"; +import { useDeleteIndex } from "../../hooks/indexes/useDeleteIndex"; + +const IndexesPage = () => { + const [currentPage, setCurrentPage] = useState(1); + const [deleteUserId, setDeleteUserId] = useState(null); + const { data, isLoading, isError } = useIndexes(currentPage); + console.log(data); + const deleteMutation = useDeleteIndex(); + const navigate = useNavigate(); + + const handleCreateIndex = () => { + navigate("/indexes/create"); + }; + + const handleUpdateIndex = (id: number) => { + navigate(`/indexes/${id}/update`); + }; + + const handleDeleteIndex = () => { + if (deleteUserId !== null) { + deleteMutation.mutate(deleteUserId, { + onSuccess: () => setDeleteUserId(null), + }); + } + }; + + if (isLoading) return ; + if (isError) + return Error loading indexes.; + + return ( + + + Indexes + + + + + + + + ID + Index + Actions + + + + + {data?.results.map((index) => ( + + {index.id} + {index.school_index} + + + + + + ))} + +
+
+ + setCurrentPage(value)} + sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "center" }} + /> + + setDeleteUserId(null)} + > + Confirm Delete + + Are you sure you want to delete this user? + + + + + + +
+ ); +}; + +export default IndexesPage; diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 8b04df6..001732e 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -30,7 +30,7 @@ const ProfilePage = () => { E-mail: {user?.email} - Index: {user?.school_index} + Index: {user?.school_index_detail.school_index} Status: {user?.is_superuser ? "Admin" : "User"} diff --git a/src/pages/UserForm/UserForm.tsx b/src/pages/UserForm/UserForm.tsx new file mode 100644 index 0000000..503fed1 --- /dev/null +++ b/src/pages/UserForm/UserForm.tsx @@ -0,0 +1,167 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Box, + Button, + Checkbox, + CircularProgress, + FormControlLabel, + TextField, + Typography, +} from "@mui/material"; +import { toast } from "react-toastify"; + +import { useUserById } from "../../hooks/users/useUserById"; +import { useCreateUser } from "../../hooks/users/useCreateUser"; +import { useUpdateUser } from "../../hooks/users/useUpdateUser"; +import type { UserPayload } from "../../api/users"; +import { useIndexes } from "../../hooks/indexes/useIndexes"; + +const UserForm = () => { + const { id } = useParams<{ id: string }>(); + const isUpdate = Boolean(id); + const navigate = useNavigate(); + + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [indexInput, setIndexInput] = useState(""); + const [role, setRole] = useState("common"); + const [isSuperuser, setIsSuperuser] = useState(false); + + const { data: user, isLoading: isLoadingUser } = useUserById( + id ? Number(id) : undefined + ); + + const { data: indexesData, isFetching } = useIndexes(undefined, indexInput); + + const createUserMutation = useCreateUser(); + const updateUserMutation = useUpdateUser(); + + useEffect(() => { + if (user) { + setUsername(user.username); + setEmail(user.email); + setRole(user.role); + setIsSuperuser(user.is_superuser); + setIndexInput(user.school_index_detail?.school_index ?? ""); + } + }, [user]); + + if (isUpdate && isLoadingUser) return ; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!indexInput.trim()) { + toast.error("School index is required"); + return; + } + + const matchedIndex = indexesData?.results?.find( + (i) => i.school_index === indexInput + ); + + if (!matchedIndex) { + toast.error("School index does not exist"); + return; + } + + const payload: UserPayload = { + username, + email, + role, + is_superuser: isSuperuser, + school_index: matchedIndex.id, + }; + + if (isUpdate && id) { + updateUserMutation.mutate( + { id: Number(id), payload }, + { onSuccess: () => navigate("/users") } + ); + } else { + createUserMutation.mutate(payload, { + onSuccess: () => navigate("/users"), + }); + } + }; + + const matchedIndex = indexInput + ? indexesData?.results?.find((i) => i.school_index === indexInput) + : undefined; + + return ( + + + {isUpdate ? "Update User" : "Create User"} + + +
+ setUsername(e.target.value)} + fullWidth + required + sx={{ mb: 2 }} + /> + + setEmail(e.target.value)} + fullWidth + required + sx={{ mb: 2 }} + /> + + setIndexInput(e.target.value)} + fullWidth + required + sx={{ mb: 2 }} + helperText={ + !indexInput + ? "Enter an existing school index" + : isFetching + ? "Checking index…" + : matchedIndex + ? "Index exists" + : "Index does not exist" + } + /> + + setRole(e.target.value)} + fullWidth + sx={{ mb: 3 }} + > + + + + + setIsSuperuser((v) => !v)} + /> + } + label="Is Superuser?" + /> + + + +
+ ); +}; + +export default UserForm; diff --git a/src/pages/UsersPage/UsersPage.tsx b/src/pages/UsersPage/UsersPage.tsx new file mode 100644 index 0000000..6c81299 --- /dev/null +++ b/src/pages/UsersPage/UsersPage.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { + Box, + Button, + CircularProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Pagination, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import { useUsers } from "../../hooks/users/useUsers"; +import { useDeleteUser } from "../../hooks/users/useDeleteUser"; + +import { useNavigate } from "react-router-dom"; +import Container from "../../components/shared/Container/Container"; +import type { User } from "../../components/shared/types/AuthTypes"; + +const UsersPage = () => { + const [currentPage, setCurrentPage] = useState(1); + const [deleteUserId, setDeleteUserId] = useState(null); + const { data, isLoading, isError } = useUsers(currentPage); + console.log(data) + const deleteMutation = useDeleteUser(); + const navigate = useNavigate(); + + + const handleUpdateUser = (id: number) => { + navigate(`/users/${id}/update`); + }; + + const handleDeleteUser = () => { + if (deleteUserId !== null) { + deleteMutation.mutate(deleteUserId, { + onSuccess: () => setDeleteUserId(null), + }); + } + }; + + if (isLoading) return ; + if (isError) + return Error loading users.; + + return ( + + + Users + + + + + + + ID + Username + Index + Email + Role + Is Admin + Actions + + + + + {data?.results.map((user: User) => ( + + {user.id} + {user.username} + {user.school_index_detail.school_index} + {user.email} + {user.role} + + {user.is_superuser ? "Yes" : "No" } + + + + + + + ))} + +
+
+ + setCurrentPage(value)} + sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "center" }} + /> + + setDeleteUserId(null)} + > + Confirm Delete + + Are you sure you want to delete this user? + + + + + + +
+ ); +}; + +export default UsersPage; diff --git a/src/router/router.tsx b/src/router/router.tsx index 98e3190..30abd1f 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -3,25 +3,59 @@ import MainLayout from "../MainLayout/MainLayout"; import { Container } from "@mui/material"; import LoginPage from "../pages/LoginPage/LoginPage"; import ProfilePage from "../pages/ProfilePage/ProfilePage"; +import UsersPage from "../pages/UsersPage/UsersPage"; +import UserForm from "../pages/UserForm/UserForm"; +import IndexesPage from "../pages/IndexesPage/IndexesPage"; +import IndexForm from "../pages/IndexForm/IndexForm"; +import CategoriesPage from "../pages/CategoriesPage/CategoriesPage"; +import CategoryForm from "../pages/CategoryForm/CategoryForm"; const router = createBrowserRouter([ - { + { path: "/", element: , children: [ - { - index: true, - element: index page - }, - { - path: "/login", - element: - }, - { - path: "/profile", - element: - } - ]} -]) + { + index: true, + element: index page, + }, + { + path: "/login", + element: , + }, + { + path: "/profile", + element: , + }, + { + path: "/users", + children: [ + { index: true, element: }, + { path: ":id/update", element: }, + ], + }, + { + path: "/indexes", + children: [ + { index: true, element: }, + { path: "create", element: }, + { path: ":id/update", element: }, + ], + }, + { + path: "/publications", + element: Publications Page, + }, + { + path: "/categories", + children: [ + { index: true, element: }, + { path: "create", element: }, + { path: ":id/update", element: }, + ], + }, + ], + }, +]); -export default router; \ No newline at end of file +export default router;