From 9d8c316e9758692e7b2b6e8d560ae8ae0de5d46f Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 28 Oct 2026 11:27:03 +0100 Subject: [PATCH] Show per-player TTR totals from completed singles only --- .../src/components/MatchReportApiDialog.vue | 80 ++++++++++--------- frontend/tests/ttrPreview.test.mjs | 59 +++++++++++--- 2 files changed, 92 insertions(+), 47 deletions(-) diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue index 811b4c38..41f55e63 100755 --- a/frontend/src/components/MatchReportApiDialog.vue +++ b/frontend/src/components/MatchReportApiDialog.vue @@ -617,26 +617,26 @@ -
+
-

TTR-Vorschau

Automatisch aus den gespeicherten Vereinswerten und – falls nötig – der myTischtennis-Rangliste geladen. Doppel zählen nicht.

+

TTR nach Ergebnissen

Berechneter TTR aus den abgeschlossenen Einzeln dieses Spielberichts. Ohne Einzelergebnis wird kein neuer Wert angezeigt. Doppel zählen nicht. Die Werte sind nicht offiziell.

{{ ttrError }}

TTR-Werte werden geladen …
- +
EinzelHeimGastErwartung HeimVorschau / Ergebnis
- - - - - - - - + + + + + + + + - +
MannschaftSpielerAusgangs-TTRAbgeschlossene EinzelBerechneter TTR
{{ entry.index + 1 }}{{ entry.homeName }} · {{ entry.homeTtr ?? 'TTR fehlt' }}{{ entry.guestName }} · {{ entry.guestTtr ?? 'TTR fehlt' }}{{ entry.preview ? formatTtrProbability(entry.preview.homeWin.expectedWinProbability) : '—' }}{{ entry.actual.winner === 'home' ? 'Heimsieg' : 'Gastsieg' }}: Heim {{ formatTtrChange(entry.actual.home.displayChange) }}, Gast {{ formatTtrChange(entry.actual.guest.displayChange) }}Heimsieg: {{ formatTtrChange(entry.preview.homeWin.displayChange) }} · Heimniederlage: {{ formatTtrChange(entry.preview.homeLoss.displayChange) }}Berechnung erst mit beiden TTR-Werten möglich.
{{ entry.side === 'home' ? 'Heim' : 'Gast' }}{{ entry.name }}{{ entry.ttr ?? 'TTR fehlt' }}{{ entry.completedCount }}{{ entry.projectedTtr }}TTR-Wert fehlt—
Noch keine Einzelpaarungen vorhanden. Bitte die Einzelaufstellungen prüfen.
Noch keine Einzelpaarungen vorhanden. Bitte die Einzelaufstellungen prüfen.

Noch ohne Wert: {{ ttrUnavailablePlayers.join(', ') }}.

@@ -872,7 +872,7 @@ import CryptoJS from 'crypto-js'; import apiClient, { backendBaseUrl } from '../apiClient'; import { onMatchReportSubmitted, offMatchReportSubmitted } from '../services/socketService.js'; -import { calculateTtrChange } from '../services/ttrCalculator.mjs'; +import { calculateTtrEvent } from '../services/ttrCalculator.mjs'; import { resolveLineupToken } from '../services/lineupResolver.mjs'; export default { @@ -1148,32 +1148,44 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr .filter(({ match }) => !this.isTtrDouble(match)) .map(({ match, index }) => { const snapshot = this.ttrSnapshot(index); - const entry = { + return { index, homeName: match.homeName || 'Heim', guestName: match.guestName || 'Gast', homeTtr: this.isAvailableTtr(snapshot?.homeTtr) ? Number(snapshot.homeTtr) : null, guestTtr: this.isAvailableTtr(snapshot?.guestTtr) ? Number(snapshot.guestTtr) : null, - preview: null, - actual: null, - }; - if (entry.homeTtr === null || entry.guestTtr === null) return entry; - const homeWin = calculateTtrChange(snapshot.homeTtr, snapshot.guestTtr, 'win', snapshot.homeAdjustmentConstant); - const homeLoss = calculateTtrChange(snapshot.homeTtr, snapshot.guestTtr, 'loss', snapshot.homeAdjustmentConstant); - const guestWin = calculateTtrChange(snapshot.guestTtr, snapshot.homeTtr, 'win', snapshot.guestAdjustmentConstant); - const guestLoss = calculateTtrChange(snapshot.guestTtr, snapshot.homeTtr, 'loss', snapshot.guestAdjustmentConstant); - const winner = this.ttrWinner(match); - return { - ...entry, - preview: { homeWin, homeLoss, guestWin, guestLoss }, - actual: winner ? { - winner, - home: winner === 'home' ? homeWin : homeLoss, - guest: winner === 'guest' ? guestWin : guestLoss, - } : null, + homeAdjustmentConstant: snapshot?.homeAdjustmentConstant ?? 16, + guestAdjustmentConstant: snapshot?.guestAdjustmentConstant ?? 16, + winner: this.ttrWinner(match), }; }); }, + ttrPlayerResults() { + const players = new Map(); + for (const single of this.ttrSingles) { + for (const side of ['home', 'guest']) { + const name = single[`${side}Name`]; + const key = `${side}:${this.normalizeTtrName(name)}`; + if (!players.has(key)) players.set(key, { + key, side, name, ttr: single[`${side}Ttr`], + adjustmentConstant: single[`${side}AdjustmentConstant`], + completedCount: 0, missingOpponentTtr: false, matches: [], + }); + const player = players.get(key); + if (!single.winner) continue; + player.completedCount++; + const opponentTtr = single[side === 'home' ? 'guestTtr' : 'homeTtr']; + if (opponentTtr === null) player.missingOpponentTtr = true; + else player.matches.push({ opponentTtr, result: single.winner === side ? 'win' : 'loss' }); + } + } + return [...players.values()].sort((a, b) => (a.side === b.side ? 0 : a.side === 'home' ? -1 : 1)).map(player => ({ + ...player, + projectedTtr: player.completedCount > 0 && player.ttr !== null && !player.missingOpponentTtr + ? calculateTtrEvent({ ownTtr: player.ttr, adjustmentConstant: player.adjustmentConstant, matches: player.matches }).projectedTtr + : null, + })); + }, ttrUnavailablePlayers() { return this.ttrPlayers.filter((player) => !this.isAvailableTtr(player.ttr)).map((player) => player.name); }, @@ -3453,12 +3465,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr if (homeWins === guestWins) return null; return homeWins > guestWins ? 'home' : 'guest'; }, - formatTtrProbability(value) { - return `${(value * 100).toFixed(1)} %`; - }, - formatTtrChange(value) { - return `${value > 0 ? '+' : ''}${value}`; - }, populateResultsFromMeetingDetails() { if (!this.meetingDetails || !Array.isArray(this.meetingDetails.matches)) { return; diff --git a/frontend/tests/ttrPreview.test.mjs b/frontend/tests/ttrPreview.test.mjs index 0136fe03..c2541876 100644 --- a/frontend/tests/ttrPreview.test.mjs +++ b/frontend/tests/ttrPreview.test.mjs @@ -2,14 +2,14 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; -import { calculateTtrChange } from '../src/services/ttrCalculator.mjs'; +import { calculateTtrEvent } from '../src/services/ttrCalculator.mjs'; const source = readFileSync(new URL('../src/components/MatchReportApiDialog.vue', import.meta.url), 'utf8'); const script = source.match(/