diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue index e9fd53fe..d263a8fb 100755 --- a/frontend/src/components/MatchReportApiDialog.vue +++ b/frontend/src/components/MatchReportApiDialog.vue @@ -626,7 +626,18 @@
TTR-Werte werden geladen …
-
EinzelHeimGastErwartung HeimVorschau / Ergebnis
{{ entry.index + 1 }}{{ entry.homeName }} · {{ ttrSnapshot(entry.index).homeTtr }}{{ entry.guestName }} · {{ ttrSnapshot(entry.index).guestTtr }}{{ 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) }} · Heimpleite: {{ formatTtrChange(entry.preview.homeLoss.displayChange) }}
+ + + {{ 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. + + Noch keine Einzelpaarungen vorhanden. Bitte die Einzelaufstellungen prüfen. +

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

@@ -1134,16 +1145,23 @@ 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); - if (!Number.isFinite(Number(snapshot?.homeTtr)) || !Number.isFinite(Number(snapshot?.guestTtr))) return null; + const entry = { + 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 { - index, - homeName: match.homeName || 'Heim', - guestName: match.guestName || 'Gast', + ...entry, preview: { homeWin, homeLoss, guestWin, guestLoss }, actual: winner ? { winner, @@ -1151,10 +1169,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr guest: winner === 'guest' ? guestWin : guestLoss, } : null, }; - }).filter(Boolean); + }); }, ttrUnavailablePlayers() { - return this.ttrPlayers.filter((player) => !Number.isFinite(Number(player.ttr))).map((player) => player.name); + return this.ttrPlayers.filter((player) => !this.isAvailableTtr(player.ttr)).map((player) => player.name); }, draftStatusState() { if (this.isDraftSyncing) { @@ -3291,6 +3309,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr : []; if (serverMatches.length > 0) { this.results = serverMatches.map((match, index) => ({ + matchType: match.matchType, label: (typeof match?.name === 'string' && match.name.trim()) || this.getMatchFormationLabel(match, index) || `Spiel ${index + 1}`, homeName: this.resolveMatchSide(match, index, 'home'), guestName: this.resolveMatchSide(match, index, 'guest'), @@ -3324,6 +3343,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr ttrSnapshot(index) { return this.ttrSnapshots[index] || null; }, + isAvailableTtr(value) { + return value !== null && value !== undefined && String(value).trim() !== '' && Number.isFinite(Number(value)); + }, normalizeTtrName(value) { return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^\p{L}\p{N}]+/gu, ' ').trim().toLocaleLowerCase('de-DE'); }, @@ -3349,21 +3371,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr homeTeamName: this.meetingData?.homeTeamname, guestTeamName: this.meetingData?.guestTeamname, }); - const resolvedByKey = new Map((response.data?.players || []).map((player) => [`${player.side}:${this.normalizeTtrName(player.name)}`, player])); - const players = ['home', 'guest'].flatMap((side) => this.ttrLineupPlayers(side).map((player, index) => ({ - side, - name: [player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '), - nuLigaPersonId: player.nuLigaPersonId || null, - ...resolvedByKey.get(`${side}:${this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '))}`), - // Test-Aufstellungen sind Platzhalternamen. Nur nicht - // über die API auflösbare Spieler erhalten Testwerte. - ttr: Number.isFinite(Number(resolvedByKey.get(`${side}:${this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '))}`)?.ttr)) - ? Number(resolvedByKey.get(`${side}:${this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '))}`)?.ttr) - : (side === 'home' ? 1320 - index * 23 : 1345 - index * 19), - adjustmentConstant: resolvedByKey.get(`${side}:${this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '))}`)?.adjustmentConstant || 16, - source: resolvedByKey.get(`${side}:${this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' '))}`)?.source || 'test', - }))); - this.applyTtrPreviewPlayers(players); + this.applyTtrPreviewPlayers(Array.isArray(response.data?.players) ? response.data.players : []); return; } if (!this.match?.id) { @@ -3391,14 +3399,22 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr if (this.isTtrDouble(match)) return; const home = index.get(`home:${this.normalizeTtrName(match.homeName)}`); const guest = index.get(`guest:${this.normalizeTtrName(match.guestName)}`); - if (!Number.isFinite(Number(home?.ttr)) || !Number.isFinite(Number(guest?.ttr))) return; - snapshots[resultIndex] = { homeTtr: Number(home.ttr), guestTtr: Number(guest.ttr), homeAdjustmentConstant: Number(home.adjustmentConstant || 16), guestAdjustmentConstant: Number(guest.adjustmentConstant || 16) }; + snapshots[resultIndex] = { + homeTtr: this.isAvailableTtr(home?.ttr) ? Number(home.ttr) : null, + guestTtr: this.isAvailableTtr(guest?.ttr) ? Number(guest.ttr) : null, + homeAdjustmentConstant: Number(home?.adjustmentConstant || 16), + guestAdjustmentConstant: Number(guest?.adjustmentConstant || 16), + }; }); this.ttrSnapshots = snapshots; }, isTtrDouble(match) { - const label = String(match?.label || '').toUpperCase(); - return label.includes('D') || String(match?.homeName || '').includes(' / ') || String(match?.guestName || '').includes(' / '); + const type = String(match?.matchType || '').toUpperCase(); + if (type === 'DOUBLE' || type === 'DOUBLES') return true; + if (type === 'SINGLE' || type === 'SINGLES') return false; + const label = String(match?.label || ''); + return /\b(?:DOPPEL|DOUBLES?)\b|(?:^|[\s–-])D[AB]?\d+(?=$|[\s–-])/i.test(label) || + String(match?.homeName || '').includes(' / ') || String(match?.guestName || '').includes(' / '); }, ttrWinner(match) { if (!match?.completed) return null; diff --git a/frontend/tests/ttrPreview.test.mjs b/frontend/tests/ttrPreview.test.mjs new file mode 100644 index 00000000..1ea83bb6 --- /dev/null +++ b/frontend/tests/ttrPreview.test.mjs @@ -0,0 +1,86 @@ +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'; + +const source = readFileSync(new URL('../src/components/MatchReportApiDialog.vue', import.meta.url), 'utf8'); +const script = source.match(/