Skip to content

Commit 0e41c53

Browse files
committed
feat: add score overview to admin page
for students this is coming soon, first let's see if the calculations work
1 parent 2f48996 commit 0e41c53

File tree

5 files changed

+245
-17
lines changed

5 files changed

+245
-17
lines changed

src/handlers/filters.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export const setupNunjucksFilters = function(app: Express): void {
88
express: app,
99
});
1010

11+
// Add formatting for floats to fixed
12+
nunjucksEnv.addFilter('toFixed', (num: number, digits: number) => {
13+
return num.toFixed(digits);
14+
});
15+
1116
// Add formatting filter for seconds to hh:mm format
1217
nunjucksEnv.addFilter('formatSeconds', (seconds: number) => {
1318
const hours = Math.floor(seconds / 3600);

src/routes/admin/charts.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,104 @@ export const setupAdminChartsRoutes = function(app: Express, prisma: PrismaClien
7474
display: false,
7575
}
7676
},
77-
}
77+
},
7878
}
7979
return res.json(chartJSData);
8080
});
81+
82+
app.get('/admin/charts/coalitions/:coalitionId/scores/distribution', async (req, res) => {
83+
try {
84+
const coalitionId = parseInt(req.params.coalitionId);
85+
const coalition = await prisma.intraCoalition.findFirst({
86+
where: {
87+
id: coalitionId,
88+
},
89+
select: {
90+
id: true,
91+
name: true,
92+
color: true,
93+
}
94+
});
95+
if (!coalition) {
96+
throw new Error('Invalid coalition ID');
97+
}
98+
const scores = await prisma.codamCoalitionScore.groupBy({
99+
by: ['user_id'],
100+
where: {
101+
coalition_id: coalitionId,
102+
},
103+
_sum: {
104+
amount: true,
105+
},
106+
_count: {
107+
id: true,
108+
},
109+
});
110+
const coalitionUsers = await prisma.intraCoalitionUser.findMany({
111+
where: {
112+
coalition_id: coalitionId,
113+
},
114+
select: {
115+
user_id: true,
116+
user: {
117+
select: {
118+
login: true,
119+
},
120+
},
121+
},
122+
});
123+
124+
const scoresPerUser = coalitionUsers.map((user) => {
125+
const score = scores.find((score) => score.user_id === user.user_id) || { _sum: { amount: 0 }, _count: { id: 0 } };
126+
return {
127+
login: user.user.login,
128+
amount: score._sum.amount,
129+
count: score._count.id,
130+
};
131+
});
132+
133+
// Compose the returnable data (in a format Chart.js can understand)
134+
const chartJSData: ChartConfiguration = {
135+
type: 'scatter',
136+
data: {
137+
labels: scoresPerUser.map((score) => score.login),
138+
datasets: [
139+
{
140+
label: 'Scores',
141+
data: scoresPerUser.map((score) => ({ x: score.amount, y: score.count })) as Chart.ChartPoint[],
142+
backgroundColor: coalition.color ? coalition.color : '#808080',
143+
borderWidth: 1,
144+
},
145+
],
146+
},
147+
options: {
148+
scales: {
149+
// @ts-ignore
150+
x: {
151+
title: {
152+
display: true,
153+
text: 'Amount of points',
154+
},
155+
},
156+
y: {
157+
title: {
158+
display: true,
159+
text: 'Amount of scores',
160+
},
161+
},
162+
},
163+
plugins: {
164+
legend: {
165+
display: false,
166+
}
167+
},
168+
}
169+
};
170+
171+
return res.json(chartJSData);
172+
}
173+
catch (err) {
174+
return res.status(400).json({ error: err });
175+
}
176+
});
81177
}

src/routes/admin/dashboard.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Express } from 'express';
22
import { PrismaClient } from '@prisma/client';
3+
import { CoalitionScore, getCoalitionScore } from '../../utils';
34

