diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..4b3a5a81 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,24 @@ +# backend/Dockerfile + +# 1. Use a lightweight Node image +FROM node:18-alpine + +# 2. Set workdir +WORKDIR /app + +# 3. Copy dependency manifests & install +COPY package*.json ./ +RUN npm ci + +# 4. Copy source code +COPY . . + +# 5. Set environment variables +ENV NODE_ENV=development +ENV PORT=4000 + +# 6. Expose the port your API listens on +EXPOSE 4000 + +# 7. Start the app +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/frontend/src/components/DataTable/AdminList.tsx b/frontend/src/components/DataTable/AdminList.tsx new file mode 100644 index 00000000..8db10859 --- /dev/null +++ b/frontend/src/components/DataTable/AdminList.tsx @@ -0,0 +1,308 @@ +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, + Row, + VisibilityState, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import request from "@/lib/request"; +import TableSkeleton from "../ui/TableSkeleton"; +import TableError from "../ui/TableError"; +import { Button } from "../ui/button"; +import { Trash, MoreVertical, ChevronDown } from "lucide-react"; +import { toast } from "sonner"; +import ShowIfPermission from "../Auth/ShowIfPermission"; +import { useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuCheckboxItem, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import TableHeading from "./TableHeading"; +import Resizer from "./Resizer"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +// Admin data interface +interface Admin { + teacher_id: string; + first_name: string; + last_name: string; + email: string; +} + +// Table columns configuration +const columns: ColumnDef[] = [ + { + id: "actions", + cell: ActionsDropdown, + size: 50, + }, + { + accessorKey: "first_name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "First Name" }, + }, + { + accessorKey: "last_name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "Last Name" }, + }, + { + accessorKey: "email", + header: ({ column }) => , + size: 200, + maxSize: 300, + minSize: 100, + meta: { heading: "Email" }, + }, +]; + +// Main admin list component +export function AdminList() { + const [sorting, setSorting] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + + // Fetch admins data + const query = useQuery({ + queryKey: ["admins"], + queryFn: async () => { + const response = await request.get("/teachers/admins-only"); + return response.data; + }, + }); + + // Initialize table with data + const table = useReactTable({ + data: query.data || [], + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnVisibility, + }, + }); + + return ( +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + onSelect={(event) => event.preventDefault()} + > + {column.columnDef.meta?.heading} + + ); + })} + + +
+ +
+ + {/* Table header */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + + ))} + + ))} + + + {/* Table body */} + + {query.isLoading ? ( + + ) : query.isError ? ( + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No admins found. + + + )} + +
+
+
+ ); +} + +// Actions dropdown menu component +function ActionsDropdown({ row }: { row: Row }) { + const admin = row.original; + const queryClient = useQueryClient(); + const [showPasswordDialog, setShowPasswordDialog] = useState(false); + const [password, setPassword] = useState(""); + + const deleteMutation = useMutation({ + mutationFn: async () => { + const response = await request.delete(`/teachers/${admin.teacher_id}`, { + data: { password } + }); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["admins"] }); + toast.success("Admin deleted successfully"); + setShowPasswordDialog(false); + setPassword(""); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete admin"); + setPassword(""); + }, + }); + + const handleDelete = () => { + setShowPasswordDialog(true); + }; + + const handleConfirmDelete = () => { + if (password.trim()) { + deleteMutation.mutate(); + } else { + toast.error("Please enter the password"); + } + }; + + return ( + + + + + + + + + Delete + + + + + + + + Confirm Admin Deletion + +
+ + setPassword(e.target.value)} + placeholder="Enter your password" + className="mt-2" + /> +
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/DataTable/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index 51e3a907..edd32b3a 100644 --- a/frontend/src/components/DataTable/SectionsTable.tsx +++ b/frontend/src/components/DataTable/SectionsTable.tsx @@ -1,11 +1,9 @@ import { ColumnDef, SortingState, - ColumnFiltersState, flexRender, getCoreRowModel, getSortedRowModel, - getFilteredRowModel, useReactTable, Row, } from "@tanstack/react-table"; @@ -16,7 +14,6 @@ import { DropdownMenuTrigger, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; - import { Table, TableBody, @@ -27,20 +24,17 @@ import { } from "@/components/ui/table"; import { useState } from "react"; import { Button } from "../ui/button"; -import { AcademicLevels } from "@/lib/temp-consts"; -import CheckboxGroupFilter from "./CheckboxGroupFilter"; -import TextFilter from "./TextFilter"; import TableHeading from "./TableHeading"; -import { isIncludedinArr } from "@/lib/filterFns"; import { cn } from "@/lib/utils"; import { Link as LinkIcon, MoreVertical, Trash } from "lucide-react"; import DeleteSectionDialog from "../Sections/DeleteSectionDialog"; import TableSkeleton from "../ui/TableSkeleton"; import TableError from "../ui/TableError"; -import { Section, Specialty, useSectionQuery } from "@/lib/queries"; +import { Section, useSectionQuery } from "@/lib/queries"; import ShowIfPermission from "../Auth/ShowIfPermission"; import { Link } from "@tanstack/react-router"; +// Define table columns const columns: ColumnDef
[] = [ { id: "actions", @@ -50,10 +44,7 @@ const columns: ColumnDef
[] = [ { id: "link", cell: ({ row }: { row: Row
}) => ( - + ), @@ -61,132 +52,68 @@ const columns: ColumnDef
[] = [ }, { accessorKey: "academic_level", - header: ({ column }) => ( - - ), + header: ({ column }) => , size: 160, - filterFn: isIncludedinArr, }, { accessorKey: "spec_name", header: ({ column }) => , size: 100, - filterFn: isIncludedinArr, }, { accessorKey: "section_name", - header: ({ column }) => ( - - ), - filterFn: "equalsString", + header: ({ column }) => , size: 100, }, { accessorKey: "student_count", - header: ({ column }) => ( - - ), + header: ({ column }) => , size: 100, }, ]; -interface SectionsTableProps { - specialties: Specialty[]; -} -export function SectionsTable({ specialties }: SectionsTableProps) { +// Main sections table component +export function SectionsTable() { + // Fetch sections data const query = useSectionQuery(); + // Track table sorting state const [sorting, setSorting] = useState([]); - const initialFilters: ColumnFiltersState = [ - { - id: "academic_level", - value: AcademicLevels, - }, - { - id: "spec_name", - value: specialties.map((spec) => spec.spec_name), - }, - ]; - const [columnFilters, setColumnFilters] = - useState(initialFilters); + // Initialize table with data and features const table = useReactTable({ data: query.data || [], columns, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), getRowId: (originalRow) => originalRow["section_id"], - state: { - sorting, - columnFilters, - }, + state: { sorting }, }); return (
-
-

Filter

-
- ({ value: lvl, label: lvl }))} - label="Academic Level" - column="academic_level" - /> - ({ - value: specialty.spec_name, - label: specialty.spec_name, - }))} - label="Specialty" - column="spec_name" - /> - -
- -
+ {/* Table header */} {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} ))} + + {/* Table body */} {query.isLoading ? ( @@ -197,30 +124,22 @@ export function SectionsTable({ specialties }: SectionsTableProps) { {row.getVisibleCells().map((cell) => ( - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( - + No results. @@ -232,34 +151,38 @@ export function SectionsTable({ specialties }: SectionsTableProps) { ); } +// Actions dropdown menu component function ActionsDropdown({ row }: { row: Row
}) { const { section_id, section_name, spec_name, academic_level } = row.original; const name = `${academic_level} ${spec_name} Section ${section_name}`; + const [open, setOpen] = useState(false); return ( - + + {/* Three dots menu */} - + - - Delete + + + Delete + + {/* Delete confirmation dialog */} - + setOpen(false)} /> diff --git a/frontend/src/components/DataTable/TeacherList.tsx b/frontend/src/components/DataTable/TeacherList.tsx new file mode 100644 index 00000000..adcee662 --- /dev/null +++ b/frontend/src/components/DataTable/TeacherList.tsx @@ -0,0 +1,272 @@ +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, + Row, + VisibilityState, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import request from "@/lib/request"; +import TableSkeleton from "../ui/TableSkeleton"; +import TableError from "../ui/TableError"; +import { Button } from "../ui/button"; +import { Trash, MoreVertical, ChevronDown } from "lucide-react"; +import { toast } from "sonner"; +import ShowIfPermission from "../Auth/ShowIfPermission"; +import { useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuCheckboxItem, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import TableHeading from "./TableHeading"; +import Resizer from "./Resizer"; + +// Teacher data interface +interface Teacher { + teacher_id: string; + first_name: string; + last_name: string; + email: string; + section_count: number; +} + +// Table columns configuration +const columns: ColumnDef[] = [ + { + id: "actions", + cell: ActionsDropdown, + size: 50, + }, + { + accessorKey: "first_name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "First Name" }, + }, + { + accessorKey: "last_name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "Last Name" }, + }, + { + accessorKey: "email", + header: ({ column }) => , + size: 200, + maxSize: 300, + minSize: 100, + meta: { heading: "Email" }, + }, + { + accessorKey: "section_count", + header: ({ column }) => , + size: 100, + maxSize: 120, + minSize: 80, + meta: { heading: "Sections" }, + cell: ({ row }) => { + const count = row.getValue("section_count") as number; + return ( +
+ 0 ? "bg-green-100 text-green-800" : "bg-slate-100 text-slate-800" + )}> + {count} + +
+ ); + }, + }, +]; + +// Main teacher list component +export function TeacherList() { + const [sorting, setSorting] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + + // Fetch teachers data + const query = useQuery({ + queryKey: ["teachers"], + queryFn: async () => { + const response = await request.get("/teachers/teachers-only"); + return response.data; + }, + }); + + // Initialize table with data + const table = useReactTable({ + data: query.data || [], + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnVisibility, + }, + }); + + return ( +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + onSelect={(event) => event.preventDefault()} + > + {column.columnDef.meta?.heading} + + ); + })} + + +
+ +
+
+ {/* Table header */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + + ))} + + ))} + + + {/* Table body */} + + {query.isLoading ? ( + + ) : query.isError ? ( + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No teachers found. + + + )} + +
+
+
+ ); +} + +// Actions dropdown menu component +function ActionsDropdown({ row }: { row: Row }) { + const teacher = row.original; + const queryClient = useQueryClient(); + + const deleteMutation = useMutation({ + mutationFn: async () => { + const response = await request.delete(`/teachers/${teacher.teacher_id}`); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["teachers"] }); + toast.success("Teacher deleted successfully"); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete teacher"); + }, + }); + + return ( + + + + + + + { + if (window.confirm("Are you sure you want to delete this teacher?")) { + deleteMutation.mutate(); + } + }} + > + + Delete + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Header/NavbarContent.tsx b/frontend/src/components/Header/NavbarContent.tsx index 6c44f39e..b97de43c 100644 --- a/frontend/src/components/Header/NavbarContent.tsx +++ b/frontend/src/components/Header/NavbarContent.tsx @@ -64,8 +64,8 @@ export default function NavbarContent({ Dashboard Global Student List - Admin List - Professor List + Admin List + Professor List Manage Specialties diff --git a/frontend/src/components/Sections/AddSection.tsx b/frontend/src/components/Sections/AddSection.tsx index fc561204..53314b53 100644 --- a/frontend/src/components/Sections/AddSection.tsx +++ b/frontend/src/components/Sections/AddSection.tsx @@ -12,37 +12,61 @@ import { Button } from "../ui/button"; import { Plus } from "lucide-react"; import { AcademicLevels } from "@/lib/temp-consts"; import { cn } from "@/lib/utils"; -import { Specialty } from "@/lib/queries"; +import { Specialty, useSectionQuery } from "@/lib/queries"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import request from "@/lib/request"; +import { AxiosError } from "axios"; +// what we need to make this work interface AddSectionProps { specialties: Specialty[]; className?: string; } +// Define the error response type +interface ErrorResponse { + error: string; +} + export default function AddSection({ specialties, className, }: AddSectionProps) { const queryClient = useQueryClient(); + const sectionsQuery = useSectionQuery(); + const [open, setOpen] = useState(false); + const [error, setError] = useState(null); + + // make new section const addMutation = useMutation({ mutationFn: async (value: { academic_level: string; specialization_id: string; section_name: string; }) => { - await request.post("/sections", value); + const response = await request.post("/sections", value); + return response.data; }, onSuccess: () => { + // update list and close queryClient.invalidateQueries({ queryKey: ["sections"], }); setOpen(false); + form.reset(); + setError(null); }, + onError: (error: AxiosError) => { + if (error.response?.data?.error === "Section already exists.") { + setError("This section code already exists for this academic level and specialty"); + } else { + setError("Error making section. Try again."); + } + } }); + // form setup const form = useAppForm({ defaultValues: { academic_level: "", @@ -50,14 +74,46 @@ export default function AddSection({ section_name: "", }, onSubmit: async ({ value }) => { - await addMutation.mutateAsync(value); + try { + // check if section code already used + const isDuplicate = sectionsQuery.data?.some( + section => + section.academic_level === value.academic_level && + section.spec_id === value.specialization_id && + section.section_name === value.section_name + ); + if (isDuplicate) { + setError("This section code already exists for this academic level and specialty"); + return; + } + await addMutation.mutateAsync(value); + } catch (error) { + console.error("Error:", error); + } + }, + validators: { + onSubmit({ value }) { + // check if all fields filled + if (!value.academic_level) { + return "Need academic level"; + } + if (!value.specialization_id) { + return "Need specialty"; + } + if (!value.section_name) { + return "Need section code"; + } + return undefined; + }, }, }); + // close form function handleCancel() { form.reset(); + setOpen(false); + setError(null); } - const [open, setOpen] = useState(false); return ( @@ -75,9 +131,10 @@ export default function AddSection({ > Add Section - This will create an new section. + Make new section.
+ {/* academic level picker */} ( @@ -92,37 +149,44 @@ export default function AddSection({ )} validators={{ onChange: ({ value }) => - !value.length ? "Academic level is required" : undefined, + !value.length ? "Need academic level" : undefined, }} /> + {/* specialty picker */} - !value ? "Specialty is required" : undefined, + !value ? "Need specialty" : undefined, }} children={(field) => ( { - console.log(specialty); - return { - label: specialty.spec_name, - value: specialty.spec_id.toString(), - }; - })} + options={specialties.map((specialty) => ({ + label: specialty.spec_name, + value: specialty.spec_id.toString(), + }))} /> )} /> + {/* section code input */} - !value.length ? "Section code is required" : undefined, + onChange: ({ value }) => { + if (!value.length) { + return "Need section code"; + } + return undefined; + }, }} children={(field) => ( - + )} />
@@ -137,10 +201,9 @@ export default function AddSection({ - {addMutation.isError && ( + {(error || addMutation.isError) && (

- {addMutation.error.message || - "An unknown error occured. Please try again."} + {error || addMutation.error?.message || "Error. Try again."}

)} diff --git a/frontend/src/components/ui/TableError.tsx b/frontend/src/components/ui/TableError.tsx index 4d20d924..e436ef75 100644 --- a/frontend/src/components/ui/TableError.tsx +++ b/frontend/src/components/ui/TableError.tsx @@ -1,29 +1,27 @@ -import { RotateCw } from "lucide-react"; +// Import required components import { Button } from "./button"; -import { TableCell, TableRow } from "./table"; +import { AlertCircle } from "lucide-react"; +// Props for the error component interface TableErrorProps { - colSpan: number; - onRetry?: () => void; + onRetry: () => void; // Function to retry loading data + colSpan?: number; // Number of columns to span in the table } -export default function TableError({ colSpan, onRetry }: TableErrorProps) { + +// Error state component for tables +export default function TableError({ onRetry, colSpan = 1 }: TableErrorProps) { return ( - - -
- Failed to load data. Please try again - -
-
-
+
+
+ {/* Error icon */} + + {/* Error message */} +

Failed to load data

+ {/* Retry button */} + +
+
); } diff --git a/frontend/src/components/ui/TableSkeleton.tsx b/frontend/src/components/ui/TableSkeleton.tsx index 4c75015a..6e1a7f9f 100644 --- a/frontend/src/components/ui/TableSkeleton.tsx +++ b/frontend/src/components/ui/TableSkeleton.tsx @@ -1,43 +1,30 @@ +// Import required components import { Table } from "@tanstack/react-table"; -import { TableCell, TableRow } from "./table"; import { Skeleton } from "./skeleton"; -import { randomBetween } from "@/lib/utils"; -interface TableSkeleton { - table: Table; +// Props for the skeleton component +interface TableSkeletonProps { + table?: Table; rowCount?: number; } -export default function TableSkeleton({ +// Loading state component for tables +export default function TableSkeleton({ table, - rowCount = 5, -}: TableSkeleton) { + rowCount = 5 +}: TableSkeletonProps) { return ( - <> +
+ {/* Table header skeleton */} +
+ {/* Table rows skeleton */} {Array.from({ length: rowCount }).map((_, i) => ( - - {table.getVisibleFlatColumns().map((col) => ( - - - - ))} - +
+ + + +
))} - +
); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 00000000..f7041809 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000', + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Add request interceptor to handle errors +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.error('API Error:', error.response.data); + } else if (error.request) { + // The request was made but no response was received + console.error('No response received:', error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Request setup error:', error.message); + } + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 4e298e1d..4bab0c85 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -11,6 +11,7 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as TeacherListImport } from './routes/teacher-list' import { Route as StudentListImport } from './routes/student-list' import { Route as NotificationsImport } from './routes/notifications' import { Route as ManageSpecialtiesImport } from './routes/manage-specialties' @@ -19,6 +20,7 @@ import { Route as LoginImport } from './routes/login' import { Route as GlobalStatisticsImport } from './routes/global-statistics' import { Route as FacultyFormImport } from './routes/faculty-form' import { Route as CreateStudentImport } from './routes/create-student' +import { Route as AdminListImport } from './routes/admin-list' import { Route as AboutImport } from './routes/about' import { Route as IndexImport } from './routes/index' import { Route as StudentsStudentIdImport } from './routes/students.$studentId' @@ -27,6 +29,12 @@ import { Route as EditStudentStudentIdImport } from './routes/edit-student.$stud // Create/Update Routes +const TeacherListRoute = TeacherListImport.update({ + id: '/teacher-list', + path: '/teacher-list', + getParentRoute: () => rootRoute, +} as any) + const StudentListRoute = StudentListImport.update({ id: '/student-list', path: '/student-list', @@ -75,6 +83,12 @@ const CreateStudentRoute = CreateStudentImport.update({ getParentRoute: () => rootRoute, } as any) +const AdminListRoute = AdminListImport.update({ + id: '/admin-list', + path: '/admin-list', + getParentRoute: () => rootRoute, +} as any) + const AboutRoute = AboutImport.update({ id: '/about', path: '/about', @@ -123,6 +137,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AboutImport parentRoute: typeof rootRoute } + '/admin-list': { + id: '/admin-list' + path: '/admin-list' + fullPath: '/admin-list' + preLoaderRoute: typeof AdminListImport + parentRoute: typeof rootRoute + } '/create-student': { id: '/create-student' path: '/create-student' @@ -179,6 +200,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StudentListImport parentRoute: typeof rootRoute } + '/teacher-list': { + id: '/teacher-list' + path: '/teacher-list' + fullPath: '/teacher-list' + preLoaderRoute: typeof TeacherListImport + parentRoute: typeof rootRoute + } '/edit-student/$studentId': { id: '/edit-student/$studentId' path: '/edit-student/$studentId' @@ -208,6 +236,7 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute + '/admin-list': typeof AdminListRoute '/create-student': typeof CreateStudentRoute '/faculty-form': typeof FacultyFormRoute '/global-statistics': typeof GlobalStatisticsRoute @@ -216,6 +245,7 @@ export interface FileRoutesByFullPath { '/manage-specialties': typeof ManageSpecialtiesRoute '/notifications': typeof NotificationsRoute '/student-list': typeof StudentListRoute + '/teacher-list': typeof TeacherListRoute '/edit-student/$studentId': typeof EditStudentStudentIdRoute '/sections/$sectionId': typeof SectionsSectionIdRoute '/students/$studentId': typeof StudentsStudentIdRoute @@ -224,6 +254,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute + '/admin-list': typeof AdminListRoute '/create-student': typeof CreateStudentRoute '/faculty-form': typeof FacultyFormRoute '/global-statistics': typeof GlobalStatisticsRoute @@ -232,6 +263,7 @@ export interface FileRoutesByTo { '/manage-specialties': typeof ManageSpecialtiesRoute '/notifications': typeof NotificationsRoute '/student-list': typeof StudentListRoute + '/teacher-list': typeof TeacherListRoute '/edit-student/$studentId': typeof EditStudentStudentIdRoute '/sections/$sectionId': typeof SectionsSectionIdRoute '/students/$studentId': typeof StudentsStudentIdRoute @@ -241,6 +273,7 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/about': typeof AboutRoute + '/admin-list': typeof AdminListRoute '/create-student': typeof CreateStudentRoute '/faculty-form': typeof FacultyFormRoute '/global-statistics': typeof GlobalStatisticsRoute @@ -249,6 +282,7 @@ export interface FileRoutesById { '/manage-specialties': typeof ManageSpecialtiesRoute '/notifications': typeof NotificationsRoute '/student-list': typeof StudentListRoute + '/teacher-list': typeof TeacherListRoute '/edit-student/$studentId': typeof EditStudentStudentIdRoute '/sections/$sectionId': typeof SectionsSectionIdRoute '/students/$studentId': typeof StudentsStudentIdRoute @@ -259,6 +293,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/about' + | '/admin-list' | '/create-student' | '/faculty-form' | '/global-statistics' @@ -267,6 +302,7 @@ export interface FileRouteTypes { | '/manage-specialties' | '/notifications' | '/student-list' + | '/teacher-list' | '/edit-student/$studentId' | '/sections/$sectionId' | '/students/$studentId' @@ -274,6 +310,7 @@ export interface FileRouteTypes { to: | '/' | '/about' + | '/admin-list' | '/create-student' | '/faculty-form' | '/global-statistics' @@ -282,6 +319,7 @@ export interface FileRouteTypes { | '/manage-specialties' | '/notifications' | '/student-list' + | '/teacher-list' | '/edit-student/$studentId' | '/sections/$sectionId' | '/students/$studentId' @@ -289,6 +327,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/about' + | '/admin-list' | '/create-student' | '/faculty-form' | '/global-statistics' @@ -297,6 +336,7 @@ export interface FileRouteTypes { | '/manage-specialties' | '/notifications' | '/student-list' + | '/teacher-list' | '/edit-student/$studentId' | '/sections/$sectionId' | '/students/$studentId' @@ -306,6 +346,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute AboutRoute: typeof AboutRoute + AdminListRoute: typeof AdminListRoute CreateStudentRoute: typeof CreateStudentRoute FacultyFormRoute: typeof FacultyFormRoute GlobalStatisticsRoute: typeof GlobalStatisticsRoute @@ -314,6 +355,7 @@ export interface RootRouteChildren { ManageSpecialtiesRoute: typeof ManageSpecialtiesRoute NotificationsRoute: typeof NotificationsRoute StudentListRoute: typeof StudentListRoute + TeacherListRoute: typeof TeacherListRoute EditStudentStudentIdRoute: typeof EditStudentStudentIdRoute SectionsSectionIdRoute: typeof SectionsSectionIdRoute StudentsStudentIdRoute: typeof StudentsStudentIdRoute @@ -322,6 +364,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AboutRoute: AboutRoute, + AdminListRoute: AdminListRoute, CreateStudentRoute: CreateStudentRoute, FacultyFormRoute: FacultyFormRoute, GlobalStatisticsRoute: GlobalStatisticsRoute, @@ -330,6 +373,7 @@ const rootRouteChildren: RootRouteChildren = { ManageSpecialtiesRoute: ManageSpecialtiesRoute, NotificationsRoute: NotificationsRoute, StudentListRoute: StudentListRoute, + TeacherListRoute: TeacherListRoute, EditStudentStudentIdRoute: EditStudentStudentIdRoute, SectionsSectionIdRoute: SectionsSectionIdRoute, StudentsStudentIdRoute: StudentsStudentIdRoute, @@ -347,6 +391,7 @@ export const routeTree = rootRoute "children": [ "/", "/about", + "/admin-list", "/create-student", "/faculty-form", "/global-statistics", @@ -355,6 +400,7 @@ export const routeTree = rootRoute "/manage-specialties", "/notifications", "/student-list", + "/teacher-list", "/edit-student/$studentId", "/sections/$sectionId", "/students/$studentId" @@ -366,6 +412,9 @@ export const routeTree = rootRoute "/about": { "filePath": "about.tsx" }, + "/admin-list": { + "filePath": "admin-list.tsx" + }, "/create-student": { "filePath": "create-student.tsx" }, @@ -390,6 +439,9 @@ export const routeTree = rootRoute "/student-list": { "filePath": "student-list.tsx" }, + "/teacher-list": { + "filePath": "teacher-list.tsx" + }, "/edit-student/$studentId": { "filePath": "edit-student.$studentId.tsx" }, diff --git a/frontend/src/routes/admin-list.tsx b/frontend/src/routes/admin-list.tsx new file mode 100644 index 00000000..900d5580 --- /dev/null +++ b/frontend/src/routes/admin-list.tsx @@ -0,0 +1,20 @@ +// Import required components and utilities +import { AdminList } from "@/components/DataTable/AdminList"; +import { checkAuth } from "@/lib/auth/authMiddleware"; +import { createFileRoute } from "@tanstack/react-router"; + +// Create route with authentication check +export const Route = createFileRoute("/admin-list")({ + component: RouteComponent, + beforeLoad: checkAuth, +}); + +// Admin list page component +function RouteComponent() { + return ( +
+

Admin List

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 4ec51fd8..8afa9e09 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,5 +1,8 @@ import { checkAuth } from "@/lib/auth/authMiddleware"; import { createFileRoute } from "@tanstack/react-router"; +import { Button } from "@/components/ui/button"; +import { Link } from "@tanstack/react-router"; +import ShowIfPermission from "@/components/Auth/ShowIfPermission"; export const Route = createFileRoute("/")({ component: Index, @@ -8,8 +11,66 @@ export const Route = createFileRoute("/")({ function Index() { return ( -
-

Welcome Home!

+
+

Dashboard

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
); } diff --git a/frontend/src/routes/manage-sections.tsx b/frontend/src/routes/manage-sections.tsx index 6ed8bdcb..ad2c6f5b 100644 --- a/frontend/src/routes/manage-sections.tsx +++ b/frontend/src/routes/manage-sections.tsx @@ -13,16 +13,16 @@ export const Route = createFileRoute("/manage-sections")({ }); function RouteComponent() { - const specialtiesQuery = useSpecialtiesQuery(); + const query = useSpecialtiesQuery(); - if (specialtiesQuery.isLoading) { + if (query.isLoading) { return (
); } - if (specialtiesQuery.isError) { + if (query.isError) { return ; } @@ -31,11 +31,11 @@ function RouteComponent() {

Manage Sections

- +
); } diff --git a/frontend/src/routes/teacher-list.tsx b/frontend/src/routes/teacher-list.tsx new file mode 100644 index 00000000..229e79e4 --- /dev/null +++ b/frontend/src/routes/teacher-list.tsx @@ -0,0 +1,20 @@ +// Import required components and utilities +import { TeacherList } from "@/components/DataTable/TeacherList"; +import { checkAuth } from "@/lib/auth/authMiddleware"; +import { createFileRoute } from "@tanstack/react-router"; + +// Create route with authentication check +export const Route = createFileRoute("/teacher-list")({ + component: RouteComponent, + beforeLoad: checkAuth, +}); + +// Teacher list page component +function RouteComponent() { + return ( +
+

Teacher List

+ +
+ ); +} \ No newline at end of file