Skip to content

Commit

Permalink
feat: add score overview to admin page
Browse files Browse the repository at this point in the history
for students this is coming soon, first let's see if the calculations work
  • Loading branch information
FreekBes committed Nov 1, 2024
1 parent 2f48996 commit 0e41c53
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 17 deletions.
5 changes: 5 additions & 0 deletions src/handlers/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const setupNunjucksFilters = function(app: Express): void {
express: app,
});

// Add formatting for floats to fixed
nunjucksEnv.addFilter('toFixed', (num: number, digits: number) => {
return num.toFixed(digits);
});

// Add formatting filter for seconds to hh:mm format
nunjucksEnv.addFilter('formatSeconds', (seconds: number) => {
const hours = Math.floor(seconds / 3600);
Expand Down
98 changes: 97 additions & 1 deletion src/routes/admin/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,104 @@ export const setupAdminChartsRoutes = function(app: Express, prisma: PrismaClien
display: false,
}
},
}
},
}
return res.json(chartJSData);
});

app.get('/admin/charts/coalitions/:coalitionId/scores/distribution', async (req, res) => {
try {
const coalitionId = parseInt(req.params.coalitionId);
const coalition = await prisma.intraCoalition.findFirst({
where: {
id: coalitionId,
},
select: {
id: true,
name: true,
color: true,
}
});
if (!coalition) {
throw new Error('Invalid coalition ID');
}
const scores = await prisma.codamCoalitionScore.groupBy({
by: ['user_id'],
where: {
coalition_id: coalitionId,
},
_sum: {
amount: true,
},
_count: {
id: true,
},
});
const coalitionUsers = await prisma.intraCoalitionUser.findMany({
where: {
coalition_id: coalitionId,
},
select: {
user_id: true,
user: {
select: {
login: true,
},
},
},
});

const scoresPerUser = coalitionUsers.map((user) => {
const score = scores.find((score) => score.user_id === user.user_id) || { _sum: { amount: 0 }, _count: { id: 0 } };
return {
login: user.user.login,
amount: score._sum.amount,
count: score._count.id,
};
});

// Compose the returnable data (in a format Chart.js can understand)
const chartJSData: ChartConfiguration = {
type: 'scatter',
data: {
labels: scoresPerUser.map((score) => score.login),
datasets: [
{
label: 'Scores',
data: scoresPerUser.map((score) => ({ x: score.amount, y: score.count })) as Chart.ChartPoint[],
backgroundColor: coalition.color ? coalition.color : '#808080',
borderWidth: 1,
},
],
},
options: {
scales: {
// @ts-ignore
x: {
title: {
display: true,
text: 'Amount of points',
},
},
y: {
title: {
display: true,
text: 'Amount of scores',
},
},
},
plugins: {
legend: {
display: false,
}
},
}
};

return res.json(chartJSData);
}
catch (err) {
return res.status(400).json({ error: err });
}
});
}
26 changes: 26 additions & 0 deletions src/routes/admin/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Express } from 'express';
import { PrismaClient } from '@prisma/client';
import { CoalitionScore, getCoalitionScore } from '../../utils';

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

// Get coalitions
const coalitions = await prisma.codamCoalition.findMany({
select: {
id: true,
description: true,
tagline: true,
intra_coalition: {
select: {
id: true,
name: true,
color: true,
image_url: true,
}
}
}
});

// Get current scores per coalition
const coalitionScores: { [key: number]: CoalitionScore } = {};
for (const coalition of coalitions) {
coalitionScores[coalition.id] = await getCoalitionScore(prisma, coalition.id);
}

return res.render('admin/dashboard.njk', {
blocDeadline,
coalitions,
coalitionScores,
});
});
};
62 changes: 62 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,65 @@ export const timeFromNow = function(date: Date | null): string {
}
return `within a minute`; // don't specify: otherwise it's weird when the amount of seconds does not go down
};

export interface NormalDistribution {
dataPoints: number[];
mean: number;
stdDev: number;
min: number;
max: number;
};

