From c1b8b2c665d55f4b61bbf842e4434c3c858767a6 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 8 Apr 2026 11:20:46 +0200 Subject: [PATCH] feat(TournamentStats): refine internal tournament scoring and enhance UI features - Updated the scoring logic for internal tournaments to reflect percentage-based placements, improving clarity and fairness in rankings. - Refactored the `groupPointsFromRankings` function to `groupPercentFromRankings` for better readability and accuracy in calculations. - Enhanced the InternalTournamentStats component with a new PDF export feature and improved dialog positioning for better user experience. - Updated localization strings across multiple languages to align with the new scoring system and UI enhancements, ensuring better accessibility and understanding for users. --- backend/controllers/tournamentController.js | 2 +- .../internalTournamentStatsService.js | 49 +++-- .../tournament/InternalTournamentStats.vue | 189 +++++++++++++++++- frontend/src/i18n/locales/de-CH.json | 9 +- frontend/src/i18n/locales/de-extended.json | 9 +- frontend/src/i18n/locales/de.json | 9 +- frontend/src/i18n/locales/en-AU.json | 9 +- frontend/src/i18n/locales/en-GB.json | 9 +- frontend/src/i18n/locales/en-US.json | 9 +- frontend/src/i18n/locales/es.json | 9 +- frontend/src/i18n/locales/fil.json | 9 +- frontend/src/i18n/locales/fr.json | 9 +- frontend/src/i18n/locales/it.json | 9 +- frontend/src/i18n/locales/ja.json | 9 +- frontend/src/i18n/locales/pl.json | 9 +- frontend/src/i18n/locales/th.json | 9 +- frontend/src/i18n/locales/tl.json | 9 +- frontend/src/i18n/locales/zh.json | 9 +- 18 files changed, 294 insertions(+), 81 deletions(-) diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index 5b5536cb..dd383232 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -59,7 +59,7 @@ export const getTournaments = async (req, res) => { } }; -/** Ranglisten interne Einzel-Turniere (Gruppen- + K.-o.-Punkte) */ +/** Ranglisten interne Einzel-Turniere (Gruppen-% + K.-o.-Bonus wie im Service) */ export const getInternalTournamentStats = async (req, res) => { const { authcode: token } = req.headers; const { clubId } = req.params; diff --git a/backend/services/internalTournamentStatsService.js b/backend/services/internalTournamentStatsService.js index 860323de..183cd5f4 100644 --- a/backend/services/internalTournamentStatsService.js +++ b/backend/services/internalTournamentStatsService.js @@ -1,10 +1,11 @@ /** - * Statistik-Punkte für interne Einzel-Turniere (Gruppenphase + K.-o.-Runde). + * Statistik-Werte für interne Einzel-Turniere (Gruppenphase + K.-o.-Runde). * - * Gruppe: 1. Platz = 100, 2. = 99, 3. = 98, … (Platzierung aus API: 1 = bestes Ergebnis). - * Gleiche Platzierung = gleiche Punkte. Ab Platz 101 wird mit 0 gekappt. - * K.-o.: Wer die K.-o.-Runde erreicht: höchste Gruppenpunkte dieser Klasse + 1, - * danach +1 pro gewonnenes K.-o.-Spiel. + * Gruppe: Platzierung als Prozent relativ zur Feldgröße N (alle mit Platzierung > 0), + * Formel (N − pos) / (N − 1) × 100 für N ≥ 2 (1. = 100 %, Letzter = 0 %), + * bei N = 1: 100 %. Nur Vereinsmitglieder werden gewertet; N zählt alle Platzierten. + * K.-o.: Wer die K.-o.-Runde erreicht: höchster Gruppen-Prozentwert dieser Klasse + 1, + * danach +1 pro gewonnenes K.-o.-Spiel (wie bisher, jetzt auf %-Skala). */ export function parseWinnerFromMatch(match) { @@ -50,19 +51,32 @@ export function parseWinnerFromMatch(match) { } /** - * @param {Array<{ position: number, id: number }>} rankings – Platz 1 = bestes Ergebnis; id = Turnier-Mitglied-ID - * @returns {Map} tournamentMemberId -> Punkte (1. Platz 100, 2. 99, …) + * @param {Array<{ position: number, id: number }>} rankings – zu wertende Spieler; id = Turnier-Mitglied-ID + * @param {number} nInGroup – Anzahl Teilnehmer in der Gruppe mit Platzierung > 0 (inkl. Externe) + * @returns {Map} tournamentMemberId -> Prozent 0–100 (Gleitkomma) */ -export function groupPointsFromRankings(rankings) { +export function groupPercentFromRankings(rankings, nInGroup) { const map = new Map(); - if (!rankings || rankings.length === 0) return map; + const N = Number(nInGroup); + if (!rankings || rankings.length === 0 || !Number.isFinite(N) || N < 1) return map; + + if (N === 1) { + for (const r of rankings) { + const id = r.id; + if (id == null) continue; + map.set(Number(id), 100); + } + return map; + } + for (const r of rankings) { const pos = Number(r.position); if (!Number.isFinite(pos) || pos < 1) continue; const id = r.id; if (id == null) continue; - const pts = Math.max(0, 101 - pos); - map.set(Number(id), pts); + const raw = ((N - pos) / (N - 1)) * 100; + const pct = Math.max(0, Math.min(100, raw)); + map.set(Number(id), pct); } return map; } @@ -130,12 +144,15 @@ export function computeInternalSinglesStatsForTournament({ }); const groupPointsByTm = new Map(); - let maxGroupPoints = 0; + let maxGroupPct = 0; for (const g of classGroups) { const parts = g.participants || []; if (parts.length === 0) continue; + const nInGroup = parts.filter((p) => Number(p.position) > 0).length; + if (nInGroup < 1) continue; + const singlesRankings = parts .filter((p) => !p.isExternal && p.id && tmToMemberId.has(Number(p.id))) .map((p) => ({ @@ -144,10 +161,10 @@ export function computeInternalSinglesStatsForTournament({ })) .filter((r) => r.position > 0); - const m = groupPointsFromRankings(singlesRankings); + const m = groupPercentFromRankings(singlesRankings, nInGroup); for (const [tmId, pts] of m) { groupPointsByTm.set(tmId, pts); - if (pts > maxGroupPoints) maxGroupPoints = pts; + if (pts > maxGroupPct) maxGroupPct = pts; } } @@ -184,7 +201,7 @@ export function computeInternalSinglesStatsForTournament({ const inKo = playedKo.has(tmId); let total; if (inKo) { - total = maxGroupPoints + 1 + wins; + total = maxGroupPct + 1 + wins; } else { total = gPts; } @@ -199,7 +216,7 @@ export function computeInternalSinglesStatsForTournament({ } export default { - groupPointsFromRankings, + groupPercentFromRankings, computeInternalSinglesStatsForTournament, parseWinnerFromMatch, }; diff --git a/frontend/src/components/tournament/InternalTournamentStats.vue b/frontend/src/components/tournament/InternalTournamentStats.vue index 2238fe17..c501957c 100644 --- a/frontend/src/components/tournament/InternalTournamentStats.vue +++ b/frontend/src/components/tournament/InternalTournamentStats.vue @@ -3,13 +3,24 @@ :model-value="modelValue" :title="$t('tournaments.internalStatsTitle')" size="large" + :is-modal="false" + :position="dialogPosition" :closable="true" - :close-on-overlay="true" + :close-on-overlay="false" @update:model-value="$emit('update:modelValue', $event)" + @update:position="dialogPosition = $event" @close="$emit('update:modelValue', false)" >
+