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...
+| User Name | +Request Date | +Actions | +|
|---|---|---|---|
| + No pending requests + | +|||
| + {formatUserName(request.userId)} + | +{request.userId?.email || 'N/A'} | +{getFormattedTime(request.createdAt)} | +
+
+
+
+
+ |
+