From f941ea6db392ce97451a0be84f076807724ee0e2 Mon Sep 17 00:00:00 2001 From: Wayne Ngo Date: Fri, 2 Jan 2026 21:23:00 -0800 Subject: [PATCH] ui page for admin or officers --- .../models/PermissionRequest.js | 6 +- .../routes/PermissionRequest.js | 35 ++++- src/APIFunctions/PermissionRequest.js | 97 ++++++++++++- src/Pages/LedSign/LedSign.js | 135 +++++++++++++++++- 4 files changed, 261 insertions(+), 12 deletions(-) diff --git a/api/main_endpoints/models/PermissionRequest.js b/api/main_endpoints/models/PermissionRequest.js index 5d42f4362..b88e1f067 100644 --- a/api/main_endpoints/models/PermissionRequest.js +++ b/api/main_endpoints/models/PermissionRequest.js @@ -14,6 +14,10 @@ const PermissionRequestSchema = new Schema( enum: Object.values(PermissionRequestTypes), required: true, }, + approved: { + type: Boolean, + default: false, + }, deletedAt: { type: Date, default: null, @@ -23,7 +27,7 @@ const PermissionRequestSchema = new Schema( ); // Compound unique index prevents duplicate active requests per user+type -PermissionRequestSchema.index({ userId: 1, type: 1 }, { unique: true, partialFilterExpression: { deletedAt: null }}); +PermissionRequestSchema.index({ userId: 1, type: 1 }, { unique: true, partialFilterExpression: { deletedAt: null, approved: false }}); module.exports = mongoose.model('PermissionRequest', PermissionRequestSchema); diff --git a/api/main_endpoints/routes/PermissionRequest.js b/api/main_endpoints/routes/PermissionRequest.js index b2ad88eb9..e3a634df1 100644 --- a/api/main_endpoints/routes/PermissionRequest.js +++ b/api/main_endpoints/routes/PermissionRequest.js @@ -39,17 +39,20 @@ router.get('/get', async (req, res) => { try { const query = { deletedAt: null }; - // If theres no userId, return all for officers and admins + // If theres no userId, return all for officers and admins (only pending requests) if (!queryUserId) { if (!isOfficer) { return res.sendStatus(UNAUTHORIZED); } + // For admin view, only show pending (non-approved) requests + query.approved = false; } else { // If there is a userId, check their perms if (!isOfficer && queryUserId !== decoded.token._id.toString()) { return res.sendStatus(FORBIDDEN); } query.userId = queryUserId; + // For member's own request, return it regardless of approval status } // If there is a type, filter by it @@ -68,6 +71,36 @@ router.get('/get', async (req, res) => { } }); +router.post('/approve', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) return res.sendStatus(decoded.status); + + const { type, _id } = req.body; + if (!type || !Object.keys(PermissionRequestTypes).includes(type)) { + return res.status(BAD_REQUEST).send({ error: 'Invalid type' }); + } + + if (!_id) { + return res.status(BAD_REQUEST).send({ error: '_id is required' }); + } + + try { + const request = await PermissionRequest.findOne({ + _id, + type, + deletedAt: null, + }); + + if (!request) return res.sendStatus(NOT_FOUND); + request.approved = true; + await request.save(); + res.sendStatus(OK); + } catch (error) { + logger.error('Failed to approve permission request:', error); + res.sendStatus(SERVER_ERROR); + } +}); + router.post('/delete', async (req, res) => { const decoded = await decodeToken(req, membershipState.MEMBER); if (decoded.status !== OK) return res.sendStatus(decoded.status); diff --git a/src/APIFunctions/PermissionRequest.js b/src/APIFunctions/PermissionRequest.js index 85adcc35a..9d37682e2 100644 --- a/src/APIFunctions/PermissionRequest.js +++ b/src/APIFunctions/PermissionRequest.js @@ -1,10 +1,13 @@ import { ApiResponse } from './ApiResponses'; import { BASE_API_URL } from '../Enums'; -export async function getPermissionRequest(type, token) { +export async function getPermissionRequest(type, userId, token) { const status = new ApiResponse(); const url = new URL('/api/PermissionRequest/get', BASE_API_URL); url.searchParams.append('type', type); + if (userId) { + url.searchParams.append('userId', userId); + } try { const res = await fetch(url.toString(), { @@ -15,11 +18,11 @@ export async function getPermissionRequest(type, token) { if (res.ok) { const data = await res.json(); - status.responseData = data; - } else if (res.status === 404) { - status.responseData = null; + // API returns an array, return first item or null + status.responseData = Array.isArray(data) && data.length > 0 ? data[0] : null; } else { status.error = true; + status.responseData = null; } } catch (err) { status.responseData = err; @@ -43,6 +46,38 @@ export async function createPermissionRequest(type, token) { body: JSON.stringify({ type }), }); + if (res.ok) { + // API returns 200 with no body, so we just mark success + status.responseData = true; + } else if (res.status === 409) { + // CONFLICT - duplicate request + status.error = true; + status.responseData = 'Request already exists'; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + +export async function getAllPermissionRequests(type, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/get', BASE_API_URL); + if (type) { + url.searchParams.append('type', type); + } + + try { + const res = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (res.ok) { const data = await res.json(); status.responseData = data; @@ -57,3 +92,57 @@ export async function createPermissionRequest(type, token) { return status; } +export async function approvePermissionRequest(type, id, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/approve', BASE_API_URL); + + try { + const res = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ type, _id: id }), + }); + + if (res.ok) { + status.responseData = true; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + +export async function deletePermissionRequest(type, id, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/delete', BASE_API_URL); + + try { + const res = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ type, _id: id }), + }); + + if (res.ok) { + status.responseData = true; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + diff --git a/src/Pages/LedSign/LedSign.js b/src/Pages/LedSign/LedSign.js index 1b9b1baed..176aa7fa3 100644 --- a/src/Pages/LedSign/LedSign.js +++ b/src/Pages/LedSign/LedSign.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { healthCheck, updateSignText } from '../../APIFunctions/LedSign'; -import { getPermissionRequest, createPermissionRequest } from '../../APIFunctions/PermissionRequest'; +import { getPermissionRequest, createPermissionRequest, getAllPermissionRequests, approvePermissionRequest, deletePermissionRequest } from '../../APIFunctions/PermissionRequest'; import { useSCE } from '../../Components/context/SceContext'; import { membershipState } from '../../Enums'; @@ -25,6 +25,10 @@ function LedSign() { const [permissionRequest, setPermissionRequest] = useState(null); const [checkingPermission, setCheckingPermission] = useState(false); const [requestingPermission, setRequestingPermission] = useState(false); + const isOfficer = user.accessLevel >= membershipState.OFFICER; + const [tab, setTab] = useState('sign'); + const [allPermissionRequests, setAllPermissionRequests] = useState([]); + const [loadingRequests, setLoadingRequests] = useState(false); const inputArray = [ { title: 'Sign Text:', @@ -220,7 +224,7 @@ function LedSign() { async function checkPermission() { if (user.accessLevel < membershipState.OFFICER) { setCheckingPermission(true); - const result = await getPermissionRequest('LED_SIGN', user.token); + const result = await getPermissionRequest('LED_SIGN', user._id, user.token); if (!result.error && result.responseData) { setPermissionRequest(result.responseData); } @@ -231,7 +235,14 @@ function LedSign() { checkSignHealth(); checkPermission(); // eslint-disable-next-line - }, []) + }, []); + + useEffect(() => { + if (isOfficer && tab === 'requests') { + fetchAllPermissionRequests(); + } + // eslint-disable-next-line + }, [tab, isOfficer]); if (loading || checkingPermission) { return ( @@ -253,7 +264,10 @@ function LedSign() { setRequestingPermission(true); const result = await createPermissionRequest('LED_SIGN', user.token); if (!result.error) { - setPermissionRequest(result.responseData); + const fetchResult = await getPermissionRequest('LED_SIGN', user._id, user.token); + if (!fetchResult.error && fetchResult.responseData) { + setPermissionRequest(fetchResult.responseData); + } } setRequestingPermission(false); } @@ -271,7 +285,7 @@ function LedSign() { ); } - if (permissionRequest) { + if (permissionRequest && !permissionRequest.approved) { return (

@@ -311,6 +325,95 @@ function LedSign() { return (11 - scrollSpeed); } + function getSelectedClassName(currTab) { + return currTab === tab + ? 'p-2 bg-gray-100 dark:bg-gray-700 rounded-md dark:text-white text-gray-700' + : 'p-2 hover:bg-gray-400 rounded-md dark:text-white text-gray-700'; + } + + async function fetchAllPermissionRequests() { + if (!isOfficer) return; + setLoadingRequests(true); + const result = await getAllPermissionRequests('LED_SIGN', user.token); + if (!result.error && result.responseData) { + setAllPermissionRequests(result.responseData); + } + setLoadingRequests(false); + } + + async function handleApprove(requestId) { + const result = await approvePermissionRequest('LED_SIGN', requestId, user.token); + if (!result.error) await fetchAllPermissionRequests(); + } + + async function handleDeny(requestId) { + const result = await deletePermissionRequest('LED_SIGN', requestId, user.token); + if (!result.error) await fetchAllPermissionRequests(); + } + + function formatUserName(userData) { + if (!userData) return 'Unknown User'; + const name = `${userData.firstName || ''} ${userData.lastName || ''}`.trim(); + return name || 'Unknown User'; + } + + function MaybeRenderListOfSignRequests() { + if (!isOfficer) return null; + if (loadingRequests) { + return ( +

+

Loading requests...

+
+ ); + } + + return ( +
+
+ + + + + + + + + + + {allPermissionRequests.length === 0 ? ( + + + + ) : ( + allPermissionRequests.map((request) => ( + + + + + + + )) + )} + +
User NameEmailRequest DateActions
+ No pending requests +
+ {formatUserName(request.userId)} + {request.userId?.email || 'N/A'}{getFormattedTime(request.createdAt)} +
+ + +
+
+
+
+ ); + } + const hasAccess = user.accessLevel >= membershipState.OFFICER; return ( @@ -321,7 +424,24 @@ function LedSign() {
)} {hasAccess && ( -
+ <> +
+ +
 | 
+ +
+ {tab === 'sign' && ( +
@@ -391,6 +511,9 @@ function LedSign() {
+ )} + {tab === 'requests' && } + )}
);