diff --git a/src/APIFunctions/SCEvents.js b/src/APIFunctions/SCEvents.js index 3e2cd82bd..7f9b999e3 100644 --- a/src/APIFunctions/SCEvents.js +++ b/src/APIFunctions/SCEvents.js @@ -20,7 +20,7 @@ export async function getAllSCEvents(token) { status.error = true; } } catch (err) { - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; status.error = true; } return status; @@ -44,7 +44,7 @@ export async function getEventByID(id, token) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -64,7 +64,7 @@ export async function getEventAttendanceSummary(id, token) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -87,7 +87,7 @@ export async function createSCEvent(token, eventBody) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -110,14 +110,57 @@ export async function updateSCEvent(id, token, eventUpdates) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } -export async function registerForSCEvent(eventId, token, payload) { +export async function getEventRegistrations(eventId, token, { limit = 50, offset = 0 } = {}) { const status = new ApiResponse(); + try { + const url = new URL(`${SCEVENTS_API_URL}/events/${eventId}/registrations`); + url.searchParams.set('limit', String(limit)); + url.searchParams.set('offset', String(offset)); + const res = await fetch(url.href, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} + +export async function getEventRegistrationByRequestId(eventId, requestId, token) { + const status = new ApiResponse(); + try { + const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/registrations/${requestId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} + +export async function registerForEvent(eventId, token, payload) { + const status = new ApiResponse(); try { const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/register`, { method: 'POST', @@ -127,18 +170,36 @@ export async function registerForSCEvent(eventId, token, payload) { }, body: JSON.stringify(payload), }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} - const body = await res.json(); - status.responseData = body; +export async function getMyEventRegistrationState(eventId, token) { + const status = new ApiResponse(); + try { + const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/registration/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; if (!res.ok) { status.error = true; } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } - return status; } diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index 679c54e9e..e18b75c14 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -42,16 +42,17 @@ export default function UserNavBar(props) { }, ]; - const sceventsAdminNavLinks = []; - sceventsAdminNavLinks.push({ - title: 'Events', - route: '/events', - icon: ( - - ), - }); + const sceventsAdminNavLinks = [ + { + title: 'Events', + route: '/events', + icon: ( + + ), + }, + ]; const adminLinks = [ { diff --git a/src/Pages/Events/CalendarView.js b/src/Pages/Events/CalendarView.js index 69dc9fc21..008b009ff 100644 --- a/src/Pages/Events/CalendarView.js +++ b/src/Pages/Events/CalendarView.js @@ -1,6 +1,7 @@ import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { getEventAttendanceSummary, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; +import { getEventAttendanceSummary, getMyEventRegistrationState, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; +import { membershipState } from '../../Enums'; // ─── tiny helpers ──────────────────────────────────────────────────────────── @@ -199,6 +200,29 @@ function getRegistrationCta(event) { } } +/** + * Checks if a user has permission to manage an event. + * @param {Object} event The event object. + * @param {Object} user The user object. + * @returns {boolean} True if the user can manage the event. + */ +function canUserManageEvent(event, user) { + const userId = user?._id != null ? String(user._id) : ''; + const eventAdmins = Array.isArray(event.admins) ? event.admins.map((id) => String(id)) : []; + + // 1. If user is explicitly listed as an admin for this event + if (eventAdmins.length > 0 && userId && eventAdmins.includes(userId)) { + return true; + } + + // 2. If the event has no admins, allow users with level 3 (ADMIN) or higher to manage it + if (eventAdmins.length === 0 && (user?.accessLevel ?? 0) >= membershipState.ADMIN) { + return true; + } + + return false; +} + // ─── icons ──────────────────────────────────────────────────────────────────── function ChevronLeft() { @@ -275,8 +299,6 @@ function EventPopup({ event, onClose, isAdminView, user }) { const popupRef = useRef(null); const colors = pillColors(event, isAdminView); const badgeText = getBadgeText(event, isAdminView); - const eventId = event.id || event._id; - const authToken = user?.token || window.localStorage.getItem('jwtToken'); const userId = user?._id != null ? String(user._id) : ''; const [attendeeCount, setAttendeeCount] = useState(null); const [attendanceLoading, setAttendanceLoading] = useState(false); @@ -284,10 +306,11 @@ function EventPopup({ event, onClose, isAdminView, user }) { const [waitlistSubmitting, setWaitlistSubmitting] = useState(false); const [waitlistMessage, setWaitlistMessage] = useState(''); const [waitlistError, setWaitlistError] = useState(''); - const canEditEvent = - Array.isArray(event.admins) && - userId && - event.admins.includes(userId); + const canManageEvent = canUserManageEvent(event, user); + const [hasRegistered, setHasRegistered] = useState(false); + const [isCheckingRegistration, setIsCheckingRegistration] = useState(false); + const eventId = event?.id || event?._id; + const authToken = user?.token; const maxAttendees = Number(event.max_attendees); const hasCapacityLimit = Number.isFinite(maxAttendees) && maxAttendees > 0; const remainingSpots = @@ -297,7 +320,7 @@ function EventPopup({ event, onClose, isAdminView, user }) { const registrationCta = getRegistrationCta(event); const isFull = hasCapacityLimit && typeof remainingSpots === 'number' && remainingSpots <= 0; const shouldShowWaitlistJoin = - !canEditEvent && + !canManageEvent && event.status === 'published' && !registrationCta.disabled && isFull && @@ -311,6 +334,28 @@ function EventPopup({ event, onClose, isAdminView, user }) { return () => document.removeEventListener('keydown', onKey); }, [onClose]); + useEffect(() => { + async function fetchMyRegistrationState() { + if (!userId || canManageEvent || !user?.token || event.status !== 'published') { + setHasRegistered(false); + setIsCheckingRegistration(false); + return; + } + + setIsCheckingRegistration(true); + const eventID = event?.id || event?._id; + const response = await getMyEventRegistrationState(eventID, user.token); + if (!response.error) { + setHasRegistered(Boolean(response.responseData?.registered)); + } else { + setHasRegistered(false); + } + setIsCheckingRegistration(false); + } + + fetchMyRegistrationState(); + }, [canManageEvent, event, user?.token, userId]); + useEffect(() => { function onClickOutside(e) { if (popupRef.current && !popupRef.current.contains(e.target)) { @@ -386,7 +431,6 @@ function EventPopup({ event, onClose, isAdminView, user }) { setWaitlistMessage('Joined waitlist successfully.'); } - return (
- {attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} registered + {event.max_attendees} spot{event.max_attendees !== 1 ? 's' : ''} available {event.waitlist_enabled && ( · waitlist available )}
)} - {hasCapacityLimit && !canEditEvent && typeof remainingSpots === 'number' && ( + {hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && ({remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left {event.waitlist_enabled && ( @@ -483,41 +527,49 @@ function EventPopup({ event, onClose, isAdminView, user }) {
)} - {hasCapacityLimit && !canEditEvent && attendanceLoading && ( + {hasCapacityLimit && !canManageEvent && attendanceLoading && (Loading live spots...
)} - {hasCapacityLimit && !canEditEvent && attendanceLoaded && typeof remainingSpots !== 'number' && ( + {hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && (Unable to load live spots
)} - - {canEditEvent && ( - - Edit event - + {canManageEvent && ( +{label}
+{value}
+Event ID: {id}
+Loading attendees...
} + {!isLoadingList && listError &&{listError}
} + + {!isLoadingList && !listError && ( +Click an attendee to open details
+No attendees found for this event yet.
+ ) : ( +Select an attendee to view answers.
} + {isLoadingDetail &&Loading attendee detail...
} + {!isLoadingDetail && detailError &&{detailError}
} + {!isLoadingDetail && !detailError && selectedAttendee && ( +Name: {selectedAttendee.registrant?.name || 'N/A'}
+Email: {selectedAttendee.registrant?.email || 'N/A'}
+Status: {selectedAttendee.status || 'N/A'}
+Submitted: {formatDateTime(selectedAttendee.created_at)}
+No answers submitted.
+ ) : ( + selectedAnswers.map(([fieldKey, value]) => ( +{fieldKey}
+