@@ -840,6 +859,7 @@
import CryptoJS from 'crypto-js';
import apiClient, { backendBaseUrl } from '../apiClient';
import { onMatchReportSubmitted, offMatchReportSubmitted } from '../services/socketService.js';
+import { calculateTtrChange } from '../services/ttrCalculator.mjs';
export default {
name: 'MatchReportDialog',
@@ -916,6 +936,12 @@ export default {
pinModalError: '',
// Ergebnisse für Ergebniserfassung
results: [],
+ // Kept separately from nuScore results: these are local event
+ // snapshots and must never alter the official match payload.
+ ttrSnapshots: {},
+ ttrPlayers: [],
+ ttrLoading: false,
+ ttrError: '',
// Fehlermeldungen für Satzeingaben
errors: [],
// Aktive Zelle der schwebenden Satz-Tastatur: { matchIndex, setIndex } oder null
@@ -1031,6 +1057,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
}
return this.areBothLineupsCertified();
},
+ canOpenTtrPreview() {
+ // Die Gastaufstellung ist der verlässliche Zeitpunkt, zu dem beide
+ // tatsächlichen Einzel-Aufstellungen feststehen.
+ return this.isGuestLineupCertified || this.viewOnly;
+ },
canOpenCompletionStage() {
if (this.viewOnly) return true;
if (this.isMatchCompleted) {
@@ -1096,6 +1127,34 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
return !this.areAllMatchResultsValid() || !this.areStartAndEndTimesValid();
},
+ ttrSingles() {
+ return this.results
+ .map((match, index) => ({ match, index }))
+ .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 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',
+ preview: { homeWin, homeLoss, guestWin, guestLoss },
+ actual: winner ? {
+ winner,
+ home: winner === 'home' ? homeWin : homeLoss,
+ guest: winner === 'guest' ? guestWin : guestLoss,
+ } : null,
+ };
+ }).filter(Boolean);
+ },
+ ttrUnavailablePlayers() {
+ return this.ttrPlayers.filter((player) => !Number.isFinite(Number(player.ttr))).map((player) => player.name);
+ },
draftStatusState() {
if (this.isDraftSyncing) {
return 'syncing';
@@ -3220,6 +3279,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
completed: false,
result: '1:0'
}));
+ this.ensureTtrSnapshots();
return;
}
@@ -3232,6 +3292,75 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
completed: false,
result: '1:0'
}));
+ this.ensureTtrSnapshots();
+ },
+ ensureTtrSnapshots() {
+ const next = {};
+ this.results.forEach((match, index) => {
+ if (this.isTtrDouble(match)) return;
+ if (this.ttrSnapshots[index]) next[index] = this.ttrSnapshots[index];
+ });
+ this.ttrSnapshots = next;
+ },
+ ttrSnapshot(index) {
+ return this.ttrSnapshots[index] || null;
+ },
+ normalizeTtrName(value) {
+ return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^\p{L}\p{N}]+/gu, ' ').trim().toLocaleLowerCase('de-DE');
+ },
+ ttrLineupPlayers(side) {
+ const key = side === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
+ return (this.meetingDetails?.[key] || []).filter((player) => player?.isSelected && player?.nuLigaPersonId > 0 && this.isSingleLineupPlayer(player));
+ },
+ async openTtrSection() {
+ if (!this.canOpenTtrPreview) return;
+ this.activeSection = 'ttr';
+ await this.loadTtrPreview();
+ },
+ async loadTtrPreview() {
+ if (!this.canOpenTtrPreview || !this.match?.id || !this.meetingDetails) return;
+ this.ttrLoading = true;
+ this.ttrError = '';
+ try {
+ const response = await apiClient.post(`/matches/${this.match.id}/ttr-preview`, {
+ homePlayers: this.ttrLineupPlayers('home'),
+ guestPlayers: this.ttrLineupPlayers('guest'),
+ homeTeamName: this.meetingData?.homeTeamname,
+ guestTeamName: this.meetingData?.guestTeamname,
+ });
+ this.ttrPlayers = Array.isArray(response.data?.players) ? response.data.players : [];
+ const index = new Map(this.ttrPlayers.map((player) => [`${player.side}:${this.normalizeTtrName(player.name)}`, player]));
+ const snapshots = {};
+ this.results.forEach((match, resultIndex) => {
+ 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) };
+ });
+ this.ttrSnapshots = snapshots;
+ } catch (error) {
+ this.ttrError = error.response?.data?.error || 'TTR-Werte konnten nicht geladen werden.';
+ } finally {
+ this.ttrLoading = false;
+ }
+ },
+ isTtrDouble(match) {
+ const label = String(match?.label || '').toUpperCase();
+ return label.includes('D') || String(match?.homeName || '').includes(' / ') || String(match?.guestName || '').includes(' / ');
+ },
+ ttrWinner(match) {
+ if (!match?.completed) return null;
+ const homeWins = this.getPlayerSetWins(match, 'home');
+ const guestWins = this.getPlayerSetWins(match, 'guest');
+ 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)) {
@@ -6522,6 +6651,24 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
text-align: center;
}
+.ttr-preview {
+ margin: 18px 0;
+ padding: 16px;
+ border: 1px solid #bfdbfe;
+ border-radius: 8px;
+ background: #f8fbff;
+}
+
+.ttr-preview-heading { display: flex; gap: 12px; justify-content: space-between; align-items: flex-start; }
+.ttr-preview-heading h3 { margin: 0; color: #1d4ed8; }
+.ttr-preview-heading p { margin: 4px 0 0; color: #475569; font-size: .9em; }
+.ttr-preview-badge { padding: 3px 8px; border-radius: 999px; color: #1e40af; background: #dbeafe; font-size: .8em; font-weight: 700; }
+.ttr-preview-table-wrap { overflow-x: auto; margin-top: 12px; }
+.ttr-preview-table { width: 100%; border-collapse: collapse; font-size: .9em; }
+.ttr-preview-table th, .ttr-preview-table td { padding: 8px; border-bottom: 1px solid #dbeafe; text-align: left; white-space: nowrap; }
+.ttr-preview-table input { width: 76px; padding: 5px; border: 1px solid #93c5fd; border-radius: 4px; }
+.ttr-preview-empty { margin: 10px 0 0; color: #64748b; }
+
.match-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));