45
export const setupAdminDashboardRoutes = function(app: Express, prisma: PrismaClient): void {
56
app.get('/admin', async (req, res) => {
@@ -10,8 +11,33 @@ export const setupAdminDashboardRoutes = function(app: Express, prisma: PrismaCl
1011
},
1112
});
1213

14+
// Get coalitions
15+
const coalitions = await prisma.codamCoalition.findMany({
16+
select: {
17+
id: true,
18+
description: true,
19+
tagline: true,
20+
intra_coalition: {
21+
select: {
22+
id: true,
23+
name: true,
24+
color: true,
25+
image_url: true,
26+
}
27+
}
28+
}
29+
});
30+
31+
// Get current scores per coalition
32+
const coalitionScores: { [key: number]: CoalitionScore } = {};
33+
for (const coalition of coalitions) {
34+
coalitionScores[coalition.id] = await getCoalitionScore(prisma, coalition.id);
35+
}
36+
1337
return res.render('admin/dashboard.njk', {
1438
blocDeadline,
39+
coalitions,
40+
coalitionScores,
1541
});
1642
});
1743
};

src/utils.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,65 @@ export const timeFromNow = function(date: Date | null): string {
192192
}
193193
return `within a minute`; // don't specify: otherwise it's weird when the amount of seconds does not go down
194194
};
195+
196+
export interface NormalDistribution {
197+
dataPoints: number[];
198+
mean: number;
199+
stdDev: number;
200+
min: number;
201+
max: number;
202+
};
203+
204+
export const getScoresNormalDistribution = async function(prisma: PrismaClient, coalitionId: number, untilDate: Date = new Date()): Promise<NormalDistribution> {
205+
const scores = await prisma.codamCoalitionScore.groupBy({
206+
by: ['user_id'],
207+
where: {
208+
coalition_id: coalitionId,
209+
created_at: {
210+
lte: untilDate,
211+
},
212+
},
213+
_sum: {
214+
amount: true,
215+
},
216+
});
217+
// console.log(scores);
218+
const scoresArray = scores.map(s => s._sum.amount ? s._sum.amount : 0);
219+
const scoresSum = scoresArray.reduce((a, b) => a + b, 0);
220+
const scoresMean = scoresSum / scoresArray.length;
221+
const scoresVariance = scoresArray.reduce((a, b) => a + Math.pow(b - scoresMean, 2), 0) / scoresArray.length;
222+
const scoresStdDev = Math.sqrt(scoresVariance);
223+
const scoresMin = Math.min(...scoresArray);
224+
const scoresMax = Math.max(...scoresArray);
225+
return {
226+
dataPoints: scoresArray,
227+
mean: scoresMean,
228+
stdDev: scoresStdDev,
229+
min: scoresMin,
230+
max: scoresMax,
231+
};
232+
};
233+
234+
export interface CoalitionScore {
235+
coalition_id: number;
236+
score: number;
237+
totalPoints: number;
238+
avgPoints: number;
239+
stdDevPoints: number;
240+
minActivePoints: number; // Minimum score for a user to be considered active
241+
}
242+
243+
export const getCoalitionScore = async function(prisma: PrismaClient, coalitionId: number, atDateTime: Date = new Date()): Promise<CoalitionScore> {
244+
const normalDist = await getScoresNormalDistribution(prisma, coalitionId, atDateTime);
245+
const minScore = Math.floor(normalDist.mean - normalDist.stdDev);
246+
const activeScores = normalDist.dataPoints.filter(s => s >= minScore);
247+
const fairScore = Math.floor(activeScores.reduce((a, b) => a + b, 0) / activeScores.length);
248+
return {
249+
coalition_id: coalitionId,
250+
totalPoints: normalDist.dataPoints.reduce((a, b) => a + b, 0),
251+
avgPoints: normalDist.mean,
252+
stdDevPoints: normalDist.stdDev,
253+
minActivePoints: minScore,
254+
score: fairScore,
255+
};
256+
};

templates/admin/dashboard.njk

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,67 @@
22
{% set title = "Admin Dashboard" %}
33

