Implement league table functionality and MyTischtennis integration. Add new endpoints for retrieving and updating league tables in matchController and matchRoutes. Enhance Team model with additional fields for match statistics. Update frontend components to display league tables and allow fetching data from MyTischtennis, improving user experience and data accuracy.

This commit is contained in:
Torsten Schulz (local)
2025-10-14 22:55:39 +02:00
parent 1517d83f6c
commit 7549fb5730
12 changed files with 634 additions and 65 deletions

View File

@@ -1,13 +1,9 @@
<template>
<div>
<h2>Spielpläne</h2>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<SeasonSelector v-model="selectedSeasonId" @season-change="onSeasonChange" :show-current-season="true" />
<button @click="openImportModal">Spielplanimport</button>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
@@ -23,7 +19,28 @@
league.name }}</li>
<li v-if="leagues.length === 0" class="no-leagues">Keine Ligen für diese Saison gefunden</li>
</ul>
<div class="flex-item">
<!-- Tab Navigation - nur anzeigen wenn Liga ausgewählt -->
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-navigation">
<button
:class="['tab-button', { active: activeTab === 'schedule' }]"
@click="activeTab = 'schedule'"
>
📅 Spielplan
</button>
<button
:class="['tab-button', { active: activeTab === 'table' }]"
@click="activeTab = 'table'"
>
📊 Tabelle
</button>
</div>
<!-- Tab Content - nur anzeigen wenn Liga ausgewählt -->
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content">
<!-- Spielplan Tab -->
<div v-show="activeTab === 'schedule'" class="tab-panel">
<button @click="generatePDF">Download PDF</button>
<div v-if="matches.length > 0">
<h3>Spiele für {{ selectedLeague }}</h3>
@@ -35,7 +52,9 @@
<th>Heimmannschaft</th>
<th>Gastmannschaft</th>
<th>Ergebnis</th>
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
<th
v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">
Altersklasse</th>
<th>Code</th>
<th>Heim-PIN</th>
<th>Gast-PIN</th>
@@ -54,21 +73,31 @@
</span>
<span v-else class="result-pending"></span>
</td>
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
<td
v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">
{{ match.leagueDetails?.name || 'N/A' }}</td>
<td class="code-cell">
<span v-if="match.code && selectedLeague && selectedLeague !== ''">
<button @click="openMatchReport(match)" class="nuscore-link" title="Spielberichtsbogen öffnen">📊</button>
<span class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
<button @click="openMatchReport(match)" class="nuscore-link"
title="Spielberichtsbogen öffnen">📊</button>
<span class="code-value clickable" @click="copyToClipboard(match.code, 'Code')"
:title="'Code kopieren: ' + match.code">{{ match.code }}</span>
</span>
<span v-else-if="match.code" class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
<span v-else-if="match.code" class="code-value clickable"
@click="copyToClipboard(match.code, 'Code')"
:title="'Code kopieren: ' + match.code">{{ match.code }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.homePin" class="pin-value clickable" @click="copyToClipboard(match.homePin, 'Heim-PIN')" :title="'Heim-PIN kopieren: ' + match.homePin">{{ match.homePin }}</span>
<span v-if="match.homePin" class="pin-value clickable"
@click="copyToClipboard(match.homePin, 'Heim-PIN')"
:title="'Heim-PIN kopieren: ' + match.homePin">{{ match.homePin }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.guestPin" class="pin-value clickable" @click="copyToClipboard(match.guestPin, 'Gast-PIN')" :title="'Gast-PIN kopieren: ' + match.guestPin">{{ match.guestPin }}</span>
<span v-if="match.guestPin" class="pin-value clickable"
@click="copyToClipboard(match.guestPin, 'Gast-PIN')"
:title="'Gast-PIN kopieren: ' + match.guestPin">{{ match.guestPin }}</span>
<span v-else class="no-data">-</span>
</td>
</tr>
@@ -78,37 +107,61 @@
<div v-else>
<p>Keine Spiele vorhanden</p>
</div>
</div>
<!-- Tabelle Tab -->
<div v-show="activeTab === 'table'" class="tab-panel">
<div class="table-section">
<div class="table-header">
<h3>Ligatabelle</h3>
</div>
<div v-if="leagueTable.length > 0">
<table id="league-table">
<thead>
<tr>
<th>Platz</th>
<th>Team</th>
<th>Matches</th>
<th>Sätze</th>
<th>Pkt.</th>
<th>Bälle</th>
</tr>
</thead>
<tbody>
<tr v-for="(team, index) in leagueTable" :key="team.teamId"
:class="{ 'our-team': isOurTeam(team.teamName) }">
<td>{{ index + 1 }}</td>
<td>{{ team.teamName }}</td>
<td>{{ team.matchPoints }}</td>
<td>{{ team.setsWon }}:{{ team.setsLost }}</td>
<td>{{ team.tablePoints }}</td>
<td>{{ team.pointRatio }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p>Keine Tabellendaten verfügbar</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Import Modal -->
<CsvImportDialog
v-model="showImportModal"
@import="handleCsvImport"
@close="closeImportModal"
/>
<CsvImportDialog v-model="showImportModal" @import="handleCsvImport" @close="closeImportModal" />
</div>
<!-- Info Dialog -->
<InfoDialog
v-model="infoDialog.isOpen"
:title="infoDialog.title"
:message="infoDialog.message"
:details="infoDialog.details"
:type="infoDialog.type"
/>
<InfoDialog v-model="infoDialog.isOpen" :title="infoDialog.title" :message="infoDialog.message"
:details="infoDialog.details" :type="infoDialog.type" />
<!-- Confirm Dialog -->
<ConfirmDialog
v-model="confirmDialog.isOpen"
:title="confirmDialog.title"
:message="confirmDialog.message"
:details="confirmDialog.details"
:type="confirmDialog.type"
@confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)"
/>
<ConfirmDialog v-model="confirmDialog.isOpen" :title="confirmDialog.title" :message="confirmDialog.message"
:details="confirmDialog.details" :type="confirmDialog.type" @confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)" />
</template>
<script>
@@ -161,6 +214,9 @@ export default {
hoveredMatch: null,
selectedSeasonId: null,
currentSeason: null,
activeTab: 'schedule',
leagueTable: [],
fetchingTable: false,
};
},
methods: {
@@ -168,11 +224,11 @@ export default {
if (!match.isCompleted) {
return '';
}
// Check if our club's team won or lost
const isOurTeamHome = this.isOurTeam(match.homeTeam?.name);
const isOurTeamGuest = this.isOurTeam(match.guestTeam?.name);
if (isOurTeamHome) {
// We are home team
return match.homeMatchPoints > match.guestMatchPoints ? 'completed won' : 'completed lost';
@@ -180,10 +236,10 @@ export default {
// We are guest team
return match.guestMatchPoints > match.homeMatchPoints ? 'completed won' : 'completed lost';
}
return 'completed';
},
isOurTeam(teamName) {
if (!teamName || !this.currentClubName) {
return false;
@@ -191,7 +247,7 @@ export default {
// Check if team name starts with our club name
return teamName.startsWith(this.currentClubName);
},
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = {
@@ -202,7 +258,7 @@ export default {
type
};
},
async showConfirm(title, message, details = '', type = 'info') {
return new Promise((resolve) => {
this.confirmDialog = {
@@ -215,7 +271,7 @@ export default {
};
});
},
handleConfirmResult(confirmed) {
if (this.confirmDialog.resolveCallback) {
this.confirmDialog.resolveCallback(confirmed);
@@ -223,7 +279,7 @@ export default {
}
this.confirmDialog.isOpen = false;
},
...mapActions(['openDialog']),
openImportModal() {
this.showImportModal = true;
@@ -338,6 +394,9 @@ export default {
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${leagueId}`);
this.matches = response.data;
// Lade auch die Tabellendaten für diese Liga
await this.loadLeagueTable(leagueId);
} catch (error) {
this.showInfo('Fehler', 'Fehler beim Laden der Matches', '', 'error');
this.matches = [];
@@ -436,24 +495,24 @@ export default {
},
getRowClass(matchDate) {
if (!matchDate) return '';
const today = new Date();
const match = new Date(matchDate);
// Setze die Zeit auf Mitternacht für genaue Datumsvergleiche
today.setHours(0, 0, 0, 0);
match.setHours(0, 0, 0, 0);
// Berechne die Differenz in Tagen
const diffTime = match.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return 'match-today'; // Heute - gelb
} else if (diffDays > 0 && diffDays <= 7) {
return 'match-next-week'; // Nächste Woche - hellblau
}
return ''; // Keine besondere Farbe
},
async copyToClipboard(text, type) {
@@ -463,12 +522,12 @@ export default {
const originalText = event.target.textContent;
event.target.textContent = '✓';
event.target.style.color = '#4CAF50';
setTimeout(() => {
event.target.textContent = originalText;
event.target.style.color = '';
}, 1000);
} catch (err) {
console.error('Fehler beim Kopieren:', err);
// Fallback für ältere Browser
@@ -478,9 +537,9 @@ export default {
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
}
},
openMatchReport(match) {
const title = `${match.homeTeam?.name || 'N/A'} vs ${match.guestTeam?.name || 'N/A'} - ${this.selectedLeague}`;
this.openDialog({
@@ -497,6 +556,42 @@ export default {
}
});
},
async loadLeagueTable(leagueId) {
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/table/${leagueId}`);
this.leagueTable = response.data;
} catch (error) {
console.error('ScheduleView: Error loading league table:', error);
this.leagueTable = [];
}
},
async fetchTableFromMyTischtennis() {
if (!this.selectedLeague || this.selectedLeague === 'Gesamtspielplan' || this.selectedLeague === 'Spielplan Erwachsene') {
this.showInfo('Info', 'Bitte wählen Sie eine spezifische Liga aus, um die Tabelle zu laden.', '', 'info');
return;
}
this.fetchingTable = true;
try {
// Find the league ID for the current selected league
const league = this.leagues.find(l => l.name === this.selectedLeague);
if (!league) {
this.showInfo('Fehler', 'Liga nicht gefunden', '', 'error');
return;
}
const response = await apiClient.post(`/matches/leagues/${this.currentClub}/table/${league.id}/fetch`);
this.leagueTable = response.data.data;
this.showInfo('Erfolg', 'Tabellendaten erfolgreich von MyTischtennis geladen!', '', 'success');
} catch (error) {
console.error('ScheduleView: Error fetching table from MyTischtennis:', error);
this.showInfo('Fehler', 'Fehler beim Laden der Tabellendaten von MyTischtennis', error.response?.data?.error || error.message, 'error');
} finally {
this.fetchingTable = false;
}
},
},
async created() {
// Ligen werden geladen, sobald eine Saison ausgewählt ist
@@ -671,7 +766,8 @@ li {
}
/* Code und PIN Styles */
.code-cell, .pin-cell {
.code-cell,
.pin-cell {
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
@@ -696,7 +792,7 @@ li {
.code-value.clickable:hover {
background: #bbdefb;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nuscore-link {
@@ -733,7 +829,7 @@ li {
.pin-value.clickable:hover {
background: #ffcc02;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.no-data {
@@ -742,18 +838,129 @@ li {
}
.match-today {
background-color: #fff3cd !important; /* Gelb für heute */
background-color: #fff3cd !important;
/* Gelb für heute */
}
.match-next-week {
background-color: #d1ecf1 !important; /* Hellblau für nächste Woche */
background-color: #d1ecf1 !important;
/* Hellblau für nächste Woche */
}
.match-today:hover {
background-color: #ffeaa7 !important; /* Dunkleres Gelb beim Hover */
background-color: #ffeaa7 !important;
/* Dunkleres Gelb beim Hover */
}
.match-next-week:hover {
background-color: #b8daff !important; /* Dunkleres Blau beim Hover */
background-color: #b8daff !important;
/* Dunkleres Blau beim Hover */
}
/* Tab Navigation */
.tab-navigation {
display: flex;
gap: 0;
border-bottom: 2px solid #e0e0e0;
margin-bottom: 20px;
}
.tab-button {
background: none;
border: none;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
color: #666;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
margin-bottom: -2px;
}
.tab-button:hover {
color: #333;
background-color: #f8f9fa;
}
.tab-button.active {
color: #28a745;
border-bottom-color: #28a745;
}
/* Tab Content */
.tab-content {
display: block !important; /* Überschreibe globales display: none */
width: 100%;
}
.tab-panel {
width: 100%;
}
/* Table Section */
.table-section {
padding: 0;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-header h3 {
margin: 0;
}
.fetch-table-btn {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.fetch-table-btn:hover:not(:disabled) {
background-color: #218838;
}
.fetch-table-btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#league-table {
width: 100%;
border-collapse: collapse;
}
#league-table th,
#league-table td {
padding: 12px 8px;
text-align: left;
border: 1px solid #ddd;
}
#league-table th {
background-color: #f8f9fa;
font-weight: 600;
}
#league-table tr:hover {
background-color: #f5f5f5;
}
#league-table tr.our-team {
background-color: #e8f5e8;
font-weight: 600;
}
#league-table tr.our-team:hover {
background-color: #d4edda;
}
</style>

View File

@@ -913,10 +913,18 @@ export default {
if (response.data.success) {
myTischtennisSuccess.value = response.data.message;
// Erstelle detaillierte Erfolgsmeldung mit Tabelleninfo
let detailsMessage = `Team: ${response.data.data.teamName}\nAbgerufene Datensätze: ${response.data.data.fetchedCount}`;
if (response.data.data.tableUpdate) {
detailsMessage += `\n\nTabellenaktualisierung:\n${response.data.data.tableUpdate}`;
}
await showInfo(
'Erfolg',
response.data.message,
`Team: ${response.data.data.teamName}\nAbgerufene Datensätze: ${response.data.data.fetchedCount}`,
detailsMessage,
'success'
);
}