diff --git a/backend/MYTISCHTENNIS_AUTO_FETCH_README.md b/backend/MYTISCHTENNIS_AUTO_FETCH_README.md index bf68d3c2..469755be 100644 --- a/backend/MYTISCHTENNIS_AUTO_FETCH_README.md +++ b/backend/MYTISCHTENNIS_AUTO_FETCH_README.md @@ -9,12 +9,12 @@ Dieses System ermöglicht den automatischen Abruf von Spielergebnissen und Stati ### 6:00 Uhr - Rating Updates - **Service:** `autoUpdateRatingsService.js` - **Funktion:** Aktualisiert TTR/QTTR-Werte für Spieler -- **TODO:** Implementierung der eigentlichen Rating-Update-Logik +- **Status:** ✅ Aktiv. Nutzt `memberService.updateRatingsFromMyTischtennisByUserId(...)` pro Verein ueber einen freigeschalteten Benutzer mit gespeichertem myTischtennis-Login. ### 6:30 Uhr - Spielergebnisse - **Service:** `autoFetchMatchResultsService.js` -- **Funktion:** Ruft Spielerbilanzen für konfigurierte Teams ab -- **Status:** ✅ Grundlegende Implementierung fertig +- **Funktion:** Ruft Team-Spielplaene, Liga-Spielplaene, Spielerbilanzen und Ligatabellen fuer konfigurierte Teams ab +- **Status:** ✅ Aktiv. Importiert neue Spiele, aktualisiert Ergebnis- und Termin-Aenderungen und synchronisiert Ligatabellen. ## Benötigte Konfiguration @@ -107,6 +107,7 @@ Von der myTischtennis API werden folgende Daten abgerufen: - Player IDs, Namen der beiden Spieler - Gewonnene/Verlorene Punkte - Anzahl Spiele +- Zuordnung der beteiligten Mitglieder ueber Player-ID oder Namensabgleich ### Team-Informationen - Teamname, Liga, Saison @@ -134,7 +135,8 @@ Von der myTischtennis API werden folgende Daten abgerufen: - Parst JSON-Response - Matched Spieler anhand von ID oder Name - Speichert myTischtennis Player-ID bei Mitgliedern - - Loggt Statistiken + - verarbeitet auch Doppelpartner-Zuordnungen + - speichert/aktualisiert Spiele und Ligatabellen ### Player-Matching-Algorithmus @@ -148,25 +150,21 @@ Von der myTischtennis API werden folgende Daten abgerufen: ## TODO / Offene Punkte -### Noch zu implementieren: +### Noch offen: -1. **TTR/QTTR Updates** (6:00 Uhr Job): - - Endpoint für TTR/QTTR-Daten identifizieren - - Daten abrufen und in Member-Tabelle speichern - -2. **Spielergebnis-Details**: +1. **Spielergebnis-Details**: - Einzelne Matches mit Satzständen speichern - Tabelle für Match-Historie erstellen -3. **History-Tabelle für Spielergebnis-Abrufe** (optional): +2. **History-Tabelle für Spielergebnis-Abrufe** (optional): - Ähnlich zu `my_tischtennis_update_history` - Speichert Erfolg/Fehler der Abrufe -4. **Benachrichtigungen** (optional): +3. **Benachrichtigungen** (optional): - Email/Push bei neuen Ergebnissen - Highlights für besondere Siege -5. **Performance-Optimierung**: +4. **Performance-Optimierung**: - Caching für Player-Matches - Incremental Updates (nur neue Daten) @@ -183,6 +181,14 @@ await schedulerService.triggerRatingUpdates(); await schedulerService.triggerMatchResultsFetch(); ``` +### Manuelle HTTP-Trigger + +```text +POST /api/scheduler/rating_updates +POST /api/scheduler/match_results +GET /api/scheduler/status +``` + ## API-Dokumentation ### MyTischtennis Spielerbilanzen-Endpoint @@ -209,4 +215,3 @@ https://www.mytischtennis.de/click-tt/{association}/{season}/ligen/{groupname}/g - ✅ Passwörter verschlüsselt gespeichert - ✅ Fehlerbehandlung und Logging - ✅ Graceful Degradation (einzelne Team-Fehler stoppen nicht den gesamten Prozess) - diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 7be70f25..f315ec02 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -504,13 +504,6 @@ class AutoFetchMatchResultsService { * Process and store team data from myTischtennis */ async processTeamData(team, data) { - // TODO: Implement data processing and storage - // This would typically involve: - // 1. Extract player statistics from data.data.balancesheet - // 2. Match players with local Member records (by player_id or name) - // 3. Update or create match statistics - // 4. Store historical data for tracking changes - devLog(`Processing data for team ${team.name}`); if (!data.data || !data.data.balancesheet || !Array.isArray(data.data.balancesheet)) { @@ -521,47 +514,8 @@ class AutoFetchMatchResultsService { let processedCount = 0; for (const teamData of data.data.balancesheet) { - // Process single player statistics - if (teamData.single_player_statistics) { - for (const playerStat of teamData.single_player_statistics) { - devLog(`Player: ${playerStat.player_firstname} ${playerStat.player_lastname} (ID: ${playerStat.player_id})`); - devLog(` Points won: ${playerStat.points_won}, Points lost: ${playerStat.points_lost}`); - - // Try to match player with local Member - const member = await this.matchPlayer( - playerStat.player_id, - playerStat.player_firstname, - playerStat.player_lastname - ); - - if (member) { - devLog(` Matched with local member: ${member.firstName} ${member.lastName} (ID: ${member.id})`); - - // Update player statistics (TTR/QTTR would be fetched from different endpoint) - // For now, we just ensure the myTischtennis ID is stored - if (!member.myTischtennisPlayerId) { - member.myTischtennisPlayerId = playerStat.player_id; - await member.save(); - devLog(` Updated myTischtennis Player ID for ${member.firstName} ${member.lastName}`); - } - } else { - devLog(` No local member found for ${playerStat.player_firstname} ${playerStat.player_lastname}`); - } - - processedCount++; - } - } - - // Process double player statistics - if (teamData.double_player_statistics) { - for (const doubleStat of teamData.double_player_statistics) { - devLog(`Double: ${doubleStat.firstname_player_1} ${doubleStat.lastname_player_1} / ${doubleStat.firstname_player_2} ${doubleStat.lastname_player_2}`); - devLog(` Points won: ${doubleStat.points_won}, Points lost: ${doubleStat.points_lost}`); - - // TODO: Store double statistics - processedCount++; - } - } + processedCount += await this.processSinglePlayerStatistics(teamData); + processedCount += await this.processDoublePlayerStatistics(teamData); } // Also process meetings from the player stats response @@ -579,6 +533,95 @@ class AutoFetchMatchResultsService { return processedCount; } + async processSinglePlayerStatistics(teamData) { + if (!Array.isArray(teamData.single_player_statistics)) { + return 0; + } + + let processedCount = 0; + + for (const playerStat of teamData.single_player_statistics) { + devLog(`Player: ${playerStat.player_firstname} ${playerStat.player_lastname} (ID: ${playerStat.player_id})`); + devLog(` Points won: ${playerStat.points_won}, Points lost: ${playerStat.points_lost}`); + + const member = await this.matchPlayer( + playerStat.player_id, + playerStat.player_firstname, + playerStat.player_lastname + ); + + if (member) { + devLog(` Matched with local member: ${member.firstName} ${member.lastName} (ID: ${member.id})`); + await this.ensureMemberPlayerId(member, playerStat.player_id); + } else { + devLog(` No local member found for ${playerStat.player_firstname} ${playerStat.player_lastname}`); + } + + processedCount++; + } + + return processedCount; + } + + async processDoublePlayerStatistics(teamData) { + if (!Array.isArray(teamData.double_player_statistics)) { + return 0; + } + + let processedCount = 0; + + for (const doubleStat of teamData.double_player_statistics) { + const firstPlayerLabel = `${doubleStat.firstname_player_1} ${doubleStat.lastname_player_1}`.trim(); + const secondPlayerLabel = `${doubleStat.firstname_player_2} ${doubleStat.lastname_player_2}`.trim(); + + devLog(`Double: ${firstPlayerLabel} / ${secondPlayerLabel}`); + devLog(` Points won: ${doubleStat.points_won}, Points lost: ${doubleStat.points_lost}`); + + const [memberOne, memberTwo] = await Promise.all([ + this.matchPlayer( + doubleStat.player_id_1 || doubleStat.player_1_id || null, + doubleStat.firstname_player_1, + doubleStat.lastname_player_1 + ), + this.matchPlayer( + doubleStat.player_id_2 || doubleStat.player_2_id || null, + doubleStat.firstname_player_2, + doubleStat.lastname_player_2 + ) + ]); + + if (memberOne) { + await this.ensureMemberPlayerId(memberOne, doubleStat.player_id_1 || doubleStat.player_1_id || null); + } else { + devLog(` No local member found for double player 1: ${firstPlayerLabel}`); + } + + if (memberTwo) { + await this.ensureMemberPlayerId(memberTwo, doubleStat.player_id_2 || doubleStat.player_2_id || null); + } else { + devLog(` No local member found for double player 2: ${secondPlayerLabel}`); + } + + if (memberOne && memberTwo) { + devLog(` Matched double with local members: ${memberOne.firstName} ${memberOne.lastName} / ${memberTwo.firstName} ${memberTwo.lastName}`); + } + + processedCount++; + } + + return processedCount; + } + + async ensureMemberPlayerId(member, playerId) { + if (!playerId || member.myTischtennisPlayerId) { + return; + } + + member.myTischtennisPlayerId = playerId; + await member.save(); + devLog(` Updated myTischtennis Player ID for ${member.firstName} ${member.lastName}`); + } + /** * Process match results from schedule/table data */ diff --git a/docs/OPTIMIZATION_TODO.md b/docs/OPTIMIZATION_TODO.md index 234d8826..5c475870 100644 --- a/docs/OPTIMIZATION_TODO.md +++ b/docs/OPTIMIZATION_TODO.md @@ -39,21 +39,33 @@ Diese Liste beschreibt die naechsten sinnvollen Optimierungsschritte nach dem zu ## Prioritaet B -- [ ] Offene Backend-TODOs in `autoFetchMatchResultsService.js` schliessen. +- [x] Offene Backend-TODOs in `autoFetchMatchResultsService.js` schliessen. Aktuelle Fundstellen: - Datenverarbeitung/Speicherung an einer Reststelle - Double-Statistiken noch nicht gespeichert Ziel: - Rest-TODOs entweder implementieren oder bewusst entfernen/dokumentieren + Erledigt am 2026-03-17: + - Single-/Double-Statistiken in explizite Verarbeitungsschritte aufgeteilt + - Doppelpartner werden jetzt ebenfalls gegen lokale Mitglieder gematcht + - myTischtennis-Spieler-IDs werden fuer beide Doppelspieler nachgezogen + - offene Inline-TODOs im Service entfernt -- [ ] Rating-Update-Logik aus `MYTISCHTENNIS_AUTO_FETCH_README.md` wirklich fertigstellen oder die Doku an den Ist-Zustand angleichen. +- [x] Rating-Update-Logik aus `MYTISCHTENNIS_AUTO_FETCH_README.md` wirklich fertigstellen oder die Doku an den Ist-Zustand angleichen. Grund: In der Doku stehen noch offene Kernpunkte, die spaeter verwirrend sind, wenn der Scheduler als "fertig" wahrgenommen wird. + Erledigt am 2026-03-17: + - Doku auf den aktuellen Scheduler-/HTTP-Trigger-Stand gebracht + - Rating-Updates und Match-Results als aktiv dokumentiert + - nur die tatsaechlich noch offenen fachlichen Restpunkte verbleiben -- [ ] Gruppenzuordnungs-REST-TODO in `DiaryView.vue` schliessen. +- [x] Gruppenzuordnungs-REST-TODO in `DiaryView.vue` schliessen. Aktuelle Fundstelle: - `// TODO: API-Call zum Speichern der Gruppenzuordnung` Ziel: - keine offenen Inline-TODOs in produktiv genutzten Hauptviews + Erledigt am 2026-03-17: + - toten lokalen Stub fuer Aktivitaets-Gruppenzuordnung entfernt + - damit keine offene Schein-REST-Stelle mehr in der Hauptview ## Prioritaet C diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 9b84a760..df0f0252 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -3245,26 +3245,6 @@ export default { return setIds && setIds.size > 0; }, - getActivityGroup(activityId) { - return this.activityGroupsMap[activityId] || ''; - }, - - async updateActivityGroup(activityId, groupId) { - try { - // Hier würde normalerweise ein API-Call gemacht werden - // Für jetzt speichern wir es nur lokal - this.activityGroupsMap[activityId] = groupId || ''; - - // TODO: API-Call zum Speichern der Gruppenzuordnung - // await apiClient.put(`/diary-date-activities/${activityId}/group`, { groupId }); - - this.showInfo('Erfolg', 'Gruppenzuordnung aktualisiert', '', 'success'); - } catch (error) { - console.error('Fehler beim Aktualisieren der Gruppenzuordnung:', error); - this.showInfo('Fehler', 'Fehler beim Aktualisieren der Gruppenzuordnung', '', 'error'); - } - }, - // Bulk-Zuordnungen für Aktivitäten async assignAllMembersToActivity(activityId) { try {