feat(test-mode): implement test mode functionality for local match verification
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 58s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 58s
This commit is contained in:
@@ -20,6 +20,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="test-mode-control" :class="{ active: isTestMode }">
|
||||
<input v-model="isTestMode" type="checkbox" @change="onTestModeChange" />
|
||||
<span>
|
||||
<strong>Testmodus</strong>
|
||||
<small>Nur lokal ausprobieren – keine Speicherung oder Übermittlung an nuScore</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div v-if="isTestMode && (meetingDetails?.isCompleted || meetingData?.isCompleted)" class="test-verification">
|
||||
<button type="button" class="btn-secondary" @click="verifyCompletedMatchProjection">
|
||||
🧪 Gespeicherte Paarungen prüfen
|
||||
</button>
|
||||
<p v-if="testVerification" :class="testVerification.matches ? 'test-success' : 'test-error'">
|
||||
{{ testVerification.summary }}
|
||||
</p>
|
||||
<ul v-if="testVerification && !testVerification.matches" class="test-mismatches">
|
||||
<li v-for="item in testVerification.items.filter(entry => !entry.matches)" :key="item.matchNumber">
|
||||
Spiel {{ item.matchNumber }}: erwartet „{{ item.expectedHome }} – {{ item.expectedGuest }}“,
|
||||
gespeichert „{{ item.actualHome }} – {{ item.actualGuest }}"
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nuscore-sections">
|
||||
<div class="section-grid">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'general' }" @click="setActiveSection('general')">
|
||||
@@ -715,6 +738,8 @@ export default {
|
||||
return {
|
||||
loading: true,
|
||||
error: null,
|
||||
isTestMode: false,
|
||||
testVerification: null,
|
||||
meetingData: null,
|
||||
meetingDetails: null,
|
||||
activeSection: 'general',
|
||||
@@ -1631,6 +1656,8 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
getSubmitButtonText() {
|
||||
if (this.isMatchCompleted) {
|
||||
return '✅ Spielbericht bereits abgesendet';
|
||||
} else if (this.isTestMode) {
|
||||
return '🧪 Testübermittlung simulieren';
|
||||
} else if (!this.areAllMatchResultsValid()) {
|
||||
const playThrough = this.getPlayThrough();
|
||||
if (playThrough && playThrough.toUpperCase().includes('ALLGAMES')) {
|
||||
@@ -1750,6 +1777,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
async submitMatchReport() {
|
||||
try {
|
||||
console.log('🚀 Starte Spielbericht-Absendung...');
|
||||
|
||||
if (this.isTestMode) {
|
||||
alert('🧪 Testmodus: Der Spielbericht wurde nur lokal geprüft und nicht an nuScore übermittelt.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn eine Mannschaft nicht angetreten ist, keine Match-Ergebnisse oder Zeiten validieren
|
||||
const isTeamNotAppeared = this.teamNotAppeared !== null;
|
||||
@@ -2025,9 +2057,12 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Spieleranzahl aktualisieren (aus Aufstellung)
|
||||
matchData.playerCountHome = this.getSelectedPlayerCount('home');
|
||||
matchData.playerCountGuest = this.getSelectedPlayerCount('guest');
|
||||
// playerCount zählt nur Einzelspieler. Ein zusätzlicher,
|
||||
// ausschließlich im Doppel eingesetzter Spieler darf die
|
||||
// Braunschweig-Matrix nicht auf eine falsche Konstellation
|
||||
// (etwa 5:3) umstellen.
|
||||
matchData.playerCountHome = this.getSinglePlayerCount('home');
|
||||
matchData.playerCountGuest = this.getSinglePlayerCount('guest');
|
||||
|
||||
// 1.1 Lokalen Aufstellungszustand in den Payload übernehmen
|
||||
this.applyCurrentLineupStateToPayload(matchData);
|
||||
@@ -2161,10 +2196,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
matchData.teamLineupHomePlayers.forEach(player => {
|
||||
const positions = [];
|
||||
|
||||
// Einzel-Position NUR hinzufügen wenn Spieler ausgewählt ist
|
||||
// Einzelposition nur übernehmen, wenn sie tatsächlich
|
||||
// gesetzt ist. Ersatzspieler im Doppel dürfen nicht über
|
||||
// ihren Rang zu Einzelspielern werden.
|
||||
if (player.isSelected) {
|
||||
// Verwende positionSingle falls verfügbar, sonst rank für Einzel-Position
|
||||
const singlePos = player.positionSingle || player.rank;
|
||||
const singlePos = player.positionSingle;
|
||||
if (singlePos) {
|
||||
player.positionSingle = singlePos;
|
||||
positions.push(`E${singlePos}`);
|
||||
@@ -2190,8 +2226,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Einzel-Position NUR hinzufügen wenn Spieler ausgewählt ist
|
||||
if (player.isSelected) {
|
||||
// Verwende positionSingle falls verfügbar, sonst rank für Einzel-Position
|
||||
const singlePos = player.positionSingle || player.rank;
|
||||
const singlePos = player.positionSingle;
|
||||
if (singlePos) {
|
||||
player.positionSingle = singlePos;
|
||||
positions.push(`E${singlePos}`);
|
||||
@@ -2277,6 +2312,18 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
|
||||
// Anzahl der tatsächlich im Einzel aufgestellten Spieler. Bei alten,
|
||||
// noch nicht positionierten Entwürfen bleibt die Auswahlzahl der
|
||||
// kompatible Fallback.
|
||||
getSinglePlayerCount(team) {
|
||||
const key = team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
|
||||
const players = this.meetingDetails?.[key] || this[team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers'] || [];
|
||||
const selected = players.filter(player => player?.isSelected && player?.nuLigaPersonId > 0);
|
||||
const singles = selected.filter(player => Number(player?.positionSingle) > 0);
|
||||
|
||||
return singles.length > 0 ? singles.length : selected.length;
|
||||
},
|
||||
|
||||
// Verwende die Matches aus dem Match-Objekt oder lasse vorhandene Ergebnisse bestehen
|
||||
initializeResults() {
|
||||
if (this.match.matches && Array.isArray(this.match.matches) && this.match.matches.length > 0) {
|
||||
@@ -2470,6 +2517,25 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
prepareResults() {
|
||||
// nuScore kann Aufstellungen liefern, die sich nicht aus der Zahl
|
||||
// ausgewählter Spieler ableiten lassen (z.B. ein Spieler nur im
|
||||
// Doppel). Die vom Server gelieferten Begegnungen sind dann für
|
||||
// Reihenfolge und Paarungen maßgeblich.
|
||||
const serverMatches = Array.isArray(this.meetingDetails?.matches)
|
||||
? this.meetingDetails.matches
|
||||
: [];
|
||||
if (serverMatches.length > 0) {
|
||||
this.results = serverMatches.map((match, index) => ({
|
||||
label: (typeof match?.name === 'string' && match.name.trim()) || `Spiel ${index + 1}`,
|
||||
homeName: this.formatMeetingMatchSide(match, 'home') || match?.homePlayer || '',
|
||||
guestName: this.formatMeetingMatchSide(match, 'guest') || match?.guestPlayer || '',
|
||||
sets: ['', '', '', '', ''],
|
||||
completed: false,
|
||||
result: '1:0'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const matrix = this.currentPlayModeMatrix;
|
||||
this.results = (matrix || []).map((label) => ({
|
||||
label,
|
||||
@@ -2735,6 +2801,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
};
|
||||
},
|
||||
persistLocalDraft() {
|
||||
if (this.isTestMode) return;
|
||||
const key = this.getDraftStorageKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
@@ -2745,6 +2812,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
persistLocalDraftDebounced() {
|
||||
if (this.isTestMode) return;
|
||||
if (this.draftPersistTimer) {
|
||||
clearTimeout(this.draftPersistTimer);
|
||||
}
|
||||
@@ -2803,6 +2871,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
syncDraftToServerDebounced(delayMs = 600) {
|
||||
if (this.isTestMode) return;
|
||||
if (this.draftSyncTimer) {
|
||||
clearTimeout(this.draftSyncTimer);
|
||||
}
|
||||
@@ -2812,7 +2881,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}, delayMs);
|
||||
},
|
||||
async syncDraftToServer() {
|
||||
if (this.isDraftSyncing || !this.meetingData?.nuLigaMeetingUuid) {
|
||||
if (this.isTestMode || this.isDraftSyncing || !this.meetingData?.nuLigaMeetingUuid) {
|
||||
return;
|
||||
}
|
||||
this.isDraftSyncing = true;
|
||||
@@ -2863,6 +2932,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
};
|
||||
},
|
||||
async broadcastDraft() {
|
||||
if (this.isTestMode) return;
|
||||
const clubId = this.$store?.getters?.currentClub;
|
||||
const gameCode = this.match?.code;
|
||||
const matchData = this.getDraftMatchData();
|
||||
@@ -2874,6 +2944,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
broadcastDraftDebounced() {
|
||||
if (this.isTestMode) return;
|
||||
if (this.broadcastDraftTimer) clearTimeout(this.broadcastDraftTimer);
|
||||
this.broadcastDraftTimer = setTimeout(() => {
|
||||
this.broadcastDraftTimer = null;
|
||||
@@ -2881,6 +2952,80 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}, 500);
|
||||
},
|
||||
|
||||
normalizeVerificationName(value) {
|
||||
return String(value || '')
|
||||
.toLocaleLowerCase('de-DE')
|
||||
.replace(/\s*\/\s*/g, '/')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
},
|
||||
|
||||
verifyCompletedMatchProjection() {
|
||||
const formations = this.meetingDetails?.meetingPlayMode?.matchFormations || [];
|
||||
const matches = this.meetingDetails?.matches || [];
|
||||
|
||||
if (!Array.isArray(formations) || formations.length === 0 || !Array.isArray(matches) || matches.length === 0) {
|
||||
this.testVerification = {
|
||||
matches: false,
|
||||
summary: 'Keine vollständigen nuScore-Daten zum Vergleichen verfügbar.',
|
||||
items: []
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const items = formations.map((formation, index) => {
|
||||
const homePosition = this.formatLineupPositionLabel(formation.homeLineupPosition, 'home');
|
||||
const guestPosition = this.formatLineupPositionLabel(formation.guestLineupPosition, 'guest');
|
||||
const label = `${homePosition} – ${guestPosition}`;
|
||||
const sourceMatch = matches.find(match => Number(match?.matchNr) === Number(formation.matchNr)) || matches[index];
|
||||
const expectedHome = this.resolveSide(label, 'home');
|
||||
const expectedGuest = this.resolveSide(label, 'guest');
|
||||
const actualHome = this.formatMeetingMatchSide(sourceMatch, 'home');
|
||||
const actualGuest = this.formatMeetingMatchSide(sourceMatch, 'guest');
|
||||
|
||||
return {
|
||||
matchNumber: formation.matchNr || index + 1,
|
||||
expectedHome,
|
||||
expectedGuest,
|
||||
actualHome,
|
||||
actualGuest,
|
||||
matches: this.normalizeVerificationName(expectedHome) === this.normalizeVerificationName(actualHome) &&
|
||||
this.normalizeVerificationName(expectedGuest) === this.normalizeVerificationName(actualGuest)
|
||||
};
|
||||
});
|
||||
|
||||
const mismatchCount = items.filter(item => !item.matches).length;
|
||||
this.testVerification = {
|
||||
matches: mismatchCount === 0,
|
||||
summary: mismatchCount === 0
|
||||
? `✓ Alle ${items.length} gespeicherten Paarungen stimmen mit der lokalen Matrixauflösung überein.`
|
||||
: `⚠️ ${mismatchCount} von ${items.length} Paarungen weichen von der lokalen Matrixauflösung ab.`,
|
||||
items
|
||||
};
|
||||
},
|
||||
|
||||
async onTestModeChange() {
|
||||
if (!this.isTestMode) return;
|
||||
|
||||
// Bereits eingeplante Nebenwirkungen abbrechen. Die Eingaben
|
||||
// existieren danach ausschließlich im aktuellen Dialog.
|
||||
['broadcastDraftTimer', 'draftPersistTimer', 'draftSyncTimer'].forEach((timerKey) => {
|
||||
if (this[timerKey]) {
|
||||
clearTimeout(this[timerKey]);
|
||||
this[timerKey] = null;
|
||||
}
|
||||
});
|
||||
this.isDraftSyncing = false;
|
||||
this.lastDraftSyncError = '';
|
||||
|
||||
// Ein Test beginnt mit dem aktuellen offiziellen Stand, damit ein
|
||||
// alter lokaler Entwurf nicht versehentlich getestet wird.
|
||||
await this.loadData();
|
||||
this.prepareResults();
|
||||
this.populateResultsFromMeetingDetails();
|
||||
this.testVerification = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Übernimmt per WebSocket empfangene Match-Daten (von anderem Gerät) in den Dialog.
|
||||
* Aktualisiert meetingDetails und meetingData, dann Ergebnisse und Aufstellungs-Bestätigung.
|
||||
@@ -4590,6 +4735,67 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.test-mode-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 0 16px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-mode-control.active {
|
||||
border-color: #d78a00;
|
||||
background: #fff3cd;
|
||||
color: #795300;
|
||||
}
|
||||
|
||||
.test-mode-control input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.test-mode-control span {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.test-mode-control small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.test-verification {
|
||||
margin: -8px 0 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #d78a00;
|
||||
border-radius: 10px;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.test-verification p {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.test-success {
|
||||
color: #216e39;
|
||||
}
|
||||
|
||||
.test-error {
|
||||
color: #9f3a38;
|
||||
}
|
||||
|
||||
.test-mismatches {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user