From 225e0505937c074682437e0fac61dc10f83150fd Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 18:42:27 -0500 Subject: [PATCH 01/25] Adding user search --- frontend/src/external/bcanSatchel/actions.ts | 5 ++ frontend/src/external/bcanSatchel/mutators.ts | 9 ++- frontend/src/external/bcanSatchel/store.ts | 2 + frontend/src/main-page/MainPage.tsx | 6 +- frontend/src/main-page/users/UserSearch.tsx | 64 ++++++++++++++++ frontend/src/main-page/users/UsersPage.tsx | 70 +++++++++++++++++ .../src/main-page/users/processUserData.ts | 75 +++++++++++++++++++ 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 frontend/src/main-page/users/UserSearch.tsx create mode 100644 frontend/src/main-page/users/UsersPage.tsx create mode 100644 frontend/src/main-page/users/processUserData.ts diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index 721f718..0d51083 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -75,6 +75,11 @@ export const updateSearchQuery = action( (searchQuery: string) => ({ searchQuery }) ); +export const updateUserQuery = action( + "updateUserQuery", + (userQuery: string) => ({ userQuery }) +); + export const setNotifications = action( 'setNotifications', (notifications: Notification[]) => ({notifications}) diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index c65065d..b88a49f 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -9,7 +9,8 @@ import { updateSearchQuery, updateYearFilter, setNotifications, - updateSort + updateSort, + updateUserQuery } from './actions'; import { getAppStore, persistToSessionStorage } from './store'; import { setActiveUsers, setInactiveUsers } from './actions'; @@ -117,3 +118,9 @@ mutator(updateSort, (actionMessage) => { const store = getAppStore(); store.sort = actionMessage.sort; }) + +mutator(updateUserQuery, (actionMessage) => { + const store = getAppStore(); + store.userQuery = actionMessage.userQuery; + console.log('Updated userQuery:', store.userQuery); +}) \ No newline at end of file diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index 4f47952..e603c48 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -19,6 +19,7 @@ export interface AppState { inactiveUsers: User[] | []; sort: {header: keyof Grant, asc: boolean} | null; notifications: Notification[]; + userQuery: string; } // Define initial state @@ -36,6 +37,7 @@ const initialState: AppState = { inactiveUsers: [], notifications: [], sort: null, + userQuery: '', }; /** diff --git a/frontend/src/main-page/MainPage.tsx b/frontend/src/main-page/MainPage.tsx index 6e05011..7b78635 100644 --- a/frontend/src/main-page/MainPage.tsx +++ b/frontend/src/main-page/MainPage.tsx @@ -2,11 +2,11 @@ import { Routes, Route } from "react-router-dom"; import Dashboard from "./dashboard/Dashboard"; import GrantPage from "./grants/GrantPage"; import NavBar from "./navbar/NavBar"; -import Users from "./users/Users"; import RestrictedPage from "./restricted/RestrictedPage"; import CashFlowPage from "./cash-flow/CashFlowPage"; import Settings from "./settings/Settings"; -import Footer from "./Footer"; +import Footer from "../main-page/Footer"; +import UsersPage from "./users/UsersPage"; function MainPage() { return ( @@ -26,7 +26,7 @@ function MainPage() { path="/my-grants" element={} /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/main-page/users/UserSearch.tsx b/frontend/src/main-page/users/UserSearch.tsx new file mode 100644 index 0000000..ea7bed6 --- /dev/null +++ b/frontend/src/main-page/users/UserSearch.tsx @@ -0,0 +1,64 @@ +import { IoMdSearch } from "react-icons/io"; +import { useState } from "react"; +import Fuse from "fuse.js"; +import { Input } from "@chakra-ui/react"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { updateUserQuery } from "../../external/bcanSatchel/actions"; +import { User } from "../../../../middle-layer/types/User"; + +function UserSearch() { + const [userInput, setUserInput] = useState(getAppStore().userQuery || ""); + // @ts-ignore + const [users, _setUsers] = useState([]); + + const handleInputChange = (e: React.ChangeEvent) => { + setUserInput(e.target.value); + performSearch(e.target.value); + }; + + const performSearch = (query: string) => { + if (!query) { + updateUserQuery(""); + return; + } + const fuse = new Fuse(users, { + keys: ["firstName", "lastName", "email"], + threshold: 0.3, + }); + // const results = + fuse.search(query).map((res) => res.item); + updateUserQuery(query); + }; + + return ( +
+ {/* Absolutely-positioned icon */} + + { + if (e.key === "Enter") { + e.preventDefault(); + } + }} + /> +
+ ); +} + +export default UserSearch; diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx new file mode 100644 index 0000000..f250a45 --- /dev/null +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { useAuthContext } from "../../context/auth/authContext"; +import { observer } from "mobx-react-lite"; +import { UserStatus } from "../../../../middle-layer/types/UserStatus.ts"; +import { Navigate } from "react-router-dom"; +import Button from "../../components/Button.tsx"; +import UserSearch from "./UserSearch.tsx"; +import { ProcessUserData } from "./processUserData.ts"; + +const UsersPage = observer(() => { + const [showAll, setShowAll] = useState(true); + + const { activeUsers, inactiveUsers } = ProcessUserData(); + const ITEMS_PER_PAGE = 8; + + const { user } = useAuthContext(); + + const [currentPage, setCurrentPage] = useState(1); + + const filteredUsers = showAll ? activeUsers : inactiveUsers; + + const numInactiveUsers = inactiveUsers.length; + const numUsers = filteredUsers.length; + const pageStartIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const pageEndIndex = + pageStartIndex + ITEMS_PER_PAGE > numUsers + ? numUsers + : pageStartIndex + ITEMS_PER_PAGE; + const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); + + return user ? ( + user?.position !== UserStatus.Inactive ? ( +
+ +
+
+
+ +
+
+ {currentPageUsers.map((user) => ( +
{user.email}
+ ))} +
+
+ + ) : ( + + ) + ) : ( + + ); +}); + +export default UsersPage; diff --git a/frontend/src/main-page/users/processUserData.ts b/frontend/src/main-page/users/processUserData.ts new file mode 100644 index 0000000..d983821 --- /dev/null +++ b/frontend/src/main-page/users/processUserData.ts @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import { User } from "../../../../middle-layer/types/User.ts"; +import { api } from "../../api.ts"; +import { getAppStore } from "../../external/bcanSatchel/store.ts"; + +// fetch grants +export const fetchActiveUsers = async (): Promise => { + try { + const response = await api("/user/active", { + method: "GET", + }); + + if (!response.ok) { + throw new Error(`HTTP Error, Status: ${response.status}`); + } + + const activeUsers = await response.json(); + return activeUsers as User[]; + } catch (error) { + console.error("Error fetching active users:", error); + return []; // Return empty array on error + } +}; + +export const fetchInactiveUsers = async () => { + try { + const response = await api("/user/inactive", { method: "GET" }); + if (!response.ok) { + throw new Error(`HTTP Error, Status: ${response.status}`); + } + const inactiveUsers = await response.json(); + return inactiveUsers as User[]; + } catch (error) { + console.error("Error fetching active users:", error); + } +}; + +const searchFilter = (searchQuery: string) => (user: User) => { + if (!searchQuery.trim()) return true; + + const query = searchQuery.toLowerCase(); + const firstName = user.firstName?.toLowerCase() || ""; + const lastName = user.lastName?.toLowerCase() || ""; + const email = user.email?.toLowerCase() || ""; + + return ( + firstName.includes(query) || + lastName.includes(query) || + email.includes(query) + ); +}; + +const filterUsers = (users: User[], predicates: ((user: User) => boolean)[]) => + users.filter((user) => predicates.every((fn) => fn(user))); + +// contains callbacks for sorting and filtering grants +// stores state for list of grants/filter +export const ProcessUserData = () => { + const { activeUsers, inactiveUsers, userQuery } = getAppStore(); + + // fetch grants on mount if empty + useEffect(() => { + if (activeUsers.length === 0) fetchActiveUsers(); + if (inactiveUsers.length === 0) fetchInactiveUsers(); + }, [activeUsers.length, inactiveUsers.length]); + + // compute filtered grants dynamically — no useState needed + const activeFiltered = filterUsers(activeUsers, [searchFilter(userQuery)]); + + const inactiveFiltered = filterUsers(inactiveUsers, [ + searchFilter(userQuery), + ]); + + return { activeUsers: activeFiltered, inactiveUsers: inactiveFiltered }; +}; From df67122c281fcec42778933ccca99dff40083c4e Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 23:06:48 -0500 Subject: [PATCH 02/25] adding the row headers and rows --- frontend/src/components/Button.tsx | 6 +- frontend/src/custom/ActionConfirmation.tsx | 2 +- frontend/src/external/bcanSatchel/actions.ts | 5 + frontend/src/external/bcanSatchel/mutators.ts | 10 +- frontend/src/external/bcanSatchel/store.ts | 2 + .../src/main-page/users/ApprovedUserCard.tsx | 179 ------------------ .../{PendingUserCard.tsx => UserApprove.tsx} | 71 +++---- frontend/src/main-page/users/UserMenu.tsx | 174 +++++++++++++++++ .../src/main-page/users/UserPositionCard.tsx | 6 +- frontend/src/main-page/users/UserRow.tsx | 33 ++++ .../src/main-page/users/UserRowHeader.tsx | 30 +++ .../main-page/users/UserRowHeaderButton.tsx | 31 +++ frontend/src/main-page/users/Users.tsx | 4 +- frontend/src/main-page/users/UsersPage.tsx | 90 +++++++-- .../src/main-page/users/processUserData.ts | 29 ++- frontend/tailwind.config.ts | 2 + 16 files changed, 425 insertions(+), 249 deletions(-) delete mode 100644 frontend/src/main-page/users/ApprovedUserCard.tsx rename frontend/src/main-page/users/{PendingUserCard.tsx => UserApprove.tsx} (58%) create mode 100644 frontend/src/main-page/users/UserMenu.tsx create mode 100644 frontend/src/main-page/users/UserRow.tsx create mode 100644 frontend/src/main-page/users/UserRowHeader.tsx create mode 100644 frontend/src/main-page/users/UserRowHeaderButton.tsx diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index f18332c..d14e8b7 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -9,18 +9,20 @@ type ButtonProps = { logoPosition?: 'left' | 'right'; disabled?: boolean; type?: "button" | "submit" | "reset"; + alignment?: "left" | "center" | "right"; } // Button component where you can pass in text, onClick handler, optional className // for styling, and an optional logo with its position. //Styling is default, but can be overridden by passing in a className prop -export default function Button({ text, onClick, className, logo, logoPosition, disabled, type }: ButtonProps) { +export default function Button({ text, onClick, className, logo, logoPosition, disabled, type, alignment }: ButtonProps) { return ( - - - - - - - - - - - ); -}; - -export default ApprovedUserCard; diff --git a/frontend/src/main-page/users/PendingUserCard.tsx b/frontend/src/main-page/users/UserApprove.tsx similarity index 58% rename from frontend/src/main-page/users/PendingUserCard.tsx rename to frontend/src/main-page/users/UserApprove.tsx index fa740e4..786de5d 100644 --- a/frontend/src/main-page/users/PendingUserCard.tsx +++ b/frontend/src/main-page/users/UserApprove.tsx @@ -1,8 +1,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import UserPositionCard from "./UserPositionCard"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; -import { api } from "../../api" -import { UserStatus } from "../../../../middle-layer/types/UserStatus"; +import { api } from "../../api"; import { getAppStore } from "../../external/bcanSatchel/store"; import { User } from "../../../../middle-layer/types/User"; import { toJS } from "mobx"; @@ -13,19 +11,11 @@ import { useState } from "react"; const store = getAppStore(); -interface PendingUserCardProps { - userId: string; - email: string; - position: UserStatus; +interface UserApproveProps { + user: User; } - -const PendingUserCard = ({ - userId, - email, - position, -}: PendingUserCardProps) => { - +const UserApprove = ({ user }: UserApproveProps) => { const [isLoading, setIsLoading] = useState(false); const approveUser = async () => { @@ -36,17 +26,17 @@ const PendingUserCard = ({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: { - email, - position + email: user.email, + position: user.position, } as User, groupName: "Employee", requestedBy: toJS(store.user) as User, }), }); if (response.ok) { - alert(`User ${userId} has been approved successfully`); + alert(`User ${user.email} has been approved successfully`); const body = await response.json(); - moveUserToActive(body as User) + moveUserToActive(body as User); } else { alert("Failed to approve user"); } @@ -66,16 +56,16 @@ const PendingUserCard = ({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: { - email: email, - position, + email: user.email, + position: user.position, } as User, requestedBy: toJS(store.user) as User, }), }); if (response.ok) { - alert(`User ${name} has been deleted successfully`); + alert(`User ${user.email} has been deleted successfully`); const body = await response.json(); - removeUser(body) + removeUser(body); } else { alert("Failed to reject user"); } @@ -88,28 +78,23 @@ const PendingUserCard = ({ }; return ( -
-

