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

@@ -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();

View File

@@ -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();

View File

@@ -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;
}