diff --git a/src/Logo.svg b/public/Logo.svg similarity index 100% rename from src/Logo.svg rename to public/Logo.svg diff --git a/src/App.css b/src/App.css index e69de29..a15fb3f 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,3 @@ +.Windows-PSOption { + color: black; +} \ No newline at end of file diff --git a/src/components/Authentication/SignUp.jsx b/src/components/Authentication/SignUp.jsx index 7cc56a4..5b9168c 100644 --- a/src/components/Authentication/SignUp.jsx +++ b/src/components/Authentication/SignUp.jsx @@ -48,7 +48,7 @@ const SignUp = () => { await registerWithEmailAndPassword(email, password, userType, navigate, '/awaitConfirmation', firstName, lastName); // send email to Debbie - const subject = "placeholder"; + const subject = "New User Created Account"; const newEmail = email; await sendEmail(subject, newEmail, emailtemplate); diff --git a/src/components/Catalog/DeleteEventModal/DeleteEventModal.jsx b/src/components/Catalog/DeleteEventModal/DeleteEventModal.jsx index 0657248..056018b 100644 --- a/src/components/Catalog/DeleteEventModal/DeleteEventModal.jsx +++ b/src/components/Catalog/DeleteEventModal/DeleteEventModal.jsx @@ -26,12 +26,12 @@ const DeleteEventModal = ({ isOpen, onClose, deleteItemId, setDataShouldRevalida - Confirm Delete + Remove Event? - Are you sure you want to delete this row? + Are you sure you want to delete this event? You cannot undo this action afterward. - - + + diff --git a/src/components/Catalog/SearchFilter/filterOptions.js b/src/components/Catalog/SearchFilter/filterOptions.js index 7ef251c..f32aefd 100644 --- a/src/components/Catalog/SearchFilter/filterOptions.js +++ b/src/components/Catalog/SearchFilter/filterOptions.js @@ -6,7 +6,6 @@ const seasonOptions = [ const yearOptions = [ { value: 'junior', name: 'Junior' }, { value: 'senior', name: 'Senior' }, - { value: 'both', name: 'Both' }, ]; const subjectOptions = [ { value: 'life skills', name: 'Life Skills' }, diff --git a/src/components/EmailTemplates/emailtemplate.jsx b/src/components/EmailTemplates/emailtemplate.jsx index cafbeab..f5bf224 100644 --- a/src/components/EmailTemplates/emailtemplate.jsx +++ b/src/components/EmailTemplates/emailtemplate.jsx @@ -1,13 +1,15 @@ //import React from 'react'; -import { Email, Item, Span, A } from 'react-html-email'; +import { Email, Item, Span } from 'react-html-email'; const emailTemplate = ({newEmail}) => ( Hello Debbie, - A user with the email {newEmail} has created an account and is waiting for your approval. - react-html-email. + + A user with the email {newEmail} has created an account and is waiting for your approval. + + Thank you. diff --git a/src/components/Events/DailyEvent.jsx b/src/components/Events/DailyEvent.jsx index 071458b..d5e60e1 100644 --- a/src/components/Events/DailyEvent.jsx +++ b/src/components/Events/DailyEvent.jsx @@ -1,10 +1,19 @@ import PropTypes from 'prop-types'; -import { Box, Flex, Text, Grid, Button, Spacer } from '@chakra-ui/react'; +import { Box, Flex, Text, Grid, Button, Spacer, Badge, VStack } from '@chakra-ui/react'; import { useState } from 'react'; import { NPOBackend } from '../../utils/auth_utils.js'; +import AUTH_ROLES from '../../utils/auth_config.js'; +import { useAuthContext } from '../../common/AuthContext.jsx'; +const { USER_ROLE } = AUTH_ROLES.AUTH_ROLES; -const DailyEvent = ({ id, startTime, endTime, eventTitle, confirmed, description }) => { +const DailyEvent = ({ id, startTime, endTime, eventTitle, confirmed, description, eventId }) => { const [confirmEvent, setConfirmEvent] = useState(confirmed); + const [cohort, setCohort] = useState(undefined); + const [cohortBadgeColor, setCohortBadgeColor] = useState(undefined); + const [cohortTextColor, setCohortTextColor] = useState(undefined); + const [cohortBorderColor, setCohortBorderColor] = useState(undefined); + const [cohortBorder, setCohortBorder] = useState(undefined); + const {currentUser} = useAuthContext(); let border_color = '#2B93D1'; let background_color = '#F7FAFC'; @@ -13,6 +22,42 @@ const DailyEvent = ({ id, startTime, endTime, eventTitle, confirmed, description background_color = '#FEF1DC'; } + const getCohort = async () => { + if (eventId) { + try { + const { data } = await NPOBackend.get(`/catalog/${eventId}`); + let year = data[0].year; + if( year.length == 2) + { + setCohort('Both'); + setCohortBadgeColor("#FFFFFF"); + setCohortTextColor("#4A5568"); + setCohortBorderColor("#4A5568"); + setCohortBorder("outline"); + } + else if(year[0] == 'junior') + { + setCohort('Junior'); + setCohortBadgeColor("#CBD5E0"); + setCohortTextColor("#171923"); + setCohortBorderColor("#CBD5E0"); + setCohortBorder("simple"); + } + else { + setCohort('Senior'); + setCohortBadgeColor("#4A5568"); + setCohortTextColor("#FFFFFF"); + setCohortBorderColor("#4A5568"); + setCohortBorder("simple"); + } + } catch (error) { + console.error('Error fetching data:', error); + } + } + } + + getCohort(); + const handleConfirm = async () => { const date = new Date(); try { @@ -50,17 +95,43 @@ const DailyEvent = ({ id, startTime, endTime, eventTitle, confirmed, description + + + {cohort} + + {!confirmEvent && ( - +
- -
-
+ +
)} +
); @@ -73,6 +144,7 @@ DailyEvent.propTypes = { eventTitle: PropTypes.string.isRequired, description: PropTypes.string, confirmed: PropTypes.bool.isRequired, + eventId: PropTypes.number.isRequired, }; export default DailyEvent; diff --git a/src/components/Events/EventInfo.jsx b/src/components/Events/EventInfo.jsx index 3f1a3c6..785fc09 100644 --- a/src/components/Events/EventInfo.jsx +++ b/src/components/Events/EventInfo.jsx @@ -4,11 +4,15 @@ import { DeleteIcon } from '@chakra-ui/icons'; import { LuPen } from 'react-icons/lu'; import PlannerModal from '../Planner/PlannerModal'; import { NPOBackend } from '../../utils/auth_utils'; +import AUTH_ROLES from '../../utils/auth_config.js'; +import { useAuthContext } from '../../common/AuthContext.jsx'; +const { USER_ROLE } = AUTH_ROLES.AUTH_ROLES; const EventInfo = ({ dayId, eventDate, day, startTime, endTime, location, notes, setShouldDataRevalidate }) => { const { isOpen: isOpenPlanner, onOpen: onOpenPlanner, onClose: onClosePlanner } = useDisclosure(); const { isOpen: isOpenDelete, onOpen: onOpenDelete, onClose: onCloseDelete } = useDisclosure(); const toast = useToast(); + const {currentUser} = useAuthContext(); const handlePlannerClose = () => { setShouldDataRevalidate(true); @@ -43,16 +47,17 @@ const EventInfo = ({ dayId, eventDate, day, startTime, endTime, location, notes, {day} {startTime} - {endTime} - {location} + Location: + {location} - Details: + Details: {notes ? notes : 'No notes.'} - + - + diff --git a/src/components/Events/Events.jsx b/src/components/Events/Events.jsx index fd3a462..f1df953 100644 --- a/src/components/Events/Events.jsx +++ b/src/components/Events/Events.jsx @@ -5,6 +5,8 @@ import { Grid } from '@chakra-ui/react'; const Events = ({ eventData }) => { + // console.log("Event Data", eventData); + const formatDate = (date) => { let time = date.split(":"); let hours = time[0]; @@ -36,7 +38,18 @@ const Events = ({ eventData }) => { eventDataWithBreaks.push(currentEvent); const currEnd = currentEvent.endTime.split(':').slice(0,2).join(":"); const nextStart = nextEvent.startTime.split(':').slice(0,2).join(":"); - if (currEnd < nextStart) { + + const endHour = currEnd.split(':')[0] + const endMin = currEnd.split(':')[1] + const convertTimeToMin = (hour, min) => { + return parseInt(hour) * 60 + parseInt(min); + }; + const startHour = nextStart.split(':')[0] + const startMin = nextStart.split(':')[1] + + const timeDiff = convertTimeToMin(startHour, startMin) - convertTimeToMin(endHour, endMin); + if (currEnd < nextStart && timeDiff >= 5) { + console.log("break"); eventDataWithBreaks.push({ id: maxId, startTime: currentEvent.endTime, @@ -62,6 +75,7 @@ const Events = ({ eventData }) => { eventTitle={item.title} description={item.description} confirmed={item.confirmed} + eventId={item.eventId} /> ))} diff --git a/src/components/Events/PublishedScheduleTable.jsx b/src/components/Events/PublishedScheduleTable.jsx index 6a72151..f6ec432 100644 --- a/src/components/Events/PublishedScheduleTable.jsx +++ b/src/components/Events/PublishedScheduleTable.jsx @@ -5,20 +5,22 @@ import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Box, IconButton, useDisclosure } from '@chakra-ui/react'; import AddDayModal from '../../pages/PublishedSchedule/AddDayModal.jsx' +import StatModal from '../../pages/PublishedSchedule/StatisticsModal.jsx'; import { AddIcon } from '@chakra-ui/icons'; +import { IoStatsChart } from "react-icons/io5"; import { useAuthContext } from '../../common/AuthContext.jsx'; import AUTH_ROLES from '../../utils/auth_config.js'; const { ADMIN_ROLE } = AUTH_ROLES.AUTH_ROLES; -const PublishedScheduleTable = ({ season }) => { +const PublishedScheduleTable = ({ season, allSeasons }) => { const {currentUser} = useAuthContext(); - const [eventsInDay, setEventsInDay] = useState([]); const seasonType = season.split(' ')[0]; const seasonYear = season.split(' ')[1]; const [dataShouldRevalidate, setShouldDataRevalidate] = useState(false); const { isOpen: isOpenDay, onOpen: onOpenDay, onClose: onCloseDay } = useDisclosure(); + const { isOpen: isOpenStats, onOpen: onOpenStats, onClose: onCloseStats } = useDisclosure(); const renderTable = async () => { const { data } = await NPOBackend.get( @@ -58,6 +60,24 @@ const PublishedScheduleTable = ({ season }) => { return ( {currentUser.type === ADMIN_ROLE && + <> + } + > + Stats + + { bottom="2rem" right={{ base: '1rem', lg: '2rem', xl: '3rem' }} fontSize="0.75rem" - w="3rem" - h="3rem" + w="3.25rem" + h="3.25rem" _hover={{ bgColor: 'blue.500' }} onClick={onOpenDay} icon={} > Create + } + { season && allSeasons && + + } - + @@ -115,6 +139,7 @@ const PublishedScheduleTable = ({ season }) => { PublishedScheduleTable.propTypes = { season: PropTypes.string.isRequired, + allSeasons: PropTypes.array.isRequired, }; export default PublishedScheduleTable; diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx index 8ab3a2c..d53032a 100644 --- a/src/components/Navbar/Navbar.jsx +++ b/src/components/Navbar/Navbar.jsx @@ -47,7 +47,7 @@ const Navbar = ({ hasLoaded, isAdmin }) => { position={'sticky'} as={'nav'} > - + {makeNavTabs('Schedule', '/publishedSchedule')} {makeNavTabs('Catalog', '/catalog')} {makeNavTabs('Accounts', '/accounts')} @@ -68,7 +68,7 @@ const Navbar = ({ hasLoaded, isAdmin }) => { position={'sticky'} as={'nav'} > - + {makeNavTabs('Schedule', '/publishedSchedule')} diff --git a/src/components/Notifications/AccountNotification.jsx b/src/components/Notifications/AccountNotification.jsx index cf4e147..1253cae 100644 --- a/src/components/Notifications/AccountNotification.jsx +++ b/src/components/Notifications/AccountNotification.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { AccountNotificationBlock } from './NotificationElement'; import { @@ -13,11 +13,21 @@ import { Grid, GridItem, Text, - useToast, + Link, + // useToast, } from '@chakra-ui/react'; -const AccountNotification = ({ notificationBlock, today, removeEntry }) => { - const toast = useToast(); +const AccountNotification = ({ + notificationBlock, + today, + removeEntry, + approveAfterTimer, + idToRemove, + setApproveAfterTimer, + declineAfterTimer, + setDeclineAfterTimer, +}) => { + // const toast = useToast(); const [accounts, setAccounts] = useState(notificationBlock.getNotificationData().accounts); const [disableChildrenButtons, setDisableChildrenButtons] = useState(false); @@ -25,39 +35,79 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { const diffTime = today - blockDate; const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - const acceptAll = async accounts => { - await Promise.all( - accounts.map(async account => { - await account.approveCallback(); - }), - ); - - toast({ - title: `Approved ${accounts?.length} accounts.`, - status: 'success', - duration: 9000, - isClosable: true, - }); + const [removeNotificationBlock, setRemoveNotificationBlock] = useState(false); + const [timeoutId, setTimeoutId] = useState(undefined); - removeEntry(notificationBlock.key); + const acceptAll = async accounts => { + const timeId = setTimeout(async () => { + await Promise.all( + accounts.map(async account => { + await account.approveCallback(); + }), + ); + setRemoveNotificationBlock(true); + setApproveAfterTimer(true); + }, 5000); // set 5 sec timer for accept all requests + setTimeoutId(timeId); }; const declineAll = async accounts => { - await Promise.all( - accounts.map(async account => { - await account.declineCallback(); - }), - ); + const timeId = setTimeout(async () => { + await Promise.all( + accounts.map(async account => { + await account.declineCallback(); + }), + ); + setRemoveNotificationBlock(true); + setDeclineAfterTimer(true); + }, 5000); // set 5 sec timer for accept all requests + setTimeoutId(timeId); + }; - toast({ - title: `Declined ${accounts?.length} accounts.`, - status: 'info', - duration: 9000, - isClosable: true, - }); - removeEntry(notificationBlock.key); + const undoAll = async () => { + clearTimeout(timeoutId); + setTimeoutId(undefined); + setRemoveNotificationBlock(false); + setApproveAfterTimer(false); + setDeclineAfterTimer(false); }; + useEffect(() => { + if (approveAfterTimer && removeNotificationBlock) { + removeEntry(notificationBlock.key); + // toast({ + // title: `Approved.`, + // status: 'success', + // duration: 9000, + // isClosable: true, + // }); + } else if (declineAfterTimer && removeNotificationBlock) { + removeEntry(notificationBlock.key); + // toast({ + // title: `Declined.`, + // status: 'info', + // duration: 9000, + // isClosable: true, + // }); + } else if (approveAfterTimer) { + setAccounts(accounts => accounts.filter(account => account.id !== idToRemove)); + // toast({ + // title: `Approved.`, + // status: 'success', + // duration: 9000, + // isClosable: true, + // }); + } else if (declineAfterTimer) { + setAccounts(accounts => accounts.filter(account => account.id !== idToRemove)); + // toast({ + // title: `Declined.`, + // status: 'info', + // duration: 9000, + // isClosable: true, + // }); + } + }, [approveAfterTimer, declineAfterTimer, removeNotificationBlock]); + return (
@@ -66,7 +116,7 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { {/* No accordion for 1 account notification */} - {accounts?.[0]?.email}{' '} + {accounts?.[0]?.firstName} ({accounts?.[0]?.email}){' '} is requesting account approval... @@ -81,6 +131,9 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { declineCallback={() => { declineAll(accounts); }} + undoCallback={() => { + undoAll(accounts); + }} pl="3.25rem" /> @@ -89,7 +142,7 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { {/* Accordion for >1 account notification in block */} - {accounts?.[0]?.email} and {accounts?.length - 1} other + {accounts?.[0]?.firstName} and {accounts?.length - 1} other {accounts?.length - 1 > 1 && 's'}{' '} @@ -118,15 +171,22 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { setDisableChildrenButtons(true); declineAll(accounts); }} + undoCallback={() => { + setDisableChildrenButtons(false); + undoAll(accounts); + }} /> - {accounts?.map(({ id, email, approveCallback, declineCallback }) => ( + {accounts?.map(({ id, firstName, email, approveCallback, declineCallback, undoCallback }) => ( - - {email} + + {firstName}{' '} + + + ({email}) @@ -135,27 +195,12 @@ const AccountNotification = ({ notificationBlock, today, removeEntry }) => { declineText="Decline" acceptCallback={async () => { await approveCallback(); - toast({ - title: `Approved ${email}.`, - status: 'success', - duration: 9000, - isClosable: true, - }); - setAccounts(accounts => - accounts.filter(account => account.id !== id), - ); }} declineCallback={async () => { await declineCallback(); - toast({ - title: `Declined ${email}.`, - status: 'info', - duration: 9000, - isClosable: true, - }); - setAccounts(accounts => - accounts.filter(account => account.id !== id), - ); + }} + undoCallback={() => { + undoCallback(); }} disableChildrenButtons={disableChildrenButtons} /> @@ -185,6 +230,11 @@ AccountNotification.propTypes = { notificationBlock: PropTypes.instanceOf(AccountNotificationBlock), today: PropTypes.instanceOf(Date), removeEntry: PropTypes.func, + approveAfterTimer: PropTypes.bool, + idToRemove: PropTypes.string, + setApproveAfterTimer: PropTypes.func, + declineAfterTimer: PropTypes.bool, + setDeclineAfterTimer: PropTypes.func, }; const AccountButtonGroup = ({ @@ -192,47 +242,96 @@ const AccountButtonGroup = ({ acceptCallback, declineText, declineCallback, + undoCallback, disableChildrenButtons, ...chakraProps }) => { const [acceptState, setAcceptState] = useState(undefined); const [declineState, setDeclineState] = useState(undefined); + const [approveClick, setApproveClicked] = useState(false); + const [declineClick, setDeclineClicked] = useState(false); return ( - - - - + <> + {!approveClick && !declineClick ? ( + + + + + ) : ( + <> + {approveClick ? ( +
+ + Accepted + { + setAcceptState(undefined); + setDeclineState(undefined); + setApproveClicked(false); + undoCallback(); + }} + > + Undo + + +
+ ) : ( +
+ + Declined + { + setAcceptState(undefined); + setDeclineState(undefined); + setDeclineClicked(false); + undoCallback(); + }} + > + Undo + + +
+ )} + + )} + ); }; AccountButtonGroup.propTypes = { @@ -240,6 +339,7 @@ AccountButtonGroup.propTypes = { acceptCallback: PropTypes.func, declineText: PropTypes.string, declineCallback: PropTypes.func, + undoCallback: PropTypes.func, disableChildrenButtons: PropTypes.bool, chakraProps: PropTypes.any, }; diff --git a/src/components/Notifications/NotificationElement.js b/src/components/Notifications/NotificationElement.js index a0d0967..0d6c8f4 100644 --- a/src/components/Notifications/NotificationElement.js +++ b/src/components/Notifications/NotificationElement.js @@ -30,12 +30,14 @@ class AccountNotificationBlock extends NotificationBlock { this.pendingAccounts = []; } - addPendingAccount(id, email, approveCallback, declineCallback) { + addPendingAccount(id, firstName, email, approveCallback, declineCallback, undoCallback) { this.pendingAccounts.push({ id: id, + firstName: firstName, email: email, approveCallback: approveCallback, declineCallback: declineCallback, + undoCallback: undoCallback, }); } diff --git a/src/components/Notifications/Notifications.jsx b/src/components/Notifications/Notifications.jsx index b9f76cf..88d493c 100644 --- a/src/components/Notifications/Notifications.jsx +++ b/src/components/Notifications/Notifications.jsx @@ -18,22 +18,44 @@ const getDateFromISOString = isoString => { const Notifications = () => { const [notificationList, setNotificationList] = useState([]); + const [approveAfterTimer, setApproveAfterTimer] = useState(false); + const [declineAfterTimer, setDeclineAfterTimer] = useState(false); + const [idToRemove, setidToRemove] = useState(undefined); + let timeoutId = undefined; + const today = useMemo(() => new Date(), []); + const undoChanges = () => { + clearTimeout(timeoutId); + timeoutId = undefined; + }; + const approveAccount = async id => { - try { - await NPOBackend.put(`/users/approve/${id}`); - } catch (e) { - console.log(e); - } + // Start timer + const timeId = setTimeout(async () => { + try { + await NPOBackend.put(`/users/approve/${id}`); + } catch (e) { + console.log(e); + } + setidToRemove(id); + setApproveAfterTimer(true); + }, 5000); // 5 second delay + timeoutId = timeId; }; const declineAccount = async id => { - try { - await NPOBackend.delete(`/users/${id}`); - } catch (e) { - console.log(e); - } + // Start timer + const timeId = setTimeout(async () => { + try { + await NPOBackend.delete(`/users/${id}`); + } catch (e) { + console.log(e); + } + setidToRemove(id); + setDeclineAfterTimer(true); + }, 5000); // 5 second delay + timeoutId = timeId; }; const removeNotificationEntry = key => { @@ -53,7 +75,7 @@ const Notifications = () => { // Map MM-DD-YY string to AccountNotificationBlock const accountsMap = new Map(); - pendingAccounts.forEach(({ approvedOn, email, id }) => { + pendingAccounts.forEach(({ approvedOn, email, id, firstName }) => { const { dateObject, formattedDateString } = getDateFromISOString(approvedOn); let notificationBlock; @@ -68,6 +90,7 @@ const Notifications = () => { notificationBlock.addPendingAccount( id, + firstName, email, async () => { await approveAccount(id); @@ -75,6 +98,7 @@ const Notifications = () => { async () => { await declineAccount(id); }, + undoChanges, ); accountsMap.set(formattedDateString, notificationBlock); }); @@ -125,6 +149,11 @@ const Notifications = () => { notificationBlock={notificationBlock} today={today} removeEntry={removeNotificationEntry} + approveAfterTimer={approveAfterTimer} + idToRemove={idToRemove} + setApproveAfterTimer={setApproveAfterTimer} + declineAfterTimer={declineAfterTimer} + setDeclineAfterTimer={setDeclineAfterTimer} /> )} {notificationType === 'event' && ( diff --git a/src/components/Planner/PlannerEvents/PlannerEvents.jsx b/src/components/Planner/PlannerEvents/PlannerEvents.jsx index 48fc1a4..83e5210 100644 --- a/src/components/Planner/PlannerEvents/PlannerEvents.jsx +++ b/src/components/Planner/PlannerEvents/PlannerEvents.jsx @@ -128,9 +128,8 @@ const PlannerEvents = ({ onClose }) => { isDisabled={!plannedEvents.length} onClick={closeModal} > - Finish Day + Save and Exit -
diff --git a/src/components/StatTable/StatTable.css b/src/components/StatTable/StatTable.css new file mode 100644 index 0000000..a3f220d --- /dev/null +++ b/src/components/StatTable/StatTable.css @@ -0,0 +1,57 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.select-container { + margin-top: 20px; + margin-bottom: 20px; + display: flex; + justify-content: center; +} + +.table-container { + margin-top: 20px; + margin-bottom: 20px; + border: 1px solid #e1e4e8; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.table-container th { + background-color: #0e509c; + color: white; + padding: 15px; + text-align: center; +} + +.table-container tbody td { + border-bottom: 1px solid #e1e4e8; + padding: 15px; + text-align: center; +} + +.table-container tbody tr:last-child td:not(:first-child) { + background-color: #f0f8ff; + color: blue; +} + +.table-container tbody tr:not(:last-child) td:last-child { + background-color: #f0f8ff; + color: blue; +} + +.table-container tbody tr:last-child td:last-child { + border: none; +} + +.table-body tr:nth-child(even) { + background-color: #f8f9fa; +} + +.table-body tr:hover { + background-color: #e2f3ff; + transition: background-color 0.3s ease; +} diff --git a/src/components/StatTable/StatTable.jsx b/src/components/StatTable/StatTable.jsx new file mode 100644 index 0000000..12d4837 --- /dev/null +++ b/src/components/StatTable/StatTable.jsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { NPOBackend } from '../../utils/auth_utils'; +import { Box, Select } from '@chakra-ui/react'; +import './StatTable.css'; + +const StatTable = ({ season, allSeasons }) => { + const [stats, setStats] = useState([]); + const [selectedSeason, setSelectedSeason] = useState(season); + + useEffect(() => { + const fetchStats = async () => { + try { + const curSeason = selectedSeason.split(' ')[0]; + const curYear = selectedSeason.split(' ')[1]; + const response = await NPOBackend.get(`/published-schedule/stats?season=${curSeason.toLowerCase()}&year=${curYear}`); + const data = response.data; + setStats(data); + } catch (error) { + console.error('Error fetching statistics:', error); + } + }; + + fetchStats(); + }, [selectedSeason]); + + const transformData = () => { + const transformedData = {}; + + stats.forEach(stat => { + const eventType = stat.eventType; + const subject = stat.subject; + const totalCount = parseInt(stat.totalCount); + + if (!transformedData[eventType]) { + transformedData[eventType] = {}; + } + + transformedData[eventType][subject] = totalCount; + }); + + return transformedData; + }; + + const transformedStats = transformData(); + + return ( + + + + +
+
+ + + + {Object.keys(transformedStats).length > 0 && + Object.keys(transformedStats[Object.keys(transformedStats)[0]]).map(subject => ( + + ))} + + + + {Object.keys(transformedStats).map((eventType) => ( + + + + {Object.keys(transformedStats[eventType]).map(subject => ( + + ))} + + + ))} + +
Event Type{subject}
{eventType}{transformedStats[eventType][subject]}
+ +
+ ); +}; + +StatTable.propTypes = { + season: PropTypes.string, + year: PropTypes.string, + allSeasons: PropTypes.array, +}; + +export default StatTable; diff --git a/src/pages/PublishedSchedule/PublishedSchedule.jsx b/src/pages/PublishedSchedule/PublishedSchedule.jsx index 839c550..82067c7 100644 --- a/src/pages/PublishedSchedule/PublishedSchedule.jsx +++ b/src/pages/PublishedSchedule/PublishedSchedule.jsx @@ -2,10 +2,9 @@ import { NPOBackend } from '../../utils/auth_utils.js'; import PublishedScheduleTable from '../../components/Events/PublishedScheduleTable.jsx'; import AUTH_ROLES from '../../utils/auth_config.js'; import { useAuthContext } from '../../common/AuthContext.jsx'; - import { useEffect, useState } from 'react'; -const { ADMIN_ROLE, USER_ROLE } = AUTH_ROLES.AUTH_ROLES; import { Box, Select, Text } from '@chakra-ui/react'; +const { ADMIN_ROLE, USER_ROLE } = AUTH_ROLES.AUTH_ROLES; const PublishedSchedule = () => { // get data from database @@ -38,7 +37,7 @@ const PublishedSchedule = () => { data.unshift(curSeason); } - setSelectedSeason(currentUser.type === USER_ROLE ? curSeason : data[0]); // We assume the current season is the first one in the list + setSelectedSeason(curSeason); const seasonOrder = ['Fall', 'Summer', 'Spring']; data.sort((a, b) => { @@ -54,7 +53,7 @@ const PublishedSchedule = () => { }; renderTable(); - }, [currentUser]); + }, [currentUser, curSeason]); //update chakra table container accordingly return ( @@ -76,19 +75,22 @@ const PublishedSchedule = () => { textColor="transparent" onChange={(e) => setSelectedSeason(e.target.value)} width="23%" + visibility={currentUser.type === USER_ROLE ? 'hidden' : 'visible'} > { currentUser.type === ADMIN_ROLE && allSeasons.map(item => ( - + )) } {/* tables for each season */} {selectedSeason != '' ? ( - + ) : ( - + )} ); diff --git a/src/pages/PublishedSchedule/StatisticsModal.jsx b/src/pages/PublishedSchedule/StatisticsModal.jsx new file mode 100644 index 0000000..038d6fa --- /dev/null +++ b/src/pages/PublishedSchedule/StatisticsModal.jsx @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalBody, + ModalCloseButton, +} from '@chakra-ui/react'; +import StatTable from '../../components/StatTable/StatTable'; + +const StatModal = ({ isOpen, onClose, season, allSeasons }) => { + return ( + <> + + + + + + + + + + + ); +}; + +StatModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + season: PropTypes.string.isRequired, + allSeasons: PropTypes.array.isRequired, +}; + +export default StatModal;