From bb2164f6664595433b4b146011e866c8e9ed5365 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 24 Oct 2025 17:06:10 +0200 Subject: [PATCH] Add league configuration endpoint and frontend integration for myTischtennis Implemented a new POST endpoint in MyTischtennisUrlController to configure leagues from table URLs, including season creation logic. Updated myTischtennisRoutes to include the new route for league configuration. Enhanced the myTischtennisUrlParserService to support parsing of table URLs and added a method for decoding group names. Updated TeamManagementView.vue to prompt users for league configuration when a table URL is detected, providing feedback upon successful configuration and reloading relevant data. --- .../controllers/myTischtennisUrlController.js | 83 ++++++++++++++++++ backend/routes/myTischtennisRoutes.js | 3 + .../services/myTischtennisUrlParserService.js | 84 +++++++++++++++---- frontend/src/views/TeamManagementView.vue | 82 +++++++++++++++++- 4 files changed, 234 insertions(+), 18 deletions(-) diff --git a/backend/controllers/myTischtennisUrlController.js b/backend/controllers/myTischtennisUrlController.js index a38bd92..802aae9 100644 --- a/backend/controllers/myTischtennisUrlController.js +++ b/backend/controllers/myTischtennisUrlController.js @@ -402,6 +402,89 @@ class MyTischtennisUrlController { next(error); } } + + /** + * Configure league from myTischtennis table URL + * POST /api/mytischtennis/configure-league + * Body: { url: string, createSeason?: boolean } + */ + async configureLeague(req, res, next) { + try { + const { url, createSeason } = req.body; + const userIdOrEmail = req.headers.userid; + + if (!url) { + throw new HttpError(400, 'URL is required'); + } + + // Parse URL + const parsedData = myTischtennisUrlParserService.parseUrl(url); + + if (parsedData.urlType !== 'table') { + throw new HttpError(400, 'URL must be a table URL (not a team URL)'); + } + + // Find or create season + let season = await Season.findOne({ + where: { season: parsedData.season } + }); + + if (!season && createSeason) { + season = await Season.create({ + season: parsedData.season, + startDate: new Date(), + endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 Jahr später + }); + } + + // Find or create league + let league = await League.findOne({ + where: { + myTischtennisGroupId: parsedData.groupId, + association: parsedData.association + } + }); + + if (!league) { + league = await League.create({ + name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version + myTischtennisGroupId: parsedData.groupId, + association: parsedData.association, + groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version + seasonId: season?.id || null + }); + } else { + // Update existing league - aber nur wenn es sich wirklich geändert hat + if (league.name !== parsedData.groupnameOriginal) { + league.name = parsedData.groupnameOriginal; + league.groupname = parsedData.groupnameOriginal; + } + league.seasonId = season?.id || league.seasonId; + await league.save(); + } + + res.json({ + success: true, + message: 'League configured successfully', + data: { + league: { + id: league.id, + name: league.name, + myTischtennisGroupId: league.myTischtennisGroupId, + association: league.association, + groupname: league.groupname + }, + season: season ? { + id: season.id, + name: season.season + } : null, + parsedData + } + }); + } catch (error) { + next(error); + } + } } export default new MyTischtennisUrlController(); diff --git a/backend/routes/myTischtennisRoutes.js b/backend/routes/myTischtennisRoutes.js index 05f457b..7e25cc2 100644 --- a/backend/routes/myTischtennisRoutes.js +++ b/backend/routes/myTischtennisRoutes.js @@ -42,6 +42,9 @@ router.post('/parse-url', myTischtennisUrlController.parseUrl); // POST /api/mytischtennis/configure-team - Configure team from URL router.post('/configure-team', myTischtennisUrlController.configureTeam); +// POST /api/mytischtennis/configure-league - Configure league from table URL +router.post('/configure-league', myTischtennisUrlController.configureLeague); + // POST /api/mytischtennis/fetch-team-data - Manually fetch team data router.post('/fetch-team-data', myTischtennisUrlController.fetchTeamData); diff --git a/backend/services/myTischtennisUrlParserService.js b/backend/services/myTischtennisUrlParserService.js index 7e3451c..ea60bab 100644 --- a/backend/services/myTischtennisUrlParserService.js +++ b/backend/services/myTischtennisUrlParserService.js @@ -15,14 +15,28 @@ class MyTischtennisUrlParserService { // Remove trailing slash if present url = url.trim().replace(/\/$/, ''); - // Extract parts using regex - // Pattern: /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/... - const pattern = /\/click-tt\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]+)\/gruppe\/([^\/]+)\/mannschaft\/([^\/]+)\/([^\/]+)/; + // Try different URL patterns - const match = url.match(pattern); + // Pattern 1: Team URL with mannschaft/{teamId}/{teamname} + // /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/... + const teamPattern = /\/click-tt\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]+)\/gruppe\/([^\/]+)\/mannschaft\/([^\/]+)\/([^\/]+)/; + + // Pattern 2: Table/Group URL without team info + // /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/tabelle/gesamt + const tablePattern = /\/click-tt\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]+)\/gruppe\/([^\/]+)\/tabelle\/([^\/]+)/; + + let match = url.match(teamPattern); + let isTeamUrl = true; if (!match) { - throw new Error('URL format not recognized. Expected format: /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/...'); + match = url.match(tablePattern); + isTeamUrl = false; + } + + if (!match) { + throw new Error('URL format not recognized. Expected formats:\n' + + '- Team URL: /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/...\n' + + '- Table URL: /click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/tabelle/gesamt'); } const [ @@ -32,28 +46,38 @@ class MyTischtennisUrlParserService { type, groupnameEncoded, groupId, - teamId, - teamnameEncoded + ...rest ] = match; // Decode and process values const seasonShort = seasonRaw.replace('--', '/'); // "25--26" -> "25/26" const season = this.convertToFullSeason(seasonShort); // "25/26" -> "2025/2026" - const groupname = decodeURIComponent(groupnameEncoded); - const teamname = decodeURIComponent(teamnameEncoded).replace(/_/g, ' '); // "Harheimer_TC_(J11)" -> "Harheimer TC (J11)" + const groupnameDecoded = this.decodeGroupName(groupnameEncoded); const result = { association, season, seasonShort, // Für API-Calls type, - groupname, + groupname: groupnameDecoded, // Dekodierte Version für Anzeige + groupnameOriginal: groupnameEncoded, // Originale URL-kodierte Version groupId, - teamId, - teamname, - originalUrl: url + originalUrl: url, + urlType: isTeamUrl ? 'team' : 'table' }; + if (isTeamUrl) { + // Team URL: extract team info + const [teamId, teamnameEncoded] = rest; + const teamname = decodeURIComponent(teamnameEncoded).replace(/_/g, ' '); // "Harheimer_TC_(J11)" -> "Harheimer TC (J11)" + result.teamId = teamId; + result.teamname = teamname; + } else { + // Table URL: no team info + result.teamId = null; + result.teamname = null; + } + devLog('Parsed myTischtennis URL:', result); return result; @@ -63,6 +87,23 @@ class MyTischtennisUrlParserService { } } + /** + * Decode group name from URL-encoded format + * "2._Kreisklasse_Gr._2" -> "2. Kreisklasse Gr. 2" + */ + decodeGroupName(encodedName) { + // First decode URI components + let decoded = decodeURIComponent(encodedName); + + // Replace underscores with spaces + decoded = decoded.replace(/_/g, ' '); + + // Clean up multiple spaces + decoded = decoded.replace(/\s+/g, ' '); + + return decoded.trim(); + } + /** * Convert short season format to full format * "25/26" -> "2025/2026" @@ -214,6 +255,10 @@ class MyTischtennisUrlParserService { return false; } } + + isValidUrl(url) { + return this.isValidTeamUrl(url); // Alias for backward compatibility + } /** * Build myTischtennis URL from components @@ -229,16 +274,23 @@ class MyTischtennisUrlParserService { groupname, groupId, teamId, - teamname + teamname, + urlType = 'team' } = config; // Convert full season to short format for URL const seasonShort = this.convertToShortSeason(season); const seasonStr = seasonShort.replace('/', '--'); - const teamnameEncoded = encodeURIComponent(teamname.replace(/\s/g, '_')); const groupnameEncoded = encodeURIComponent(groupname); - return `https://www.mytischtennis.de/click-tt/${association}/${seasonStr}/${type}/${groupnameEncoded}/gruppe/${groupId}/mannschaft/${teamId}/${teamnameEncoded}/spielerbilanzen/gesamt`; + if (urlType === 'table' || !teamId || !teamname) { + // Build table URL + return `https://www.mytischtennis.de/click-tt/${association}/${seasonStr}/${type}/${groupnameEncoded}/gruppe/${groupId}/tabelle/gesamt`; + } else { + // Build team URL + const teamnameEncoded = encodeURIComponent(teamname.replace(/\s/g, '_')); + return `https://www.mytischtennis.de/click-tt/${association}/${seasonStr}/${type}/${groupnameEncoded}/gruppe/${groupId}/mannschaft/${teamId}/${teamnameEncoded}/spielerbilanzen/gesamt`; + } } } diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index 20b33b0..96c58d3 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -841,7 +841,33 @@ export default { }; const configureTeamFromUrl = async () => { - if (!parsedMyTischtennisData.value || !teamToEdit.value) { + if (!parsedMyTischtennisData.value) { + return; + } + + // Für Tabellen-URLs: Biete Liga-Konfiguration an + if (parsedMyTischtennisData.value.urlType === 'table') { + const confirmed = await showConfirm( + 'Liga konfigurieren?', + 'Tabellen-URL erkannt', + `Verband: ${parsedMyTischtennisData.value.association}\nSaison: ${parsedMyTischtennisData.value.season}\nLiga: ${parsedMyTischtennisData.value.groupname}\nGruppen-ID: ${parsedMyTischtennisData.value.groupId}\n\nMöchten Sie diese Liga in der Datenbank konfigurieren? Dies ermöglicht es, Tabellendaten automatisch abzurufen.`, + 'info' + ); + + if (confirmed) { + await configureLeagueFromUrl(); + } + return; + } + + // Für Team-URLs: Normale Konfiguration + if (!teamToEdit.value) { + await showInfo( + 'Team auswählen', + 'Bitte wählen Sie zuerst ein Team aus', + 'Um die MyTischtennis-Konfiguration zu aktivieren, müssen Sie zuerst ein Team aus der Liste auswählen.', + 'warning' + ); return; } @@ -866,8 +892,19 @@ export default { 'success' ); - // Teams neu laden + // Teams und Ligen neu laden await loadTeams(); + await loadLeagues(); + + // Aktuelles Team mit neuen Daten aktualisieren + if (teamToEdit.value) { + const updatedTeam = teams.value.find(t => t.id === teamToEdit.value.id); + if (updatedTeam) { + teamToEdit.value = updatedTeam; + // Team-Dokumente neu laden + await loadTeamDocuments(); + } + } // Parsed Data löschen clearParsedData(); @@ -881,6 +918,46 @@ export default { } }; + const configureLeagueFromUrl = async () => { + if (!parsedMyTischtennisData.value || parsedMyTischtennisData.value.urlType !== 'table') { + return; + } + + configuringTeam.value = true; + myTischtennisError.value = ''; + myTischtennisSuccess.value = ''; + + try { + const response = await apiClient.post('/mytischtennis/configure-league', { + url: myTischtennisUrl.value.trim(), + createSeason: true + }); + + if (response.data.success) { + myTischtennisSuccess.value = 'Liga erfolgreich konfiguriert! Tabellendaten können jetzt automatisch abgerufen werden.'; + await showInfo( + 'Erfolg', + 'Liga erfolgreich konfiguriert!', + `Liga: ${response.data.data.league.name}\nSaison: ${response.data.data.season?.name || 'Nicht erstellt'}\nVerband: ${response.data.data.league.association}\nGruppen-ID: ${response.data.data.league.myTischtennisGroupId}\n\nTabellendaten können jetzt automatisch abgerufen werden.`, + 'success' + ); + + // Teams und Ligen neu laden + await loadTeams(); + await loadLeagues(); + + // Parsed Data löschen + clearParsedData(); + } + } catch (error) { + console.error('Fehler bei der Liga-Konfiguration:', error); + myTischtennisError.value = error.response?.data?.message || 'Liga konnte nicht konfiguriert werden.'; + await showInfo('Fehler', myTischtennisError.value, '', 'error'); + } finally { + configuringTeam.value = false; + } + }; + const clearParsedData = () => { parsedMyTischtennisData.value = null; myTischtennisUrl.value = ''; @@ -1074,6 +1151,7 @@ export default { onSeasonChange, parseMyTischtennisUrl, configureTeamFromUrl, + configureLeagueFromUrl, clearParsedData, getMyTischtennisStatus, fetchTeamDataManually,