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}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+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"}
+
+
+
+
+ );
+};
+
+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"}
+
+
+
+
+ );
+};
+
+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" }}
+ />
+
+
+
+ );
+};
+
+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"}
+
+
+
+
+ );
+};
+
+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" }}
+ />
+
+
+
+ );
+};
+
+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;