diff --git a/backend/controllers/matchController.js b/backend/controllers/matchController.js index 52f24e1..8448860 100644 --- a/backend/controllers/matchController.js +++ b/backend/controllers/matchController.js @@ -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' }); + } +}; diff --git a/backend/controllers/myTischtennisUrlController.js b/backend/controllers/myTischtennisUrlController.js index 3b97b2e..a38bd92 100644 --- a/backend/controllers/myTischtennisUrlController.js +++ b/backend/controllers/myTischtennisUrlController.js @@ -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) { diff --git a/backend/migrations/add_matches_tied_to_team.sql b/backend/migrations/add_matches_tied_to_team.sql new file mode 100644 index 0000000..36ebc3d --- /dev/null +++ b/backend/migrations/add_matches_tied_to_team.sql @@ -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; + diff --git a/backend/migrations/add_table_fields_to_team.sql b/backend/migrations/add_table_fields_to_team.sql new file mode 100644 index 0000000..b1d7674 --- /dev/null +++ b/backend/migrations/add_table_fields_to_team.sql @@ -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; diff --git a/backend/migrations/add_table_points_won_lost_to_team.sql b/backend/migrations/add_table_points_won_lost_to_team.sql new file mode 100644 index 0000000..67f05fe --- /dev/null +++ b/backend/migrations/add_table_points_won_lost_to_team.sql @@ -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; + diff --git a/backend/models/Team.js b/backend/models/Team.js index 781231e..cfd1126 100644 --- a/backend/models/Team.js +++ b/backend/models/Team.js @@ -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', diff --git a/backend/routes/matchRoutes.js b/backend/routes/matchRoutes.js index ae25b35..5c14f35 100644 --- a/backend/routes/matchRoutes.js +++ b/backend/routes/matchRoutes.js @@ -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; diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 0fc3aa4..4a2271a 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -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(); diff --git a/backend/services/matchService.js b/backend/services/matchService.js index b2e0610..a2676e6 100644 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -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(); diff --git a/backend/services/myTischtennisService.js b/backend/services/myTischtennisService.js index 66094c2..07a211b 100644 --- a/backend/services/myTischtennisService.js +++ b/backend/services/myTischtennisService.js @@ -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; } diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index 9ad872a..474aeb2 100644 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -1,13 +1,9 @@