diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index e6465406b..4e68e4689 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -509,4 +509,34 @@ router.post('/usersSubscribedAndVerified', function(req, res) { }); }); +// For Club Revenue page +router.post('/countMembers', async (req, res) => { + if (!checkIfTokenSent(req)) { + return res.sendStatus(FORBIDDEN); + } else if (!checkIfTokenValid(req)) { + return res.sendStatus(UNAUTHORIZED); + } + + const currentYear = new Date().getFullYear(); + const today = new Date(); + + let beginningOfSemester = new Date(currentYear, 5, 1); + let endOfThisSemester = new Date(currentYear + 1, 0, 1); + let endOfNextSemester = new Date(currentYear + 1, 5, 1); + + if (today < new Date(currentYear, 5, 1)) { + beginningOfSemester = new Date(currentYear, 0, 1); + endOfThisSemester = new Date(currentYear, 5, 1); + endOfNextSemester = new Date(currentYear + 1, 0, 1); + } + + const totalNewMembersThisYear = await User.countDocuments({ emailVerified: true, accessLevel: membershipState.MEMBER, joinDate: { $gte: new Date(currentYear, 0, 1), $lte: today } }); + const currentActiveMembers = await User.countDocuments({ emailVerified: true, accessLevel: membershipState.MEMBER, membershipValidUntil: { $gte: today } }); + const newSingleAndAnnualMembers = await User.countDocuments({ emailVerified: true, accessLevel: membershipState.MEMBER, joinDate: { $gte: beginningOfSemester, $lte: today}}); + const newSingleSemester = await User.countDocuments({ emailVerified: true, accessLevel: membershipState.MEMBER, joinDate: { $gte: beginningOfSemester, $lte: today }, membershipValidUntil: endOfThisSemester }); + const newAnnualMembers = await User.countDocuments({ emailVerified: true, accessLevel: membershipState.MEMBER, joinDate: { $gte: beginningOfSemester, $lte: today }, membershipValidUntil: endOfNextSemester }); + + return res.json({ newSingleAndAnnualMembers, newSingleSemester, newAnnualMembers, totalNewMembersThisYear, currentActiveMembers }); +}); + module.exports = router; diff --git a/src/APIFunctions/User.js b/src/APIFunctions/User.js index 3de571a0c..56091452f 100644 --- a/src/APIFunctions/User.js +++ b/src/APIFunctions/User.js @@ -298,3 +298,19 @@ export async function getAllUserSubscribedAndVerified(token) { }); return status; } + +export async function countMembers(token) { + console.log(token); + let status = new UserApiResponse(); + await axios + .post(GENERAL_API_URL + '/user/countMembers', { token }) + .then((res) => { + status.responseData = res.data; + }) + .catch((err) => { + status.error = true; + }); + return status; +} + + diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index 3751de0bb..f3255ddab 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -1,6 +1,7 @@ import React from 'react'; export default function UserNavBar(props) { + const getLinkClassName = (path) => { const weAreAtGivenPath = path === window.location.pathname; let className = 'flex items-center p-2 text-gray-900 rounded-lg dark:text-white'; @@ -81,6 +82,15 @@ export default function UserNavBar(props) { ), }, + { + title: 'Club Revenue', + route: '/club-revenue', + icon: ( + + + + ), + }, ]; const renderRoutesForNavbar = (navbarLinks) => { diff --git a/src/Pages/ClubRevenue/ClubRevenue.js b/src/Pages/ClubRevenue/ClubRevenue.js new file mode 100644 index 000000000..33970b618 --- /dev/null +++ b/src/Pages/ClubRevenue/ClubRevenue.js @@ -0,0 +1,40 @@ +import React, { useEffect, useState } from 'react'; +import { countMembers } from '../../APIFunctions/User'; + + +export default function ClubRevenue(props) { + + const [newSingleSemester, setNewSingleSemester] = useState(); + const [newAnnualMembers, setNewAnnualMembers] = useState(); + const [totalNewMembersThisYear, setNewTotalMembers] = useState(); + const [currentActiveMembers, setCurrentActiveMembers] = useState(); + + useEffect(() => { + async function fetchMembers() { + const status = await countMembers(props.user.token); + setNewSingleSemester(status.responseData.newSingleSemester); + setNewAnnualMembers(status.responseData.newAnnualMembers); + setNewTotalMembers(status.responseData.totalNewMembersThisYear); + setCurrentActiveMembers(status.responseData.currentActiveMembers); + } + fetchMembers(); + }, []); + + return ( +
+

Club Revenue

+
+

Total Earnings from New Members This Semester:

+

${newSingleSemester * 20 + newAnnualMembers * 30}

+
+
+

Total New Members This Year:

+

{totalNewMembersThisYear}

+
+
+

Current Active Members:

+

{currentActiveMembers}

