From a0fdf256e7f53e724223a8c1ff3e6c7c5ce96495 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 3 Oct 2025 22:49:05 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCgt=20neue=20Funktionen=20zur=20Bearbeitu?= =?UTF-8?q?ng=20von=20Spielberichten=20im=20MatchReportApiDialog=20hinzu.?= =?UTF-8?q?=20Implementiert=20eine=20Anzeige=20f=C3=BCr=20den=20Abschlusss?= =?UTF-8?q?tatus=20des=20Spiels,=20einschlie=C3=9Flich=20Warnungen=20und?= =?UTF-8?q?=20Deaktivierungen=20von=20Eingabefeldern,=20wenn=20das=20Match?= =?UTF-8?q?=20bereits=20abgeschlossen=20ist.=20Aktualisiert=20die=20Logik?= =?UTF-8?q?=20zur=20Berechnung=20der=20Gesamtpunkte=20und=20S=C3=A4tze=20s?= =?UTF-8?q?owie=20zur=20Validierung=20von=20PINs.=20Verbessert=20die=20Ben?= =?UTF-8?q?utzeroberfl=C3=A4che=20mit=20neuen=20CSS-Stilen=20f=C3=BCr=20ab?= =?UTF-8?q?geschlossene=20Matches=20und=20Dialoge=20zur=20Anzeige=20von=20?= =?UTF-8?q?Match-Daten.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MatchReportApiDialog.vue | 648 ++++++++++++++++-- 1 file changed, 595 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue index 0b7d163..5f6f30a 100644 --- a/frontend/src/components/MatchReportApiDialog.vue +++ b/frontend/src/components/MatchReportApiDialog.vue @@ -51,6 +51,7 @@ @click="setActiveSection('result')" :disabled="!canOpenNextStages">
Ergebniserfassung + 🔒 - +
+ + + +
-
@@ -458,33 +475,43 @@
-
- +
-
- -
- -
+ +
+ +
+ ⚠️ Dieser Spielbericht wurde bereits abgesendet. Keine weiteren Änderungen möglich. +
+
@@ -653,6 +680,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr return norm(this.meetingData.league) === norm(this.meetingData.group); }, canOpenNextStages() { + // Wenn das Match bereits abgeschlossen ist, dürfen keine Änderungen mehr gemacht werden + if (this.isMatchCompleted) { + return false; + } return this.isHomeLineupCertified && this.isGuestLineupCertified; }, currentPlayMode() { @@ -777,22 +808,26 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr }, getOverallSetScore() { - let totalHomeSets = 0; - let totalGuestSets = 0; + let totalHomeSetsWon = 0; + let totalGuestSetsWon = 0; this.results.forEach(match => { if (match.completed) { match.sets.forEach(set => { if (set && set.includes(':')) { const [home, guest] = set.split(':').map(s => parseInt(s) || 0); - totalHomeSets += home; - totalGuestSets += guest; + + if (home > guest) { + totalHomeSetsWon++; + } else if (guest > home) { + totalGuestSetsWon++; + } } }); } }); - return `(${totalHomeSets}:${totalGuestSets})`; + return `(${totalHomeSetsWon}:${totalGuestSetsWon})`; }, initializeFinalPins() { @@ -803,22 +838,341 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr async submitMatchReport() { try { - const reportData = { - matchId: this.match.id, - startTime: this.match.startDate, - endTime: this.match.endDate, - protestText: this.protestText, - homePin: this.finalHomePin, - guestPin: this.finalGuestPin, - results: this.results, - finalScore: this.getOverallScore() - }; + console.log('🚀 Starte Spielbericht-Absendung...'); + + // Validiere PINs (für Test: Gast-PIN "1234" akzeptieren) + if (!this.finalHomePin || this.finalHomePin.trim() === '') { + alert('Bitte geben Sie die PIN des Heimvereins ein.'); + return; + } + + if (!this.finalGuestPin || this.finalGuestPin.trim() === '') { + alert('Bitte geben Sie die PIN des Gastvereins ein.'); + return; + } + + // Test-Validierung: Gast-PIN "1234" akzeptieren + if (this.finalGuestPin !== '1234') { + alert('Für den Test wird nur die Gast-PIN "1234" akzeptiert.'); + return; + } + + console.log('✅ PIN-Validierung erfolgreich'); + + // Erstelle eine Kopie des ursprünglichen Match-Objekts + console.log('📋 Erstelle Kopie des Match-Objekts...'); + const matchData = JSON.parse(JSON.stringify(this.match)); + console.log('✅ Match-Objekt kopiert'); + + // Aktualisiere die Match-Daten mit unseren Eingaben + console.log('🔄 Aktualisiere Match-Daten...'); + this.updateMatchData(matchData); + console.log('✅ Match-Daten aktualisiert'); + + // Zeige das vollständige Objekt in einem Dialog an + console.log('📊 Zeige Match-Daten Dialog...'); + this.showMatchDataDialog(matchData); + console.log('✅ Dialog angezeigt'); - console.log('Spielbericht-Daten:', reportData); - alert('Spielbericht erfolgreich abgesendet!'); } catch (error) { - console.error('Fehler beim Absenden:', error); - alert('Fehler beim Absenden des Spielberichts'); + console.error('❌ Fehler beim Absenden:', error); + console.error('❌ Fehler-Stack:', error.stack); + alert(`Fehler beim Absenden des Spielberichts: ${error.message}`); + } + }, + + showMatchDataDialog(matchData) { + // Erstelle einen neuen Dialog für die Datenanzeige + const dialog = document.createElement('div'); + dialog.className = 'match-data-dialog-overlay'; + + const dialogContent = document.createElement('div'); + dialogContent.className = 'match-data-dialog'; + + const header = document.createElement('div'); + header.className = 'match-data-dialog-header'; + header.innerHTML = ` +

📋 Vollständiges Match-Objekt

+ + `; + + const content = document.createElement('div'); + content.className = 'match-data-dialog-content'; + + // Pretty-print das JSON + const jsonString = JSON.stringify(matchData, null, 2); + const pre = document.createElement('pre'); + pre.textContent = jsonString; + pre.className = 'json-display'; + + const copyButton = document.createElement('button'); + copyButton.className = 'copy-json-btn'; + copyButton.textContent = '📋 JSON kopieren'; + copyButton.onclick = () => { + navigator.clipboard.writeText(jsonString).then(() => { + copyButton.textContent = '✅ Kopiert!'; + setTimeout(() => { + copyButton.textContent = '📋 JSON kopieren'; + }, 2000); + }); + }; + + content.appendChild(pre); + content.appendChild(copyButton); + + dialogContent.appendChild(header); + dialogContent.appendChild(content); + dialog.appendChild(dialogContent); + + // Dialog zum Body hinzufügen + document.body.appendChild(dialog); + + // Dialog schließen bei Klick außerhalb + dialog.onclick = (e) => { + if (e.target === dialog) { + dialog.remove(); + } + }; + + // ESC-Taste zum Schließen + const handleEsc = (e) => { + if (e.key === 'Escape') { + dialog.remove(); + document.removeEventListener('keydown', handleEsc); + } + }; + document.addEventListener('keydown', handleEsc); + }, + + updateMatchData(matchData) { + try { + console.log('🔄 updateMatchData: Verwende kompletten Original-Meeting-Daten...'); + + // Verwende ausschließlich die Meeting-Details vom /meetingdetails/:uuid Endpoint + if (!this.meetingDetails) { + throw new Error('❌ meetingDetails nicht verfügbar - Meeting-Details müssen zuerst geladen werden'); + } + const baseData = this.meetingDetails; + + // Das gesamte Original-Objekt als Basis verwenden (bestehende matchData überschreiben) + Object.assign(matchData, baseData); + + // Unerwünschte Felder entfernen, die nicht vom /meetingdetails Endpoint kommen sollten: + delete matchData.championsTieBreakSelected; + delete matchData.code; + delete matchData.date; + delete matchData.guestMatchPoints; + delete matchData.guestTeam; + delete matchData.guestTeamId; + delete matchData.homeMatchPoints; + delete matchData.homeTeam; + delete matchData.homeTeamId; + delete matchData.id; + delete matchData.leagueDetails; + delete matchData.leagueId; + delete matchData.locationId; + delete matchData.time; + + // NUR unsere spezifischen Änderungen eintragen: + + // 1. Spieleranzahl aktualisieren (aus Aufstellung) + matchData.playerCountHome = this.getSelectedPlayerCount('home'); + matchData.playerCountGuest = this.getSelectedPlayerCount('guest'); + + // 1.1 Player-Positionen aktualisieren (Einzel + Doppel Positionen erfassen) + this.updatePlayerPositions(matchData); + + // 2. Zeitangaben aktualisieren + if (this.match.startDate) { + matchData.startDate = this.match.startDate.toISOString(); + } + if (this.match.endDate) { + matchData.endDate = this.match.endDate.toISOString(); + } + matchData.meetingsStartEndTimeEnabled = true; + + // 3. Protest-Informationen + if (this.protestText && this.protestText.trim() !== '') { + matchData.protest = true; + matchData.remarks = this.protestText.trim(); + } else { + matchData.protest = false; + matchData.remarks = null; + } + + // 4. PINs - Original-Hashes beibehalten (werden vom Backend beim Senden aktualisiert) + // matchData.homePin und matchData.guestPin bleiben die ursprünglichen Hashes aus baseData + + // 5. Match-Status auf abgeschlossen setzen + matchData.isCompleted = true; + + // 6. Gesamtstatistik berechnen und eintragen + const overallScore = this.getOverallMatchScore(); + + // Gesamtpunkte berechnen und eintragen + const [homeMatches, guestMatches] = overallScore.split(':').map(Number); + matchData.homeMatches = homeMatches; + matchData.guestMatches = guestMatches; + + // Match-Punkte (für Liga-Tabelle) - diese Felder werden vom Backend berechnet + // homeMatchPoints und guestMatchPoints bleiben ursprünglich aus baseData + + // Gesamtsätze berechnen und eintragen + const setScore = this.getOverallSetScore(); + const setScoreMatch = setScore.match(/\((\d+):(\d+)\)/); + if (setScoreMatch) { + matchData.homeSets = parseInt(setScoreMatch[1]); + matchData.guestSets = parseInt(setScoreMatch[2]); + } + + // 7. Gesamtpunkte (Games) berechnen und eintragen + let totalHomeGames = 0; + let totalGuestGames = 0; + + // Einzelne Matches aktualisieren - Originalarray verwenden und nur unsere Ergebnisse eintragen + if (matchData.matches && Array.isArray(matchData.matches)) { + this.results.forEach((result, index) => { + if (matchData.matches[index]) { + const match = matchData.matches[index]; + + // Reset aller Satzfelder auf 0 (Original hat meist schon Werte) + const setFields = ['set1A', 'set2A', 'set3A', 'set4A', 'set5A', 'set1B', 'set2B', 'set3B', 'set4B', 'set5B']; + setFields.forEach(field => { + match[field] = 0; + }); + + // Satzergebnisse aus unseren eingegebenen Daten eintragen + result.sets.forEach((set, setIndex) => { + if (set && set.includes(':')) { + const [home, guest] = set.split(':').map(s => parseInt(s) || 0); + const setFieldA = `set${setIndex + 1}A`; + const setFieldB = `set${setIndex + 1}B`; + + match[setFieldA] = home; + match[setFieldB] = guest; + + // Gesamtpunkte (Games) sammeln + totalHomeGames += home; + totalGuestGames += guest; + } + }); + + // Match-Gewinner berechnen und eintragen + let homeWins = 0; + let guestWins = 0; + + if (result.completed) { + result.sets.forEach(set => { + if (set && set.includes(':')) { + const [home, guest] = set.split(':').map(s => parseInt(s) || 0); + if (home > guest) { + homeWins++; + } else if (guest > home) { + guestWins++; + } + } + }); + } + + // Nur unsere geänderten Werte aktualisieren (Original bleibt erhalten) + match.matchesA = homeWins > guestWins ? 1 : 0; + match.matchesB = guestWins > homeWins ? 1 : 0; + match.setsA = homeWins; + match.setsB = guestWins; + match.isCompleted = result.completed; + } + }); + } + + // 8. Gesamtpunkte (Games) eintragen + matchData.homeGames = totalHomeGames; + matchData.guestGames = totalGuestGames; + + // 9. Champions Tie-Break - wird NICHT hinzugefügt (Feld bleibt aus Original) + + console.log('✅ updateMatchData: Aktualisierung abgeschlossen'); + + } catch (error) { + console.error('❌ updateMatchData Fehler:', error); + console.error('❌ updateMatchData Stack:', error.stack); + throw error; + } + }, + + // Aktualisiere die Player-Positionen um alle Positionen (Einzel + Doppel) zu erfassen + updatePlayerPositions(matchData) { + console.log('🔄 Aktualisiere Player-Positionen...'); + + // Home Players Positionen aktualisieren + if (matchData.teamLineupHomePlayers && Array.isArray(matchData.teamLineupHomePlayers)) { + matchData.teamLineupHomePlayers.forEach(player => { + const positions = []; + + // 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; + if (singlePos) { + positions.push(`E${singlePos}`); + } + } + + // Doppel-Position NUR hinzufügen wenn Spieler Doppel-Position hat + if (player.positionDouble) { + positions.push(`D${player.positionDouble}`); + } + + player.positions = positions; + console.log(`✅ Home Player ${player.firstname} ${player.lastname}: [${positions.join(', ')}] (selected: ${player.isSelected})`); + }); + } + + // Guest Players Positionen aktualisieren + if (matchData.teamLineupGuestPlayers && Array.isArray(matchData.teamLineupGuestPlayers)) { + matchData.teamLineupGuestPlayers.forEach(player => { + const positions = []; + + // 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; + if (singlePos) { + positions.push(`E${singlePos}`); + } + } + + // Doppel-Position NUR hinzufügen wenn Spieler Doppel-Position hat + if (player.positionDouble) { + positions.push(`D${player.positionDouble}`); + } + + player.positions = positions; + console.log(`✅ Guest Player ${player.firstname} ${player.lastname}: [${positions.join(', ')}] (selected: ${player.isSelected})`); + }); + } + + console.log('✅ Player-Positionen aktualisiert'); + }, + + // Prüfe ob das Match bereits abgeschlossen ist + get isMatchCompleted() { + return this.match && this.match.isCompleted === true; + }, + + // Zähle die ausgewählten Spieler für ein Team + getSelectedPlayerCount(team) { + try { + if (team === 'home') { + const homePlayers = this.meetingDetails?.teamLineupHomePlayers || this.teamLineupHomePlayers || []; + return homePlayers.filter(p => p.isSelected === true).length; + } else if (team === 'guest') { + const guestPlayers = this.meetingDetails?.teamLineupGuestPlayers || this.teamLineupGuestPlayers || []; + return guestPlayers.filter(p => p.isSelected === true).length; + } + return 0; + } catch (error) { + console.error('❌ getSelectedPlayerCount Fehler:', error); + return 0; } }, @@ -1456,25 +1810,25 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr getOverallScore() { let homeWins = 0; let guestWins = 0; - let totalHomeSets = 0; - let totalGuestSets = 0; + let totalHomeSetsWon = 0; + let totalGuestSetsWon = 0; this.results.forEach(match => { if (match.completed) { - // Source gewonnene Sätze für dieses Match und Gesamtsätze + // Zähle gewonnene Sätze für dieses Match und Gesamtsätze let matchHomeWins = 0; let matchGuestWins = 0; match.sets.forEach(set => { if (set && set.includes(':')) { const [home, guest] = set.split(':').map(s => parseInt(s) || 0); - totalHomeSets += home; - totalGuestSets += guest; if (home > guest) { matchHomeWins++; + totalHomeSetsWon++; } else if (guest > home) { matchGuestWins++; + totalGuestSetsWon++; } } }); @@ -1488,7 +1842,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr } }); - return `${homeWins}:${guestWins} (${totalHomeSets}:${totalGuestSets} Sätze)`; + return `${homeWins}:${guestWins} (${totalHomeSetsWon}:${totalGuestSetsWon} Sätze)`; }, getMatchResult(match) { @@ -3054,6 +3408,194 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr font-style: italic; } +/* Match Data Dialog Styles */ +.match-data-dialog-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + padding: 20px; + box-sizing: border-box; +} + +.match-data-dialog { + background: white; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + max-width: 90vw; + max-height: 90vh; + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.match-data-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.match-data-dialog-header h3 { + margin: 0; + color: #333; + font-size: 18px; +} + +.close-dialog-btn { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + color: #666; + transition: all 0.2s; +} + +.close-dialog-btn:hover { + background: #e0e0e0; + color: #333; +} + +.match-data-dialog-content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 20px; +} + +.json-display { + background: #f8f9fa; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 16px; + margin: 0 0 16px 0; + overflow: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + line-height: 1.4; + color: #333; + white-space: pre-wrap; + word-wrap: break-word; + max-height: 60vh; + flex: 1; +} + +.copy-json-btn { + background: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + align-self: flex-start; +} + +.copy-json-btn:hover { + background: #0056b3; + transform: translateY(-1px); +} + +.copy-json-btn:active { + transform: translateY(0); +} + +/* Deaktivierte Zustände für abgeschlossene Matches */ +.submit-btn.disabled { + background-color: #6c757d; + color: white; + cursor: not-allowed; + opacity: 0.7; +} + +.submit-btn.disabled:hover { + background-color: #6c757d; + transform: none; +} + +.completion-notice { + margin-top: 8px; + padding: 8px 12px; + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; + color: #856404; + font-size: 12px; + text-align: center; +} + +.time-input:disabled, +.protest-input:disabled, +.pin-input:disabled { + background-color: #f8f9fa; + color: #6c757d; + cursor: not-allowed; + opacity: 0.7; +} + +.time-btn:disabled { + background-color: #f8f9fa; + color: #6c757d; + cursor: not-allowed; + opacity: 0.7; +} + +.time-btn:disabled:hover { + background-color: #f8f9fa; + transform: none; +} + +/* Locked indicator für deaktivierte Tabs */ +.locked-indicator { + margin-left: 8px; + font-size: 12px; + opacity: 0.7; +} + +/* Completion warning auf Ergebniserfassungs-Seite */ +.completion-warning { + display: flex; + align-items: center; + padding: 16px; + margin-bottom: 20px; + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 8px; + color: #856404; +} + +.warning-icon { + font-size: 24px; + margin-right: 12px; + flex-shrink: 0; +} + +.warning-text { + flex: 1; + font-size: 14px; + line-height: 1.4; +} + +.warning-text strong { + display: block; + margin-bottom: 4px; + font-size: 15px; +} + .pin-modal-error { color: #dc3545; background-color: #f8d7da;