{userId}

-

{email}

-
- -
-
- - -
+
+ +
); }; -export default PendingUserCard; +export default UserApprove; diff --git a/frontend/src/main-page/users/UserMenu.tsx b/frontend/src/main-page/users/UserMenu.tsx new file mode 100644 index 0000000..0192e55 --- /dev/null +++ b/frontend/src/main-page/users/UserMenu.tsx @@ -0,0 +1,174 @@ +import { Menu } from "@chakra-ui/react"; +import { UserStatus } from "../../../../middle-layer/types/UserStatus"; +import { faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import ActionConfirmation from "../../custom/ActionConfirmation"; +import { useState } from "react"; +import { api } from "../../api"; +import { User } from "../../../../middle-layer/types/User"; +import { toJS } from "mobx"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { setActiveUsers } from "../../external/bcanSatchel/actions"; +import Button from "../../components/Button"; +import { FaEllipsis } from "react-icons/fa6"; + +interface UserMenuProps { + user: User; +} + +const UserMenu = ({ user }: UserMenuProps) => { + const store = getAppStore(); + const [isChangeGroupModalOpen, setIsChangeGroupModalOpen] = useState(false); + const [isDeleteUserModalOpen, setIsDeleteUserModalOpen] = useState(false); + + const changeUserGroup = async () => { + console.log( + `Changing user ${user.email} to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }...`, + ); + + try { + const response = await api("/user/change-role", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user: { + email: user.email, + position: user.position, + } as User, + groupName: + user.position === UserStatus.Admin + ? UserStatus.Employee + : UserStatus.Admin, + requestedBy: toJS(store.user) as User, + }), + }); + + if (response.ok) { + console.log( + `User ${user.email} successfully changed to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }`, + ); + alert( + `User ${user.email} successfully changed to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }`, + ); + const updatedUser = await response.json(); + setActiveUsers([ + ...store.activeUsers.filter((u) => u.email !== user.email), + updatedUser as User, + ]); + + setIsChangeGroupModalOpen(false); + } else { + const errorBody = await response.json(); + console.error("Error: ", errorBody); + } + } catch (error) { + console.error("Error changing user group: ", error); + } + }; + + const deleteUser = async () => { + try { + const response = await api("user/delete-user", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user: { + email: user.email, + position: user.position, + } as User, + requestedBy: toJS(store.user) as User, + }), + }); + + if (response.ok) { + console.log(`User ${user.email} has been deleted successfully`); + alert(`User ${user.email} has been deleted successfully`); + setActiveUsers(store.activeUsers.filter((u) => u.email !== user.email)); + } else { + const errorBody = await response.json(); + console.error("Error: ", errorBody); + alert("Failed to delete user"); + } + setIsDeleteUserModalOpen(false); + } catch (error) { + console.error("Error deleting user:", error); + alert("Error deleting user"); + } + }; + + return ( +
+ setIsChangeGroupModalOpen(false)} + onConfirmDelete={changeUserGroup} + title={`Change User to ${ + user.position === UserStatus.Admin ? "Employee" : "Admin" + }`} + subtitle="Are you sure you want to change to" + boldSubtitle={user.position === UserStatus.Admin ? "employee" : "admin"} + warningMessage={`By changing to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }, they will ${ + user.position === UserStatus.Admin + ? "lose access to sensitive data." + : "gain access to admin pages." + }`} + /> + setIsDeleteUserModalOpen(false)} + onConfirmDelete={deleteUser} + title="Delete User" + subtitle="Are you sure you want to delete" + boldSubtitle={user.email} + warningMessage="By deleting this user, they won't be available in the system anymore." + /> +
+ + +
+ +
+
+ + +
+
+
+
+
+
+
+ ); +}; + +export default UserMenu; diff --git a/frontend/src/main-page/users/UserPositionCard.tsx b/frontend/src/main-page/users/UserPositionCard.tsx index 4cc2988..fa7779d 100644 --- a/frontend/src/main-page/users/UserPositionCard.tsx +++ b/frontend/src/main-page/users/UserPositionCard.tsx @@ -14,13 +14,13 @@ const UserPositionCard = ({ position }: UserPositionCardProps) => { return "bg-yellow-light border-yellow-dark text-yellow-dark"; case UserStatus.Inactive: default: - return "bg-grey-400 border-gray text-gray"; + return "bg-grey-400 border-grey text-grey-700"; } }, [position]); return ( -
-

{position}

+
+

{position}

); }; diff --git a/frontend/src/main-page/users/UserRow.tsx b/frontend/src/main-page/users/UserRow.tsx new file mode 100644 index 0000000..c218f4a --- /dev/null +++ b/frontend/src/main-page/users/UserRow.tsx @@ -0,0 +1,33 @@ +import { User } from "../../../../middle-layer/types/User"; +import UserPositionCard from "./UserPositionCard"; +import logo from "../../images/logo.svg"; + +// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway +interface UserRowProps { + user: User; + action: React.ReactNode; +} +const UserRow = ({ user, action }: UserRowProps) => { + return ( +
+
+ Profile + {user.firstName} {user.lastName} +
+
{user.email}
+
+ +
+
{action}
+
+ ); +}; + +export default UserRow; diff --git a/frontend/src/main-page/users/UserRowHeader.tsx b/frontend/src/main-page/users/UserRowHeader.tsx new file mode 100644 index 0000000..82b27c8 --- /dev/null +++ b/frontend/src/main-page/users/UserRowHeader.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; +import { User } from "../../../../middle-layer/types/User"; +import { updateUserSort } from "../../external/bcanSatchel/actions"; +import UserRowHeaderButton from "./UserRowHeaderButton"; + +// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway + +const UserRowHeader = () => { + const [labels, setLabels] = useState({ + header: "lastName", + sort: "desc", + } as { header: keyof User; sort: "asc" | "desc" | "none" }); + + function buttonHandler(header: keyof User) { + const isAsc = labels.header == header ? (labels.sort == "asc" ? "desc" : labels.sort == "desc" ? "asc" : "none") : "desc"; + updateUserSort({ header, sort: isAsc }); + setLabels({ header: header, sort: isAsc }); + } + + return ( +
+ buttonHandler("lastName")} /> + buttonHandler("email")} /> + buttonHandler("position")} /> +
{"Action"}
+
+ ); +}; + +export default UserRowHeader; diff --git a/frontend/src/main-page/users/UserRowHeaderButton.tsx b/frontend/src/main-page/users/UserRowHeaderButton.tsx new file mode 100644 index 0000000..61620b2 --- /dev/null +++ b/frontend/src/main-page/users/UserRowHeaderButton.tsx @@ -0,0 +1,31 @@ +import Button from "../../components/Button"; +import { + faSort, + faSortUp, + faSortDown, +} from "@fortawesome/free-solid-svg-icons"; + +// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway + +interface UserRowHeaderButtonProps { + header: string; + sort: "asc" | "desc" | "none"; + onClick: () => void; +} + +const UserRowHeaderButton = (props: UserRowHeaderButtonProps) => { + + + return ( +
-
-
+
+
+ {currentPageUsers.map((user) => ( -
{user.email}
+
+ + ) : ( + + ) + } + /> +
))}
+ {/* { + setCurrentPage(e.page); + }} + > + + + + + + + + {({ pages }) => + pages.map((page, index) => + page.type === "page" ? ( + setCurrentPage(page.value)} + aria-label={`Go to page ${page.value}`} + > + {page.value} + + ) : ( + "..." + ), + ) + } + + + + + + + + */}
) : ( diff --git a/frontend/src/main-page/users/processUserData.ts b/frontend/src/main-page/users/processUserData.ts index d983821..32a61f3 100644 --- a/frontend/src/main-page/users/processUserData.ts +++ b/frontend/src/main-page/users/processUserData.ts @@ -53,10 +53,27 @@ const searchFilter = (searchQuery: string) => (user: User) => { const filterUsers = (users: User[], predicates: ((user: User) => boolean)[]) => users.filter((user) => predicates.every((fn) => fn(user))); +const sortUsers = (users: User[], header: keyof User, sort: "asc" | "desc" | "none") => + [...users].sort((a: User, b: User) => { + const direction = sort === "asc" ? -1 : 1; + + const aValue = a[header]; + const bValue = b[header]; + + if (aValue == null) return -1 * direction; + if (bValue == null) return 1 * direction; + + if (aValue > bValue) return 1 * direction; + if (aValue < bValue) return -1 * direction; + + return 0; + }) + + // contains callbacks for sorting and filtering grants // stores state for list of grants/filter export const ProcessUserData = () => { - const { activeUsers, inactiveUsers, userQuery } = getAppStore(); + const { activeUsers, inactiveUsers, userQuery, userSort } = getAppStore(); // fetch grants on mount if empty useEffect(() => { @@ -71,5 +88,13 @@ export const ProcessUserData = () => { searchFilter(userQuery), ]); - return { activeUsers: activeFiltered, inactiveUsers: inactiveFiltered }; + const sortedActive = userSort && userSort.sort !== "none" + ? sortUsers(activeFiltered, userSort.header, userSort.sort) + : activeFiltered; + + const sortedInactive = userSort && userSort.sort !== "none" + ? sortUsers(inactiveFiltered, userSort.header, userSort.sort) + : inactiveFiltered; + + return { activeUsers: sortedActive, inactiveUsers: sortedInactive }; }; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 435c676..ecf2d3b 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -90,6 +90,8 @@ export default { }, borderWidth: { DEFAULT: "2px", + 2: "2px", + 1: "1px", 0: "0", }, borderRadius: { From 5b406475eb88b9f81f6cd982d4edd1bf41920fc1 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 23:29:45 -0500 Subject: [PATCH 03/25] Updating spacing and approve tab --- .../dashboard/Charts/DonutMoneyApplied.tsx | 2 +- frontend/src/main-page/users/UserApprove.tsx | 6 +- frontend/src/main-page/users/UserMenu.tsx | 4 +- frontend/src/main-page/users/UserRow.tsx | 2 +- .../main-page/users/UserRowHeaderButton.tsx | 32 +- frontend/src/main-page/users/Users.tsx | 416 +++++++++--------- 6 files changed, 236 insertions(+), 226 deletions(-) diff --git a/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx index 936f8a8..c12d82c 100644 --- a/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx +++ b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx @@ -77,7 +77,7 @@ const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => { }, )}M`}
-
250 ? "mt-12" : "mt-2"}`}> +
250 ? "mt-12" : "mt-2"}`}> { }; return ( -
+
); }; diff --git a/frontend/src/main-page/users/Users.tsx b/frontend/src/main-page/users/Users.tsx index 60043d2..0c8058e 100644 --- a/frontend/src/main-page/users/Users.tsx +++ b/frontend/src/main-page/users/Users.tsx @@ -1,221 +1,221 @@ -import { useEffect, useState } from "react"; -import ApprovedUserCard from "./UserMenu"; -import PendingUserCard from "./UserApprove"; -import { User } from "../../../../middle-layer/types/User"; -import { Pagination, ButtonGroup, IconButton } from "@chakra-ui/react"; -import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; -import { observer } from "mobx-react-lite"; -import { getAppStore } from "../../external/bcanSatchel/store"; -import { api } from "../../api"; -import { Navigate } from "react-router-dom"; -import { UserStatus } from "../../../../middle-layer/types/UserStatus"; -import { useAuthContext } from "../../context/auth/authContext"; +// import { useEffect, useState } from "react"; +// import ApprovedUserCard from "./UserMenu"; +// import PendingUserCard from "./UserApprove"; +// import { User } from "../../../../middle-layer/types/User"; +// import { Pagination, ButtonGroup, IconButton } from "@chakra-ui/react"; +// import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; +// import { observer } from "mobx-react-lite"; +// import { getAppStore } from "../../external/bcanSatchel/store"; +// import { api } from "../../api"; +// import { Navigate } from "react-router-dom"; +// import { UserStatus } from "../../../../middle-layer/types/UserStatus"; +// import { useAuthContext } from "../../context/auth/authContext"; -// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway -// Represents a specific tab to show on the user page -enum UsersTab { - PendingUsers, - CurrentUsers, -} +// // Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway +// // Represents a specific tab to show on the user page +// enum UsersTab { +// PendingUsers, +// CurrentUsers, +// } -const fetchActiveUsers = async (): Promise => { - try { - const response = await api("/user/active", { - method: "GET", - }); +// const fetchActiveUsers = async (): Promise => { +// try { +// const response = await api("/user/active", { +// method: "GET", +// }); - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); - } +// if (!response.ok) { +// throw new Error(`HTTP Error, Status: ${response.status}`); +// } - const activeUsers = await response.json(); - return activeUsers as User[]; - } catch (error) { - console.error("Error fetching active users:", error); - return []; // Return empty array on error - } -}; +// const activeUsers = await response.json(); +// return activeUsers as User[]; +// } catch (error) { +// console.error("Error fetching active users:", error); +// return []; // Return empty array on error +// } +// }; -const fetchInactiveUsers = async () => { - try { - const response = await api("/user/inactive", { method: "GET" }); - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); - } - const inactiveUsers = await response.json(); - return inactiveUsers as User[]; - } catch (error) { - console.error("Error fetching active users:", error); - } -}; +// const fetchInactiveUsers = async () => { +// try { +// const response = await api("/user/inactive", { method: "GET" }); +// if (!response.ok) { +// throw new Error(`HTTP Error, Status: ${response.status}`); +// } +// const inactiveUsers = await response.json(); +// return inactiveUsers as User[]; +// } catch (error) { +// console.error("Error fetching active users:", error); +// } +// }; -const ITEMS_PER_PAGE = 8; +// const ITEMS_PER_PAGE = 8; -const Users = observer(() => { - const store = getAppStore(); - const { user } = useAuthContext(); +// const Users = observer(() => { +// const store = getAppStore(); +// const { user } = useAuthContext(); - useEffect(() => { - const fetchUsers = async () => { - const active = await fetchActiveUsers(); - const inactive = await fetchInactiveUsers(); - if (active) { - store.activeUsers = active; - } - if (inactive) { - store.inactiveUsers = inactive; - } - }; - fetchUsers(); - }, []); +// useEffect(() => { +// const fetchUsers = async () => { +// const active = await fetchActiveUsers(); +// const inactive = await fetchInactiveUsers(); +// if (active) { +// store.activeUsers = active; +// } +// if (inactive) { +// store.inactiveUsers = inactive; +// } +// }; +// fetchUsers(); +// }, []); - const [usersTabStatus, setUsersTabStatus] = useState( - UsersTab.CurrentUsers - ); - const [currentPage, setCurrentPage] = useState(1); +// const [usersTabStatus, setUsersTabStatus] = useState( +// UsersTab.CurrentUsers +// ); +// const [currentPage, setCurrentPage] = useState(1); - const filteredUsers = - usersTabStatus === UsersTab.PendingUsers - ? store.inactiveUsers - : store.activeUsers; +// const filteredUsers = +// usersTabStatus === UsersTab.PendingUsers +// ? store.inactiveUsers +// : store.activeUsers; - const numInactiveUsers = store.inactiveUsers.length; - const numUsers = filteredUsers.length; - const pageStartIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const pageEndIndex = - pageStartIndex + ITEMS_PER_PAGE > numUsers - ? numUsers - : pageStartIndex + ITEMS_PER_PAGE; - const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); +// const numInactiveUsers = store.inactiveUsers.length; +// const numUsers = filteredUsers.length; +// const pageStartIndex = (currentPage - 1) * ITEMS_PER_PAGE; +// const pageEndIndex = +// pageStartIndex + ITEMS_PER_PAGE > numUsers +// ? numUsers +// : pageStartIndex + ITEMS_PER_PAGE; +// const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); - return user ? ( - user?.position !== UserStatus.Inactive ? ( -
-
-