+
+
+ ); +} \ No newline at end of file diff --git a/src/Routing.js b/src/Routing.js index 87e1fcdfb..a1a7aded0 100644 --- a/src/Routing.js +++ b/src/Routing.js @@ -26,9 +26,10 @@ import URLShortenerPage from './Pages/URLShortener/URLShortener'; import EmailPreferencesPage from './Pages/EmailPreferences/EmailPreferences'; import sendUnsubscribeEmail from './Pages/Profile/admin/SendUnsubscribeEmail'; - +import ClubRevenue from './Pages/ClubRevenue/ClubRevenue.js'; export default function Routing({ appProps }) { + const userIsAuthenticated = appProps.authenticated; const userIsMember = userIsAuthenticated && @@ -117,6 +118,13 @@ export default function Routing({ appProps }) { inAdminNavbar: true, redirect: '/', }, + { + Component: ClubRevenue, + path: '/club-revenue', + allowedIf: userIsOfficerOrAdmin, + inAdminNavbar: true, + redirect: '/', + }, ]; const signedOutRoutes = [ { Component: Home, path: '/' }, diff --git a/test/api/User.js b/test/api/User.js index c7251a00c..a22fc1a90 100644 --- a/test/api/User.js +++ b/test/api/User.js @@ -42,6 +42,8 @@ const { restoreDiscordAPIMock, initializeDiscordAPIMock } = require('../util/mocks/DiscordApiFunction'); +const { mockDayMonthAndYear, revertClock } = require('../util/mocks/Date.js'); +const membershipState = require('../../api/util/constants.js').MEMBERSHIP_STATE; chai.should(); chai.use(chaiHttp); @@ -73,6 +75,7 @@ describe('User', () => { }); afterEach(() => { + revertClock(); resetTokenMock(); resetDiscordAPIMock(); }); @@ -494,4 +497,146 @@ describe('User', () => { result.body.should.have.property('message'); }); }); + + describe('/POST countMembers', () => { + it('Should return statusCode 403 if no token is passed in', async () => { + const user = { + email: 'a@b.c' + }; + const result = await test.sendPostRequest('/api/User/countMembers', user); + expect(result).to.have.status(FORBIDDEN); + }); + + it('Should return statusCode 401 if an invalid token is passed in', async () => { + const user = { + token: 'Invalid token' + } + const result = await test.sendPostRequest('/api/User/users', user); + expect(result).to.have.status(UNAUTHORIZED); + }); + + it('Should return statusCode 200 and the member counts correctly for fall semester if a valid token is passed in', async () => { + const mockCurrentDate = mockDayMonthAndYear(31, 11, 2023); // December 31, 2023 + const user = { + email: 'a@b.c', + token: token + }; + const addUsers = [ + { + email: 'user1@example.com', + password: 'password', + firstName: 'User', + lastName: 'One', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 5, 1), + membershipValidUntil: new Date(2024, 0, 1) // 1 Semester + }, + { + email: 'user2@example.com', + password: 'password', + firstName: 'User', + lastName: 'Two', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 5, 1), + membershipValidUntil: new Date(2024, 5, 1) //2 Semesters + }, + { + email: 'user3@example.com', + password: 'password', + firstName: 'User', + lastName: 'Three', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 7, 13), + membershipValidUntil: new Date(2024, 0, 1) // 1 Semester + }, + { + email: 'user4@example.com', + password: 'password', + firstName: 'User', + lastName: 'Four', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 4, 31), + membershipValidUntil: new Date(2024, 0, 1) // 2 Semesters but shouldn't be included since join date was last semester + } + ]; + setTokenStatus(true); + await User.insertMany(addUsers); + const result = await test.sendPostRequestWithToken(token, '/api/User/countMembers', user); + + expect(result).to.have.status(OK); + result.body.should.be.a('object'); + result.body.should.have.property('newSingleAndAnnualMembers').that.equals(3); + result.body.should.have.property('newSingleSemester').that.equals(2); + result.body.should.have.property('newAnnualMembers').that.equals(1); + result.body.should.have.property('totalNewMembersThisYear').that.equals(4); + result.body.should.have.property('currentActiveMembers').that.equals(4); + + tools.emptySchema(User); + }); + + it('Should return statusCode 200 and the member counts correctly for spring semester if a valid token is passed in', async () => { + const mockCurrentDate = mockDayMonthAndYear(2, 0, 2023); // January 2, 2023 + const user = { + email: 'a@b.c', + token: token + }; + const addUsers = [ + { + email: 'user5@example.com', + password: 'password', + firstName: 'User', + lastName: 'Five', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 0, 1), + membershipValidUntil: new Date(2023, 5, 1) // 1 Semester + }, + { + email: 'user6@example.com', + password: 'password', + firstName: 'User', + lastName: 'Six', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 0, 2), + membershipValidUntil: new Date(2024, 0, 1) // 2 Semesters + }, + { + email: 'user7@example.com', + password: 'password', + firstName: 'User', + lastName: 'Seven', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2023, 0, 2), + membershipValidUntil: new Date(2023, 5, 1) // 1 Semester + }, + { + email: 'user8@example.com', + password: 'password', + firstName: 'User', + lastName: 'Eight', + emailVerified: true, + accessLevel: membershipState.MEMBER, + joinDate: new Date(2022, 11, 31), + membershipValidUntil: new Date(2023, 5, 1) // 2 Semesters but shouldn't be included since join date was last semester + } + ]; + setTokenStatus(true); + await User.insertMany(addUsers); + const result = await test.sendPostRequestWithToken(token, '/api/User/countMembers', user); + + expect(result).to.have.status(OK); + result.body.should.be.a('object'); + result.body.should.have.property('newSingleAndAnnualMembers').that.equals(3); + result.body.should.have.property('newSingleSemester').that.equals(2); + result.body.should.have.property('newAnnualMembers').that.equals(1); + result.body.should.have.property('totalNewMembersThisYear').that.equals(3); + result.body.should.have.property('currentActiveMembers').that.equals(4); + }); + }); });