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);
+ });
+ });
});