- {usersTabStatus === UsersTab.CurrentUsers - ? "All Users" - : "Pending Users"} -

-

{numInactiveUsers} new users

-
-
-
- - -
-
- {usersTabStatus === UsersTab.CurrentUsers ? ( - <> -
-

User ID

-

Email

-

Position

-
- {currentPageUsers.map((user) => ( - - ))} - - ) : ( - <> -
-

User ID

-

Email

-

Position

-
-
- {currentPageUsers.map((user) => ( - - ))} - - )} -
- { - setCurrentPage(e.page); - }} - > - - - - - - - - {({ pages }) => - pages.map((page, index) => - page.type === "page" ? ( - setCurrentPage(page.value)} - aria-label={`Go to page ${page.value}`} - > - {page.value} - - ) : ( - "..." - ) - ) - } - - - - - - - - -
-
- ) : ( - - ) - ) : ( - - ); -}); +// return user ? ( +// user?.position !== UserStatus.Inactive ? ( +//
+//
+//

+// {usersTabStatus === UsersTab.CurrentUsers +// ? "All Users" +// : "Pending Users"} +//

+//

{numInactiveUsers} new users

+//
+//
+//
+// +// +//
+//
+// {usersTabStatus === UsersTab.CurrentUsers ? ( +// <> +//
+//

