Fügt neue Funktionen zur Bearbeitung von Spielberichten im MatchReportApiDialog hinzu. Implementiert eine Anzeige für den Abschlussstatus des Spiels, einschließlich Warnungen und Deaktivierungen von Eingabefeldern, wenn das Match bereits abgeschlossen ist. Aktualisiert die Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von PINs. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für abgeschlossene Matches und Dialoge zur Anzeige von Match-Daten.

This commit is contained in:
Torsten Schulz (local)
2025-10-03 22:49:05 +02:00
parent d23a9f086c
commit a0fdf256e7

View File

@@ -51,6 +51,7 @@
@click="setActiveSection('result')" :disabled="!canOpenNextStages">
<div class="section-icon"></div>
<span>Ergebniserfassung</span>
<span v-if="isMatchCompleted" class="locked-indicator">🔒</span>
</button>
<button class="section-btn" :class="{ active: activeSection === 'completion', disabled: !canOpenNextStages }"
@@ -304,6 +305,15 @@
<div v-else-if="activeSection === 'result'" class="result-content">
<h3>Ergebniserfassung</h3>
<!-- Warnung bei abgeschlossenem Match -->
<div v-if="isMatchCompleted" class="completion-warning">
<div class="warning-icon">🔒</div>
<div class="warning-text">
<strong>Spielbericht bereits abgesendet</strong><br>
Diese Ergebniserfassung kann nicht mehr bearbeitet werden.
</div>
</div>
<!-- Spielstand oben -->
<div class="score-summary">
<div class="score-display">
@@ -431,26 +441,33 @@
<span class="time-value">{{ getFormattedTime(match.startDate) || 'nicht gesetzt' }}</span>
</div>
<div class="time-input-group">
<label>Endzeit:</label>
<input
type="time"
:value="getFormattedTime(match.endDate)"
@change="setEndTime($event.target.value)"
class="time-input"
/>
<button @click="setCurrentEndTime" class="time-btn" title="Aktuelle Zeit setzen">🕐</button>
</div>
<div class="time-input-group">
<label>Endzeit:</label>
<input
type="time"
:value="getFormattedTime(match.endDate)"
@change="setEndTime($event.target.value)"
class="time-input"
:disabled="isMatchCompleted"
/>
<button
@click="setCurrentEndTime"
class="time-btn"
title="Aktuelle Zeit setzen"
:disabled="isMatchCompleted"
>🕐</button>
</div>
</div>
<!-- Protest-Eingabe -->
<div class="protest-section">
<label for="protestInput">Protest:</label>
<textarea
id="protestInput"
v-model="protestText"
<textarea
id="protestInput"
v-model="protestText"
placeholder="Protestgrund eingeben..."
class="protest-input"
:disabled="isMatchCompleted"
></textarea>
</div>
@@ -458,33 +475,43 @@
<div class="final-pins">
<div class="pin-group">
<label for="finalHomePin">PIN Heimverein:</label>
<input
id="finalHomePin"
v-model="finalHomePin"
type="password"
<input
id="finalHomePin"
v-model="finalHomePin"
type="password"
class="pin-input"
:placeholder="match.homePin || 'PIN eingeben'"
:disabled="isMatchCompleted"
/>
</div>
<div class="pin-group">
<label for="finalGuestPin">PIN Gastverein:</label>
<input
id="finalGuestPin"
v-model="finalGuestPin"
type="password"
<input
id="finalGuestPin"
v-model="finalGuestPin"
type="password"
class="pin-input"
:placeholder="match.guestPin || 'PIN eingeben'"
:disabled="isMatchCompleted"
/>
</div>
</div>
<!-- Absenden-Button -->
<div class="submit-section">
<button @click="submitMatchReport" class="btn-primary submit-btn">
📝 Spielbericht absenden
</button>
</div>
<!-- Absenden-Button -->
<div class="submit-section">
<button
@click="submitMatchReport"
class="btn-primary submit-btn"
:disabled="isMatchCompleted"
:class="{ 'disabled': isMatchCompleted }"
>
{{ isMatchCompleted ? '✅ Spielbericht bereits abgesendet' : '📝 Spielbericht absenden' }}
</button>
<div v-if="isMatchCompleted" class="completion-notice">
Dieser Spielbericht wurde bereits abgesendet. Keine weiteren Änderungen möglich.
</div>
</div>
</div>
</div>
@@ -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 = `
<h3>📋 Vollständiges Match-Objekt</h3>
<button class="close-dialog-btn" onclick="this.closest('.match-data-dialog-overlay').remove()">✕</button>
`;
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;