diff --git a/README.md b/README.md index dd16fa8c748b64731973d4f5c1f0453526f922c8..a2b051e2779c71e8524f49bf2892de24413e950b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/lib/attendance.ts b/src/lib/attendance.ts index 66f53805fb149fbf39fd9a058c6a0ab018116f34..e84a290b84f101f8945abc9ef00d3b9368838240 100644 --- a/src/lib/attendance.ts +++ b/src/lib/attendance.ts @@ -1,6 +1,36 @@ 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; } diff --git a/src/lib/rendering.ts b/src/lib/rendering.ts index f1487f166041ff0a9ae9f63c13390655cfcf460f..0be563737452c59fae3e0f619446815074e000f5 100644 --- a/src/lib/rendering.ts +++ b/src/lib/rendering.ts @@ -1,4 +1,5 @@ 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 () { @@ -618,24 +629,30 @@ export function generateAttendanceHtmlWrapper( <header> <nav class="navbar navbar-expand bg-body-tertiary"> <ul class="navbar-nav"> - <li class="nav-item"> - <a class="nav-link" href="../">Protokolle</a> - <i class="fa fa-link"></i> </li> - <li class="nav-item"> - <a class="nav-link" href="../resolutions/">Beschlüsse</a> - </li> - <!-- WIP: - <li class="nav-item"> - <a class="nav-link" href="../relevant/">Relevante Beschlüsse</a> - </li> - --> - <li class="nav-item"> - <a class="nav-link" href="../attendance/">Anwesenheit</a> - </li> - <li class="nav-item"> - <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> + <li class="nav-item"> + <a class="nav-link" href="../">Protokolle</a> + <i class="fa fa-link"></i> </li> + <li class="nav-item"> + <a class="nav-link" href="../resolutions/">Beschlüsse</a> + </li> + <!-- WIP: + <li class="nav-item"> + <a class="nav-link" href="../relevant/">Relevante Beschlüsse</a> + </li> + --> + <li class="nav-item"> + <a class="nav-link" href="../attendance/">Anwesenheit</a> + </li> + <li class="nav-item"> + <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>`; +} diff --git a/test/sitzungen b/test/sitzungen index bca907530f65c7088b6cd362f2f6f6fff463d05b..4284e35c4f0af500eef8bb6fbbeaa306507625da 160000 --- a/test/sitzungen +++ b/test/sitzungen @@ -1 +1 @@ -Subproject commit bca907530f65c7088b6cd362f2f6f6fff463d05b +Subproject commit 4284e35c4f0af500eef8bb6fbbeaa306507625da