User ID

+//

Email

+//

Position

+//
+// {currentPageUsers.map((user) => ( +// +// ))} +// +// ) : ( +// <> +//
+//

User ID

+//

Email

+//

Position

+//
+//
+// {currentPageUsers.map((user) => ( +// +// ))} +// +// )} +//
+// { +// setCurrentPage(e.page); +// }} +// > +// +// +// +// +// +// +// +// {({ pages }) => +// pages.map((page, index) => +// page.type === "page" ? ( +// setCurrentPage(page.value)} +// aria-label={`Go to page ${page.value}`} +// > +// {page.value} +// +// ) : ( +// "..." +// ) +// ) +// } +// +// +// +// +// +// +// +// +//
+//
+// ) : ( +// +// ) +// ) : ( +// +// ); +// }); -export default Users; +// export default Users; From 1f148d5ec5faccc4387c14e7b22bab5f70b284d3 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 23:36:06 -0500 Subject: [PATCH 04/25] Reorganizing components into folders --- frontend/src/main-page/users/UsersPage.tsx | 8 ++--- .../src/main-page/users/processUserData.ts | 34 +------------------ .../users/{ => user-rows}/UserApprove.tsx | 8 ++--- .../users/{ => user-rows}/UserMenu.tsx | 14 ++++---- .../{ => user-rows}/UserPositionCard.tsx | 2 +- .../users/{ => user-rows}/UserRow.tsx | 4 +-- .../users/{ => user-rows}/UserRowHeader.tsx | 4 +-- .../{ => user-rows}/UserRowHeaderButton.tsx | 0 8 files changed, 21 insertions(+), 53 deletions(-) rename frontend/src/main-page/users/{ => user-rows}/UserApprove.tsx (92%) rename frontend/src/main-page/users/{ => user-rows}/UserMenu.tsx (92%) rename frontend/src/main-page/users/{ => user-rows}/UserPositionCard.tsx (91%) rename frontend/src/main-page/users/{ => user-rows}/UserRow.tsx (90%) rename frontend/src/main-page/users/{ => user-rows}/UserRowHeader.tsx (91%) rename frontend/src/main-page/users/{ => user-rows}/UserRowHeaderButton.tsx (100%) diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx index 330f2d5..97ef32b 100644 --- a/frontend/src/main-page/users/UsersPage.tsx +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -8,10 +8,10 @@ import { Navigate } from "react-router-dom"; import Button from "../../components/Button.tsx"; import UserSearch from "./UserSearch.tsx"; import { ProcessUserData } from "./processUserData.ts"; -import UserRow from "./UserRow.tsx"; -import UserMenu from "./UserMenu.tsx"; -import UserRowHeader from "./UserRowHeader.tsx"; -import UserApprove from "./UserApprove.tsx"; +import UserRow from "./user-rows/UserRow.tsx"; +import UserMenu from "./user-rows/UserMenu.tsx"; +import UserRowHeader from "./user-rows/UserRowHeader.tsx"; +import UserApprove from "./user-rows/UserApprove.tsx"; import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; const UsersPage = observer(() => { diff --git a/frontend/src/main-page/users/processUserData.ts b/frontend/src/main-page/users/processUserData.ts index 32a61f3..b244b7d 100644 --- a/frontend/src/main-page/users/processUserData.ts +++ b/frontend/src/main-page/users/processUserData.ts @@ -1,39 +1,7 @@ import { useEffect } from "react"; import { User } from "../../../../middle-layer/types/User.ts"; -import { api } from "../../api.ts"; import { getAppStore } from "../../external/bcanSatchel/store.ts"; - -// fetch grants -export const fetchActiveUsers = async (): Promise => { - try { - const response = await api("/user/active", { - method: "GET", - }); - - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); - } - - const activeUsers = await response.json(); - return activeUsers as User[]; - } catch (error) { - console.error("Error fetching active users:", error); - return []; // Return empty array on error - } -}; - -export const fetchInactiveUsers = async () => { - try { - const response = await api("/user/inactive", { method: "GET" }); - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); - } - const inactiveUsers = await response.json(); - return inactiveUsers as User[]; - } catch (error) { - console.error("Error fetching active users:", error); - } -}; +import { fetchActiveUsers, fetchInactiveUsers } from "./UserActions.ts"; const searchFilter = (searchQuery: string) => (user: User) => { if (!searchQuery.trim()) return true; diff --git a/frontend/src/main-page/users/UserApprove.tsx b/frontend/src/main-page/users/user-rows/UserApprove.tsx similarity index 92% rename from frontend/src/main-page/users/UserApprove.tsx rename to frontend/src/main-page/users/user-rows/UserApprove.tsx index 26e4b4b..1736ba7 100644 --- a/frontend/src/main-page/users/UserApprove.tsx +++ b/frontend/src/main-page/users/user-rows/UserApprove.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; -import { api } from "../../api"; -import { getAppStore } from "../../external/bcanSatchel/store"; -import { User } from "../../../../middle-layer/types/User"; +import { api } from "../../../api"; +import { getAppStore } from "../../../external/bcanSatchel/store"; +import { User } from "../../../../../middle-layer/types/User"; import { toJS } from "mobx"; -import { moveUserToActive, removeUser } from "./UserActions"; +import { moveUserToActive, removeUser } from "../UserActions"; import { useState } from "react"; // Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway diff --git a/frontend/src/main-page/users/UserMenu.tsx b/frontend/src/main-page/users/user-rows/UserMenu.tsx similarity index 92% rename from frontend/src/main-page/users/UserMenu.tsx rename to frontend/src/main-page/users/user-rows/UserMenu.tsx index 636cdb0..4c8cd56 100644 --- a/frontend/src/main-page/users/UserMenu.tsx +++ b/frontend/src/main-page/users/user-rows/UserMenu.tsx @@ -1,14 +1,14 @@ import { Menu } from "@chakra-ui/react"; -import { UserStatus } from "../../../../middle-layer/types/UserStatus"; +import { UserStatus } from "../../../../../middle-layer/types/UserStatus"; import { faUserPen, faTrash } from "@fortawesome/free-solid-svg-icons"; -import ActionConfirmation from "../../custom/ActionConfirmation"; +import ActionConfirmation from "../../../custom/ActionConfirmation"; import { useState } from "react"; -import { api } from "../../api"; -import { User } from "../../../../middle-layer/types/User"; +import { api } from "../../../api"; +import { User } from "../../../../../middle-layer/types/User"; import { toJS } from "mobx"; -import { getAppStore } from "../../external/bcanSatchel/store"; -import { setActiveUsers } from "../../external/bcanSatchel/actions"; -import Button from "../../components/Button"; +import { getAppStore } from "../../../external/bcanSatchel/store"; +import { setActiveUsers } from "../../../external/bcanSatchel/actions"; +import Button from "../../../components/Button"; import { FaEllipsis } from "react-icons/fa6"; interface UserMenuProps { diff --git a/frontend/src/main-page/users/UserPositionCard.tsx b/frontend/src/main-page/users/user-rows/UserPositionCard.tsx similarity index 91% rename from frontend/src/main-page/users/UserPositionCard.tsx rename to frontend/src/main-page/users/user-rows/UserPositionCard.tsx index fa7779d..45c5f20 100644 --- a/frontend/src/main-page/users/UserPositionCard.tsx +++ b/frontend/src/main-page/users/user-rows/UserPositionCard.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { UserStatus } from "../../../../middle-layer/types/UserStatus"; +import { UserStatus } from "../../../../../middle-layer/types/UserStatus"; interface UserPositionCardProps { position: UserStatus; diff --git a/frontend/src/main-page/users/UserRow.tsx b/frontend/src/main-page/users/user-rows/UserRow.tsx similarity index 90% rename from frontend/src/main-page/users/UserRow.tsx rename to frontend/src/main-page/users/user-rows/UserRow.tsx index 7f76ea2..2ebcef1 100644 --- a/frontend/src/main-page/users/UserRow.tsx +++ b/frontend/src/main-page/users/user-rows/UserRow.tsx @@ -1,6 +1,6 @@ -import { User } from "../../../../middle-layer/types/User"; +import { User } from "../../../../../middle-layer/types/User"; import UserPositionCard from "./UserPositionCard"; -import logo from "../../images/logo.svg"; +import logo from "../../../images/logo.svg"; // Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway interface UserRowProps { diff --git a/frontend/src/main-page/users/UserRowHeader.tsx b/frontend/src/main-page/users/user-rows/UserRowHeader.tsx similarity index 91% rename from frontend/src/main-page/users/UserRowHeader.tsx rename to frontend/src/main-page/users/user-rows/UserRowHeader.tsx index 82b27c8..e2e43ef 100644 --- a/frontend/src/main-page/users/UserRowHeader.tsx +++ b/frontend/src/main-page/users/user-rows/UserRowHeader.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { User } from "../../../../middle-layer/types/User"; -import { updateUserSort } from "../../external/bcanSatchel/actions"; +import { User } from "../../../../../middle-layer/types/User"; +import { updateUserSort } from "../../../external/bcanSatchel/actions"; import UserRowHeaderButton from "./UserRowHeaderButton"; // Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway diff --git a/frontend/src/main-page/users/UserRowHeaderButton.tsx b/frontend/src/main-page/users/user-rows/UserRowHeaderButton.tsx similarity index 100% rename from frontend/src/main-page/users/UserRowHeaderButton.tsx rename to frontend/src/main-page/users/user-rows/UserRowHeaderButton.tsx From 2d28693b9b5ad83830125790375aae958f762f01 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 23:50:21 -0500 Subject: [PATCH 05/25] Format and remove unused imports --- frontend/src/main-page/users/UsersPage.tsx | 4 +- .../src/main-page/users/processUserData.ts | 47 ++++++++++--------- .../src/main-page/users/user-rows/UserRow.tsx | 1 - .../users/user-rows/UserRowHeader.tsx | 27 +++++++++-- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx index 97ef32b..8245c72 100644 --- a/frontend/src/main-page/users/UsersPage.tsx +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { ButtonGroup, IconButton, Pagination } from "@chakra-ui/react"; +//import { ButtonGroup, IconButton, Pagination } from "@chakra-ui/react"; import { useAuthContext } from "../../context/auth/authContext"; import { observer } from "mobx-react-lite"; @@ -12,7 +12,6 @@ import UserRow from "./user-rows/UserRow.tsx"; import UserMenu from "./user-rows/UserMenu.tsx"; import UserRowHeader from "./user-rows/UserRowHeader.tsx"; import UserApprove from "./user-rows/UserApprove.tsx"; -import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; const UsersPage = observer(() => { const [showAll, setShowAll] = useState(true); @@ -74,6 +73,7 @@ const UsersPage = observer(() => { ))}
+ {/* Commenting out pagination for now to check if needed */} {/* (user: User) => { const filterUsers = (users: User[], predicates: ((user: User) => boolean)[]) => users.filter((user) => predicates.every((fn) => fn(user))); -const sortUsers = (users: User[], header: keyof User, sort: "asc" | "desc" | "none") => +const sortUsers = ( + users: User[], + header: keyof User, + sort: "asc" | "desc" | "none", +) => [...users].sort((a: User, b: User) => { - const direction = sort === "asc" ? -1 : 1; + const direction = sort === "asc" ? -1 : 1; - const aValue = a[header]; - const bValue = b[header]; + const aValue = a[header]; + const bValue = b[header]; - if (aValue == null) return -1 * direction; - if (bValue == null) return 1 * direction; + if (aValue == null) return -1 * direction; + if (bValue == null) return 1 * direction; - if (aValue > bValue) return 1 * direction; - if (aValue < bValue) return -1 * direction; + if (aValue > bValue) return 1 * direction; + if (aValue < bValue) return -1 * direction; - return 0; - }) + return 0; + }); - -// contains callbacks for sorting and filtering grants -// stores state for list of grants/filter +// contains callbacks for sorting and filtering users +// stores state for list of users/filter export const ProcessUserData = () => { const { activeUsers, inactiveUsers, userQuery, userSort } = getAppStore(); - // fetch grants on mount if empty + // fetch users on mount if empty useEffect(() => { if (activeUsers.length === 0) fetchActiveUsers(); if (inactiveUsers.length === 0) fetchInactiveUsers(); }, [activeUsers.length, inactiveUsers.length]); - // compute filtered grants dynamically — no useState needed + // compute filtered users dynamically — no useState needed const activeFiltered = filterUsers(activeUsers, [searchFilter(userQuery)]); const inactiveFiltered = filterUsers(inactiveUsers, [ searchFilter(userQuery), ]); - const sortedActive = userSort && userSort.sort !== "none" - ? sortUsers(activeFiltered, userSort.header, userSort.sort) - : activeFiltered; + const sortedActive = + userSort && userSort.sort !== "none" + ? sortUsers(activeFiltered, userSort.header, userSort.sort) + : activeFiltered; - const sortedInactive = userSort && userSort.sort !== "none" - ? sortUsers(inactiveFiltered, userSort.header, userSort.sort) - : inactiveFiltered; + const sortedInactive = + userSort && userSort.sort !== "none" + ? sortUsers(inactiveFiltered, userSort.header, userSort.sort) + : inactiveFiltered; return { activeUsers: sortedActive, inactiveUsers: sortedInactive }; }; diff --git a/frontend/src/main-page/users/user-rows/UserRow.tsx b/frontend/src/main-page/users/user-rows/UserRow.tsx index 2ebcef1..c8b9974 100644 --- a/frontend/src/main-page/users/user-rows/UserRow.tsx +++ b/frontend/src/main-page/users/user-rows/UserRow.tsx @@ -2,7 +2,6 @@ import { User } from "../../../../../middle-layer/types/User"; import UserPositionCard from "./UserPositionCard"; import logo from "../../../images/logo.svg"; -// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway interface UserRowProps { user: User; action: React.ReactNode; diff --git a/frontend/src/main-page/users/user-rows/UserRowHeader.tsx b/frontend/src/main-page/users/user-rows/UserRowHeader.tsx index e2e43ef..bdd5496 100644 --- a/frontend/src/main-page/users/user-rows/UserRowHeader.tsx +++ b/frontend/src/main-page/users/user-rows/UserRowHeader.tsx @@ -12,16 +12,35 @@ const UserRowHeader = () => { } as { header: keyof User; sort: "asc" | "desc" | "none" }); function buttonHandler(header: keyof User) { - const isAsc = labels.header == header ? (labels.sort == "asc" ? "desc" : labels.sort == "desc" ? "asc" : "none") : "desc"; + const isAsc = + labels.header == header + ? labels.sort == "asc" + ? "desc" + : labels.sort == "desc" + ? "asc" + : "none" + : "desc"; updateUserSort({ header, sort: isAsc }); setLabels({ header: header, sort: isAsc }); } return (
- buttonHandler("lastName")} /> - buttonHandler("email")} /> - buttonHandler("position")} /> + buttonHandler("lastName")} + /> + buttonHandler("email")} + /> + buttonHandler("position")} + />
{"Action"}
); From 50bed584d9c6e0d63ee26ee73a4047c679c592df Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 24 Feb 2026 23:59:10 -0500 Subject: [PATCH 06/25] Action confirmation formatting --- frontend/src/custom/ActionConfirmation.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/frontend/src/custom/ActionConfirmation.tsx b/frontend/src/custom/ActionConfirmation.tsx index f80b6a8..34dafc2 100644 --- a/frontend/src/custom/ActionConfirmation.tsx +++ b/frontend/src/custom/ActionConfirmation.tsx @@ -1,3 +1,4 @@ +import Button from "../components/Button"; import { IoIosWarning } from "react-icons/io"; {/* The popup that appears on delete */} @@ -63,21 +64,11 @@ import { IoIosWarning } from "react-icons/io"; {/* Buttons */}
- - + }} className="border-grey-500" />
From 5a4a06bf0f002130c7d50ca7c73b28060a7d4348 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 00:27:53 -0500 Subject: [PATCH 07/25] Moving api calls into user actions file --- frontend/src/main-page/users/UserActions.ts | 179 +++++++++++++++--- frontend/src/main-page/users/UsersPage.tsx | 5 + .../main-page/users/user-rows/UserApprove.tsx | 77 +------- .../main-page/users/user-rows/UserMenu.tsx | 93 +-------- 4 files changed, 168 insertions(+), 186 deletions(-) diff --git a/frontend/src/main-page/users/UserActions.ts b/frontend/src/main-page/users/UserActions.ts index f726fbd..dcdf4c1 100644 --- a/frontend/src/main-page/users/UserActions.ts +++ b/frontend/src/main-page/users/UserActions.ts @@ -1,12 +1,19 @@ -import { api } from "../../api" +import { api } from "../../api"; import { User } from "../../../../middle-layer/types/User"; -import { setActiveUsers, setInactiveUsers } from "../../external/bcanSatchel/actions"; +import { + setActiveUsers, + setInactiveUsers, +} from "../../external/bcanSatchel/actions"; import { getAppStore } from "../../external/bcanSatchel/store"; import { toJS } from "mobx"; +import { UserStatus } from "../../../../middle-layer/types/UserStatus"; + +const store = getAppStore(); + export const fetchActiveUsers = async (): Promise => { try { const response = await api("/user/active", { - method: 'GET' + method: "GET", }); if (!response.ok && response.status !== 200) { @@ -19,43 +26,163 @@ export const fetchActiveUsers = async (): Promise => { console.error("Error fetching active users:", error); return []; // Return empty array on error } -} +}; export const fetchInactiveUsers = async (): Promise => { try { - const response = await api("/user/inactive", { method: 'GET' }); + const response = await api("/user/inactive", { method: "GET" }); if (!response.ok && response.status !== 200) { throw new Error(`HTTP Error, Status: ${response.status}`); } const inactiveUsers = await response.json(); return inactiveUsers as User[]; - } - catch (error) { + } catch (error) { console.error("Error fetching active users:", error); - return []; // Return empty array on error - + return []; // Return empty array on error } -} +}; export const fetchUsers = async () => { console.log("Fetching users..."); - const active = await fetchActiveUsers(); - const inactive = await fetchInactiveUsers(); - if (active) { - setActiveUsers(active); - console.log("Active users fetched:", toJS(getAppStore().activeUsers)); - } - if (inactive) { - setInactiveUsers(inactive); - console.log("Inactive users fetched:", toJS(getAppStore().inactiveUsers)); - } - }; + const active = await fetchActiveUsers(); + const inactive = await fetchInactiveUsers(); + if (active) { + setActiveUsers(active); + console.log("Active users fetched:", toJS(store.activeUsers)); + } + if (inactive) { + setInactiveUsers(inactive); + console.log("Inactive users fetched:", toJS(store.inactiveUsers)); + } +}; export const moveUserToActive = (user: User) => { - setActiveUsers([...getAppStore().activeUsers, user]); - setInactiveUsers(getAppStore().inactiveUsers.filter(u => u.email !== user.email)); -} + setActiveUsers([...store.activeUsers, user]); + setInactiveUsers(store.inactiveUsers.filter((u) => u.email !== user.email)); +}; export const removeUser = (user: User) => { - setInactiveUsers(getAppStore().inactiveUsers.filter(u => u.email !== user.email)); -} \ No newline at end of file + setInactiveUsers(store.inactiveUsers.filter((u) => u.email !== user.email)); + setActiveUsers(store.activeUsers.filter((u) => u.email !== user.email)); +}; + +export const approveUser = async ( + user: User, + setIsLoading: (loading: boolean) => void, +) => { + setIsLoading(true); + try { + const response = await api("/user/change-role", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user: { + email: user.email, + position: user.position, + } as User, + groupName: "Employee", + requestedBy: toJS(store.user) as User, + }), + }); + if (response.ok) { + alert(`User ${user.email} has been approved successfully`); + const body = await response.json(); + moveUserToActive(body as User); + } else { + alert("Failed to approve user"); + } + } catch (error) { + console.error("Error approving user:", error); + alert("Error approving user"); + } finally { + setIsLoading(false); + } +}; + +export const deleteUser = async ( + user: User, + setIsLoading: (loading: boolean) => void, +) => { + setIsLoading(true); + try { + const response = await api( + `user/delete-user/${encodeURIComponent(user.email)}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user: { + email: user.email, + position: user.position, + } as User, + requestedBy: toJS(store.user) as User, + }), + }, + ); + + if (response.ok) { + console.log(`User ${user.email} has been deleted successfully`); + alert(`User ${user.email} has been deleted successfully`); + const body = await response.json(); + removeUser(body); + } else { + const errorBody = await response.json(); + console.error("Error: ", errorBody); + alert("Failed to delete user"); + } + } catch (error) { + console.error("Error deleting user:", error); + alert("Error deleting user"); + } finally { + setIsLoading(false); + } +}; + +export const changeUserGroup = async (user: User) => { + console.log( + `Changing user ${user.email} to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }...`, + ); + + try { + const response = await api("/user/change-role", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user: { + email: user.email, + position: user.position, + } as User, + groupName: + user.position === UserStatus.Admin + ? UserStatus.Employee + : UserStatus.Admin, + requestedBy: toJS(store.user) as User, + }), + }); + + if (response.ok) { + console.log( + `User ${user.email} successfully changed to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }`, + ); + alert( + `User ${user.email} successfully changed to ${ + user.position === UserStatus.Admin ? "employee" : "admin" + }`, + ); + const updatedUser = await response.json(); + setActiveUsers([ + ...store.activeUsers.filter((u) => u.email !== user.email), + updatedUser as User, + ]); + } else { + const errorBody = await response.json(); + console.error("Error: ", errorBody); + } + } catch (error) { + console.error("Error changing user group: ", error); + } +}; diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx index 8245c72..49e7e70 100644 --- a/frontend/src/main-page/users/UsersPage.tsx +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -71,6 +71,11 @@ const UsersPage = observer(() => { />
))} + {currentPageUsers.length === 0 && ( +
+

No users to display

+
+ )} {/* Commenting out pagination for now to check if needed */} diff --git a/frontend/src/main-page/users/user-rows/UserApprove.tsx b/frontend/src/main-page/users/user-rows/UserApprove.tsx index 1736ba7..4cfc7d6 100644 --- a/frontend/src/main-page/users/user-rows/UserApprove.tsx +++ b/frontend/src/main-page/users/user-rows/UserApprove.tsx @@ -1,16 +1,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; -import { api } from "../../../api"; -import { getAppStore } from "../../../external/bcanSatchel/store"; import { User } from "../../../../../middle-layer/types/User"; -import { toJS } from "mobx"; -import { moveUserToActive, removeUser } from "../UserActions"; +import { approveUser, deleteUser } from "../UserActions"; import { useState } from "react"; - -// Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway - -const store = getAppStore(); - interface UserApproveProps { user: User; } @@ -18,77 +10,18 @@ interface UserApproveProps { const UserApprove = ({ user }: UserApproveProps) => { const [isLoading, setIsLoading] = useState(false); - const approveUser = async () => { - setIsLoading(true); - try { - const response = await api("/user/change-role", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - user: { - email: user.email, - position: user.position, - } as User, - groupName: "Employee", - requestedBy: toJS(store.user) as User, - }), - }); - if (response.ok) { - alert(`User ${user.email} has been approved successfully`); - const body = await response.json(); - moveUserToActive(body as User); - } else { - alert("Failed to approve user"); - } - } catch (error) { - console.error("Error approving user:", error); - alert("Error approving user"); - } finally { - setIsLoading(false); - } - }; - - const rejectUser = async () => { - setIsLoading(true); - try { - const response = await api("user/delete-user", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - user: { - email: user.email, - position: user.position, - } as User, - requestedBy: toJS(store.user) as User, - }), - }); - if (response.ok) { - alert(`User ${user.email} has been deleted successfully`); - const body = await response.json(); - removeUser(body); - } else { - alert("Failed to reject user"); - } - } catch (error) { - console.error("Error rejecting user:", error); - alert("Error rejecting user"); - } finally { - setIsLoading(false); - } - }; - return (
diff --git a/frontend/src/main-page/users/user-rows/UserMenu.tsx b/frontend/src/main-page/users/user-rows/UserMenu.tsx index 2eab142..e94e739 100644 --- a/frontend/src/main-page/users/user-rows/UserMenu.tsx +++ b/frontend/src/main-page/users/user-rows/UserMenu.tsx @@ -62,7 +62,6 @@ const UserMenu = ({ user }: UserMenuProps) => { : "admin") + " " } - alignment="left" logo={faUserPen} logoPosition="left" onClick={() => setIsChangeGroupModalOpen(true)} @@ -74,7 +73,6 @@ const UserMenu = ({ user }: UserMenuProps) => { text={"Delete user "} logo={faTrash} logoPosition="left" - alignment="left" disabled={isChangeGroupModalOpen || isDeleteUserModalOpen} onClick={() => setIsDeleteUserModalOpen(true)} className="text-sm focus:outline-none block w-full hover:border-red active:bg-red text-red" From ffae7191412f8c5f896594e6f4c0ab5761d74cc0 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 01:11:40 -0500 Subject: [PATCH 13/25] Commenting out pagination --- frontend/src/main-page/users/UsersPage.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx index adcfdc7..4418ac0 100644 --- a/frontend/src/main-page/users/UsersPage.tsx +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -17,7 +17,7 @@ const UsersPage = observer(() => { const [showAll, setShowAll] = useState(true); const { activeUsers, inactiveUsers } = ProcessUserData(); - const ITEMS_PER_PAGE = 8; + //const ITEMS_PER_PAGE = 8; const { user } = useAuthContext(); @@ -25,14 +25,15 @@ const UsersPage = observer(() => { const filteredUsers = showAll ? activeUsers : inactiveUsers; - const numUsers = filteredUsers.length; + // const numUsers = filteredUsers.length; // const pageStartIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const pageStartIndex = 1; // Temporarily disable pagination by always starting at index 0 - const pageEndIndex = - pageStartIndex + ITEMS_PER_PAGE > numUsers - ? numUsers - : pageStartIndex + ITEMS_PER_PAGE; - const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); + // const pageStartIndex = 1; // Temporarily disable pagination by always starting at index 0 + // const pageEndIndex = + // pageStartIndex + ITEMS_PER_PAGE > numUsers + // ? numUsers + // : pageStartIndex + ITEMS_PER_PAGE; + // const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); + const currentPageUsers = filteredUsers; // Temporarily disable pagination by showing all users return user ? ( user?.position !== UserStatus.Inactive ? (
From 184921088ba16fb1715953fad0a2b8eea86fef83 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 01:18:43 -0500 Subject: [PATCH 14/25] Button active border --- frontend/src/components/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index f18332c..148dbf0 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -19,7 +19,7 @@ export default function Button({ text, onClick, className, logo, logoPosition, d return (
+ return ( +
+ +
+
-
-
- {grants.map((grant) => ( - setCurGrant(grant)} - /> - ))} -
-
- {curGrant ? ( - - ) : ( -
- No grants found. -
- )} -
+
+
+ {grants.map((grant) => ( + setCurGrant(grant)} + /> + ))} +
+
+ {curGrant ? ( + + ) : ( +
+ No grants found. +
+ )}
+
- {/*
+ {/*
@@ -156,24 +151,19 @@ function GrantPage({}: GrantPageProps) {
*/} -
- {showNewGrantModal && ( - { - setShowNewGrantModal(false); - setWasGrantSubmitted(true); - }} - isOpen={showNewGrantModal} - /> - )} -
+
+ {showNewGrantModal && ( + { + setShowNewGrantModal(false); + setWasGrantSubmitted(true); + }} + isOpen={showNewGrantModal} + /> + )}
- ) : ( - - ) - ) : ( - +
); } diff --git a/frontend/src/main-page/grants/grant-view/ContactCard.tsx b/frontend/src/main-page/grants/grant-view/ContactCard.tsx index 5d2005a..c65505f 100644 --- a/frontend/src/main-page/grants/grant-view/ContactCard.tsx +++ b/frontend/src/main-page/grants/grant-view/ContactCard.tsx @@ -1,3 +1,4 @@ +import { getAppStore } from "../../../external/bcanSatchel/store"; import POC from "../../../../../middle-layer/types/POC"; import logo from "../../../images/logo.svg"; @@ -6,11 +7,21 @@ type ContactCardProps = { type?: "BCAN" | "Granter"; }; +const store = getAppStore() +const activeUsers = store.activeUsers || []; + export default function ContactCard({ contact, type }: ContactCardProps) { + +const contactPhoto = + type === "BCAN" + ? activeUsers.find((user) => user.email === contact?.POC_email) + ?.profilePicUrl + : logo; + return (
Profile diff --git a/frontend/src/main-page/notifications/NotificationPopup.tsx b/frontend/src/main-page/notifications/NotificationPopup.tsx index b38721b..846faa2 100644 --- a/frontend/src/main-page/notifications/NotificationPopup.tsx +++ b/frontend/src/main-page/notifications/NotificationPopup.tsx @@ -51,7 +51,7 @@ const NotificationPopup: React.FC = observer(({ return createPortal( -
+

Alerts

- +
+
+
+

So Sorry!

+

+ You don't have access to this page. Contact the admin if you think + there's a mistake. +

+ +
+
+
+
- BCAN logo
); } diff --git a/frontend/src/main-page/users/UsersPage.tsx b/frontend/src/main-page/users/UsersPage.tsx index 4418ac0..639527a 100644 --- a/frontend/src/main-page/users/UsersPage.tsx +++ b/frontend/src/main-page/users/UsersPage.tsx @@ -1,10 +1,7 @@ import { useState } from "react"; //import { ButtonGroup, IconButton, Pagination } from "@chakra-ui/react"; -import { useAuthContext } from "../../context/auth/authContext"; import { observer } from "mobx-react-lite"; -import { UserStatus } from "../../../../middle-layer/types/UserStatus.ts"; -import { Navigate } from "react-router-dom"; import Button from "../../components/Button.tsx"; import UserSearch from "./UserSearch.tsx"; import { ProcessUserData } from "./processUserData.ts"; @@ -19,8 +16,6 @@ const UsersPage = observer(() => { const { activeUsers, inactiveUsers } = ProcessUserData(); //const ITEMS_PER_PAGE = 8; - const { user } = useAuthContext(); - //const [currentPage, setCurrentPage] = useState(1); const filteredUsers = showAll ? activeUsers : inactiveUsers; @@ -34,54 +29,53 @@ const UsersPage = observer(() => { // : pageStartIndex + ITEMS_PER_PAGE; // const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); const currentPageUsers = filteredUsers; // Temporarily disable pagination by showing all users - return user ? ( - user?.position !== UserStatus.Inactive ? ( -
- -
+ return ( +
+ +
+
+ {inactiveUsers.length > 0 && ( + + )}
+
-
-
- - {currentPageUsers.map((user) => ( -
- - ) : ( - - ) - } - /> -
- ))} - {currentPageUsers.length === 0 && ( -
-

No users to display

-
- )} -
+
+
+ + {currentPageUsers.map((user) => ( +
+ + ) : ( + + ) + } + /> +
+ ))} + {currentPageUsers.length === 0 && ( +
+

No users to display

+
+ )}
- {/* Commenting out pagination for now to check if needed */} - {/* + {/* Commenting out pagination for now to check if needed */} + {/* { */} -
- ) : ( - - ) - ) : ( - +
); }); diff --git a/frontend/src/styles/notification.css b/frontend/src/styles/notification.css index 7f794c1..942690e 100644 --- a/frontend/src/styles/notification.css +++ b/frontend/src/styles/notification.css @@ -1,12 +1,9 @@ .notification-popup { position: absolute; - right: 7rem; - top: 110px; + right: 6rem; + top: 2.5rem; width: min(340px, 70%); background-color: white; - border: 1px solid black; - border-radius: 6px; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); padding: 0.5rem; z-index: 1000; } diff --git a/middle-layer/types/POC.ts b/middle-layer/types/POC.ts index efe5d4e..7f87c56 100644 --- a/middle-layer/types/POC.ts +++ b/middle-layer/types/POC.ts @@ -1,4 +1,5 @@ export default interface POC { POC_name: string, POC_email: string, + profilePicUrl?: string | null } \ No newline at end of file From 9d4c649ef2e985741f9ed4f66b798dea7ae3e296 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:09:48 -0500 Subject: [PATCH 17/25] Removing profile pic from poc type --- middle-layer/types/POC.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/middle-layer/types/POC.ts b/middle-layer/types/POC.ts index 7f87c56..efe5d4e 100644 --- a/middle-layer/types/POC.ts +++ b/middle-layer/types/POC.ts @@ -1,5 +1,4 @@ export default interface POC { POC_name: string, POC_email: string, - profilePicUrl?: string | null } \ No newline at end of file From c872bfc366575a8d002ec5be486a5c30f056fdba Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:25:03 -0500 Subject: [PATCH 18/25] Abstracting search formatting --- frontend/src/components/SearchBar.tsx | 40 +++++++++++++++++++ .../grants/filter-bar/GrantSearch.tsx | 29 +------------- frontend/src/main-page/users/UserSearch.tsx | 29 +------------- 3 files changed, 44 insertions(+), 54 deletions(-) create mode 100644 frontend/src/components/SearchBar.tsx diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx new file mode 100644 index 0000000..786bf3a --- /dev/null +++ b/frontend/src/components/SearchBar.tsx @@ -0,0 +1,40 @@ +import { IoMdSearch } from "react-icons/io"; + +type SearchBarProps = { + handleInputChange: (e: React.ChangeEvent) => void; + userInput: string; +}; + +export default function SearchBar({ + handleInputChange, + userInput, +}: SearchBarProps) { + return ( +
+ {/* Absolutely-positioned icon */} + + { + if (e.key === "Enter") { + e.preventDefault(); + } + }} + /> +
+ ); +} diff --git a/frontend/src/main-page/grants/filter-bar/GrantSearch.tsx b/frontend/src/main-page/grants/filter-bar/GrantSearch.tsx index 6e6123d..b2d2e35 100644 --- a/frontend/src/main-page/grants/filter-bar/GrantSearch.tsx +++ b/frontend/src/main-page/grants/filter-bar/GrantSearch.tsx @@ -1,9 +1,9 @@ -import { IoMdSearch } from "react-icons/io"; import { useState } from "react"; import Fuse from "fuse.js"; import { updateSearchQuery } from "../../../external/bcanSatchel/actions"; import { getAppStore } from "../../../external/bcanSatchel/store"; import { Grant } from "../../../../../middle-layer/types/Grant"; +import SearchBar from "../../../components/SearchBar"; function GrantSearch() { const [userInput, setUserInput] = useState(getAppStore().searchQuery || ""); @@ -30,32 +30,7 @@ function GrantSearch() { }; return ( -
- {/* Absolutely-positioned icon */} - - { - if (e.key === "Enter") { - e.preventDefault(); - } - }} - /> -
+ ); } diff --git a/frontend/src/main-page/users/UserSearch.tsx b/frontend/src/main-page/users/UserSearch.tsx index 75ce631..884b595 100644 --- a/frontend/src/main-page/users/UserSearch.tsx +++ b/frontend/src/main-page/users/UserSearch.tsx @@ -1,9 +1,9 @@ -import { IoMdSearch } from "react-icons/io"; import { useState } from "react"; import Fuse from "fuse.js"; import { getAppStore } from "../../external/bcanSatchel/store"; import { updateUserQuery } from "../../external/bcanSatchel/actions"; import { User } from "../../../../middle-layer/types/User"; +import SearchBar from "../../components/SearchBar"; function UserSearch() { const [userInput, setUserInput] = useState(getAppStore().userQuery || ""); @@ -30,32 +30,7 @@ function UserSearch() { }; return ( -
- {/* Absolutely-positioned icon */} - - { - if (e.key === "Enter") { - e.preventDefault(); - } - }} - /> -
+ ); } From a9dadbfb8068930aea317f0c95a958e1a2fb2ade Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:27:39 -0500 Subject: [PATCH 19/25] Fixing action log --- frontend/src/main-page/users/UserActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main-page/users/UserActions.ts b/frontend/src/main-page/users/UserActions.ts index dcdf4c1..ddffc50 100644 --- a/frontend/src/main-page/users/UserActions.ts +++ b/frontend/src/main-page/users/UserActions.ts @@ -37,7 +37,7 @@ export const fetchInactiveUsers = async (): Promise => { const inactiveUsers = await response.json(); return inactiveUsers as User[]; } catch (error) { - console.error("Error fetching active users:", error); + console.error("Error fetching inactive users:", error); return []; // Return empty array on error } }; From 7b0b699c70a2772e2fa22316789a486b0a145b38 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:42:55 -0500 Subject: [PATCH 20/25] Navbar responsive style fix --- frontend/src/main-page/navbar/NavBar.tsx | 6 +++--- frontend/src/main-page/navbar/NavTab.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/main-page/navbar/NavBar.tsx b/frontend/src/main-page/navbar/NavBar.tsx index f2729ab..69f5e6c 100644 --- a/frontend/src/main-page/navbar/NavBar.tsx +++ b/frontend/src/main-page/navbar/NavBar.tsx @@ -31,9 +31,9 @@ const NavBar: React.FC = observer(() => { }; return ( -
diff --git a/frontend/src/main-page/navbar/NavTab.tsx b/frontend/src/main-page/navbar/NavTab.tsx index 9c698d0..3fc0a81 100644 --- a/frontend/src/main-page/navbar/NavTab.tsx +++ b/frontend/src/main-page/navbar/NavTab.tsx @@ -23,7 +23,7 @@ const NavTab: React.FC = ({ name, linkTo, icon }) => { }`} > - {name} + {name} ); }; From 980bd228d9883f4e17553ba97a1d3f2dbd5fea42 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:45:56 -0500 Subject: [PATCH 21/25] Rounded styling --- frontend/src/main-page/grants/new-grant/NewGrantModal.tsx | 2 +- frontend/src/main-page/settings/components/InfoCard.tsx | 2 +- frontend/tailwind.config.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx b/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx index f4942cd..7ffdbdf 100644 --- a/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx +++ b/frontend/src/main-page/grants/new-grant/NewGrantModal.tsx @@ -950,7 +950,7 @@ const NewGrantModal: React.FC<{ {showErrorPopup && (

Error diff --git a/frontend/src/main-page/settings/components/InfoCard.tsx b/frontend/src/main-page/settings/components/InfoCard.tsx index a8ffa24..cbe9d8f 100644 --- a/frontend/src/main-page/settings/components/InfoCard.tsx +++ b/frontend/src/main-page/settings/components/InfoCard.tsx @@ -13,7 +13,7 @@ type InfoCardProps = { export default function InfoCard({ title, fields, action }: InfoCardProps) { return ( -
+
{(title || action) && (
{title && ( diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index ecf2d3b..e326bed 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -98,6 +98,7 @@ export default { sm: "0.5rem", md: "0.75rem", DEFAULT: "0.75rem", + lg: "1rem", "4xl": "2rem", }, }, From f75f44858b6a7407d14b0149a41f9d3fb6877f90 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:54:58 -0500 Subject: [PATCH 22/25] Adding inactive restricted page vs not admin --- frontend/src/main-page/restricted/RestrictedPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/main-page/restricted/RestrictedPage.tsx b/frontend/src/main-page/restricted/RestrictedPage.tsx index 4e7ed50..193f826 100644 --- a/frontend/src/main-page/restricted/RestrictedPage.tsx +++ b/frontend/src/main-page/restricted/RestrictedPage.tsx @@ -2,16 +2,18 @@ import { Link } from "react-router-dom"; import { useAuthContext } from "../../context/auth/authContext"; import Button from "../../components/Button"; import BrandingPanel from "../../components/BrandingPanel"; +import { UserStatus } from "../../../../middle-layer/types/UserStatus"; + function RestrictedPage() { - const { logout } = useAuthContext(); + const { logout, user } = useAuthContext(); return (

So Sorry!

- You don't have access to this page. Contact the admin if you think + {user?.position === UserStatus.Inactive ? "Your account is currently inactive or pending approval." : "You don't have access to this page."} Contact the admin if you think there's a mistake.

From 7b40fb34798bb19127449cb1b60b28c75a8a4025 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Wed, 25 Feb 2026 23:58:36 -0500 Subject: [PATCH 23/25] Removing old users component --- frontend/src/main-page/users/Users.tsx | 221 ------------------------- 1 file changed, 221 deletions(-) delete mode 100644 frontend/src/main-page/users/Users.tsx diff --git a/frontend/src/main-page/users/Users.tsx b/frontend/src/main-page/users/Users.tsx deleted file mode 100644 index 0c8058e..0000000 --- a/frontend/src/main-page/users/Users.tsx +++ /dev/null @@ -1,221 +0,0 @@ -// import { useEffect, useState } from "react"; -// import ApprovedUserCard from "./UserMenu"; -// import PendingUserCard from "./UserApprove"; -// import { User } from "../../../../middle-layer/types/User"; -// import { Pagination, ButtonGroup, IconButton } from "@chakra-ui/react"; -// import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; -// import { observer } from "mobx-react-lite"; -// import { getAppStore } from "../../external/bcanSatchel/store"; -// import { api } from "../../api"; -// import { Navigate } from "react-router-dom"; -// import { UserStatus } from "../../../../middle-layer/types/UserStatus"; -// import { useAuthContext } from "../../context/auth/authContext"; - -// // Did not change this to using the email/first name last name due to user page redesign so someone will be changing all of this anyway -// // Represents a specific tab to show on the user page -// enum UsersTab { -// PendingUsers, -// CurrentUsers, -// } - -// const fetchActiveUsers = async (): Promise => { -// try { -// const response = await api("/user/active", { -// method: "GET", -// }); - -// if (!response.ok) { -// throw new Error(`HTTP Error, Status: ${response.status}`); -// } - -// const activeUsers = await response.json(); -// return activeUsers as User[]; -// } catch (error) { -// console.error("Error fetching active users:", error); -// return []; // Return empty array on error -// } -// }; - -// const fetchInactiveUsers = async () => { -// try { -// const response = await api("/user/inactive", { method: "GET" }); -// if (!response.ok) { -// throw new Error(`HTTP Error, Status: ${response.status}`); -// } -// const inactiveUsers = await response.json(); -// return inactiveUsers as User[]; -// } catch (error) { -// console.error("Error fetching active users:", error); -// } -// }; - -// const ITEMS_PER_PAGE = 8; - -// const Users = observer(() => { -// const store = getAppStore(); -// const { user } = useAuthContext(); - -// useEffect(() => { -// const fetchUsers = async () => { -// const active = await fetchActiveUsers(); -// const inactive = await fetchInactiveUsers(); -// if (active) { -// store.activeUsers = active; -// } -// if (inactive) { -// store.inactiveUsers = inactive; -// } -// }; -// fetchUsers(); -// }, []); - -// const [usersTabStatus, setUsersTabStatus] = useState( -// UsersTab.CurrentUsers -// ); -// const [currentPage, setCurrentPage] = useState(1); - -// const filteredUsers = -// usersTabStatus === UsersTab.PendingUsers -// ? store.inactiveUsers -// : store.activeUsers; - -// const numInactiveUsers = store.inactiveUsers.length; -// const numUsers = filteredUsers.length; -// const pageStartIndex = (currentPage - 1) * ITEMS_PER_PAGE; -// const pageEndIndex = -// pageStartIndex + ITEMS_PER_PAGE > numUsers -// ? numUsers -// : pageStartIndex + ITEMS_PER_PAGE; -// const currentPageUsers = filteredUsers.slice(pageStartIndex, pageEndIndex); - -// return user ? ( -// user?.position !== UserStatus.Inactive ? ( -//
-//
-//

-// {usersTabStatus === UsersTab.CurrentUsers -// ? "All Users" -// : "Pending Users"} -//

-//

{numInactiveUsers} new users

-//
-//
-//
-// -// -//
-//
-// {usersTabStatus === UsersTab.CurrentUsers ? ( -// <> -//
-//

User ID

-//

Email

-//

Position

-//
-// {currentPageUsers.map((user) => ( -// -// ))} -// -// ) : ( -// <> -//
-//

User ID

-//

Email

-//

Position

-//
-//
-// {currentPageUsers.map((user) => ( -// -// ))} -// -// )} -//
-// { -// setCurrentPage(e.page); -// }} -// > -// -// -// -// -// -// -// -// {({ pages }) => -// pages.map((page, index) => -// page.type === "page" ? ( -// setCurrentPage(page.value)} -// aria-label={`Go to page ${page.value}`} -// > -// {page.value} -// -// ) : ( -// "..." -// ) -// ) -// } -// -// -// -// -// -// -// -// -//
-//
-// ) : ( -// -// ) -// ) : ( -// -// ); -// }); - -// export default Users; From 3b678c66c78d4a4fd27bd843b642a4944010e7d4 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Thu, 26 Feb 2026 00:04:55 -0500 Subject: [PATCH 24/25] Fixing placeholder search value --- frontend/src/components/SearchBar.tsx | 4 +++- frontend/src/main-page/grants/filter-bar/GrantSearch.tsx | 2 +- frontend/src/main-page/users/UserSearch.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 786bf3a..debac71 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -3,11 +3,13 @@ import { IoMdSearch } from "react-icons/io"; type SearchBarProps = { handleInputChange: (e: React.ChangeEvent) => void; userInput: string; + text: string; }; export default function SearchBar({ handleInputChange, userInput, + text, }: SearchBarProps) { return (
@@ -24,7 +26,7 @@ export default function SearchBar({ }} /> + ); } diff --git a/frontend/src/main-page/users/UserSearch.tsx b/frontend/src/main-page/users/UserSearch.tsx index 884b595..2f1c328 100644 --- a/frontend/src/main-page/users/UserSearch.tsx +++ b/frontend/src/main-page/users/UserSearch.tsx @@ -30,7 +30,7 @@ function UserSearch() { }; return ( - + ); } From bf6f4b61c1ae637db3fdeb0b8a439c8f42bc16a7 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Thu, 26 Feb 2026 00:14:43 -0500 Subject: [PATCH 25/25] notification popup border --- frontend/src/main-page/notifications/NotificationPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main-page/notifications/NotificationPopup.tsx b/frontend/src/main-page/notifications/NotificationPopup.tsx index 846faa2..9c8eabf 100644 --- a/frontend/src/main-page/notifications/NotificationPopup.tsx +++ b/frontend/src/main-page/notifications/NotificationPopup.tsx @@ -51,7 +51,7 @@ const NotificationPopup: React.FC = observer(({ return createPortal( -
+

Alerts