From d08f67682f2534e12eb02f8c4b95a5d449bdcab4 Mon Sep 17 00:00:00 2001 From: Mohit Mohan Date: Wed, 29 Jul 2026 01:46:14 +0530 Subject: [PATCH] feat(admin): add Courses Without BR page (#142) Adds an admin page listing courses that have no branch representative assigned, backed by the getCoursesWithoutBR endpoint. - fetchCoursesWithoutBR in admin/src/apis/br.js - CoursesWithoutBR page + coursesWithoutBRTable component - Wire up /admin/courses-without-br route and sidebar nav item Co-Authored-By: Claude Opus 4.8 --- admin/src/App.jsx | 9 ++++ admin/src/apis/br.js | 16 +++++++ admin/src/components/Sidebar.jsx | 3 +- .../src/components/coursesWithoutBRTable.jsx | 37 ++++++++++++++++ admin/src/pages/CoursesWithoutBR.jsx | 44 +++++++++++++++++++ server/modules/br/br.controller.js | 28 +++++++++++- server/modules/br/br.routes.js | 3 +- 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 admin/src/components/coursesWithoutBRTable.jsx create mode 100644 admin/src/pages/CoursesWithoutBR.jsx diff --git a/admin/src/App.jsx b/admin/src/App.jsx index 3c594a38..5ecfcff8 100644 --- a/admin/src/App.jsx +++ b/admin/src/App.jsx @@ -1,6 +1,7 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import Sidebar from "./components/Sidebar"; import BranchRepresentatives from "./pages/BranchRepresentatives"; +import CoursesWithoutBR from "./pages/CoursesWithoutBR"; import Students from "./pages/Students"; import Courses from "./pages/Courses"; import CourseLinking from "./pages/CourseLinking"; @@ -48,6 +49,14 @@ function App() { } /> + + + + } + /> { } }; +// Fetch all courses that don't have a branch representative +export const fetchCoursesWithoutBR = async () => { + try { + const response = await fetch(`${API_BASE_URL}api/br/coursesWithoutBR`, { + credentials: "include", + headers: { + Authorization: "Bearer admin-coursehub-cc23-golang", + }, + }); + return await response.json(); + } catch (error) { + console.error("Error fetching courses without BR:", error); + throw error; + } +}; + // Create a single BR export const createBR = async (email) => { try { diff --git a/admin/src/components/Sidebar.jsx b/admin/src/components/Sidebar.jsx index 352f03da..47e4b673 100644 --- a/admin/src/components/Sidebar.jsx +++ b/admin/src/components/Sidebar.jsx @@ -1,6 +1,6 @@ import React from "react"; import { Link, useLocation } from "react-router-dom"; -import { FaBook, FaUsers, FaLayerGroup, FaLink, FaUserGraduate } from "react-icons/fa"; +import { FaBook, FaUsers, FaLayerGroup, FaLink, FaUserGraduate, FaExclamationTriangle } from "react-icons/fa"; import { adminLogout } from "@/apis/auth"; const navItems = [ @@ -8,6 +8,7 @@ const navItems = [ { label: "Students", to: "/admin/students", icon: FaUserGraduate }, { label: "Courses", to: "/admin/courses", icon: FaBook }, { label: "Course Linking", to: "/admin/course-linking", icon: FaLink }, + { label: "Courses Without BR", to: "/admin/courses-without-br", icon: FaExclamationTriangle }, ]; const Sidebar = () => { diff --git a/admin/src/components/coursesWithoutBRTable.jsx b/admin/src/components/coursesWithoutBRTable.jsx new file mode 100644 index 00000000..83656a2f --- /dev/null +++ b/admin/src/components/coursesWithoutBRTable.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +const CoursesWithoutBRTable = ({ courses }) => { + return ( +
+
+

Courses Without BR

+
+
+ + + + + + + + + {courses.map((course) => ( + + + + + ))} + +
+ Code + + Name +
+ {course.code} + {course.name}
+
+
+ ); +}; + +export default CoursesWithoutBRTable; diff --git a/admin/src/pages/CoursesWithoutBR.jsx b/admin/src/pages/CoursesWithoutBR.jsx new file mode 100644 index 00000000..3d2225e8 --- /dev/null +++ b/admin/src/pages/CoursesWithoutBR.jsx @@ -0,0 +1,44 @@ +import React, { useState, useEffect } from "react"; +import CoursesWithoutBRTable from "../components/coursesWithoutBRTable"; +import { fetchCoursesWithoutBR } from "@/apis/br"; + +export default function CoursesWithoutBR() { + const [courses, setCourses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadCourses = async () => { + try { + setLoading(true); + const response = await fetchCoursesWithoutBR(); + setCourses(response.coursesWithoutBR); + setLoading(false); + } catch (err) { + setError(err.message || "An error occurred while fetching courses without BR."); + setLoading(false); + } + }; + + useEffect(() => { + loadCourses(); + }, []); + + return ( +
+
+
+

Courses Without BR

+

+ Courses that don't have a branch representative assigned yet +

+
+
+ +
+ {loading &&

Loading...

} + {error &&

{error}

} + {!loading && !error && } +
+
+ ); +} diff --git a/server/modules/br/br.controller.js b/server/modules/br/br.controller.js index bd9a9588..9f149d82 100644 --- a/server/modules/br/br.controller.js +++ b/server/modules/br/br.controller.js @@ -2,6 +2,8 @@ import BR from "./br.model.js"; import User from "../user/user.model.js"; import { fetchCoursesForBr } from "../auth/auth.controller.js"; import logger from "../../utils/logger.js"; +import CourseModel from "../course/course.model.js"; +import { normalizeCourseCode } from "../../utils/course.js"; const normalizeEmail = (email) => email?.toString().trim().toLowerCase(); @@ -142,5 +144,29 @@ const getBRs = async (req, res) => { res.status(500).json({ error: "Internal Server Error" }); } }; +const getCoursesWithoutBR = async (req, res) => { + try { + const brRecords = await BR.find({}); + const brEmails = brRecords.map((br) => br.email.toLowerCase()); + + const brUsers = await User.find({ email: { $in: brEmails } }); + + const coveredCodes = new Set( + brUsers.flatMap((user) => + (user.courses || []).map((c) => normalizeCourseCode(c.code)) + ) + ); + + const allCourses = await CourseModel.find({}); + + const coursesWithoutBR = allCourses.filter( + (course) => !coveredCodes.has(normalizeCourseCode(course.code)) + ); -export { updateBRs, createBR, getAll, deleteBR, getBRs }; + res.status(200).json({ coursesWithoutBR }); + } catch (error) { + logger.error(error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; +export { updateBRs, createBR, getAll, deleteBR, getBRs, getCoursesWithoutBR }; diff --git a/server/modules/br/br.routes.js b/server/modules/br/br.routes.js index 803d21b5..55ed1923 100644 --- a/server/modules/br/br.routes.js +++ b/server/modules/br/br.routes.js @@ -1,5 +1,5 @@ import express from "express"; -import { updateBRs, createBR, getAll, deleteBR, getBRs } from "./br.controller.js"; +import { updateBRs, createBR, getAll, deleteBR, getBRs, getCoursesWithoutBR } from "./br.controller.js"; const router = express.Router(); @@ -7,6 +7,7 @@ router.post("/updateList", updateBRs); router.post("/create", createBR); router.get("/all", getAll); router.get("/allBRs", getBRs); +router.get("/coursesWithoutBR", getCoursesWithoutBR); router.delete("/delete", deleteBR); export default router;