diff --git a/components/AdminStatsCard.tsx b/components/AdminStatsCard.tsx index a058d2f9..7a24d81a 100644 --- a/components/AdminStatsCard.tsx +++ b/components/AdminStatsCard.tsx @@ -1,6 +1,3 @@ -import { SvgIconTypeMap } from '@mui/material'; -import { OverridableComponent } from '@mui/material/OverridableComponent'; - interface AdminStatsCardProps { title: string; value: number; diff --git a/components/StatsBarChart.tsx b/components/StatsBarChart.tsx index 3c2fb945..f5b505be 100644 --- a/components/StatsBarChart.tsx +++ b/components/StatsBarChart.tsx @@ -7,16 +7,33 @@ import { Tooltip, ValueAxis, } from '@devexpress/dx-react-chart-material-ui'; +import { useState } from 'react'; interface StatsBarChartProps { name: string; - items: Array<{ - itemName: string; - itemCount: number; - }>; + fieldName: string; + statsData: Record; + rolesDict: Record; } -export default function StatsBarChart({ name, items }: StatsBarChartProps) { +export default function StatsBarChart({ + name, + fieldName, + statsData, + rolesDict, +}: StatsBarChartProps) { + const items = Object.entries( + Object.entries(statsData) + .filter(([k, _]) => rolesDict[k]) + .map(([k, v]) => v[fieldName]) + .reduce((acc: Record, curr: Record) => { + for (let key of Object.keys(curr)) { + if (!acc.hasOwnProperty(key)) acc[key] = curr[key]; + else acc[key] += curr[key]; + } + return acc; + }, {}) as Record, + ).map(([k, v]) => ({ itemName: k, itemCount: v })); const coordinates = []; /** * diff --git a/components/StatsPieChart.tsx b/components/StatsPieChart.tsx index a1d7dceb..85269aea 100644 --- a/components/StatsPieChart.tsx +++ b/components/StatsPieChart.tsx @@ -1,16 +1,33 @@ import { Animation, EventTracker, Palette } from '@devexpress/dx-react-chart'; import { Chart, Legend, PieSeries, Title, Tooltip } from '@devexpress/dx-react-chart-material-ui'; -import React from 'react'; +import React, { useState } from 'react'; interface StatsPieChartProps { name: string; - items: Array<{ - itemName: string; - itemCount: number; - }>; + fieldName: string; + statsData: Record; + rolesDict: Record; } -export default function StatsPieChart({ name, items }: StatsPieChartProps) { +export default function StatsPieChart({ + name, + fieldName, + statsData, + rolesDict, +}: StatsPieChartProps) { + const items = Object.entries( + Object.entries(statsData) + .filter(([k, _]) => rolesDict[k]) + .map(([k, v]) => v[fieldName]) + .reduce((acc: Record, curr: Record) => { + for (let key of Object.keys(curr)) { + if (!acc.hasOwnProperty(key)) acc[key] = curr[key]; + else acc[key] += curr[key]; + } + return acc; + }, {}) as Record, + ).map(([k, v]) => ({ itemName: k, itemCount: v })); + return (
diff --git a/lib/types.d.ts b/lib/types.d.ts index 84b04052..adc47fff 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -245,11 +245,22 @@ type Sponsor = { }; type GeneralStats = { - superAdminCount: number; + count: number; checkedInCount: number; - hackerCount: number; - adminCount: number; scans: Record; + companies: Record; + dietary: Record; + + age: Record; + ethnicity: Record; + race: Record; + size: Record; + softwareExperience: Record; + studyLevel: Record; + university: Record; + gender: Record; + hackathonExperience: Record; + heardFrom: Record; }; /** diff --git a/pages/admin/stats.tsx b/pages/admin/stats.tsx index c2f9c0f8..092db54e 100644 --- a/pages/admin/stats.tsx +++ b/pages/admin/stats.tsx @@ -12,31 +12,104 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; import EngineeringIcon from '@mui/icons-material/Engineering'; import StatsPieChart from '../../components/StatsPieChart'; -import { fieldToName } from '../../lib/stats/field'; +import { arrayFields, fieldToName, singleFields } from '../../lib/stats/field'; +import FilterComponent from '../../components/FilterComponent'; function isAuthorized(user): boolean { if (!user || !user.permissions) return false; return (user.permissions as string[]).includes('super_admin'); } +function mergeStatsData( + statsData: Record>, + checkInFilter: Record, + roleFilter: Record, +): Record { + return Object.entries(statsData).reduce((acc, [role, statsDataByRole]) => { + return { + ...acc, + [role]: Object.entries(roleFilter[role] ? statsDataByRole : {}).reduce( + (roleAcc, [checkedInStatus, checkedInData]) => { + if (!checkInFilter[checkedInStatus]) return roleAcc; + return { + ...roleAcc, + count: roleAcc.count + checkedInData.count, + checkedInCount: roleAcc.checkedInCount + checkedInData.checkedInCount, + ...[...singleFields, ...arrayFields].reduce((fieldAcc, fieldCurr) => { + return { + ...fieldAcc, + [fieldCurr]: Object.entries(checkedInData[fieldCurr] as Record).reduce( + (qAcc, [qCurr, _]) => ({ + ...qAcc, + [qCurr]: (roleAcc[fieldCurr][qCurr] || 0) + checkedInData[fieldCurr][qCurr], + }), + {} as Record, + ), + }; + }, {}), + }; + }, + { + count: 0, + checkedInCount: 0, + ...[...singleFields, ...arrayFields].reduce( + (fieldAcc, fieldCurr) => ({ + ...fieldAcc, + [fieldCurr]: {}, + }), + {}, + ), + } as GeneralStats, + ), + }; + }, {}); +} + export default function AdminStatsPage() { const [loading, setLoading] = useState(true); const { user, isSignedIn } = useAuthContext(); - const [statsData, setStatsData] = useState(); + const [unfilteredData, setUnfilteredData] = useState< + Record> + >({}); + const [statsData, setStatsData] = useState>(); + const [roles, setRoles] = useState>({ + hacker: true, + admin: true, + super_admin: true, + }); + + const [checkInFilter, setCheckInFilter] = useState>({ + checked_in: true, + not_checked_in: true, + }); useEffect(() => { async function getData() { - const { data } = await RequestHelper.get('/api/stats', { + const { data } = await RequestHelper.get< + Record> + >('/api/stats', { headers: { Authorization: user.token, }, }); - setStatsData(data); + setUnfilteredData(data); + setStatsData(mergeStatsData(data, checkInFilter, roles)); setLoading(false); } getData(); }, []); + useEffect(() => { + setStatsData(mergeStatsData(unfilteredData, checkInFilter, roles)); + }, [checkInFilter, roles]); + + const updateFilter = (name: string) => { + setRoles((prev) => ({ + ...prev, + [name]: !prev[name], + })); + }; + if (!isSignedIn || !isAuthorized(user)) { return
Unauthorized
; } @@ -53,25 +126,75 @@ export default function AdminStatsPage() {
+
+

Filter Stats by:

+ { + updateFilter('hacker'); + }} + title="Hackers" + /> + + { + updateFilter('admin'); + }} + title="Admin" + /> + { + updateFilter('super_admin'); + }} + title="Super Admin" + /> +
+
+

Filter Stats by:

+ { + setCheckInFilter((prev) => ({ ...prev, checked_in: !prev['checked_in'] })); + }} + title="Checked In" + /> + + { + setCheckInFilter((prev) => ({ ...prev, not_checked_in: !prev['not_checked_in'] })); + }} + title="Not checked in" + /> +
- } title="Check-Ins" value={statsData.checkedInCount} /> + } + title="Check-Ins" + value={Object.entries(statsData) + .filter(([k, v]) => roles[k]) + .reduce((acc, [k, v]) => acc + v.checkedInCount, 0)} + /> } title="Hackers" - value={statsData.hackerCount} + value={statsData['hacker'].count} /> + } title="Admins" - value={statsData.adminCount} + value={statsData['admin'].count} /> } title="Super Admin" - value={statsData.superAdminCount} + value={statsData['super_admin'].count} />
- {Object.entries(statsData) + {Object.entries(statsData['hacker']) .filter(([k, v]) => typeof v === 'object') .map(([key, value]) => { if (Object.keys(value).length <= 6) @@ -79,20 +202,18 @@ export default function AdminStatsPage() { ).map(([k, v]) => ({ - itemName: k, - itemCount: v, - }))} + fieldName={key} + statsData={statsData} + rolesDict={roles} /> ); return ( ).map(([k, v]) => ({ - itemName: k, - itemCount: v, - }))} + fieldName={key} + statsData={statsData} + rolesDict={roles} /> ); })} diff --git a/pages/api/stats.ts b/pages/api/stats.ts index 55be97c6..731a3926 100644 --- a/pages/api/stats.ts +++ b/pages/api/stats.ts @@ -23,54 +23,87 @@ async function getCheckInEventName() { async function getStatsData() { const checkInEventName = await getCheckInEventName(); // const swagData: Record = {}; - const generalStats: GeneralStats & statRecordTypes = { - superAdminCount: 0, - checkedInCount: 0, - hackerCount: 0, - adminCount: 0, - scans: {}, - ...statRecords, - }; - - const snapshot = await db.collection(USERS_COLLECTION).get(); - snapshot.forEach((doc) => { - const userData = doc.data(); + const generalStats: Record> = {}; + for (const role of ['hacker', 'admin', 'super_admin']) { + generalStats[role] = { + checked_in: { + count: 0, + checkedInCount: 0, + scans: {}, + age: {}, + companies: {}, + dietary: {}, + ethnicity: {}, + gender: {}, + hackathonExperience: {}, + heardFrom: {}, + race: {}, + size: {}, + softwareExperience: {}, + studyLevel: {}, + university: {}, + }, + not_checked_in: { + count: 0, + checkedInCount: 0, + scans: {}, + age: {}, + companies: {}, + dietary: {}, + ethnicity: {}, + gender: {}, + hackathonExperience: {}, + heardFrom: {}, + race: {}, + size: {}, + softwareExperience: {}, + studyLevel: {}, + university: {}, + }, + }; + } + const addUserToRoleGroup = ( + userData: any, + userPermission: string, + checkedInStatus: 'checked_in' | 'not_checked_in', + ) => { for (let arrayField of arrayFields) { if (!userData[arrayField]) continue; userData[arrayField].forEach((data: string) => { - if (arrayField === 'scans' && data === checkInEventName) generalStats.checkedInCount++; + if (arrayField === 'scans' && data === checkInEventName) + generalStats[userPermission][checkedInStatus].checkedInCount++; else { - if (!generalStats[arrayField].hasOwnProperty(data)) generalStats[arrayField][data] = 0; - generalStats[arrayField][data]++; + if (!generalStats[userPermission][checkedInStatus][arrayField].hasOwnProperty(data)) + generalStats[userPermission][checkedInStatus][arrayField][data] = 0; + generalStats[userPermission][checkedInStatus][arrayField][data]++; } }); } for (let singleField of singleFields) { if (!userData[singleField] || userData[singleField] === '') continue; - if (!generalStats[singleField].hasOwnProperty(userData[singleField])) { - generalStats[singleField][userData[singleField]] = 0; + if ( + !generalStats[userPermission][checkedInStatus][singleField].hasOwnProperty( + userData[singleField], + ) + ) { + generalStats[userPermission][checkedInStatus][singleField][userData[singleField]] = 0; } - generalStats[singleField][userData[singleField]]++; + generalStats[userPermission][checkedInStatus][singleField][userData[singleField]]++; } + generalStats[userPermission][checkedInStatus].count++; + }; + + const snapshot = await db.collection(USERS_COLLECTION).get(); + snapshot.forEach((doc) => { + const userData = doc.data(); const userPermission = userData.user.permissions[0]; + const userCheckedInStatus = + userData.scans && userData.scans.includes(checkInEventName) ? 'checked_in' : 'not_checked_in'; - switch (userPermission) { - case 'super_admin': { - generalStats.superAdminCount++; - break; - } - case 'admin': { - generalStats.adminCount++; - break; - } - case 'hacker': { - generalStats.hackerCount++; - break; - } - } + addUserToRoleGroup(userData, userPermission, userCheckedInStatus); }); return generalStats;