export const getScoresNormalDistribution = async function(prisma: PrismaClient, coalitionId: number, untilDate: Date = new Date()): Promise<NormalDistribution> {
const scores = await prisma.codamCoalitionScore.groupBy({
by: ['user_id'],
where: {
coalition_id: coalitionId,
created_at: {
lte: untilDate,
},
},
_sum: {
amount: true,
},
});
// console.log(scores);
const scoresArray = scores.map(s => s._sum.amount ? s._sum.amount : 0);
const scoresSum = scoresArray.reduce((a, b) => a + b, 0);
const scoresMean = scoresSum / scoresArray.length;
const scoresVariance = scoresArray.reduce((a, b) => a + Math.pow(b - scoresMean, 2), 0) / scoresArray.length;
const scoresStdDev = Math.sqrt(scoresVariance);
const scoresMin = Math.min(...scoresArray);
const scoresMax = Math.max(...scoresArray);
return {
dataPoints: scoresArray,
mean: scoresMean,
stdDev: scoresStdDev,
min: scoresMin,
max: scoresMax,
};
};

export interface CoalitionScore {
coalition_id: number;
score: number;
totalPoints: number;
avgPoints: number;
stdDevPoints: number;
minActivePoints: number; // Minimum score for a user to be considered active
}

export const getCoalitionScore = async function(prisma: PrismaClient, coalitionId: number, atDateTime: Date = new Date()): Promise<CoalitionScore> {
const normalDist = await getScoresNormalDistribution(prisma, coalitionId, atDateTime);
const minScore = Math.floor(normalDist.mean - normalDist.stdDev);
const activeScores = normalDist.dataPoints.filter(s => s >= minScore);
const fairScore = Math.floor(activeScores.reduce((a, b) => a + b, 0) / activeScores.length);
return {
coalition_id: coalitionId,
totalPoints: normalDist.dataPoints.reduce((a, b) => a + b, 0),
avgPoints: normalDist.mean,
stdDevPoints: normalDist.stdDev,
minActivePoints: minScore,
score: fairScore,
};
};
71 changes: 55 additions & 16 deletions templates/admin/dashboard.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,67 @@
{% set title = "Admin Dashboard" %}

{% block content %}
<h1>Admin Dashboard</h1>
<div class="row ms-0 me-0">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">User distribution</h5>
<div class="container-lg">
<h1 class="mb-4">Admin Dashboard</h1>

<div class="row ms-0 me-0 mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">User distribution</h5>
</div>
<div class="card-body">
<canvas class="codam-chart" data-url="/admin/charts/coalitions/users/distribution" id="coalition-user-distribution"></canvas>
</div>
</div>
<div class="card-body">
<canvas class="codam-chart" data-url="/admin/charts/coalitions/users/distribution" id="coalition-user-distribution"></canvas>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">Season deadlines</h5>
</div>
<div class="card-body">
<p>Start: <span id="season-start" data-date="{{ blocDeadline.begin_at | timestamp }}">{{ blocDeadline.begin_at | timeAgo }}</span></p>
<p>End: <span id="season-end" data-date="{{ blocDeadline.end_at | timestamp }}">{{ blocDeadline.end_at | timeFromNow }}</span></p>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Season deadlines</h5>

<div class="row ms-0 me-0 mb-4">
{% for coalition in coalitions %}
<div class="col">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">Scores for {{ coalition.intra_coalition.name }} </h5>
</div>
<div class="card-body">
<ul>
<li>Score: {{ coalitionScores[coalition.id].score }}</li>
<li>Total points: {{ coalitionScores[coalition.id].totalPoints }}</li>
<li>Minimum contrition: {{ coalitionScores[coalition.id].minActivePoints }}</li>
<li>Average points: {{ coalitionScores[coalition.id].avgPoints | toFixed(2) }}</li>
<li>Standard deviation points: {{ coalitionScores[coalition.id].stdDevPoints | toFixed(2) }}</li>
</ul>
</div>
</div>
</div>
<div class="card-body">
<p>Start: <span id="season-start" data-date="{{ blocDeadline.begin_at | timestamp }}">{{ blocDeadline.begin_at | timeAgo }}</span></p>
<p>End: <span id="season-end" data-date="{{ blocDeadline.end_at | timestamp }}">{{ blocDeadline.end_at | timeFromNow }}</span></p>
{% endfor %}
</div>

<div class="row ms-0 me-0 mb-4">
{% for coalition in coalitions %}
<div class="col">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">Score Distributions for {{ coalition.intra_coalition.name }} </h5>
</div>
<div class="card-body">
<canvas class="codam-chart" data-url="/admin/charts/coalitions/{{ coalition.id }}/scores/distribution" id="coalition-{{ coalition.id }}-scores-distribution"></canvas>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

0 comments on commit 0e41c53

Please sign in to comment.