Skip to content
Snippets Groups Projects
Commit 9c1c5285 authored by Cedric Greiten's avatar Cedric Greiten :speech_balloon:
Browse files

Merge branch 'ordered-attendance' into 'main'

Ordered attendance

See merge request !101
parents 3236fb8b e6c69ecd
No related branches found
No related tags found
1 merge request!101Ordered attendance
Pipeline #284457 passed
......@@ -29,7 +29,7 @@ If you dont have access to the `Sitzungen` Repository, you can use the `local_te
To stay consistent in e.g. spacing and have the chance to track changes, please format the files with
```
pnpm formt
pnpm format
```
in the root of the repository.
import fs from "node:fs/promises";
import { FinishedTranscriptMeta, Resolution } from "./parsing";
import { Attendance, generateAttendanceHtmlWrapper } from "./rendering";
import { FinishedTranscriptMeta } from "./parsing";
import { generateAttendanceHtmlWrapper } from "./rendering";
export class Attendance {
presents: number[] = [];
noshows: number[] = [];
noshow_excuseds: number[] = [];
get present(): number {
return this.presents.length;
}
get noshow(): number {
return this.noshows.length;
}
get noshow_excused(): number {
return this.noshow_excuseds.length;
}
sum(): number {
return this.present + this.noshow + this.noshow_excused;
}
union(other: Attendance) {
this.presents = this.presents.concat(other.presents).sort();
this.noshows = this.noshows.concat(other.noshows).sort();
this.noshow_excuseds = this.noshow_excuseds
.concat(other.noshow_excuseds)
.sort();
}
}
export async function generateAttendancePage(
outputDir: string,
......@@ -49,25 +79,25 @@ function _generateAttendancePart(
): Map<string, Attendance> {
const attendanceMap = new Map<string, Attendance>();
for (const { present, absent } of relevantTranscripts) {
for (const { present, absent, number } of relevantTranscripts) {
present.forEach((name) =>
_incrementAttendance(name, attendanceMap, true),
_incrementAttendance(name, attendanceMap, true, number!),
);
absent.forEach((name) =>
_incrementAttendance(name, attendanceMap, false),
_incrementAttendance(name, attendanceMap, false, number!),
);
}
_mergeAttendance(attendanceMap);
_mergeAttendance(attendanceMap, relevantTranscripts.length);
return attendanceMap;
}
function _mergeAttendance(attendanceMap: Map<string, Attendance>) {
function _mergeAttendance(
attendanceMap: Map<string, Attendance>,
maxAttendance: number,
) {
const names: string[] = [...attendanceMap.keys()];
const maxAttendance: number = Math.max(
...[...attendanceMap.values()].map(_sumAttendance),
);
for (const shortName of names) {
if (shortName.includes(" ")) {
......@@ -83,16 +113,13 @@ function _mergeAttendance(attendanceMap: Map<string, Attendance>) {
const longAttendance: Attendance = attendanceMap.get(
nameMatches[0],
)!;
longAttendance.present += shortAttendance.present;
longAttendance.noshow += shortAttendance.noshow;
longAttendance.noshow_excused += shortAttendance.noshow_excused;
longAttendance.union(shortAttendance);
attendanceMap.delete(shortName);
}
}
let missingNames = [...attendanceMap.keys()].filter(
(name: string) =>
_sumAttendance(attendanceMap.get(name)!) != maxAttendance,
(name: string) => attendanceMap.get(name)!.sum() != maxAttendance,
);
for (const missingName of missingNames) {
const missingAttendance = attendanceMap.get(missingName);
......@@ -112,15 +139,12 @@ function _mergeAttendance(attendanceMap: Map<string, Attendance>) {
const otherName = potentialMatches.pop()!;
const otherAttendance = attendanceMap.get(otherName)!;
if (
_sumAttendance(missingAttendance)
+ _sumAttendance(otherAttendance)
missingAttendance.sum() + otherAttendance.sum()
> maxAttendance
) {
continue;
}
missingAttendance.present += otherAttendance.present;
missingAttendance.noshow += otherAttendance.noshow;
missingAttendance.noshow_excused += otherAttendance.noshow_excused;
missingAttendance.union(otherAttendance);
attendanceMap.delete(otherName);
missingNames = missingNames.filter((e) => e != otherName);
}
......@@ -131,30 +155,24 @@ function _incrementAttendance(
name: string,
attendanceMap: Map<string, Attendance>,
present: boolean,
transscriptNumber: number,
) {
const cleanName = _cleanName(name);
const attendance: Attendance = attendanceMap.get(cleanName) ?? {
present: 0,
noshow: 0,
noshow_excused: 0,
};
const attendance: Attendance =
attendanceMap.get(cleanName) ?? new Attendance();
if (present) {
attendance.present += 1;
attendance.presents.push(transscriptNumber);
} else {
if (_isExcused(name)) {
attendance.noshow_excused += 1;
attendance.noshow_excuseds.push(transscriptNumber);
} else {
attendance.noshow += 1;
attendance.noshows.push(transscriptNumber);
}
}
attendanceMap.set(cleanName, attendance);
}
function _sumAttendance(attendance: Attendance): number {
return attendance.present + attendance.noshow_excused + attendance.noshow;
}
function _isExcused(name: string): boolean {
return name.search(/([(\[]).*?(([eE])(ntschuldigt)?).*?([)\]])/) !== -1;
}
......
import { FinishedTranscriptMeta, Resolution, Todo } from "./parsing";
import { Attendance } from "./attendance";
export function renderContainerToAlert(context: String, title?: String) {
return function (tokens, idx) {
......@@ -535,12 +536,6 @@ ${resolution.date} ${resolution.number}: beschlossen
return `<details><summary><small class="text-body-secondary fw-light">Hledger-Statement für die Buchhaltung</small></summary><pre><code id="reso-text-${resolution.number}">${hledgerText}</code></pre><button class="btn btn-outline-primary btn-sm" onclick="navigator.clipboard.writeText(document.getElementById(\'reso-text-${resolution.number}\').innerText)"><svg width="16" height="16" version="2.0"><use href="#clipboard-icon" /></svg> Hledger-Statement kopieren</button></details>`;
}
export interface Attendance {
present: number;
noshow: number;
noshow_excused: number;
}
export function generateAttendanceHtmlWrapper(
attendanceParts: [
Map<string, Attendance>,
......@@ -567,6 +562,22 @@ export function generateAttendanceHtmlWrapper(
border: 1px solid #ccc;
margin-top: 5px;
}
body:has(#grouped-bar-toggle:checked) .grouped-bar {
visibility: visible;
position: static;
}
body:has(#grouped-bar-toggle:checked) .ungrouped-bar {
visibility: hidden;
position: absolute;
}
.grouped-bar {
visibility: hidden;
position: absolute;
}
.ungrouped-bar {
visibility: visible;
position: static;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
......@@ -636,6 +647,12 @@ export function generateAttendanceHtmlWrapper(
<a class="nav-link" href="https://gitlab.fachschaften.org/search?group_id=29&project_id=77&repository_ref=master&scope=blobs&search=">Suche</a>
</li>
</ul>
<div class="ms-auto d-flex-flex align-items-center form-check me-3">
<input class="form-check-input me-2" type="checkbox" id="grouped-bar-toggle" value="" checked />
<label class="form-check-label" for="grouped-bar-toggle">
Group attendance bar
</label>
</div>
</nav>
</header>
</div>
......@@ -689,22 +706,43 @@ export function generateAttendanceHtml(
}
function generateAttendanceRowHtml(name: string, data: Attendance): string {
const sum = (data.present + data.noshow + data.noshow_excused) / 100;
const bar = `
<div class="percentage-bar">
<div style="width: ${data.present / sum}%;background-color: darkgreen"></div>
<div style="width: ${data.noshow_excused / sum}%;background-color: yellow"></div>
<div style="width: ${data.noshow / sum}%;background-color: red"></div>
</div>`;
return `
<tr>
<td> ${name} </td>
<td> ${bar} </td>
<td> ${generateGroupedPercentageBar(data)} ${generateUngroupedPercentageBar(data)} </td>
<td> ${data.present} </td>
<td> ${data.noshow_excused} </td>
<td> ${data.noshow}</td>
<td> ${(data.present / sum).toFixed(1)}% </td>
<td> ${((data.present * 100) / data.sum()).toFixed(1)}% </td>
</tr>`;
}
function generateUngroupedPercentageBar(data: Attendance): string {
const width = 100 / data.sum();
const tagged: { value: number; color: string }[] = [
...data.presents.map((value) => ({ value, color: "darkgreen" })),
...data.noshows.map((value) => ({ value, color: "red" })),
...data.noshow_excuseds.map((value) => ({ value, color: "yellow" })),
];
// Sort by number
tagged.sort((a, b) => a.value - b.value);
var barHtml = '<div class="percentage-bar ungrouped-bar">';
// Iterate in order
for (const { color } of tagged) {
barHtml += `<div style="width: ${width}%;background-color: ${color}"></div>`;
}
barHtml += "</div>";
return barHtml;
}
function generateGroupedPercentageBar(data: Attendance): string {
return `
<div class="percentage-bar grouped-bar">
<div style="width: ${(data.present * 100) / data.sum()}%;background-color: darkgreen"></div>
<div style="width: ${(data.noshow_excused * 100) / data.sum()}%;background-color: yellow"></div>
<div style="width: ${(data.noshow * 100) / data.sum()}%;background-color: red"></div>
</div>`;
}
Subproject commit bca907530f65c7088b6cd362f2f6f6fff463d05b
Subproject commit 4284e35c4f0af500eef8bb6fbbeaa306507625da
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment