From f7587d17d2e838ea8f64de0df98e80777f872a24 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 10:11:03 +0100 Subject: [PATCH 01/12] removed the filters --- backend/Dockerfile | 24 +++++++ .../components/DataTable/SectionsTable.tsx | 71 ++----------------- frontend/src/lib/api.ts | 27 +++++++ 3 files changed, 57 insertions(+), 65 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 frontend/src/lib/api.ts 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/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index 51e3a907..fd7fdc14 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"; @@ -27,11 +25,7 @@ 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"; @@ -65,20 +59,17 @@ const columns: ColumnDef
[] = [ ), size: 160, - filterFn: isIncludedinArr, }, { accessorKey: "spec_name", header: ({ column }) => , size: 100, - filterFn: isIncludedinArr, }, { accessorKey: "section_name", header: ({ column }) => ( ), - filterFn: "equalsString", size: 100, }, { @@ -89,6 +80,7 @@ const columns: ColumnDef
[] = [ size: 100, }, ]; + interface SectionsTableProps { specialties: Specialty[]; } @@ -96,18 +88,6 @@ interface SectionsTableProps { export function SectionsTable({ specialties }: SectionsTableProps) { const query = useSectionQuery(); 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); const table = useReactTable({ data: query.data || [], @@ -115,52 +95,14 @@ export function SectionsTable({ specialties }: SectionsTableProps) { getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), getRowId: (originalRow) => originalRow["section_id"], state: { sorting, - columnFilters, }, }); 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" - /> - -
- -
@@ -250,17 +192,16 @@ function ActionsDropdown({ row }: { row: Row
}) { - + - - Delete + + + Delete - - - + ); 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 From 4f7362db9e3624377ee893aa8220311b20e8a3ee Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:10:51 +0100 Subject: [PATCH 02/12] i screwed up everything has gone so first resotring the sectionwindo --- .../components/DataTable/SectionsTable.tsx | 33 ++++- .../components/Sections/EditSectionDialog.tsx | 135 ++++++++++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Sections/EditSectionDialog.tsx diff --git a/frontend/src/components/DataTable/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index fd7fdc14..bb0ead71 100644 --- a/frontend/src/components/DataTable/SectionsTable.tsx +++ b/frontend/src/components/DataTable/SectionsTable.tsx @@ -29,18 +29,22 @@ import TableHeading from "./TableHeading"; import { cn } from "@/lib/utils"; import { Link as LinkIcon, MoreVertical, Trash } from "lucide-react"; import DeleteSectionDialog from "../Sections/DeleteSectionDialog"; +import EditSectionDialog from "../Sections/EditSectionDialog"; import TableSkeleton from "../ui/TableSkeleton"; import TableError from "../ui/TableError"; import { Section, Specialty, useSectionQuery } from "@/lib/queries"; import ShowIfPermission from "../Auth/ShowIfPermission"; import { Link } from "@tanstack/react-router"; +// Define what each column in the table should look like const columns: ColumnDef
[] = [ + // Column for actions (delete button) { id: "actions", cell: ActionsDropdown, size: 50, }, + // Column for the link icon { id: "link", cell: ({ row }: { row: Row
}) => ( @@ -53,6 +57,7 @@ const columns: ColumnDef
[] = [ ), size: 30, }, + // Column for academic level { accessorKey: "academic_level", header: ({ column }) => ( @@ -60,11 +65,13 @@ const columns: ColumnDef
[] = [ ), size: 160, }, + // Column for specialty name { accessorKey: "spec_name", header: ({ column }) => , size: 100, }, + // Column for section code { accessorKey: "section_name", header: ({ column }) => ( @@ -72,6 +79,7 @@ const columns: ColumnDef
[] = [ ), size: 100, }, + // Column for number of students { accessorKey: "student_count", header: ({ column }) => ( @@ -81,14 +89,19 @@ const columns: ColumnDef
[] = [ }, ]; +// Props that this component needs to work interface SectionsTableProps { specialties: Specialty[]; } +// The main table component export function SectionsTable({ specialties }: SectionsTableProps) { + // Get the list of sections from the server const query = useSectionQuery(); + // Keep track of how the table is sorted const [sorting, setSorting] = useState([]); + // Set up the table with all its features const table = useReactTable({ data: query.data || [], columns, @@ -105,6 +118,7 @@ export function SectionsTable({ specialties }: SectionsTableProps) {
+ {/* Table header */} {table.getHeaderGroups().map((headerGroup) => ( @@ -129,12 +143,16 @@ export function SectionsTable({ specialties }: SectionsTableProps) { ))} + {/* Table body */} + {/* Show loading spinner while data is being fetched */} {query.isLoading ? ( ) : query.isError ? ( + // Show error message if something went wrong ) : table.getRowModel().rows?.length ? ( + // Show the actual data rows table.getRowModel().rows.map((row, i) => ( )) ) : ( + // Show message if no data is found }) { + // Get the section information from the current row const { section_id, section_name, spec_name, academic_level } = row.original; const name = `${academic_level} ${spec_name} Section ${section_name}`; + // Keep track of whether the delete dialog is open + const [open, setOpen] = useState(false); return ( + // Only show if user has permission to delete sections - + + {/* Dropdown menu with 3 dots */} + {/* Delete option in the dropdown */} @@ -201,7 +227,10 @@ function ActionsDropdown({ row }: { row: Row
}) { - + {/* Confirmation dialog for delete action */} + + setOpen(false)} /> +
); diff --git a/frontend/src/components/Sections/EditSectionDialog.tsx b/frontend/src/components/Sections/EditSectionDialog.tsx new file mode 100644 index 00000000..b77517e1 --- /dev/null +++ b/frontend/src/components/Sections/EditSectionDialog.tsx @@ -0,0 +1,135 @@ +import { + DialogTitle, + DialogHeader, + DialogFooter, + DialogClose, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "../ui/button"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Section, Specialty } from "@/lib/queries"; +import request from "@/lib/request"; +import { useAppForm } from "../Forms"; +import { AcademicLevels } from "@/lib/temp-consts"; + +interface EditSectionDialogProps { + id: string; + name: string; + academic_level: string; + specialization_id: string; + section_name: string; + specialties: Specialty[]; + onSuccess?: () => void; +} + +export default function EditSectionDialog({ + id, + name, + academic_level, + specialization_id, + section_name, + specialties, + onSuccess, +}: EditSectionDialogProps) { + const queryClient = useQueryClient(); + + const form = useAppForm({ + defaultValues: { + academic_level, + specialization_id, + section_name, + }, + onSubmit: async ({ value }) => { + await mutation.mutateAsync(value); + }, + validators: { + onSubmit({ value }) { + if (!value.academic_level) { + return "Academic level is required"; + } + if (!value.specialization_id) { + return "Specialty is required"; + } + if (!value.section_name) { + return "Section code is required"; + } + return undefined; + }, + }, + }); + + const mutation = useMutation({ + mutationFn: async (data: { + academic_level: string; + specialization_id: string; + section_name: string; + }) => { + await request.patch(`/sections/${id}`, data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["sections"] }); + onSuccess?.(); + }, + }); + + return ( + <> + + Edit Section: {name} + + Make changes to the section information below. + + +
+ ( + ({ + label: lvl, + value: lvl, + }))} + className="w-full" + /> + )} + /> + ( + ({ + label: specialty.spec_name, + value: specialty.spec_id.toString(), + }))} + /> + )} + /> + ( + + )} + /> +
+ +
+ + Save Changes + + + + +
+ {mutation.isError && ( +

+ {mutation.error.message || + "An unknown error occurred. Please try again."} +

+ )} +
+ + ); +} \ No newline at end of file From 07f4fc8004d10afaced6d3eba757679f9a6d3cd0 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:23:05 +0100 Subject: [PATCH 03/12] section code shouldnt exist for the selected academic --- .../components/DataTable/SectionsTable.tsx | 1 - .../src/components/Sections/AddSection.tsx | 102 ++++++++++--- .../components/Sections/EditSectionDialog.tsx | 135 ------------------ 3 files changed, 84 insertions(+), 154 deletions(-) delete mode 100644 frontend/src/components/Sections/EditSectionDialog.tsx diff --git a/frontend/src/components/DataTable/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index bb0ead71..909d5d22 100644 --- a/frontend/src/components/DataTable/SectionsTable.tsx +++ b/frontend/src/components/DataTable/SectionsTable.tsx @@ -29,7 +29,6 @@ import TableHeading from "./TableHeading"; import { cn } from "@/lib/utils"; import { Link as LinkIcon, MoreVertical, Trash } from "lucide-react"; import DeleteSectionDialog from "../Sections/DeleteSectionDialog"; -import EditSectionDialog from "../Sections/EditSectionDialog"; import TableSkeleton from "../ui/TableSkeleton"; import TableError from "../ui/TableError"; import { Section, Specialty, useSectionQuery } from "@/lib/queries"; diff --git a/frontend/src/components/Sections/AddSection.tsx b/frontend/src/components/Sections/AddSection.tsx index fc561204..0346cac3 100644 --- a/frontend/src/components/Sections/AddSection.tsx +++ b/frontend/src/components/Sections/AddSection.tsx @@ -12,7 +12,7 @@ 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"; @@ -27,20 +27,52 @@ export default function AddSection({ className, }: AddSectionProps) { const queryClient = useQueryClient(); + const sectionsQuery = useSectionQuery(); + const [open, setOpen] = useState(false); + const [error, setError] = useState(null); + + // Function to check if a section code already exists + const isSectionCodeDuplicate = ( + academicLevel: string, + specializationId: string, + sectionName: string + ) => { + return sectionsQuery.data?.some( + section => + section.academic_level === academicLevel && + section.spec_id === specializationId && + section.section_name === sectionName + ); + }; + const addMutation = useMutation({ mutationFn: async (value: { academic_level: string; specialization_id: string; section_name: string; }) => { - await request.post("/sections", value); + // Check for duplicates before making the API call + if (isSectionCodeDuplicate(value.academic_level, value.specialization_id, value.section_name)) { + throw new Error("This section code is already in use for this academic level and specialty"); + } + const response = await request.post("/sections", value); + return response.data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["sections"], }); setOpen(false); + form.reset(); + setError(null); }, + onError: (error: any) => { + if (error.response?.data?.error === "Section already exists." || error.message === "This section code is already in use for this academic level and specialty") { + setError("This section code is already in use for this academic level and specialty"); + } else { + setError("An error occurred while creating the section. Please try again."); + } + } }); const form = useAppForm({ @@ -50,14 +82,44 @@ export default function AddSection({ section_name: "", }, onSubmit: async ({ value }) => { - await addMutation.mutateAsync(value); + try { + // Check for duplicates before submission + 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 creating section:", error); + } + }, + validators: { + onSubmit({ value }) { + if (!value.academic_level) { + return "Academic level is required"; + } + if (!value.specialization_id) { + return "Specialty is required"; + } + if (!value.section_name) { + return "Section code is required"; + } + return undefined; + }, }, }); function handleCancel() { form.reset(); + setOpen(false); + setError(null); } - const [open, setOpen] = useState(false); return ( @@ -75,7 +137,7 @@ export default function AddSection({ > Add Section - This will create an new section. + This will create a new section.
{ - 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(), + }))} /> )} /> - !value.length ? "Section code is required" : undefined, + onChange: ({ value }) => { + if (!value.length) { + return "Section code is required"; + } + return undefined; + }, }} children={(field) => ( - + )} />
@@ -137,10 +204,9 @@ export default function AddSection({ - {addMutation.isError && ( + {(error || addMutation.isError) && (

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

)} diff --git a/frontend/src/components/Sections/EditSectionDialog.tsx b/frontend/src/components/Sections/EditSectionDialog.tsx deleted file mode 100644 index b77517e1..00000000 --- a/frontend/src/components/Sections/EditSectionDialog.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - DialogTitle, - DialogHeader, - DialogFooter, - DialogClose, - DialogDescription, -} from "@/components/ui/dialog"; -import { Button } from "../ui/button"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { Section, Specialty } from "@/lib/queries"; -import request from "@/lib/request"; -import { useAppForm } from "../Forms"; -import { AcademicLevels } from "@/lib/temp-consts"; - -interface EditSectionDialogProps { - id: string; - name: string; - academic_level: string; - specialization_id: string; - section_name: string; - specialties: Specialty[]; - onSuccess?: () => void; -} - -export default function EditSectionDialog({ - id, - name, - academic_level, - specialization_id, - section_name, - specialties, - onSuccess, -}: EditSectionDialogProps) { - const queryClient = useQueryClient(); - - const form = useAppForm({ - defaultValues: { - academic_level, - specialization_id, - section_name, - }, - onSubmit: async ({ value }) => { - await mutation.mutateAsync(value); - }, - validators: { - onSubmit({ value }) { - if (!value.academic_level) { - return "Academic level is required"; - } - if (!value.specialization_id) { - return "Specialty is required"; - } - if (!value.section_name) { - return "Section code is required"; - } - return undefined; - }, - }, - }); - - const mutation = useMutation({ - mutationFn: async (data: { - academic_level: string; - specialization_id: string; - section_name: string; - }) => { - await request.patch(`/sections/${id}`, data); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["sections"] }); - onSuccess?.(); - }, - }); - - return ( - <> - - Edit Section: {name} - - Make changes to the section information below. - - -
- ( - ({ - label: lvl, - value: lvl, - }))} - className="w-full" - /> - )} - /> - ( - ({ - label: specialty.spec_name, - value: specialty.spec_id.toString(), - }))} - /> - )} - /> - ( - - )} - /> -
- -
- - Save Changes - - - - -
- {mutation.isError && ( -

- {mutation.error.message || - "An unknown error occurred. Please try again."} -

- )} -
- - ); -} \ No newline at end of file From 9238db6c210ec60242bf1c060327e9dade97fa43 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:23:18 +0100 Subject: [PATCH 04/12] section code shouldnt exist for the selected academic2 --- .../src/components/Sections/AddSection.tsx | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/Sections/AddSection.tsx b/frontend/src/components/Sections/AddSection.tsx index 0346cac3..e9d11285 100644 --- a/frontend/src/components/Sections/AddSection.tsx +++ b/frontend/src/components/Sections/AddSection.tsx @@ -17,6 +17,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import request from "@/lib/request"; +// what we need to make this work interface AddSectionProps { specialties: Specialty[]; className?: string; @@ -31,34 +32,18 @@ export default function AddSection({ const [open, setOpen] = useState(false); const [error, setError] = useState(null); - // Function to check if a section code already exists - const isSectionCodeDuplicate = ( - academicLevel: string, - specializationId: string, - sectionName: string - ) => { - return sectionsQuery.data?.some( - section => - section.academic_level === academicLevel && - section.spec_id === specializationId && - section.section_name === sectionName - ); - }; - + // make new section const addMutation = useMutation({ mutationFn: async (value: { academic_level: string; specialization_id: string; section_name: string; }) => { - // Check for duplicates before making the API call - if (isSectionCodeDuplicate(value.academic_level, value.specialization_id, value.section_name)) { - throw new Error("This section code is already in use for this academic level and specialty"); - } const response = await request.post("/sections", value); return response.data; }, onSuccess: () => { + // update list and close queryClient.invalidateQueries({ queryKey: ["sections"], }); @@ -67,14 +52,15 @@ export default function AddSection({ setError(null); }, onError: (error: any) => { - if (error.response?.data?.error === "Section already exists." || error.message === "This section code is already in use for this academic level and specialty") { - setError("This section code is already in use for this academic level and specialty"); + if (error.response?.data?.error === "Section already exists.") { + setError("This section code already exists for this academic level and specialty"); } else { - setError("An error occurred while creating the section. Please try again."); + setError("Error making section. Try again."); } } }); + // form setup const form = useAppForm({ defaultValues: { academic_level: "", @@ -83,7 +69,7 @@ export default function AddSection({ }, onSubmit: async ({ value }) => { try { - // Check for duplicates before submission + // check if section code already used const isDuplicate = sectionsQuery.data?.some( section => section.academic_level === value.academic_level && @@ -96,25 +82,27 @@ export default function AddSection({ } await addMutation.mutateAsync(value); } catch (error) { - console.error("Error creating section:", error); + console.error("Error:", error); } }, validators: { onSubmit({ value }) { + // check if all fields filled if (!value.academic_level) { - return "Academic level is required"; + return "Need academic level"; } if (!value.specialization_id) { - return "Specialty is required"; + return "Need specialty"; } if (!value.section_name) { - return "Section code is required"; + return "Need section code"; } return undefined; }, }, }); + // close form function handleCancel() { form.reset(); setOpen(false); @@ -137,9 +125,10 @@ export default function AddSection({ > Add Section - This will create a new section. + Make new section.
+ {/* academic level picker */} ( @@ -154,14 +143,15 @@ 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) => ( )} /> + {/* section code input */} { if (!value.length) { - return "Section code is required"; + return "Need section code"; } return undefined; }, @@ -206,7 +197,7 @@ export default function AddSection({
{(error || addMutation.isError) && (

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

)} From 8646bd8b4c5b99a7c9570e72f83fc7300603335e Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:34:11 +0100 Subject: [PATCH 05/12] redo the admin and professor lists --- .../src/components/DataTable/AdminList.tsx | 111 +++++++++++++++++ .../src/components/DataTable/TeacherList.tsx | 116 ++++++++++++++++++ .../src/components/Header/NavbarContent.tsx | 4 +- frontend/src/components/ui/TableError.tsx | 41 +++---- frontend/src/components/ui/TableSkeleton.tsx | 51 +++----- frontend/src/routeTree.gen.ts | 52 ++++++++ frontend/src/routes/admin-list.tsx | 20 +++ frontend/src/routes/teacher-list.tsx | 20 +++ 8 files changed, 354 insertions(+), 61 deletions(-) create mode 100644 frontend/src/components/DataTable/AdminList.tsx create mode 100644 frontend/src/components/DataTable/TeacherList.tsx create mode 100644 frontend/src/routes/admin-list.tsx create mode 100644 frontend/src/routes/teacher-list.tsx diff --git a/frontend/src/components/DataTable/AdminList.tsx b/frontend/src/components/DataTable/AdminList.tsx new file mode 100644 index 00000000..9beedfec --- /dev/null +++ b/frontend/src/components/DataTable/AdminList.tsx @@ -0,0 +1,111 @@ +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useQuery } from "@tanstack/react-query"; +import request from "@/lib/request"; +import TableSkeleton from "../ui/TableSkeleton"; +import TableError from "../ui/TableError"; + +interface Admin { + teacher_id: string; + first_name: string; + last_name: string; + email: string; +} + +const columns: ColumnDef[] = [ + { + accessorKey: "first_name", + header: "First Name", + }, + { + accessorKey: "last_name", + header: "Last Name", + }, + { + accessorKey: "email", + header: "Email", + }, +]; + +export function AdminList() { + const query = useQuery({ + queryKey: ["admins"], + queryFn: async () => { + const response = await request.get("/teachers/admins-only"); + return response.data; + }, + }); + + const table = useReactTable({ + data: query.data || [], + columns, + getCoreRowModel: getCoreRowModel(), + }); + + if (query.isLoading) { + return ; + } + + if (query.isError) { + return ; + } + + return ( +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No admins found. + + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/DataTable/TeacherList.tsx b/frontend/src/components/DataTable/TeacherList.tsx new file mode 100644 index 00000000..7c0a2f57 --- /dev/null +++ b/frontend/src/components/DataTable/TeacherList.tsx @@ -0,0 +1,116 @@ +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useQuery } from "@tanstack/react-query"; +import request from "@/lib/request"; +import TableSkeleton from "../ui/TableSkeleton"; +import TableError from "../ui/TableError"; + +interface Teacher { + teacher_id: string; + first_name: string; + last_name: string; + email: string; + section_count: number; +} + +const columns: ColumnDef[] = [ + { + accessorKey: "first_name", + header: "First Name", + }, + { + accessorKey: "last_name", + header: "Last Name", + }, + { + accessorKey: "email", + header: "Email", + }, + { + accessorKey: "section_count", + header: "Sections", + }, +]; + +export function TeacherList() { + const query = useQuery({ + queryKey: ["teachers"], + queryFn: async () => { + const response = await request.get("/teachers/teachers-only"); + return response.data; + }, + }); + + const table = useReactTable({ + data: query.data || [], + columns, + getCoreRowModel: getCoreRowModel(), + }); + + if (query.isLoading) { + return ; + } + + if (query.isError) { + return ; + } + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No teachers found. + + + )} + +
+
+ ); +} \ 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/ui/TableError.tsx b/frontend/src/components/ui/TableError.tsx index 4d20d924..56bab67e 100644 --- a/frontend/src/components/ui/TableError.tsx +++ b/frontend/src/components/ui/TableError.tsx @@ -1,29 +1,26 @@ -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 } -export default function TableError({ colSpan, onRetry }: TableErrorProps) { + +// Error state component for tables +export default function TableError({ onRetry }: 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..fd9bc65d 100644 --- a/frontend/src/components/ui/TableSkeleton.tsx +++ b/frontend/src/components/ui/TableSkeleton.tsx @@ -1,43 +1,20 @@ -import { Table } from "@tanstack/react-table"; -import { TableCell, TableRow } from "./table"; +// Import required components import { Skeleton } from "./skeleton"; -import { randomBetween } from "@/lib/utils"; -interface TableSkeleton { - table: Table; - rowCount?: number; -} - -export default function TableSkeleton({ - table, - rowCount = 5, -}: TableSkeleton) { +// Loading state component for tables +export default function TableSkeleton() { return ( - <> - {Array.from({ length: rowCount }).map((_, i) => ( - - {table.getVisibleFlatColumns().map((col) => ( - - - - ))} - +
+ {/* Table header skeleton */} +
+ {/* Table rows skeleton */} + {Array.from({ length: 5 }).map((_, i) => ( +
+ + + +
))} - +
); } 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/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 From 2a07dd21d9864a601de6fa6542f02c652676fc17 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:44:39 +0100 Subject: [PATCH 06/12] minor changes --- .../src/components/DataTable/AdminList.tsx | 38 +++--- .../components/DataTable/SectionsTable.tsx | 118 ++++++------------ .../src/components/DataTable/TeacherList.tsx | 38 +++--- .../src/components/Sections/AddSection.tsx | 8 +- frontend/src/components/ui/TableError.tsx | 3 +- frontend/src/components/ui/TableSkeleton.tsx | 14 ++- frontend/src/routes/manage-sections.tsx | 10 +- 7 files changed, 105 insertions(+), 124 deletions(-) diff --git a/frontend/src/components/DataTable/AdminList.tsx b/frontend/src/components/DataTable/AdminList.tsx index 9beedfec..10712015 100644 --- a/frontend/src/components/DataTable/AdminList.tsx +++ b/frontend/src/components/DataTable/AdminList.tsx @@ -17,6 +17,7 @@ import request from "@/lib/request"; import TableSkeleton from "../ui/TableSkeleton"; import TableError from "../ui/TableError"; +// Admin data interface interface Admin { teacher_id: string; first_name: string; @@ -24,6 +25,7 @@ interface Admin { email: string; } +// Table columns configuration const columns: ColumnDef[] = [ { accessorKey: "first_name", @@ -39,7 +41,9 @@ const columns: ColumnDef[] = [ }, ]; +// Main admin list component export function AdminList() { + // Fetch admins data const query = useQuery({ queryKey: ["admins"], queryFn: async () => { @@ -48,48 +52,46 @@ export function AdminList() { }, }); + // Initialize table with data const table = useReactTable({ data: query.data || [], columns, getCoreRowModel: getCoreRowModel(), }); + // Show loading state if (query.isLoading) { - return ; + return ; } + // Show error state if (query.isError) { - return ; + return ; } return (
+ {/* 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 */} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/frontend/src/components/DataTable/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index 909d5d22..fb7c6c31 100644 --- a/frontend/src/components/DataTable/SectionsTable.tsx +++ b/frontend/src/components/DataTable/SectionsTable.tsx @@ -14,7 +14,6 @@ import { DropdownMenuTrigger, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; - import { Table, TableBody, @@ -31,76 +30,62 @@ 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 what each column in the table should look like +// Table columns configuration const columns: ColumnDef
[] = [ - // Column for actions (delete button) + // Actions column (delete button) { id: "actions", cell: ActionsDropdown, size: 50, }, - // Column for the link icon + // Link to section details { id: "link", cell: ({ row }: { row: Row
}) => ( - + ), size: 30, }, - // Column for academic level + // Academic level column { accessorKey: "academic_level", - header: ({ column }) => ( - - ), + header: ({ column }) => , size: 160, }, - // Column for specialty name + // Specialty column { accessorKey: "spec_name", header: ({ column }) => , size: 100, }, - // Column for section code + // Section code column { accessorKey: "section_name", - header: ({ column }) => ( - - ), + header: ({ column }) => , size: 100, }, - // Column for number of students + // Student count column { accessorKey: "student_count", - header: ({ column }) => ( - - ), + header: ({ column }) => , size: 100, }, ]; -// Props that this component needs to work -interface SectionsTableProps { - specialties: Specialty[]; -} - -// The main table component -export function SectionsTable({ specialties }: SectionsTableProps) { - // Get the list of sections from the server +// Main sections table component +export function SectionsTable() { + // Fetch sections data const query = useSectionQuery(); - // Keep track of how the table is sorted + // Track table sorting state const [sorting, setSorting] = useState([]); - // Set up the table with all its features + // Initialize table with data and features const table = useReactTable({ data: query.data || [], columns, @@ -108,9 +93,7 @@ export function SectionsTable({ specialties }: SectionsTableProps) { onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), getRowId: (originalRow) => originalRow["section_id"], - state: { - sorting, - }, + state: { sorting }, }); return ( @@ -121,66 +104,48 @@ export function SectionsTable({ specialties }: SectionsTableProps) { {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 */} - {/* Show loading spinner while data is being fetched */} {query.isLoading ? ( ) : query.isError ? ( - // Show error message if something went wrong ) : table.getRowModel().rows?.length ? ( - // Show the actual data rows table.getRowModel().rows.map((row, i) => ( {row.getVisibleCells().map((cell) => ( - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( - // Show message if no data is found - + No results. @@ -192,32 +157,26 @@ export function SectionsTable({ specialties }: SectionsTableProps) { ); } -// Component for the actions dropdown menu (3 dots menu) +// Actions dropdown menu component function ActionsDropdown({ row }: { row: Row
}) { - // Get the section information from the current row const { section_id, section_name, spec_name, academic_level } = row.original; const name = `${academic_level} ${spec_name} Section ${section_name}`; - // Keep track of whether the delete dialog is open const [open, setOpen] = useState(false); return ( - // Only show if user has permission to delete sections - {/* Dropdown menu with 3 dots */} + {/* Three dots menu */} - {/* Delete option in the dropdown */} @@ -226,7 +185,8 @@ function ActionsDropdown({ row }: { row: Row
}) { - {/* Confirmation dialog for delete action */} + + {/* Delete confirmation dialog */} setOpen(false)} /> diff --git a/frontend/src/components/DataTable/TeacherList.tsx b/frontend/src/components/DataTable/TeacherList.tsx index 7c0a2f57..8e174b25 100644 --- a/frontend/src/components/DataTable/TeacherList.tsx +++ b/frontend/src/components/DataTable/TeacherList.tsx @@ -17,6 +17,7 @@ import request from "@/lib/request"; import TableSkeleton from "../ui/TableSkeleton"; import TableError from "../ui/TableError"; +// Teacher data interface interface Teacher { teacher_id: string; first_name: string; @@ -25,6 +26,7 @@ interface Teacher { section_count: number; } +// Table columns configuration const columns: ColumnDef[] = [ { accessorKey: "first_name", @@ -44,7 +46,9 @@ const columns: ColumnDef[] = [ }, ]; +// Main teacher list component export function TeacherList() { + // Fetch teachers data const query = useQuery({ queryKey: ["teachers"], queryFn: async () => { @@ -53,48 +57,46 @@ export function TeacherList() { }, }); + // Initialize table with data const table = useReactTable({ data: query.data || [], columns, getCoreRowModel: getCoreRowModel(), }); + // Show loading state if (query.isLoading) { - return ; + return ; } + // Show error state if (query.isError) { - return ; + return ; } return (
+ {/* 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 */} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/frontend/src/components/Sections/AddSection.tsx b/frontend/src/components/Sections/AddSection.tsx index e9d11285..53314b53 100644 --- a/frontend/src/components/Sections/AddSection.tsx +++ b/frontend/src/components/Sections/AddSection.tsx @@ -16,6 +16,7 @@ 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 { @@ -23,6 +24,11 @@ interface AddSectionProps { className?: string; } +// Define the error response type +interface ErrorResponse { + error: string; +} + export default function AddSection({ specialties, className, @@ -51,7 +57,7 @@ export default function AddSection({ form.reset(); setError(null); }, - onError: (error: any) => { + onError: (error: AxiosError) => { if (error.response?.data?.error === "Section already exists.") { setError("This section code already exists for this academic level and specialty"); } else { diff --git a/frontend/src/components/ui/TableError.tsx b/frontend/src/components/ui/TableError.tsx index 56bab67e..e436ef75 100644 --- a/frontend/src/components/ui/TableError.tsx +++ b/frontend/src/components/ui/TableError.tsx @@ -5,10 +5,11 @@ import { AlertCircle } from "lucide-react"; // Props for the error component interface TableErrorProps { onRetry: () => void; // Function to retry loading data + colSpan?: number; // Number of columns to span in the table } // Error state component for tables -export default function TableError({ onRetry }: TableErrorProps) { +export default function TableError({ onRetry, colSpan = 1 }: TableErrorProps) { return (
diff --git a/frontend/src/components/ui/TableSkeleton.tsx b/frontend/src/components/ui/TableSkeleton.tsx index fd9bc65d..6e1a7f9f 100644 --- a/frontend/src/components/ui/TableSkeleton.tsx +++ b/frontend/src/components/ui/TableSkeleton.tsx @@ -1,14 +1,24 @@ // Import required components +import { Table } from "@tanstack/react-table"; import { Skeleton } from "./skeleton"; +// Props for the skeleton component +interface TableSkeletonProps { + table?: Table; + rowCount?: number; +} + // Loading state component for tables -export default function TableSkeleton() { +export default function TableSkeleton({ + table, + rowCount = 5 +}: TableSkeletonProps) { return (
{/* Table header skeleton */}
{/* Table rows skeleton */} - {Array.from({ length: 5 }).map((_, i) => ( + {Array.from({ length: rowCount }).map((_, i) => (
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

- +
); } From 27bf6c562652f955c40db0164abc7137cf5d0af8 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 12:48:32 +0100 Subject: [PATCH 07/12] minor changes2 --- frontend/src/components/DataTable/SectionsTable.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/components/DataTable/SectionsTable.tsx b/frontend/src/components/DataTable/SectionsTable.tsx index fb7c6c31..edd32b3a 100644 --- a/frontend/src/components/DataTable/SectionsTable.tsx +++ b/frontend/src/components/DataTable/SectionsTable.tsx @@ -34,15 +34,13 @@ import { Section, useSectionQuery } from "@/lib/queries"; import ShowIfPermission from "../Auth/ShowIfPermission"; import { Link } from "@tanstack/react-router"; -// Table columns configuration +// Define table columns const columns: ColumnDef
[] = [ - // Actions column (delete button) { id: "actions", cell: ActionsDropdown, size: 50, }, - // Link to section details { id: "link", cell: ({ row }: { row: Row
}) => ( @@ -52,25 +50,21 @@ const columns: ColumnDef
[] = [ ), size: 30, }, - // Academic level column { accessorKey: "academic_level", header: ({ column }) => , size: 160, }, - // Specialty column { accessorKey: "spec_name", header: ({ column }) => , size: 100, }, - // Section code column { accessorKey: "section_name", header: ({ column }) => , size: 100, }, - // Student count column { accessorKey: "student_count", header: ({ column }) => , From 8a861a8efb91f635490e6dc94d04e80f9eba4f29 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 13:15:27 +0100 Subject: [PATCH 08/12] minor changes3 --- .../src/components/DataTable/AdminList.tsx | 235 ++++++++++++---- .../src/components/DataTable/TeacherList.tsx | 254 ++++++++++++++---- 2 files changed, 390 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/DataTable/AdminList.tsx b/frontend/src/components/DataTable/AdminList.tsx index 10712015..d231a723 100644 --- a/frontend/src/components/DataTable/AdminList.tsx +++ b/frontend/src/components/DataTable/AdminList.tsx @@ -2,7 +2,11 @@ import { ColumnDef, flexRender, getCoreRowModel, + getSortedRowModel, + SortingState, useReactTable, + Row, + VisibilityState, } from "@tanstack/react-table"; import { Table, @@ -12,10 +16,25 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useQuery } from "@tanstack/react-query"; +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"; // Admin data interface interface Admin { @@ -27,22 +46,42 @@ interface Admin { // Table columns configuration const columns: ColumnDef[] = [ + { + id: "actions", + cell: ActionsDropdown, + size: 50, + }, { accessorKey: "first_name", - header: "First Name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "First Name" }, }, { accessorKey: "last_name", - header: "Last Name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "Last Name" }, }, { accessorKey: "email", - header: "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"], @@ -57,57 +96,155 @@ export function AdminList() { data: query.data || [], columns, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnVisibility, + }, }); - // Show loading state - if (query.isLoading) { - return ; - } - - // Show error state - if (query.isError) { - return ; - } - return ( -
-
- {/* Table header */} - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + onSelect={(event) => event.preventDefault()} + > + {column.columnDef.meta?.heading} + + ); + })} + + +
- {/* Table body */} - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+
+ {/* Table header */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} - )) - ) : ( - - - No admins found. - - - )} - -
+ ))} + + + {/* 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 deleteMutation = useMutation({ + mutationFn: async () => { + const response = await request.delete(`/teachers/${admin.teacher_id}`); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["admins"] }); + toast.success("Admin deleted successfully"); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete admin"); + }, + }); + + return ( + + + + + + + { + if (window.confirm("Are you sure you want to delete this admin?")) { + deleteMutation.mutate(); + } + }} + > + + Delete + + + + + ); } \ No newline at end of file diff --git a/frontend/src/components/DataTable/TeacherList.tsx b/frontend/src/components/DataTable/TeacherList.tsx index 8e174b25..adcee662 100644 --- a/frontend/src/components/DataTable/TeacherList.tsx +++ b/frontend/src/components/DataTable/TeacherList.tsx @@ -2,7 +2,11 @@ import { ColumnDef, flexRender, getCoreRowModel, + getSortedRowModel, + SortingState, useReactTable, + Row, + VisibilityState, } from "@tanstack/react-table"; import { Table, @@ -12,10 +16,25 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useQuery } from "@tanstack/react-query"; +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 { @@ -28,26 +47,63 @@ interface Teacher { // Table columns configuration const columns: ColumnDef[] = [ + { + id: "actions", + cell: ActionsDropdown, + size: 50, + }, { accessorKey: "first_name", - header: "First Name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "First Name" }, }, { accessorKey: "last_name", - header: "Last Name", + header: ({ column }) => , + size: 160, + maxSize: 250, + minSize: 50, + meta: { heading: "Last Name" }, }, { accessorKey: "email", - header: "Email", + header: ({ column }) => , + size: 200, + maxSize: 300, + minSize: 100, + meta: { heading: "Email" }, }, { accessorKey: "section_count", - header: "Sections", + 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"], @@ -62,57 +118,155 @@ export function TeacherList() { data: query.data || [], columns, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnVisibility, + }, }); - // Show loading state - if (query.isLoading) { - return ; - } - - // Show error state - if (query.isError) { - return ; - } - return ( -
- - {/* Table header */} - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + onSelect={(event) => event.preventDefault()} + > + {column.columnDef.meta?.heading} + + ); + })} + + +
- {/* Table body */} - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+
+ {/* Table header */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} - )) - ) : ( - - - No teachers found. - - - )} - -
+ ))} + + + {/* 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 From 53121d90c12356c3a425cc6f17203d1cdb353fb6 Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 15:10:42 +0100 Subject: [PATCH 09/12] minor changes5 --- .../src/components/DataTable/AdminList.tsx | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/DataTable/AdminList.tsx b/frontend/src/components/DataTable/AdminList.tsx index d231a723..8db10859 100644 --- a/frontend/src/components/DataTable/AdminList.tsx +++ b/frontend/src/components/DataTable/AdminList.tsx @@ -35,6 +35,15 @@ import { 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 { @@ -205,21 +214,40 @@ export function AdminList() { 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}`); + 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 ( @@ -234,17 +262,47 @@ function ActionsDropdown({ row }: { row: Row }) { { - if (window.confirm("Are you sure you want to delete this admin?")) { - deleteMutation.mutate(); - } - }} + onClick={handleDelete} > Delete + + + + + Confirm Admin Deletion + +
+ + setPassword(e.target.value)} + placeholder="Enter your password" + className="mt-2" + /> +
+ + + + +
+
); } \ No newline at end of file From 33921fa99e41d8f764996721f7a1e3921c2463ef Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 15:30:02 +0100 Subject: [PATCH 10/12] add the dashboard view --- frontend/src/routes/index.tsx | 65 +++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) 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

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
); } From 04611c36a4ef08b059e9ec1db1f05ccda3efd49f Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 16:39:11 +0100 Subject: [PATCH 11/12] minor --- frontend/src/lib/request.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/lib/request.ts b/frontend/src/lib/request.ts index 993cd441..f9d014e7 100644 --- a/frontend/src/lib/request.ts +++ b/frontend/src/lib/request.ts @@ -12,7 +12,6 @@ const request = axios.create({ request.interceptors.request.use( (config) => { - console.log("nint"); const token = localStorage.getItem("jwt_token"); if (token) { config.headers["Authorization"] = `Bearer ${token}`; @@ -20,7 +19,6 @@ request.interceptors.request.use( return config; }, (error) => { - console.log("error here"); return Promise.reject(error); }, ); From e28ed463a7e07b73be2fe183bd6038ec855738af Mon Sep 17 00:00:00 2001 From: chabane250 Date: Sun, 25 May 2025 16:40:10 +0100 Subject: [PATCH 12/12] minor_rev --- frontend/src/lib/request.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/lib/request.ts b/frontend/src/lib/request.ts index f9d014e7..993cd441 100644 --- a/frontend/src/lib/request.ts +++ b/frontend/src/lib/request.ts @@ -12,6 +12,7 @@ const request = axios.create({ request.interceptors.request.use( (config) => { + console.log("nint"); const token = localStorage.getItem("jwt_token"); if (token) { config.headers["Authorization"] = `Bearer ${token}`; @@ -19,6 +20,7 @@ request.interceptors.request.use( return config; }, (error) => { + console.log("error here"); return Promise.reject(error); }, );