44
{% block content %}
5-
<h1>Admin Dashboard</h1>
6-
<div class="row ms-0 me-0">
7-
<div class="col-md-6">
8-
<div class="card">
9-
<div class="card-header">
10-
<h5 class="card-title mb-0">User distribution</h5>
5+
<div class="container-lg">
6+
<h1 class="mb-4">Admin Dashboard</h1>
7+
8+
<div class="row ms-0 me-0 mb-4">
9+
<div class="col-md-6">
10+
<div class="card h-100">
11+
<div class="card-header">
12+
<h5 class="card-title mb-0">User distribution</h5>
13+
</div>
14+
<div class="card-body">
15+
<canvas class="codam-chart" data-url="/admin/charts/coalitions/users/distribution" id="coalition-user-distribution"></canvas>
16+
</div>
1117
</div>
12-
<div class="card-body">
13-
<canvas class="codam-chart" data-url="/admin/charts/coalitions/users/distribution" id="coalition-user-distribution"></canvas>
18+
</div>
19+
<div class="col-md-6">
20+
<div class="card h-100">
21+
<div class="card-header">
22+
<h5 class="card-title mb-0">Season deadlines</h5>
23+
</div>
24+
<div class="card-body">
25+
<p>Start: <span id="season-start" data-date="{{ blocDeadline.begin_at | timestamp }}">{{ blocDeadline.begin_at | timeAgo }}</span></p>
26+
<p>End: <span id="season-end" data-date="{{ blocDeadline.end_at | timestamp }}">{{ blocDeadline.end_at | timeFromNow }}</span></p>
27+
</div>
1428
</div>
1529
</div>
1630
</div>
17-
<div class="col-md-6">
18-
<div class="card">
19-
<div class="card-header">
20-
<h5 class="card-title mb-0">Season deadlines</h5>
31+
32+
<div class="row ms-0 me-0 mb-4">
33+
{% for coalition in coalitions %}
34+
<div class="col">
35+
<div class="card h-100">
36+
<div class="card-header">
37+
<h5 class="card-title mb-0">Scores for {{ coalition.intra_coalition.name }} </h5>
38+
</div>
39+
<div class="card-body">
40+
<ul>
41+
<li>Score: {{ coalitionScores[coalition.id].score }}</li>
42+
<li>Total points: {{ coalitionScores[coalition.id].totalPoints }}</li>
43+
<li>Minimum contrition: {{ coalitionScores[coalition.id].minActivePoints }}</li>
44+
<li>Average points: {{ coalitionScores[coalition.id].avgPoints | toFixed(2) }}</li>
45+
<li>Standard deviation points: {{ coalitionScores[coalition.id].stdDevPoints | toFixed(2) }}</li>
46+
</ul>
47+
</div>
48+
</div>
2149
</div>
22-
<div class="card-body">
23-
<p>Start: <span id="season-start" data-date="{{ blocDeadline.begin_at | timestamp }}">{{ blocDeadline.begin_at | timeAgo }}</span></p>
24-
<p>End: <span id="season-end" data-date="{{ blocDeadline.end_at | timestamp }}">{{ blocDeadline.end_at | timeFromNow }}</span></p>
50+
{% endfor %}
51+
</div>
52+
53+
<div class="row ms-0 me-0 mb-4">
54+
{% for coalition in coalitions %}
55+
<div class="col">
56+
<div class="card h-100">
57+
<div class="card-header">
58+
<h5 class="card-title mb-0">Score Distributions for {{ coalition.intra_coalition.name }} </h5>
59+
</div>
60+
<div class="card-body">
61+
<canvas class="codam-chart" data-url="/admin/charts/coalitions/{{ coalition.id }}/scores/distribution" id="coalition-{{ coalition.id }}-scores-distribution"></canvas>
62+
</div>
63+
</div>
2564
</div>
26-
</div>
65+
{% endfor %}
2766
</div>
2867
</div>
2968
{% endblock %}

0 commit comments

Comments
 (0)