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,