From 8f1c90386df23ff8be8d0d511f7779c54e9cbe51 Mon Sep 17 00:00:00 2001 From: VaibhaviShetty23 Date: Sat, 26 Oct 2024 02:13:39 -0400 Subject: [PATCH 01/12] Check on institution changes added - only admins/superadmin --- src/pages/Institutions/Institutions.tsx | 60 ++++++++++++++++--------- src/utils/axios_client.ts | 1 - 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/pages/Institutions/Institutions.tsx b/src/pages/Institutions/Institutions.tsx index 7be8c79f..7304ae47 100644 --- a/src/pages/Institutions/Institutions.tsx +++ b/src/pages/Institutions/Institutions.tsx @@ -8,6 +8,8 @@ import axiosClient from "../../utils/axios_client"; import InstitutionDelete from "./InstitutionDelete"; import { BsPlusSquareFill } from "react-icons/bs"; import { IInstitution } from "../../utils/interfaces"; +import { useSelector } from "react-redux"; +import { RootState } from "store/store"; /** * @author Ankur Mundra on June, 2023 @@ -17,6 +19,11 @@ const Institutions = () => { const navigate = useNavigate(); const institutions: any = useLoaderData(); + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: IInstitution; @@ -55,28 +62,39 @@ const Institutions = () => {
- - - - - {showDeleteConfirmation.visible && ( - + + + + + {showDeleteConfirmation.visible && ( + + )} + + + - )} - - -
- + + + ) } + + {(!['Super Administrator','Administrator'].includes(auth.user.role)) + && ( +

Institution changes not allowed

+ )} + diff --git a/src/utils/axios_client.ts b/src/utils/axios_client.ts index b5fe47d7..4911ff4d 100644 --- a/src/utils/axios_client.ts +++ b/src/utils/axios_client.ts @@ -7,7 +7,6 @@ import { getAuthToken } from "./auth"; const axiosClient = axios.create({ baseURL: "http://localhost:3002/api/v1", - timeout: 1000, headers: { "Content-Type": "application/json", Accept: "application/json", From de6b0d635cc1a0446928dbe7c838be3ad913f73d Mon Sep 17 00:00:00 2001 From: Soubarnica Somangali Suresh Date: Sun, 27 Oct 2024 19:57:53 -0400 Subject: [PATCH 02/12] Added Manage Notifications page --- src/App.tsx | 18 +++ src/components/Form/FormInput.tsx | 65 ++++---- src/components/Form/interfaces.ts | 1 + src/layout/Header.tsx | 3 + .../Notifications/NotificationColumns.tsx | 67 ++++++++ .../Notifications/NotificationDelete.tsx | 63 ++++++++ .../Notifications/NotificationEditor.tsx | 97 +++++++++++ src/pages/Notifications/Notifications.tsx | 153 ++++++++++++++++++ src/utils/interfaces.ts | 9 ++ 9 files changed, 445 insertions(+), 31 deletions(-) create mode 100644 src/pages/Notifications/NotificationColumns.tsx create mode 100644 src/pages/Notifications/NotificationDelete.tsx create mode 100644 src/pages/Notifications/NotificationEditor.tsx create mode 100644 src/pages/Notifications/Notifications.tsx diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..87a89b6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,8 @@ import Login from "./pages/Authentication/Login"; import Logout from "./pages/Authentication/Logout"; import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor"; import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions"; +import NotificationEditor from "./pages/Notifications/NotificationEditor" +import Notifications from "./pages/Notifications/Notifications" import RoleEditor, { loadAvailableRole } from "./pages/Roles/RoleEditor"; import Roles, { loadRoles } from "./pages/Roles/Roles"; import Assignment from "./pages/Assignments/Assignment"; @@ -266,6 +268,22 @@ function App() { }, ], }, + { + path: "notifications", + element: , + //loader: loadNotifications, + children: [ + { + path: "new", + element: , + }, + { + path: "edit/:id", + element: , + //loader: loadNotification, + }, + ], + }, { path: ":user_type", element: , diff --git a/src/components/Form/FormInput.tsx b/src/components/Form/FormInput.tsx index cf74ed88..dde890e5 100644 --- a/src/components/Form/FormInput.tsx +++ b/src/components/Form/FormInput.tsx @@ -8,7 +8,7 @@ import { IFormikFieldProps, IFormProps } from "./interfaces"; * @author Ankur Mundra on May, 2023 */ -const FormInput: React.FC = (props) => { +const FormInput: React.FC = (props) => { // Make sure it's being used here const { name, label, @@ -21,43 +21,46 @@ const FormInput: React.FC = (props) => { inputGroupPrepend, inputGroupAppend, tooltipPlacement, + rows, // rows should be passed as part of props now } = props; const displayLabel = tooltip ? ( - <> - {label}  - - + <> + {label}  + + ) : ( - label + label ); return ( - - {({ field, form }: IFormikFieldProps) => { - const isValid = !form.errors[field.name]; - const isInvalid = form.touched[field.name] && !isValid; - return ( - - {label && {displayLabel}} - - {inputGroupPrepend} - - {inputGroupAppend} - - {form.errors[field.name]} - - - - ); - }} - + + {({ field, form }: IFormikFieldProps) => { + const isValid = !form.errors[field.name]; + const isInvalid = form.touched[field.name] && !isValid; + return ( + + {label && {displayLabel}} + + {inputGroupPrepend} + + {inputGroupAppend} + + {form.errors[field.name]} + + + + ); + }} + ); }; diff --git a/src/components/Form/interfaces.ts b/src/components/Form/interfaces.ts index 5300dad4..5ab37ca3 100644 --- a/src/components/Form/interfaces.ts +++ b/src/components/Form/interfaces.ts @@ -16,6 +16,7 @@ export interface IFormProps { tooltipPlacement?: "top" | "right" | "bottom" | "left"; inputGroupPrepend?: ReactNode; inputGroupAppend?: ReactNode; + rows?: number; // rows would be passed as part of props now } export interface IFormOption { diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 5b278dd8..393eb50d 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -103,6 +103,9 @@ const Header: React.FC = () => { Institutions + + Notifications + Instructors diff --git a/src/pages/Notifications/NotificationColumns.tsx b/src/pages/Notifications/NotificationColumns.tsx new file mode 100644 index 00000000..709b324d --- /dev/null +++ b/src/pages/Notifications/NotificationColumns.tsx @@ -0,0 +1,67 @@ +import { createColumnHelper, Row } from "@tanstack/react-table"; +import { MdOutlineDeleteForever as Remove } from "react-icons/md"; +import { BsPencilFill as Edit } from "react-icons/bs"; +import { Button } from "react-bootstrap"; +import { INotification } from "../../utils/interfaces"; +import React from "react"; + +type Fn = (row: Row) => void; +const columnHelper = createColumnHelper(); + +export const notificationColumns = (handleEdit: Fn, handleDelete: Fn) => [ + columnHelper.accessor("id", { + header: "Id", + enableSorting: false, + enableColumnFilter: false, + }), + + columnHelper.accessor("course", { + header: "Course", + enableSorting: true, + }), + + columnHelper.accessor("subject", { + header: "Subject", + enableSorting: true, + }), + + columnHelper.accessor("description", { + header: "Description", + enableSorting: true, + }), + + columnHelper.accessor("expirationDate", { + header: "Expiration Date", + enableSorting: true, + }), + + columnHelper.accessor("isActive", { + header: "Active Flag", + enableSorting: true, + cell: ({ row }) => (row.original.isActive ? "True" : "False"), + }), + + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + + + + ), + }), +]; diff --git a/src/pages/Notifications/NotificationDelete.tsx b/src/pages/Notifications/NotificationDelete.tsx new file mode 100644 index 00000000..e286bc74 --- /dev/null +++ b/src/pages/Notifications/NotificationDelete.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from "react"; +import { Button, Modal } from "react-bootstrap"; +import { useDispatch } from "react-redux"; +import { alertActions } from "store/slices/alertSlice"; +import { HttpMethod } from "utils/httpMethods"; +import useAPI from "../../hooks/useAPI"; +import { INotification } from "../../utils/interfaces"; + +interface IDeleteNotification { + notificationData: INotification; + onClose: () => void; +} + +const DeleteNotification: React.FC = ({ notificationData, onClose }) => { + const { data: response, error: userError, sendRequest: deleteNotification } = useAPI(); + const [show, setShow] = useState(true); + const dispatch = useDispatch(); + + const deleteHandler = () => + deleteNotification({ + url: `/notifications/${notificationData.id}`, + method: HttpMethod.DELETE, + }); + + useEffect(() => { + if (userError) { + dispatch(alertActions.showAlert({ variant: "danger", message: userError })); + } + }, [userError, dispatch]); + + useEffect(() => { + if (response?.status && response?.status >= 200 && response?.status < 300) { + setShow(false); + dispatch(alertActions.showAlert({ + variant: "success", + message: `Notification ${notificationData.subject} deleted successfully!`, + })); + onClose(); + } + }, [response?.status, dispatch, onClose, notificationData.subject]); + + const closeHandler = () => { + setShow(false); + onClose(); + }; + + return ( + + + Delete Notification + + +

Are you sure you want to delete notification {notificationData.subject}?

+
+ + + + +
+ ); +}; + +export default DeleteNotification; diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx new file mode 100644 index 00000000..10558fda --- /dev/null +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -0,0 +1,97 @@ +import React, { useEffect } from "react"; +import { Form, Formik, FormikHelpers } from "formik"; +import { Button, Modal } from "react-bootstrap"; +import FormInput from "components/Form/FormInput"; +import { alertActions } from "store/slices/alertSlice"; +import { useDispatch } from "react-redux"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import { HttpMethod } from "utils/httpMethods"; +import useAPI from "hooks/useAPI"; +import * as Yup from "yup"; +import axiosClient from "../../utils/axios_client"; +import { INotification } from "../../utils/interfaces"; + +const initialValues: INotification = { + id: "", + course: "", + subject: "", + description: "", + expirationDate: "", + isActive: false, +}; + +const validationSchema = Yup.object({ + subject: Yup.string().required("Required").min(3, "Subject must be at least 3 characters"), + description: Yup.string().required("Required").min(5, "Description must be at least 5 characters"), +}); + +const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) => { + const { data: notificationResponse, error, sendRequest } = useAPI(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const notification: any = useLoaderData(); + + useEffect(() => { + if (notificationResponse && notificationResponse.status >= 200 && notificationResponse.status < 300) { + dispatch(alertActions.showAlert({ + variant: "success", + message: `Notification ${mode}d successfully!`, + })); + navigate("/administrator/notifications"); + } + }, [dispatch, mode, navigate, notificationResponse]); + + useEffect(() => { + error && dispatch(alertActions.showAlert({ variant: "danger", message: error })); + }, [error, dispatch]); + + const onSubmit = (values: INotification, submitProps: FormikHelpers) => { + const method = mode === "update" ? HttpMethod.PATCH : HttpMethod.POST; + const url = mode === "update" ? `/notifications/${values.id}` : "/notifications"; + + sendRequest({ url, method, data: values }); + submitProps.setSubmitting(false); + }; + + const handleClose = () => navigate("/administrator/notifications"); + + return ( + + + {mode === "update" ? "Update " : "Create "}Notification + + + + {(formik) => ( +
+ + + + + + + + + + + )} +
+
+
+ ); +}; + +export async function loadNotification({ params }: any) { + const notificationResponse = await axiosClient.get(`/notifications/${params.id}`); + return await notificationResponse.data; +} + +export default NotificationEditor; diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx new file mode 100644 index 00000000..f2d786c4 --- /dev/null +++ b/src/pages/Notifications/Notifications.tsx @@ -0,0 +1,153 @@ +import { useCallback, useMemo, useState, useEffect } from "react"; +import { Outlet, useNavigate, useLocation } from "react-router-dom"; +import { Button, Col, Container, Row } from "react-bootstrap"; +import { Row as TRow } from "@tanstack/react-table"; +import useAPI from "hooks/useAPI"; +import Table from "components/Table/Table"; +import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; +import axiosClient from "../../utils/axios_client"; +import NotificationDelete from "./NotificationDelete"; +import { BsPlusSquareFill } from "react-icons/bs"; +import { INotification } from "../../utils/interfaces"; +import { useSelector, useDispatch } from "react-redux"; +import { RootState } from "../../store/store"; +import { alertActions } from "store/slices/alertSlice"; +import React from "react"; + +// Mock Data to be used instead of actual API calls +const mockNotifications: INotification[] = [ + { + id: "1", + course: "CS101", + subject: "New Homework", + description: "Homework 1 due next week", + expirationDate: "2024-10-31", + isActive: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "No class tomorrow", + expirationDate: "2024-11-01", + isActive: false, + }, + // Add more mock notifications as needed +]; + +const Notifications = () => { + const navigate = useNavigate(); + //const location = useLocation(); + const dispatch = useDispatch(); + + // Fetch the current authenticated user + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + + // const { error, isLoading, data: notificationsResponse, sendRequest: fetchNotifications } = useAPI(); + + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ + visible: boolean; + data?: INotification; + }>({ visible: false }); + + /* + useEffect(() => { + if (!showDeleteConfirmation.visible) fetchNotifications({ url: `/notifications/${auth.user.id}` }); + }, [fetchNotifications, location, showDeleteConfirmation.visible, auth.user.id]); + */ + + // Error alert + /* + useEffect(() => { + if (error) { + dispatch(alertActions.showAlert({ variant: "danger", message: error })); + } + }, [error, dispatch]); + */ + + // Use mock data instead of fetching from an API + const notificationsResponse = { data: mockNotifications }; + + const onDeleteNotificationHandler = useCallback( + () => setShowDeleteConfirmation({ visible: false }), + [] + ); + + const onEditHandle = useCallback( + (row: TRow) => navigate(`edit/${row.original.id}`), + [navigate] + ); + + const onDeleteHandle = useCallback( + (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), + [] + ); + + const tableColumns = useMemo( + () => NOTIFICATION_COLUMNS(onEditHandle, onDeleteHandle), + [onDeleteHandle, onEditHandle] + ); + + /* + const tableData = useMemo( + () => (isLoading || !notificationsResponse?.data ? [] : notificationsResponse.data), + [notificationsResponse?.data, isLoading] + ); + */ + + // Use mock data instead of fetching data + const tableData = useMemo( + () => (notificationsResponse ? notificationsResponse.data : []), + [notificationsResponse] + ); + + return ( + <> + +
+ + +
+

Manage Notifications

+ +
+ + + + + + {showDeleteConfirmation.visible && ( + + )} + + +
+ + + + + ); +}; + +/* +export async function loadNotifications() { + const notificationsResponse = await axiosClient.get("/notifications"); + return await notificationsResponse.data; +} + */ + +export default Notifications; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 213909c9..bf402b82 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -17,6 +17,15 @@ export interface IInstitution { name: string; } +export interface INotification { + id: string; + course: string; + subject: string; + description: string; + expirationDate: string; + isActive: boolean; +} + export interface IInstructor { id?: number; name: string; From db83be68a154592e1c80d9ec3910ff7b981a2948 Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Sun, 27 Oct 2024 20:47:48 -0400 Subject: [PATCH 03/12] Added authentication to manage notifications --- src/pages/Notifications/Notifications.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index f2d786c4..4facd3fa 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -115,7 +115,10 @@ const Notifications = () => {
- + {(['Super Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].includes(auth.user.role)) + && ( + <> +
- + + ); diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx index 10558fda..9ca63976 100644 --- a/src/pages/Notifications/NotificationEditor.tsx +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -1,15 +1,47 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Form, Formik, FormikHelpers } from "formik"; import { Button, Modal } from "react-bootstrap"; import FormInput from "components/Form/FormInput"; import { alertActions } from "store/slices/alertSlice"; -import { useDispatch } from "react-redux"; -import { useLoaderData, useNavigate } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { useLoaderData, useNavigate, useParams } from "react-router-dom"; import { HttpMethod } from "utils/httpMethods"; import useAPI from "hooks/useAPI"; import * as Yup from "yup"; import axiosClient from "../../utils/axios_client"; import { INotification } from "../../utils/interfaces"; +import { RootState } from "../../store/store"; +import { FormGroup, FormLabel, FormControl } from "react-bootstrap"; + +// Mock notification data for edit +const mockNotifications: INotification[] = [ + { + id: "1", + course: "CS101", + subject: "New Homework", + description: "Homework 1 due next week", + expirationDate: "2024-10-31", + isActive: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "No class tomorrow", + expirationDate: "2024-11-01", + isActive: false, + }, + { + id: "3", + course: "CS103", + subject: "Exam scheduled", + description: "Please prepare for the exam", + expirationDate: "2024-11-05", + isActive: true, + }, +]; + +const mockAssignedCourses = ["CS101", "CS102", "CS103"]; const initialValues: INotification = { id: "", @@ -26,30 +58,39 @@ const validationSchema = Yup.object({ }); const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) => { - const { data: notificationResponse, error, sendRequest } = useAPI(); + const dispatch = useDispatch(); const navigate = useNavigate(); - const notification: any = useLoaderData(); + const { id } = useParams<{ id: string }>(); // Get the ID from the URL parameters + const [notification, setNotification] = useState(initialValues); + useEffect(() => { - if (notificationResponse && notificationResponse.status >= 200 && notificationResponse.status < 300) { - dispatch(alertActions.showAlert({ - variant: "success", - message: `Notification ${mode}d successfully!`, - })); - navigate("/administrator/notifications"); + if (mode === "update" && id) { + // Simulate loading data for the edit mode from mock notifications + const notificationToEdit = mockNotifications.find((notif) => notif.id === id); + if (notificationToEdit) { + setNotification(notificationToEdit); + } else { + dispatch(alertActions.showAlert({ + variant: "danger", + message: "Notification not found!", + })); + navigate("/administrator/notifications"); // Navigate back if the notification doesn't exist + } } - }, [dispatch, mode, navigate, notificationResponse]); + }, [id, mode, dispatch, navigate]); - useEffect(() => { - error && dispatch(alertActions.showAlert({ variant: "danger", message: error })); - }, [error, dispatch]); const onSubmit = (values: INotification, submitProps: FormikHelpers) => { - const method = mode === "update" ? HttpMethod.PATCH : HttpMethod.POST; - const url = mode === "update" ? `/notifications/${values.id}` : "/notifications"; - sendRequest({ url, method, data: values }); + // Simulate submission logic here (no backend call) + const message = mode === "update" ? "Notification updated successfully!" : "Notification created successfully!"; + dispatch(alertActions.showAlert({ + variant: "success", + message: message, + })); + navigate("/administrator/notifications"); submitProps.setSubmitting(false); }; @@ -62,7 +103,7 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = = ({ mode }) = > {(formik) => (
+ + Course + + + {mockAssignedCourses.map((course) => ( + + ))} + + - + @@ -89,9 +144,8 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = ); }; -export async function loadNotification({ params }: any) { - const notificationResponse = await axiosClient.get(`/notifications/${params.id}`); - return await notificationResponse.data; -} + + + export default NotificationEditor; diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index f2d786c4..d5ab3ce7 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -32,12 +32,22 @@ const mockNotifications: INotification[] = [ expirationDate: "2024-11-01", isActive: false, }, + { + id: "3", + course: "CS103", + subject: "Exam scheduled", + description: "Please prepare for the exam", + expirationDate: "2024-11-05", + isActive: true, + }, // Add more mock notifications as needed ]; +const mockAssignedCourses = ["CS101", "CS102", "CS103"]; // Courses assigned to the TA + const Notifications = () => { const navigate = useNavigate(); - //const location = useLocation(); + const location = useLocation(); const dispatch = useDispatch(); // Fetch the current authenticated user @@ -53,6 +63,11 @@ const Notifications = () => { data?: INotification; }>({ visible: false }); + // Filter notifications based on assigned courses + const filteredNotifications = mockNotifications.filter((notification) => + mockAssignedCourses.includes(notification.course) + ); + /* useEffect(() => { if (!showDeleteConfirmation.visible) fetchNotifications({ url: `/notifications/${auth.user.id}` }); @@ -69,7 +84,7 @@ const Notifications = () => { */ // Use mock data instead of fetching from an API - const notificationsResponse = { data: mockNotifications }; + // const notificationsResponse = { data: mockNotifications }; const onDeleteNotificationHandler = useCallback( () => setShowDeleteConfirmation({ visible: false }), @@ -99,10 +114,14 @@ const Notifications = () => { */ // Use mock data instead of fetching data + /* const tableData = useMemo( () => (notificationsResponse ? notificationsResponse.data : []), [notificationsResponse] ); + */ + + const tableData = useMemo(() => filteredNotifications, [filteredNotifications]); return ( <> From c998ced0962c1d786d5e137c638f9cf051488e20 Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Mon, 28 Oct 2024 17:14:17 -0400 Subject: [PATCH 05/12] Added view notifications --- src/App.tsx | 6 + src/layout/Header.tsx | 3 + .../Notifications/NotificationColumns.tsx | 125 ++++++++++-------- src/pages/Notifications/ViewNotifications.tsx | 52 ++++++++ 4 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 src/pages/Notifications/ViewNotifications.tsx diff --git a/src/App.tsx b/src/App.tsx index 87a89b6e..4aa95181 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import InstitutionEditor, { loadInstitution } from "./pages/Institutions/Institu import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions"; import NotificationEditor from "./pages/Notifications/NotificationEditor" import Notifications from "./pages/Notifications/Notifications" +import ViewNotifications from "./pages/Notifications/ViewNotifications"; import RoleEditor, { loadAvailableRole } from "./pages/Roles/RoleEditor"; import Roles, { loadRoles } from "./pages/Roles/Roles"; import Assignment from "./pages/Assignments/Assignment"; @@ -144,6 +145,10 @@ function App() { path: "profile", element: } />, }, + { + path: "view-notifications", + element: } />, + }, { path: "assignments/edit/:assignmentId/participants", element: , @@ -284,6 +289,7 @@ function App() { }, ], }, + { path: ":user_type", element: , diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 393eb50d..5acd86de 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -158,6 +158,9 @@ const Header: React.FC = () => { Grades View + + My Notifications + setVisible(!visible)}> Anonymized View diff --git a/src/pages/Notifications/NotificationColumns.tsx b/src/pages/Notifications/NotificationColumns.tsx index 709b324d..c2f9352c 100644 --- a/src/pages/Notifications/NotificationColumns.tsx +++ b/src/pages/Notifications/NotificationColumns.tsx @@ -1,4 +1,4 @@ -import { createColumnHelper, Row } from "@tanstack/react-table"; +import { createColumnHelper, Row, ColumnDef } from "@tanstack/react-table"; import { MdOutlineDeleteForever as Remove } from "react-icons/md"; import { BsPencilFill as Edit } from "react-icons/bs"; import { Button } from "react-bootstrap"; @@ -8,60 +8,75 @@ import React from "react"; type Fn = (row: Row) => void; const columnHelper = createColumnHelper(); -export const notificationColumns = (handleEdit: Fn, handleDelete: Fn) => [ - columnHelper.accessor("id", { - header: "Id", - enableSorting: false, - enableColumnFilter: false, - }), +// Define columns with the ability to conditionally include the Actions column +export const notificationColumns = ( + handleEdit?: Fn, + handleDelete?: Fn, + showActions: boolean = true +): ColumnDef[] => { // Explicitly typing the return as ColumnDef array - columnHelper.accessor("course", { - header: "Course", - enableSorting: true, - }), + const columns: ColumnDef[] = [ + columnHelper.accessor("id", { + header: "Id", + enableSorting: false, + enableColumnFilter: false, + }), + + columnHelper.accessor("course", { + header: "Course", + enableSorting: true, + }), + + columnHelper.accessor("subject", { + header: "Subject", + enableSorting: true, + }), + + columnHelper.accessor("description", { + header: "Description", + enableSorting: true, + }), + + columnHelper.accessor("expirationDate", { + header: "Expiration Date", + enableSorting: true, + }), + + columnHelper.accessor("isActive", { + header: "Active Flag", + enableSorting: true, + cell: ({ row }) => (row.original.isActive ? "True" : "False"), + }) + ]; - columnHelper.accessor("subject", { - header: "Subject", - enableSorting: true, - }), + // Conditionally add Actions column if `showActions` is true + if (showActions && handleEdit && handleDelete) { + columns.push( + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + + + + ), + }) + ); + } - columnHelper.accessor("description", { - header: "Description", - enableSorting: true, - }), - - columnHelper.accessor("expirationDate", { - header: "Expiration Date", - enableSorting: true, - }), - - columnHelper.accessor("isActive", { - header: "Active Flag", - enableSorting: true, - cell: ({ row }) => (row.original.isActive ? "True" : "False"), - }), - - columnHelper.display({ - id: "actions", - header: "Actions", - cell: ({ row }) => ( - <> - - - - ), - }), -]; + return columns; +}; diff --git a/src/pages/Notifications/ViewNotifications.tsx b/src/pages/Notifications/ViewNotifications.tsx new file mode 100644 index 00000000..56df6bd6 --- /dev/null +++ b/src/pages/Notifications/ViewNotifications.tsx @@ -0,0 +1,52 @@ +import React, { useMemo } from "react"; +import { Container, Row, Col } from "react-bootstrap"; +import Table from "components/Table/Table"; +import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; +import { INotification } from "../../utils/interfaces"; + +// Mock Data for Notifications +const mockStudentNotifications: INotification[] = [ + { + id: "3", + course: "CS103", + subject: "New Quiz", + description: "Quiz 1 scheduled for next class", + expirationDate: "2024-10-25", + isActive: true, + }, + { + id: "4", + course: "CS104", + subject: "Project Deadline", + description: "Project 2 deadline extended", + expirationDate: "2024-11-02", + isActive: true, + }, +]; + +const ViewNotifications = () => { + // For now, using mock data; replace this with API data later + const notifications = useMemo(() => mockStudentNotifications, []); + + return ( + + +
+

My Notifications

+ +
+ + +
+ + + ); +}; + +export default ViewNotifications; From 0eb2a19b2aa2454f4e5e51b3a02afd28ec79e63c Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Mon, 28 Oct 2024 17:47:14 -0400 Subject: [PATCH 06/12] Added authentication for institutions --- src/pages/Institutions/Institutions.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Institutions/Institutions.tsx b/src/pages/Institutions/Institutions.tsx index 7304ae47..f51d6cef 100644 --- a/src/pages/Institutions/Institutions.tsx +++ b/src/pages/Institutions/Institutions.tsx @@ -7,9 +7,10 @@ import { institutionColumns as INSTITUTION_COLUMNS } from "./institutionColumns" import axiosClient from "../../utils/axios_client"; import InstitutionDelete from "./InstitutionDelete"; import { BsPlusSquareFill } from "react-icons/bs"; -import { IInstitution } from "../../utils/interfaces"; +import { IInstitution, ROLE } from "../../utils/interfaces"; import { useSelector } from "react-redux"; import { RootState } from "store/store"; +import { hasAllPrivilegesOf } from "utils/util"; /** * @author Ankur Mundra on June, 2023 @@ -62,8 +63,7 @@ const Institutions = () => {
- {(['Super Administrator','Administrator'].includes(auth.user.role)) - && ( + {hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&( <>
@@ -90,7 +90,7 @@ const Institutions = () => { ) } - {(!['Super Administrator','Administrator'].includes(auth.user.role)) + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && (

Institution changes not allowed

)} From 00f350e8f4e7c98cd92d2e5b76623d9a389c0dbe Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Mon, 28 Oct 2024 19:35:44 -0400 Subject: [PATCH 07/12] used function to determine authentication --- src/layout/Header.tsx | 3 +- src/pages/Institutions/Institutions.tsx | 4 +-- src/pages/Notifications/Notifications.tsx | 11 +++---- src/pages/Notifications/ViewNotifications.tsx | 33 +++++++++++++++---- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 58357211..2942349a 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -158,9 +158,10 @@ const Header: React.FC = () => { Grades View + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( My Notifications - + )} setVisible(!visible)}> Anonymized View diff --git a/src/pages/Institutions/Institutions.tsx b/src/pages/Institutions/Institutions.tsx index f51d6cef..2a15e123 100644 --- a/src/pages/Institutions/Institutions.tsx +++ b/src/pages/Institutions/Institutions.tsx @@ -63,7 +63,7 @@ const Institutions = () => {
- {hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&( + {hasAllPrivilegesOf(auth.user.role, ROLE.INSTRUCTOR) &&( <>
@@ -90,7 +90,7 @@ const Institutions = () => { ) } - {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) + {!hasAllPrivilegesOf(auth.user.role, ROLE.INSTRUCTOR) && (

Institution changes not allowed

)} diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index 6654b124..255efcb8 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -8,9 +8,10 @@ import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColum import axiosClient from "../../utils/axios_client"; import NotificationDelete from "./NotificationDelete"; import { BsPlusSquareFill } from "react-icons/bs"; -import { INotification } from "../../utils/interfaces"; +import { INotification, ROLE } from "../../utils/interfaces"; import { useSelector, useDispatch } from "react-redux"; import { RootState } from "../../store/store"; +import { hasAllPrivilegesOf } from "utils/util"; import { alertActions } from "store/slices/alertSlice"; import React from "react"; @@ -134,8 +135,7 @@ const Notifications = () => {
- {(['Super Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].includes(auth.user.role)) - && ( + {hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&( <> @@ -156,13 +156,12 @@ const Notifications = () => { columns={tableColumns} showColumnFilter={false} columnVisibility={{ id: false }} - tableSize={{ span: 8, offset: 3 }} + tableSize={{ span: 12, offset: 3 }} /> ) } - {(!['Super Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].includes(auth.user.role)) - && ( + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&(

Notification changes not allowed

)} diff --git a/src/pages/Notifications/ViewNotifications.tsx b/src/pages/Notifications/ViewNotifications.tsx index 56df6bd6..3a79f4aa 100644 --- a/src/pages/Notifications/ViewNotifications.tsx +++ b/src/pages/Notifications/ViewNotifications.tsx @@ -4,8 +4,24 @@ import Table from "components/Table/Table"; import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; import { INotification } from "../../utils/interfaces"; -// Mock Data for Notifications -const mockStudentNotifications: INotification[] = [ +// Dummy data for notifications +const mockNotifications: INotification[] = [ + { + id: "1", + course: "CS101", + subject: "Assignment Due", + description: "Assignment 1 is due on Friday.", + expirationDate: "2024-10-20", + isActive: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "Class is canceled tomorrow.", + expirationDate: "2024-10-21", + isActive: true, + }, { id: "3", course: "CS103", @@ -24,9 +40,14 @@ const mockStudentNotifications: INotification[] = [ }, ]; +const enrolledCourses = ["CS101", "CS103"]; // Courses the student is enrolled in + const ViewNotifications = () => { - // For now, using mock data; replace this with API data later - const notifications = useMemo(() => mockStudentNotifications, []); + // Filter notifications to show only those related to enrolled courses + const filteredNotifications = useMemo( + () => mockNotifications.filter((notification) => enrolledCourses.includes(notification.course)), + [] + ); return ( @@ -38,8 +59,8 @@ const ViewNotifications = () => {
Date: Mon, 28 Oct 2024 19:39:08 -0400 Subject: [PATCH 08/12] Fixed bug for close and update handles in NotificationEditor.tsx to navigate to Manage Notifications page --- src/pages/Notifications/NotificationEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx index 9ca63976..649372ea 100644 --- a/src/pages/Notifications/NotificationEditor.tsx +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -76,7 +76,7 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = variant: "danger", message: "Notification not found!", })); - navigate("/administrator/notifications"); // Navigate back if the notification doesn't exist + navigate("/notifications"); // Navigate back if the notification doesn't exist } } }, [id, mode, dispatch, navigate]); @@ -90,11 +90,11 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = variant: "success", message: message, })); - navigate("/administrator/notifications"); + navigate("/notifications"); submitProps.setSubmitting(false); }; - const handleClose = () => navigate("/administrator/notifications"); + const handleClose = () => navigate("/notifications"); return ( From ca8dc08a458be8bd9ef41c1821114e63807b6742 Mon Sep 17 00:00:00 2001 From: Soubarnica Somangali Suresh Date: Mon, 28 Oct 2024 19:44:58 -0400 Subject: [PATCH 09/12] Fixed bug for close and update handles in InstitutionEditor.tsx to navigate to Manage Institutions page --- src/pages/Institutions/InstitutionEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Institutions/InstitutionEditor.tsx b/src/pages/Institutions/InstitutionEditor.tsx index 2e2595cf..6fa447c5 100644 --- a/src/pages/Institutions/InstitutionEditor.tsx +++ b/src/pages/Institutions/InstitutionEditor.tsx @@ -45,7 +45,7 @@ const InstitutionEditor: React.FC = ({ mode }) => { message: `Institution ${mode}d successfully!`, }) ); - navigate("/administrator/institutions"); + navigate("/institutions"); } }, [dispatch, mode, navigate, institutionResponse]); @@ -71,7 +71,7 @@ const InstitutionEditor: React.FC = ({ mode }) => { submitProps.setSubmitting(false); }; - const handleClose = () => navigate("/administrator/institutions"); + const handleClose = () => navigate("/institutions"); return ( From d341cdfea89b85a24fb5c9be4340981b46bccba5 Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Mon, 28 Oct 2024 23:14:40 -0400 Subject: [PATCH 10/12] Added toggle notifications and common mock data for notifications --- .../Notifications/NotificationColumns.tsx | 29 +-- src/pages/Notifications/Notifications.tsx | 171 ++++++------------ src/pages/Notifications/ViewNotifications.tsx | 48 +---- src/pages/Notifications/mock_data.tsx | 38 ++++ 4 files changed, 116 insertions(+), 170 deletions(-) create mode 100644 src/pages/Notifications/mock_data.tsx diff --git a/src/pages/Notifications/NotificationColumns.tsx b/src/pages/Notifications/NotificationColumns.tsx index c2f9352c..5af73a7e 100644 --- a/src/pages/Notifications/NotificationColumns.tsx +++ b/src/pages/Notifications/NotificationColumns.tsx @@ -1,20 +1,21 @@ +// notificationColumns.tsx import { createColumnHelper, Row, ColumnDef } from "@tanstack/react-table"; import { MdOutlineDeleteForever as Remove } from "react-icons/md"; import { BsPencilFill as Edit } from "react-icons/bs"; -import { Button } from "react-bootstrap"; +import { Button, Form } from "react-bootstrap"; import { INotification } from "../../utils/interfaces"; import React from "react"; type Fn = (row: Row) => void; +type ToggleFn = (row: Row, isActive: boolean) => void; const columnHelper = createColumnHelper(); -// Define columns with the ability to conditionally include the Actions column export const notificationColumns = ( handleEdit?: Fn, handleDelete?: Fn, - showActions: boolean = true -): ColumnDef[] => { // Explicitly typing the return as ColumnDef array - + showActions: boolean = true, + handleToggle?: ToggleFn // Added handleToggle parameter +): ColumnDef[] => { const columns: ColumnDef[] = [ columnHelper.accessor("id", { header: "Id", @@ -41,15 +42,21 @@ export const notificationColumns = ( header: "Expiration Date", enableSorting: true, }), - - columnHelper.accessor("isActive", { - header: "Active Flag", - enableSorting: true, - cell: ({ row }) => (row.original.isActive ? "True" : "False"), + + columnHelper.display({ + id: "isActive", + header: "Active", + cell: ({ row }) => handleToggle ? ( + handleToggle(row, !row.original.isActive)} + /> + ) : row.original.isActive ? "True" : "False", }) ]; - // Conditionally add Actions column if `showActions` is true if (showActions && handleEdit && handleDelete) { columns.push( columnHelper.display({ diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index 255efcb8..0e05c618 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -1,92 +1,38 @@ -import { useCallback, useMemo, useState, useEffect } from "react"; -import { Outlet, useNavigate, useLocation } from "react-router-dom"; +// Notifications.tsx +import { useCallback, useMemo, useState } from "react"; +import { Outlet, useNavigate } from "react-router-dom"; import { Button, Col, Container, Row } from "react-bootstrap"; -import { Row as TRow } from "@tanstack/react-table"; -import useAPI from "hooks/useAPI"; import Table from "components/Table/Table"; import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; -import axiosClient from "../../utils/axios_client"; +import { mockNotifications } from "./mock_data"; import NotificationDelete from "./NotificationDelete"; import { BsPlusSquareFill } from "react-icons/bs"; import { INotification, ROLE } from "../../utils/interfaces"; -import { useSelector, useDispatch } from "react-redux"; +import { useSelector } from "react-redux"; import { RootState } from "../../store/store"; import { hasAllPrivilegesOf } from "utils/util"; -import { alertActions } from "store/slices/alertSlice"; import React from "react"; +import { Row as TRow } from "@tanstack/react-table"; -// Mock Data to be used instead of actual API calls -const mockNotifications: INotification[] = [ - { - id: "1", - course: "CS101", - subject: "New Homework", - description: "Homework 1 due next week", - expirationDate: "2024-10-31", - isActive: true, - }, - { - id: "2", - course: "CS102", - subject: "Class Canceled", - description: "No class tomorrow", - expirationDate: "2024-11-01", - isActive: false, - }, - { - id: "3", - course: "CS103", - subject: "Exam scheduled", - description: "Please prepare for the exam", - expirationDate: "2024-11-05", - isActive: true, - }, - // Add more mock notifications as needed -]; - -const mockAssignedCourses = ["CS101", "CS102", "CS103"]; // Courses assigned to the TA +const mockAssignedCourses = ["CS101", "CS102", "CS103"]; const Notifications = () => { const navigate = useNavigate(); - const location = useLocation(); - const dispatch = useDispatch(); - - // Fetch the current authenticated user - const auth = useSelector( - (state: RootState) => state.authentication, - (prev, next) => prev.isAuthenticated === next.isAuthenticated - ); - - // const { error, isLoading, data: notificationsResponse, sendRequest: fetchNotifications } = useAPI(); + const auth = useSelector((state: RootState) => state.authentication); + const [notifications, setNotifications] = useState(mockNotifications); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: INotification; }>({ visible: false }); - // Filter notifications based on assigned courses - const filteredNotifications = mockNotifications.filter((notification) => - mockAssignedCourses.includes(notification.course) + const filteredNotifications = useMemo( + () => notifications.filter((notification) => + mockAssignedCourses.includes(notification.course) + ), + [notifications] ); - /* - useEffect(() => { - if (!showDeleteConfirmation.visible) fetchNotifications({ url: `/notifications/${auth.user.id}` }); - }, [fetchNotifications, location, showDeleteConfirmation.visible, auth.user.id]); - */ - - // Error alert - /* - useEffect(() => { - if (error) { - dispatch(alertActions.showAlert({ variant: "danger", message: error })); - } - }, [error, dispatch]); - */ - - // Use mock data instead of fetching from an API - // const notificationsResponse = { data: mockNotifications }; - const onDeleteNotificationHandler = useCallback( () => setShowDeleteConfirmation({ visible: false }), [] @@ -102,28 +48,22 @@ const Notifications = () => { [] ); + // Toggle the isActive status of a notification + const handleToggle = (row: TRow, newIsActive: boolean) => { + setNotifications((prevNotifications) => + prevNotifications.map((notification) => + notification.id === row.original.id + ? { ...notification, isActive: newIsActive } + : notification + ) + ); + }; + const tableColumns = useMemo( - () => NOTIFICATION_COLUMNS(onEditHandle, onDeleteHandle), + () => NOTIFICATION_COLUMNS(onEditHandle, onDeleteHandle, true, handleToggle), [onDeleteHandle, onEditHandle] ); - /* - const tableData = useMemo( - () => (isLoading || !notificationsResponse?.data ? [] : notificationsResponse.data), - [notificationsResponse?.data, isLoading] - ); - */ - - // Use mock data instead of fetching data - /* - const tableData = useMemo( - () => (notificationsResponse ? notificationsResponse.data : []), - [notificationsResponse] - ); - */ - - const tableData = useMemo(() => filteredNotifications, [filteredNotifications]); - return ( <> @@ -135,33 +75,33 @@ const Notifications = () => {
- {hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&( + {hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( <> - -
- - - {showDeleteConfirmation.visible && ( - - )} - - -
- - - ) } - {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) &&( + + + + + {showDeleteConfirmation.visible && ( + + )} + + +
+ + + )} + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && (

Notification changes not allowed

)} @@ -170,11 +110,4 @@ const Notifications = () => { ); }; -/* -export async function loadNotifications() { - const notificationsResponse = await axiosClient.get("/notifications"); - return await notificationsResponse.data; -} - */ - export default Notifications; diff --git a/src/pages/Notifications/ViewNotifications.tsx b/src/pages/Notifications/ViewNotifications.tsx index 3a79f4aa..eab13773 100644 --- a/src/pages/Notifications/ViewNotifications.tsx +++ b/src/pages/Notifications/ViewNotifications.tsx @@ -1,51 +1,19 @@ +// ViewNotifications.tsx import React, { useMemo } from "react"; import { Container, Row, Col } from "react-bootstrap"; import Table from "components/Table/Table"; import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; +import { mockNotifications } from "./mock_data"; import { INotification } from "../../utils/interfaces"; -// Dummy data for notifications -const mockNotifications: INotification[] = [ - { - id: "1", - course: "CS101", - subject: "Assignment Due", - description: "Assignment 1 is due on Friday.", - expirationDate: "2024-10-20", - isActive: true, - }, - { - id: "2", - course: "CS102", - subject: "Class Canceled", - description: "Class is canceled tomorrow.", - expirationDate: "2024-10-21", - isActive: true, - }, - { - id: "3", - course: "CS103", - subject: "New Quiz", - description: "Quiz 1 scheduled for next class", - expirationDate: "2024-10-25", - isActive: true, - }, - { - id: "4", - course: "CS104", - subject: "Project Deadline", - description: "Project 2 deadline extended", - expirationDate: "2024-11-02", - isActive: true, - }, -]; - -const enrolledCourses = ["CS101", "CS103"]; // Courses the student is enrolled in +const enrolledCourses = ["CS101", "CS103"]; const ViewNotifications = () => { - // Filter notifications to show only those related to enrolled courses + // Filter notifications to show only active ones related to enrolled courses const filteredNotifications = useMemo( - () => mockNotifications.filter((notification) => enrolledCourses.includes(notification.course)), + () => mockNotifications.filter( + (notification) => notification.isActive && enrolledCourses.includes(notification.course) + ), [] ); @@ -62,7 +30,7 @@ const ViewNotifications = () => { data={filteredNotifications} columns={NOTIFICATION_COLUMNS(undefined, undefined, false)} // Hide actions column showColumnFilter={false} - columnVisibility={{ id: false }} + columnVisibility={{ id: false, isActive: false }} // Hide isActive column tableSize={{ span: 10, offset: 2 }} /> diff --git a/src/pages/Notifications/mock_data.tsx b/src/pages/Notifications/mock_data.tsx new file mode 100644 index 00000000..24b0cbd6 --- /dev/null +++ b/src/pages/Notifications/mock_data.tsx @@ -0,0 +1,38 @@ + +import { INotification } from "../../utils/interfaces"; + +// Shared dummy data for notifications +export const mockNotifications: INotification[] = [ + { + id: "1", + course: "CS101", + subject: "Assignment Due", + description: "Assignment 1 is due on Friday.", + expirationDate: "2024-10-20", + isActive: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "Class is canceled tomorrow.", + expirationDate: "2024-10-21", + isActive: true, + }, + { + id: "3", + course: "CS103", + subject: "New Quiz", + description: "Quiz 1 scheduled for next class", + expirationDate: "2024-10-25", + isActive: true, + }, + { + id: "4", + course: "CS104", + subject: "Project Deadline", + description: "Project 2 deadline extended", + expirationDate: "2024-11-02", + isActive: true, + }, +]; From a66f55a3907c6bf969a2b1751624cf81ab353583 Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Tue, 29 Oct 2024 01:16:25 -0400 Subject: [PATCH 11/12] Added notification badge for unread notifications, made common mock data file --- src/layout/Header.tsx | 155 ++++++++---------- src/pages/Home.tsx | 52 +++++- .../Notifications/NotificationDelete.tsx | 40 +---- .../Notifications/NotificationEditor.tsx | 71 +++----- src/pages/Notifications/mock_data.tsx | 65 ++++---- src/utils/interfaces.ts | 1 + 6 files changed, 176 insertions(+), 208 deletions(-) diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 2942349a..053565f8 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -1,11 +1,12 @@ import React, { Fragment, useState, useEffect } from "react"; -import { Button, Container, Nav, Navbar, NavDropdown } from "react-bootstrap"; +import { Button, Container, Nav, Navbar, NavDropdown, Badge } from "react-bootstrap"; import { useSelector } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; import { RootState } from "../store/store"; import { ROLE } from "../utils/interfaces"; import { hasAllPrivilegesOf } from "../utils/util"; import detective from "../assets/detective.png"; +import { mockNotifications } from "../pages/Notifications/mock_data"; // Import the mock notifications /** * @author Ankur Mundra on May, 2023 @@ -19,53 +20,56 @@ const Header: React.FC = () => { const navigate = useNavigate(); const [visible, setVisible] = useState(true); + const [unreadCount, setUnreadCount] = useState(0); - const CustomBtn = () => { - return ( + // Calculate unread notifications on component load + useEffect(() => { + const countUnread = mockNotifications.filter( + (notification) => notification.isUnread && notification.isActive + ).length; + setUnreadCount(countUnread); + }, []); + + const CustomBtn = () => ( +
-
+
Anonymized View
+ -
+ x +
- ); - }; - - // useEffect(() => { - // console.log(visible, 'Changed'); - // }, [visible]); +
+ ); return ( @@ -116,55 +120,34 @@ const Header: React.FC = () => { )} {hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( - - Users - - - Courses - - - Notifications - - - Institutions - - - Assignments - - - Questionnaire - - - Edit Questionnaire - + Users + Courses + Notifications + Institutions + Assignments + Questionnaire + Edit Questionnaire - - Impersonate User - - - Anonymized View - + Impersonate User + Anonymized View )} - - Assignments - - - Profile - - - Student View - - - Grades View - + Assignments + Profile + Student View + Grades View + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( - - My Notifications - )} - setVisible(!visible)}> - Anonymized View - + + My Notifications{" "} + {unreadCount > 0 && ( + + {unreadCount} + + )} + + )} + setVisible(!visible)}>Anonymized View {visible ? ( @@ -184,9 +167,7 @@ const Header: React.FC = () => { )} - + )} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index c696c49e..352b1094 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,11 +1,53 @@ -/** - * @author Ankur Mundra on May, 2023 - */ +import React, { useEffect, useState } from "react"; +import { Alert, Container } from "react-bootstrap"; + +// Mock data for notifications +const mockNotifications = [ + { + id: "1", + subject: "New Assignment Posted", + description: "Please check the new assignment in your course.", + isUnread: true, + isActive: true, + }, + { + id: "2", + subject: "Exam Scheduled", + description: "An exam has been scheduled for next week.", + isUnread: false, + isActive: true, + }, + { + id: "3", + subject: "Class Canceled", + description: "Class is canceled tomorrow.", + isUnread: true, + isActive: true, + }, +]; + const Home = () => { + // State to manage if there are unread notifications + const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); + + useEffect(() => { + // Check for any unread and active notifications + const unreadNotifications = mockNotifications.some( + (notification) => notification.isUnread && notification.isActive + ); + setHasUnreadNotifications(unreadNotifications); + }, []); + return ( -
+

