Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added feature to filter stats at a glance by user type #122

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions components/AdminStatsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { SvgIconTypeMap } from '@mui/material';
import { OverridableComponent } from '@mui/material/OverridableComponent';

interface AdminStatsCardProps {
title: string;
value: number;
Expand Down
27 changes: 22 additions & 5 deletions components/StatsBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, GeneralStats>;
rolesDict: Record<string, boolean>;
}

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<string, number>, curr: Record<string, number>) => {
for (let key of Object.keys(curr)) {
if (!acc.hasOwnProperty(key)) acc[key] = curr[key];
else acc[key] += curr[key];
}
return acc;
}, {}) as Record<string, number>,
).map(([k, v]) => ({ itemName: k, itemCount: v }));
const coordinates = [];
/**
*
Expand Down
29 changes: 23 additions & 6 deletions components/StatsPieChart.tsx
Original file line number Diff line number Diff line change
@@ -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<string, GeneralStats>;
rolesDict: Record<string, boolean>;
}

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<string, number>, curr: Record<string, number>) => {
for (let key of Object.keys(curr)) {
if (!acc.hasOwnProperty(key)) acc[key] = curr[key];
else acc[key] += curr[key];
}
return acc;
}, {}) as Record<string, number>,
).map(([k, v]) => ({ itemName: k, itemCount: v }));

return (
<div className="w-full flex-grow border-2 my-2 rounded-2xl p-6">
<Chart data={items}>
Expand Down
17 changes: 14 additions & 3 deletions lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,22 @@ type Sponsor = {
};

type GeneralStats = {
superAdminCount: number;
count: number;
checkedInCount: number;
hackerCount: number;
adminCount: number;
scans: Record<string, number>;
companies: Record<string, number>;
dietary: Record<string, number>;

age: Record<number, number>;
ethnicity: Record<string, number>;
race: Record<string, number>;
size: Record<string, number>;
softwareExperience: Record<string, number>;
studyLevel: Record<string, number>;
university: Record<string, number>;
gender: Record<string, number>;
hackathonExperience: Record<number, number>;
heardFrom: Record<string, number>;
};

/**
Expand Down
155 changes: 138 additions & 17 deletions pages/admin/stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<'checked_in' | 'not_checked_in', GeneralStats>>,
checkInFilter: Record<string, boolean>,
roleFilter: Record<string, boolean>,
): Record<string, GeneralStats> {
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<any, number>).reduce(
(qAcc, [qCurr, _]) => ({
...qAcc,
[qCurr]: (roleAcc[fieldCurr][qCurr] || 0) + checkedInData[fieldCurr][qCurr],
}),
{} as Record<any, number>,
),
};
}, {}),
};
},
{
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<GeneralStats>();
const [unfilteredData, setUnfilteredData] = useState<
Record<string, Record<string, GeneralStats>>
>({});
const [statsData, setStatsData] = useState<Record<string, GeneralStats>>();
const [roles, setRoles] = useState<Record<string, boolean>>({
hacker: true,
admin: true,
super_admin: true,
});

const [checkInFilter, setCheckInFilter] = useState<Record<string, boolean>>({
checked_in: true,
not_checked_in: true,
});

useEffect(() => {
async function getData() {
const { data } = await RequestHelper.get<GeneralStats>('/api/stats', {
const { data } = await RequestHelper.get<
Record<string, Record<'checked_in' | 'not_checked_in', GeneralStats>>
>('/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 <div className="text-2xl font-black text-center">Unauthorized</div>;
}
Expand All @@ -53,46 +126,94 @@ export default function AdminStatsPage() {
</Head>
<AdminHeader />
<div className="w-full xl:w-3/5 mx-auto p-6 flex flex-col gap-y-6">
<div className="border-2 rounded-xl p-3">
<h1 className="text-center text-xl font-bold">Filter Stats by:</h1>
<FilterComponent
checked={roles['hacker']}
onCheck={() => {
updateFilter('hacker');
}}
title="Hackers"
/>

<FilterComponent
checked={roles['admin']}
onCheck={() => {
updateFilter('admin');
}}
title="Admin"
/>
<FilterComponent
checked={roles['super_admin']}
onCheck={() => {
updateFilter('super_admin');
}}
title="Super Admin"
/>
</div>
<div className="border-2 rounded-xl p-3">
<h1 className="text-center text-xl font-bold">Filter Stats by:</h1>
<FilterComponent
checked={checkInFilter['checked_in']}
onCheck={() => {
setCheckInFilter((prev) => ({ ...prev, checked_in: !prev['checked_in'] }));
}}
title="Checked In"
/>

<FilterComponent
checked={checkInFilter['not_checked_in']}
onCheck={() => {
setCheckInFilter((prev) => ({ ...prev, not_checked_in: !prev['not_checked_in'] }));
}}
title="Not checked in"
/>
</div>
<div className="flex-col gap-y-3 w-full md:flex-row flex justify-around gap-x-2">
<AdminStatsCard icon={<CheckIcon />} title="Check-Ins" value={statsData.checkedInCount} />
<AdminStatsCard
icon={<CheckIcon />}
title="Check-Ins"
value={Object.entries(statsData)
.filter(([k, v]) => roles[k])
.reduce((acc, [k, v]) => acc + v.checkedInCount, 0)}
/>
<AdminStatsCard
icon={<AccountCircleIcon />}
title="Hackers"
value={statsData.hackerCount}
value={statsData['hacker'].count}
/>

<AdminStatsCard
icon={<SupervisorAccountIcon />}
title="Admins"
value={statsData.adminCount}
value={statsData['admin'].count}
/>
<AdminStatsCard
icon={<EngineeringIcon />}
title="Super Admin"
value={statsData.superAdminCount}
value={statsData['super_admin'].count}
/>
</div>
{Object.entries(statsData)
{Object.entries(statsData['hacker'])
.filter(([k, v]) => typeof v === 'object')
.map(([key, value]) => {
if (Object.keys(value).length <= 6)
return (
<StatsPieChart
key={key}
name={fieldToName[key]}
items={Object.entries(statsData[key] as Record<any, any>).map(([k, v]) => ({
itemName: k,
itemCount: v,
}))}
fieldName={key}
statsData={statsData}
rolesDict={roles}
/>
);
return (
<StatsBarChart
key={key}
name={fieldToName[key]}
items={Object.entries(statsData[key] as Record<any, any>).map(([k, v]) => ({
itemName: k,
itemCount: v,
}))}
fieldName={key}
statsData={statsData}
rolesDict={roles}
/>
);
})}
Expand Down
Loading