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:
@@ -58,3 +58,47 @@ export const getMatchesForLeague = async (req, res) => {
|
||||
return res.status(500).json({ error: 'Failed to retrieve matches' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getLeagueTable = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, leagueId } = req.params;
|
||||
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
|
||||
return res.status(200).json(table);
|
||||
} catch (error) {
|
||||
console.error('Error retrieving league table:', error);
|
||||
return res.status(500).json({ error: 'Failed to retrieve league table' });
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchLeagueTableFromMyTischtennis = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, leagueId } = req.params;
|
||||
const { userid: userIdOrEmail } = req.headers;
|
||||
|
||||
// Convert email to userId if needed
|
||||
let userId = userIdOrEmail;
|
||||
if (isNaN(userIdOrEmail)) {
|
||||
const User = (await import('../models/User.js')).default;
|
||||
const user = await User.findOne({ where: { email: userIdOrEmail } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
const autoFetchService = (await import('../services/autoFetchMatchResultsService.js')).default;
|
||||
await autoFetchService.fetchAndUpdateLeagueTable(userId, leagueId);
|
||||
|
||||
// Return updated table data
|
||||
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
|
||||
return res.status(200).json({
|
||||
message: 'League table updated from MyTischtennis',
|
||||
data: table
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching league table from MyTischtennis:', error);
|
||||
return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -326,12 +326,25 @@ class MyTischtennisUrlController {
|
||||
team
|
||||
);
|
||||
|
||||
// Also fetch and update league table data
|
||||
let tableUpdateResult = null;
|
||||
try {
|
||||
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
|
||||
tableUpdateResult = 'League table updated successfully';
|
||||
console.log('✓ League table updated for league:', team.league.id);
|
||||
} catch (error) {
|
||||
console.error('Error fetching league table data:', error);
|
||||
tableUpdateResult = 'League table update failed: ' + error.message;
|
||||
// Don't fail the entire request if table update fails
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
|
||||
data: {
|
||||
fetchedCount: result.fetchedCount,
|
||||
teamName: team.name
|
||||
teamName: team.name,
|
||||
tableUpdate: tableUpdateResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
4
backend/migrations/add_matches_tied_to_team.sql
Normal file
4
backend/migrations/add_matches_tied_to_team.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Add matches_tied column to team table
|
||||
ALTER TABLE team
|
||||
ADD COLUMN matches_tied INTEGER NOT NULL DEFAULT 0 AFTER matches_lost;
|
||||
|
||||
11
backend/migrations/add_table_fields_to_team.sql
Normal file
11
backend/migrations/add_table_fields_to_team.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Migration: Add table fields to team table
|
||||
-- Add fields for league table calculations
|
||||
|
||||
ALTER TABLE team ADD COLUMN matches_played INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN matches_won INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN matches_lost INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN sets_won INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN sets_lost INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN points_won INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN points_lost INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE team ADD COLUMN table_points INT NOT NULL DEFAULT 0;
|
||||
5
backend/migrations/add_table_points_won_lost_to_team.sql
Normal file
5
backend/migrations/add_table_points_won_lost_to_team.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add table_points_won and table_points_lost columns to team table
|
||||
ALTER TABLE team
|
||||
ADD COLUMN table_points_won INTEGER NOT NULL DEFAULT 0 AFTER table_points,
|
||||
ADD COLUMN table_points_lost INTEGER NOT NULL DEFAULT 0 AFTER table_points_won;
|
||||
|
||||
@@ -45,6 +45,62 @@ const Team = sequelize.define('Team', {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
// Tabellenfelder
|
||||
matchesPlayed: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
matchesWon: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
matchesLost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
matchesTied: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
setsWon: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
setsLost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
pointsWon: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
pointsLost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
tablePoints: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
tablePointsWon: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
tablePointsLost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'team',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague } from '../controllers/matchController.js';
|
||||
import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis } from '../controllers/matchController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import multer from 'multer';
|
||||
|
||||
@@ -11,6 +11,8 @@ router.post('/import', authenticate, upload.single('file'), uploadCSV);
|
||||
router.get('/leagues/current/:clubId', authenticate, getLeaguesForCurrentSeason);
|
||||
router.get('/leagues/:clubId/matches/:leagueId', authenticate, getMatchesForLeague);
|
||||
router.get('/leagues/:clubId/matches', authenticate, getMatchesForLeagues);
|
||||
router.get('/leagues/:clubId/table/:leagueId', authenticate, getLeagueTable);
|
||||
router.post('/leagues/:clubId/table/:leagueId/fetch', authenticate, fetchLeagueTableFromMyTischtennis);
|
||||
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -218,6 +218,19 @@ class AutoFetchMatchResultsService {
|
||||
// Note: Match results are already included in the player stats response above
|
||||
// in tableData.meetings_excerpt.meetings, so we don't need a separate call
|
||||
|
||||
// Also fetch and update league table data for this team
|
||||
if (account.userId) {
|
||||
try {
|
||||
await this.fetchAndUpdateLeagueTable(account.userId, team.leagueId);
|
||||
devLog(`✓ League table updated for league ${team.leagueId}`);
|
||||
} catch (error) {
|
||||
console.error(`Error updating league table for league ${team.leagueId}:`, error);
|
||||
// Don't fail the entire process if table update fails
|
||||
}
|
||||
} else {
|
||||
devLog(`Skipping league table update - no userId available`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fetchedCount: totalProcessed
|
||||
@@ -651,6 +664,165 @@ class AutoFetchMatchResultsService {
|
||||
attributes: ['userId', 'email', 'autoUpdateRatings']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update league table data from MyTischtennis
|
||||
* @param {number} userId - User ID
|
||||
* @param {number} leagueId - League ID
|
||||
*/
|
||||
async fetchAndUpdateLeagueTable(userId, leagueId) {
|
||||
try {
|
||||
devLog(`Fetching league table for user ${userId}, league ${leagueId}`);
|
||||
|
||||
// Get user's MyTischtennis account
|
||||
const myTischtennisAccount = await MyTischtennis.findOne({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
if (!myTischtennisAccount) {
|
||||
throw new Error('MyTischtennis account not found');
|
||||
}
|
||||
|
||||
// Get league info
|
||||
const league = await League.findByPk(leagueId, {
|
||||
include: [{ model: Season, as: 'season' }]
|
||||
});
|
||||
|
||||
if (!league) {
|
||||
throw new Error('League not found');
|
||||
}
|
||||
|
||||
// Login to MyTischtennis if needed
|
||||
let session = await myTischtennisService.getSession(userId);
|
||||
if (!session || !session.isValid) {
|
||||
if (!myTischtennisAccount.savePassword) {
|
||||
throw new Error('MyTischtennis account not connected or session expired');
|
||||
}
|
||||
|
||||
devLog('Session expired, re-logging in...');
|
||||
await myTischtennisService.verifyLogin(userId);
|
||||
session = await myTischtennisService.getSession(userId);
|
||||
}
|
||||
|
||||
// Convert full season (e.g. "2025/2026") to short format (e.g. "25/26") and then to URL format (e.g. "25--26")
|
||||
const seasonFull = league.season.season; // e.g. "2025/2026"
|
||||
const seasonParts = seasonFull.split('/');
|
||||
const seasonShort = seasonParts.length === 2
|
||||
? `${seasonParts[0].slice(-2)}/${seasonParts[1].slice(-2)}`
|
||||
: seasonFull;
|
||||
const seasonStr = seasonShort.replace('/', '--'); // e.g. "25/26" -> "25--26"
|
||||
|
||||
// Fetch table data from MyTischtennis
|
||||
const tableUrl = `https://www.mytischtennis.de/click-tt/${league.association}/${seasonStr}/ligen/${league.groupname}/gruppe/${league.myTischtennisGroupId}/tabelle/gesamt?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%24groupname.gruppe.%24urlid%2B%2Ftabelle.%24filter`;
|
||||
|
||||
console.log(`[fetchAndUpdateLeagueTable] Fetching table from URL: ${tableUrl}`);
|
||||
const response = await myTischtennisClient.authenticatedRequest(tableUrl, session.cookie, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(`Failed to fetch table data: ${response.error}`);
|
||||
}
|
||||
|
||||
const tableData = await this.parseTableData(JSON.stringify(response.data), leagueId);
|
||||
|
||||
// Update teams with table data
|
||||
await this.updateTeamsWithTableData(tableData, leagueId);
|
||||
|
||||
devLog(`✓ Updated league table for league ${leagueId} with ${tableData.length} teams`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching league table for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse table data from MyTischtennis response
|
||||
* @param {string} jsonResponse - JSON response from MyTischtennis
|
||||
* @param {number} leagueId - League ID
|
||||
* @returns {Array} Parsed table data
|
||||
*/
|
||||
async parseTableData(jsonResponse, leagueId) {
|
||||
devLog('Parsing table data from MyTischtennis response...');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonResponse);
|
||||
|
||||
if (!data.data || !data.data.league_table) {
|
||||
devLog('No league table data found in response');
|
||||
return [];
|
||||
}
|
||||
|
||||
const leagueTable = data.data.league_table;
|
||||
const parsedData = [];
|
||||
|
||||
for (const teamData of leagueTable) {
|
||||
parsedData.push({
|
||||
teamName: teamData.team_name,
|
||||
matchesPlayed: teamData.matches_won + teamData.matches_lost,
|
||||
matchesWon: teamData.matches_won,
|
||||
matchesLost: teamData.matches_lost,
|
||||
matchesTied: teamData.meetings_tie || 0, // Unentschiedene Begegnungen
|
||||
setsWon: teamData.sets_won,
|
||||
setsLost: teamData.sets_lost,
|
||||
pointsWon: teamData.games_won, // MyTischtennis uses "games" for Ballpunkte
|
||||
pointsLost: teamData.games_lost,
|
||||
tablePointsWon: teamData.points_won, // Liga-Tabellenpunkte gewonnen
|
||||
tablePointsLost: teamData.points_lost, // Liga-Tabellenpunkte verloren
|
||||
tableRank: teamData.table_rank
|
||||
});
|
||||
}
|
||||
|
||||
devLog(`Parsed ${parsedData.length} teams from MyTischtennis table data`);
|
||||
return parsedData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing MyTischtennis table data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update teams with table data
|
||||
* @param {Array} tableData - Parsed table data
|
||||
* @param {number} leagueId - League ID
|
||||
*/
|
||||
async updateTeamsWithTableData(tableData, leagueId) {
|
||||
for (const teamData of tableData) {
|
||||
try {
|
||||
// Find team by name in this league
|
||||
const team = await Team.findOne({
|
||||
where: {
|
||||
leagueId: leagueId,
|
||||
name: { [Op.like]: `%${teamData.teamName}%` }
|
||||
}
|
||||
});
|
||||
|
||||
if (team) {
|
||||
await team.update({
|
||||
matchesPlayed: teamData.matchesPlayed || 0,
|
||||
matchesWon: teamData.matchesWon || 0,
|
||||
matchesLost: teamData.matchesLost || 0,
|
||||
matchesTied: teamData.matchesTied || 0,
|
||||
setsWon: teamData.setsWon || 0,
|
||||
setsLost: teamData.setsLost || 0,
|
||||
pointsWon: teamData.pointsWon || 0,
|
||||
pointsLost: teamData.pointsLost || 0,
|
||||
tablePoints: (teamData.tablePointsWon || 0), // Legacy field (keep for compatibility)
|
||||
tablePointsWon: teamData.tablePointsWon || 0,
|
||||
tablePointsLost: teamData.tablePointsLost || 0
|
||||
});
|
||||
|
||||
devLog(` ✓ Updated team ${team.name} with table data`);
|
||||
} else {
|
||||
devLog(` ⚠ Team not found: ${teamData.teamName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating team ${teamData.teamName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutoFetchMatchResultsService();
|
||||
|
||||
@@ -383,6 +383,53 @@ class MatchService {
|
||||
return enrichedMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league table for a specific league
|
||||
* @param {string} userToken - User authentication token
|
||||
* @param {string} clubId - Club ID
|
||||
* @param {string} leagueId - League ID
|
||||
* @returns {Array} League table data
|
||||
*/
|
||||
async getLeagueTable(userToken, clubId, leagueId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
try {
|
||||
// Get all teams in this league
|
||||
const teams = await Team.findAll({
|
||||
where: {
|
||||
leagueId: leagueId
|
||||
},
|
||||
attributes: [
|
||||
'id', 'name', 'matchesPlayed', 'matchesWon', 'matchesLost', 'matchesTied',
|
||||
'setsWon', 'setsLost', 'pointsWon', 'pointsLost', 'tablePoints', 'tablePointsWon', 'tablePointsLost'
|
||||
],
|
||||
order: [
|
||||
['tablePointsWon', 'DESC'], // Highest table points first
|
||||
['matchesWon', 'DESC'], // Then by matches won
|
||||
['setsWon', 'DESC'] // Then by sets won
|
||||
]
|
||||
});
|
||||
|
||||
// Format table data
|
||||
const tableData = teams.map(team => {
|
||||
return {
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
setsWon: team.setsWon,
|
||||
setsLost: team.setsLost,
|
||||
matchPoints: team.matchesWon + ':' + team.matchesLost,
|
||||
tablePoints: team.tablePointsWon + ':' + team.tablePointsLost, // Tabellenpunkte (points_won:points_lost)
|
||||
pointRatio: team.pointsWon + ':' + team.pointsLost // Ballpunkte (games_won:games_lost)
|
||||
};
|
||||
});
|
||||
|
||||
return tableData;
|
||||
} catch (error) {
|
||||
console.error('Error getting league table:', error);
|
||||
throw new Error('Failed to get league table');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new MatchService();
|
||||
|
||||
@@ -12,7 +12,7 @@ class MyTischtennisService {
|
||||
async getAccount(userId) {
|
||||
const account = await MyTischtennis.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id', 'email', 'savePassword', 'autoUpdateRatings', 'lastLoginAttempt', 'lastLoginSuccess', 'lastUpdateRatings', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
attributes: ['id', 'userId', 'email', 'savePassword', 'autoUpdateRatings', 'lastLoginAttempt', 'lastLoginSuccess', 'lastUpdateRatings', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
});
|
||||
return account;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user