Welcome Home!

-
+ {hasUnreadNotifications && ( + + You have new notifications. Please check your notifications page. + + )} + + ); }; diff --git a/src/pages/Notifications/NotificationDelete.tsx b/src/pages/Notifications/NotificationDelete.tsx index 0ae7b06d..1341552e 100644 --- a/src/pages/Notifications/NotificationDelete.tsx +++ b/src/pages/Notifications/NotificationDelete.tsx @@ -2,37 +2,8 @@ import React, { useEffect, useState } from "react"; import { Button, Modal } from "react-bootstrap"; import { useDispatch } from "react-redux"; import { alertActions } from "store/slices/alertSlice"; -import { HttpMethod } from "utils/httpMethods"; -import useAPI from "../../hooks/useAPI"; import { INotification } from "../../utils/interfaces"; - -// Mock notifications to handle deletion for now -let mockNotifications: INotification[] = [ - { - id: "1", - course: "CS101", - subject: "New Homework", - description: "Homework 1 due next week", - expirationDate: "2024-10-31", - isActive: true, - }, - { - id: "2", - course: "CS102", - subject: "Class Canceled", - description: "No class tomorrow", - expirationDate: "2024-11-01", - isActive: false, - }, - { - id: "3", - course: "CS103", - subject: "Exam scheduled", - description: "Please prepare for the exam", - expirationDate: "2024-11-05", - isActive: true, - }, -]; +import { mockNotifications } from "./mock_data"; // Import the centralized mock data interface IDeleteNotification { notificationData: INotification; @@ -44,10 +15,15 @@ const DeleteNotification: React.FC = ({ notificationData, o const dispatch = useDispatch(); const deleteHandler = () => { - // Handle deletion locally by filtering out the notification - mockNotifications = mockNotifications.filter( + // Simulate deletion by filtering out the selected notification + const updatedNotifications = mockNotifications.filter( (notif) => notif.id !== notificationData.id ); + + // Update the mockNotifications in the data file (replaceable by API call in production) + mockNotifications.splice(0, mockNotifications.length, ...updatedNotifications); + + // Show success message setShow(false); dispatch( alertActions.showAlert({ diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx index 649372ea..d951ab28 100644 --- a/src/pages/Notifications/NotificationEditor.tsx +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -1,47 +1,13 @@ import React, { useEffect, useState } from "react"; import { Form, Formik, FormikHelpers } from "formik"; -import { Button, Modal } from "react-bootstrap"; +import { Button, Modal, FormGroup, FormLabel, FormControl } from "react-bootstrap"; import FormInput from "components/Form/FormInput"; import { alertActions } from "store/slices/alertSlice"; -import { useDispatch, useSelector } from "react-redux"; -import { useLoaderData, useNavigate, useParams } from "react-router-dom"; -import { HttpMethod } from "utils/httpMethods"; -import useAPI from "hooks/useAPI"; +import { useDispatch } from "react-redux"; +import { useNavigate, useParams } from "react-router-dom"; import * as Yup from "yup"; -import axiosClient from "../../utils/axios_client"; import { INotification } from "../../utils/interfaces"; -import { RootState } from "../../store/store"; -import { FormGroup, FormLabel, FormControl } from "react-bootstrap"; - -// Mock notification data for edit -const mockNotifications: INotification[] = [ - { - id: "1", - course: "CS101", - subject: "New Homework", - description: "Homework 1 due next week", - expirationDate: "2024-10-31", - isActive: true, - }, - { - id: "2", - course: "CS102", - subject: "Class Canceled", - description: "No class tomorrow", - expirationDate: "2024-11-01", - isActive: false, - }, - { - id: "3", - course: "CS103", - subject: "Exam scheduled", - description: "Please prepare for the exam", - expirationDate: "2024-11-05", - isActive: true, - }, -]; - -const mockAssignedCourses = ["CS101", "CS102", "CS103"]; +import { mockNotifications, mockAssignedCourses } from "./mock_data"; // Import centralized mock data const initialValues: INotification = { id: "", @@ -50,6 +16,7 @@ const initialValues: INotification = { description: "", expirationDate: "", isActive: false, + isUnread: true, }; const validationSchema = Yup.object({ @@ -58,16 +25,14 @@ const validationSchema = Yup.object({ }); const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) => { - const dispatch = useDispatch(); const navigate = useNavigate(); - const { id } = useParams<{ id: string }>(); // Get the ID from the URL parameters + const { id } = useParams<{ id: string }>(); // Get the ID from URL parameters const [notification, setNotification] = useState(initialValues); - useEffect(() => { if (mode === "update" && id) { - // Simulate loading data for the edit mode from mock notifications + // Load data from centralized mockNotifications const notificationToEdit = mockNotifications.find((notif) => notif.id === id); if (notificationToEdit) { setNotification(notificationToEdit); @@ -76,16 +41,26 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = variant: "danger", message: "Notification not found!", })); - navigate("/notifications"); // Navigate back if the notification doesn't exist + navigate("/notifications"); // Navigate back if notification doesn't exist } } }, [id, mode, dispatch, navigate]); - const onSubmit = (values: INotification, submitProps: FormikHelpers) => { - - // Simulate submission logic here (no backend call) + // Handle submission logic (no backend call for mock data) const message = mode === "update" ? "Notification updated successfully!" : "Notification created successfully!"; + + // Update mock data directly for edit mode + if (mode === "update" && id) { + const index = mockNotifications.findIndex((notif) => notif.id === id); + if (index !== -1) { + mockNotifications[index] = values; + } + } else { + // For "create" mode, add a new notification + mockNotifications.push({ ...values, id: (mockNotifications.length + 1).toString() }); + } + dispatch(alertActions.showAlert({ variant: "success", message: message, @@ -144,8 +119,4 @@ const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) = ); }; - - - - export default NotificationEditor; diff --git a/src/pages/Notifications/mock_data.tsx b/src/pages/Notifications/mock_data.tsx index 24b0cbd6..f8cdb6c0 100644 --- a/src/pages/Notifications/mock_data.tsx +++ b/src/pages/Notifications/mock_data.tsx @@ -1,38 +1,35 @@ - +// mockData.ts import { INotification } from "../../utils/interfaces"; -// Shared dummy data for notifications export const mockNotifications: INotification[] = [ - { - id: "1", - course: "CS101", - subject: "Assignment Due", - description: "Assignment 1 is due on Friday.", - expirationDate: "2024-10-20", - isActive: true, - }, - { - id: "2", - course: "CS102", - subject: "Class Canceled", - description: "Class is canceled tomorrow.", - expirationDate: "2024-10-21", - isActive: true, - }, - { - id: "3", - course: "CS103", - subject: "New Quiz", - description: "Quiz 1 scheduled for next class", - expirationDate: "2024-10-25", - isActive: true, - }, - { - id: "4", - course: "CS104", - subject: "Project Deadline", - description: "Project 2 deadline extended", - expirationDate: "2024-11-02", - isActive: true, - }, + { + id: "1", + course: "CS101", + subject: "New Homework", + description: "Homework 1 due next week", + expirationDate: "2024-10-31", + isActive: true, + isUnread: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "No class tomorrow", + expirationDate: "2024-11-01", + isActive: false, + isUnread: false, + }, + { + id: "3", + course: "CS103", + subject: "Exam scheduled", + description: "Please prepare for the exam", + expirationDate: "2024-11-05", + isActive: true, + isUnread: true, + }, ]; + +// Define mock courses for assigned courses +export const mockAssignedCourses = ["CS101", "CS102", "CS103"]; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index bf402b82..93f26164 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -24,6 +24,7 @@ export interface INotification { description: string; expirationDate: string; isActive: boolean; + isUnread: boolean; } export interface IInstructor { From 074d60e67718f38350f49f6383dbc138569a580f Mon Sep 17 00:00:00 2001 From: Vaibhavi Shetty Date: Tue, 29 Oct 2024 16:59:01 -0400 Subject: [PATCH 12/12] Enhance the UI for Institution and Notifications to make the table look presentable and well aligned --- src/pages/Institutions/InstitutionDelete.tsx | 4 ---- src/pages/Institutions/InstitutionEditor.tsx | 3 --- src/pages/Institutions/Institutions.tsx | 13 ++++++------- src/pages/Institutions/institutionColumns.tsx | 7 +++---- src/pages/Notifications/NotificationColumns.tsx | 8 ++++++++ src/pages/Notifications/NotificationDelete.tsx | 4 ++++ src/pages/Notifications/NotificationEditor.tsx | 4 ++++ src/pages/Notifications/Notifications.tsx | 9 +++++++-- src/pages/Notifications/ViewNotifications.tsx | 8 +++++++- 9 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/pages/Institutions/InstitutionDelete.tsx b/src/pages/Institutions/InstitutionDelete.tsx index ef567279..9fae0ad8 100644 --- a/src/pages/Institutions/InstitutionDelete.tsx +++ b/src/pages/Institutions/InstitutionDelete.tsx @@ -6,10 +6,6 @@ import {HttpMethod} from "utils/httpMethods"; import useAPI from "../../hooks/useAPI"; import {IInstitution} from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ - interface IDeleteInstitution { institutionData: IInstitution; onClose: () => void; diff --git a/src/pages/Institutions/InstitutionEditor.tsx b/src/pages/Institutions/InstitutionEditor.tsx index 6fa447c5..a9112458 100644 --- a/src/pages/Institutions/InstitutionEditor.tsx +++ b/src/pages/Institutions/InstitutionEditor.tsx @@ -11,9 +11,6 @@ import * as Yup from "yup"; import axiosClient from "../../utils/axios_client"; import { IEditor, IInstitution } from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ const initialValues: IInstitution = { name: "", diff --git a/src/pages/Institutions/Institutions.tsx b/src/pages/Institutions/Institutions.tsx index 2a15e123..c432ac1f 100644 --- a/src/pages/Institutions/Institutions.tsx +++ b/src/pages/Institutions/Institutions.tsx @@ -12,10 +12,6 @@ import { useSelector } from "react-redux"; import { RootState } from "store/store"; import { hasAllPrivilegesOf } from "utils/util"; -/** - * @author Ankur Mundra on June, 2023 - */ - const Institutions = () => { const navigate = useNavigate(); const institutions: any = useLoaderData(); @@ -63,10 +59,11 @@ const Institutions = () => {
+ {/*Added authetication for manage institution to be accessed by roles higher than instructor */} {hasAllPrivilegesOf(auth.user.role, ROLE.INSTRUCTOR) &&( <> -
+ @@ -78,14 +75,16 @@ const Institutions = () => { /> )} - + +
+ ) } diff --git a/src/pages/Institutions/institutionColumns.tsx b/src/pages/Institutions/institutionColumns.tsx index d15f9755..5d1e377a 100644 --- a/src/pages/Institutions/institutionColumns.tsx +++ b/src/pages/Institutions/institutionColumns.tsx @@ -4,10 +4,6 @@ import {BsPencilFill as Edit} from "react-icons/bs"; import {Button} from "react-bootstrap"; import {IInstitution} from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ - type Fn = (row: Row) => void; const columnHelper = createColumnHelper(); export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ @@ -15,11 +11,13 @@ export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ header: "Id", enableSorting: false, enableColumnFilter: false, + minSize: 100, }), columnHelper.accessor("name", { header: "Name", enableSorting: true, + minSize: 300, }), columnHelper.display({ @@ -40,5 +38,6 @@ export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ ), + minSize: 300, }), ]; diff --git a/src/pages/Notifications/NotificationColumns.tsx b/src/pages/Notifications/NotificationColumns.tsx index 5af73a7e..25de1c21 100644 --- a/src/pages/Notifications/NotificationColumns.tsx +++ b/src/pages/Notifications/NotificationColumns.tsx @@ -6,6 +6,10 @@ import { Button, Form } from "react-bootstrap"; import { INotification } from "../../utils/interfaces"; import React from "react"; +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + type Fn = (row: Row) => void; type ToggleFn = (row: Row, isActive: boolean) => void; const columnHelper = createColumnHelper(); @@ -21,21 +25,25 @@ export const notificationColumns = ( header: "Id", enableSorting: false, enableColumnFilter: false, + minSize: 100, }), columnHelper.accessor("course", { header: "Course", enableSorting: true, + minSize: 300, }), columnHelper.accessor("subject", { header: "Subject", enableSorting: true, + minSize: 300, }), columnHelper.accessor("description", { header: "Description", enableSorting: true, + minSize: 1000, }), columnHelper.accessor("expirationDate", { diff --git a/src/pages/Notifications/NotificationDelete.tsx b/src/pages/Notifications/NotificationDelete.tsx index 1341552e..29724257 100644 --- a/src/pages/Notifications/NotificationDelete.tsx +++ b/src/pages/Notifications/NotificationDelete.tsx @@ -5,6 +5,10 @@ import { alertActions } from "store/slices/alertSlice"; import { INotification } from "../../utils/interfaces"; import { mockNotifications } from "./mock_data"; // Import the centralized mock data +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + interface IDeleteNotification { notificationData: INotification; onClose: () => void; diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx index d951ab28..132b4990 100644 --- a/src/pages/Notifications/NotificationEditor.tsx +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -9,6 +9,10 @@ import * as Yup from "yup"; import { INotification } from "../../utils/interfaces"; import { mockNotifications, mockAssignedCourses } from "./mock_data"; // Import centralized mock data +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + const initialValues: INotification = { id: "", course: "", diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index 0e05c618..941efbe4 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -13,6 +13,9 @@ import { RootState } from "../../store/store"; import { hasAllPrivilegesOf } from "utils/util"; import React from "react"; import { Row as TRow } from "@tanstack/react-table"; +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ const mockAssignedCourses = ["CS101", "CS102", "CS103"]; @@ -90,14 +93,16 @@ const Notifications = () => { /> )} - + +
+ )} diff --git a/src/pages/Notifications/ViewNotifications.tsx b/src/pages/Notifications/ViewNotifications.tsx index eab13773..8f6fd0e9 100644 --- a/src/pages/Notifications/ViewNotifications.tsx +++ b/src/pages/Notifications/ViewNotifications.tsx @@ -6,6 +6,10 @@ import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColum import { mockNotifications } from "./mock_data"; import { INotification } from "../../utils/interfaces"; +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + const enrolledCourses = ["CS101", "CS103"]; const ViewNotifications = () => { @@ -25,7 +29,8 @@ const ViewNotifications = () => {
- + +
{ columnVisibility={{ id: false, isActive: false }} // Hide isActive column tableSize={{ span: 10, offset: 2 }} /> + );