diff --git a/backend/controllers/myTischtennisUrlController.js b/backend/controllers/myTischtennisUrlController.js index 7d4fd84..a5fefe0 100644 --- a/backend/controllers/myTischtennisUrlController.js +++ b/backend/controllers/myTischtennisUrlController.js @@ -20,12 +20,12 @@ class MyTischtennisUrlController { const { url } = req.body; if (!url) { - throw new HttpError(400, 'URL is required'); + throw new HttpError('URL is required', 400); } // Validate URL if (!myTischtennisUrlParserService.isValidTeamUrl(url)) { - throw new HttpError(400, 'Invalid myTischtennis URL format'); + throw new HttpError('Invalid myTischtennis URL format', 400); } // Parse URL @@ -229,11 +229,11 @@ class MyTischtennisUrlController { const userIdOrEmail = req.headers.userid; if (!clubTeamId) { - throw new HttpError(400, 'clubTeamId is required'); + throw new HttpError('clubTeamId is required', 400); } if (!userIdOrEmail) { - throw new HttpError(401, 'User-ID fehlt. Bitte melden Sie sich an.'); + throw new HttpError('User-ID fehlt. Bitte melden Sie sich an.', 401); } // Get actual user ID (userid header might be email address) @@ -269,7 +269,7 @@ class MyTischtennisUrlController { session = await myTischtennisService.getSession(userId); } catch (loginError) { const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen'; - throw new HttpError(401, `MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`); + throw new HttpError(`MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`, 401); } } @@ -277,7 +277,7 @@ class MyTischtennisUrlController { account = await myTischtennisService.getAccount(userId); if (!account) { - throw new HttpError(404, 'MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.'); + throw new HttpError('MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.', 404); } @@ -298,25 +298,25 @@ class MyTischtennisUrlController { }); if (!team) { - throw new HttpError(404, `Team mit ID ${clubTeamId} nicht gefunden`); + throw new HttpError(`Team mit ID ${clubTeamId} nicht gefunden`, 404); } // Verbesserte Validierung mit detaillierten Fehlermeldungen if (!team.myTischtennisTeamId) { - throw new HttpError(400, `Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`); + throw new HttpError(`Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`, 400); } if (!team.league) { - throw new HttpError(400, 'Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.'); + throw new HttpError('Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.', 400); } if (!team.league.myTischtennisGroupId) { - throw new HttpError(400, 'Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.'); + throw new HttpError('Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.', 400); } // Validate season before proceeding if (!team.league.season || !team.league.season.season) { - throw new HttpError(400, 'Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.'); + throw new HttpError('Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.', 400); } // Build the URL that will be used - do this early so we can log it even if errors occur @@ -424,7 +424,10 @@ class MyTischtennisUrlController { } } - const status = error.statusCode || error.status || 500; + // Normalize HTTP status code (guard against strings) + const rawCode = error && (error.statusCode != null ? error.statusCode : error.status); + const parsed = Number(rawCode); + const status = Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : 500; const debug = { message: error.message || String(error), name: error.name, @@ -434,7 +437,14 @@ class MyTischtennisUrlController { url: typeof myTischtennisUrl !== 'undefined' ? myTischtennisUrl : null }; try { - res.status(status).json({ success: false, error: debug.message, debug }); + if (!res.headersSent) { + // Spezieller Fall: myTischtennis-Reauth nötig → nicht 401 an FE senden, um App-Logout zu vermeiden + const isMyTischtennisAuthIssue = status === 401 && /MyTischtennis-Session abgelaufen|Automatischer Login fehlgeschlagen|Passwort gespeichert/i.test(debug.message || ''); + if (isMyTischtennisAuthIssue) { + return res.status(200).json({ success: false, error: debug.message, debug, needsMyTischtennisReauth: true }); + } + res.status(status).json({ success: false, error: debug.message, debug }); + } } catch (writeErr) { // Fallback, falls Headers schon gesendet wurden // eslint-disable-next-line no-console @@ -510,7 +520,7 @@ class MyTischtennisUrlController { const parsedData = myTischtennisUrlParserService.parseUrl(url); if (parsedData.urlType !== 'table') { - throw new HttpError(400, 'URL must be a table URL (not a team URL)'); + throw new HttpError('URL must be a table URL (not a team URL)', 400); } // Find or create season diff --git a/backend/services/memberService.js b/backend/services/memberService.js index 10af219..1060ffc 100644 --- a/backend/services/memberService.js +++ b/backend/services/memberService.js @@ -264,26 +264,33 @@ class MemberService { const notFound = []; const matched = []; - // Maps für schnelleres Matching - const mapByName = (entries) => { - const m = new Map(); + // Indizes für schnelleres Matching (nach personId und Name) + const createIndex = (entries) => { + const byName = new Map(); + const byPersonId = new Map(); for (const e of (entries || [])) { - const key = `${(e.firstname||'').toLowerCase()}|${(e.lastname||'').toLowerCase()}`; - if (!m.has(key)) m.set(key, e); + const key = `${(e.firstname||'').toLowerCase().trim()}|${(e.lastname||'').toLowerCase().trim()}`; + if (!byName.has(key)) byName.set(key, e); + if (e.personId && !byPersonId.has(String(e.personId))) { + byPersonId.set(String(e.personId), e); + } } - return m; + return { byName, byPersonId }; }; - const currentMap = mapByName(rankingsCurrent.entries); - const quarterMap = rankingsQuarter.success ? mapByName(rankingsQuarter.entries) : new Map(); + const currentIdx = createIndex(rankingsCurrent.entries); + const quarterIdx = rankingsQuarter.success ? createIndex(rankingsQuarter.entries) : { byName: new Map(), byPersonId: new Map() }; // 4. Für jedes Mitglied TTR und QTTR aktualisieren + let updatedTtr = 0; + let updatedQttr = 0; for (const member of members) { const firstName = member.firstName; const lastName = member.lastName; - const key = `${(firstName||'').toLowerCase()}|${(lastName||'').toLowerCase()}`; - const rankingEntry = currentMap.get(key); - const rankingQuarterEntry = quarterMap.get(key); + const key = `${(firstName||'').toLowerCase().trim()}|${(lastName||'').toLowerCase().trim()}`; + const personId = member.myTischtennisPlayerId ? String(member.myTischtennisPlayerId).trim() : null; + const rankingEntry = personId ? (currentIdx.byPersonId.get(personId) || currentIdx.byName.get(key)) : currentIdx.byName.get(key); + const rankingQuarterEntry = personId ? (quarterIdx.byPersonId.get(personId) || quarterIdx.byName.get(key)) : quarterIdx.byName.get(key); if (rankingEntry || rankingQuarterEntry) { try { @@ -291,9 +298,18 @@ class MemberService { const oldQttr = member.qttr; if (rankingEntry && typeof rankingEntry.fedRank === 'number') { member.ttr = rankingEntry.fedRank; + if (member.ttr !== oldTtr) updatedTtr++; } if (rankingQuarterEntry && typeof rankingQuarterEntry.fedRank === 'number') { member.qttr = rankingQuarterEntry.fedRank; + if (member.qttr !== oldQttr) updatedQttr++; + } else if (!rankingsQuarter.success && (member.qttr == null)) { + // Fallback: wenn QTTR-Abruf fehlgeschlagen ist und kein Wert vorhanden war, + // setze QTTR ersatzweise auf aktuellen TTR, damit die Anzeige nicht leer bleibt + if (member.ttr != null) { + member.qttr = member.ttr; + if (member.qttr !== oldQttr) updatedQttr++; + } } await member.save(); updated++; @@ -318,7 +334,7 @@ class MemberService { devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`); - let message = `${updated} Mitglied(er) aktualisiert.`; + let message = `${updated} Mitglied(er) aktualisiert. (TTR: ${updatedTtr}, QTTR: ${updatedQttr})`; if (notFound.length > 0) { message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`; } diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index af0a0d3..1283023 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -1060,9 +1060,9 @@ export default { try { const response = await apiClient.post('/mytischtennis/fetch-team-data', { clubTeamId: teamToEdit.value.id - }); + }, { timeout: 30000 }); - if (response.data.success) { + if (response.data && response.data.success) { myTischtennisSuccess.value = response.data.message; // Erstelle detaillierte Erfolgsmeldung mit Tabelleninfo @@ -1078,22 +1078,32 @@ export default { detailsMessage, 'success' ); + } else if (response.data && response.data.success === false) { + const details = response.data.debug ? JSON.stringify(response.data.debug, null, 2) : ''; + await showInfo( + response.data.needsMyTischtennisReauth ? 'Login bei myTischtennis erforderlich' : 'Fehler', + response.data.error || 'Daten konnten nicht abgerufen werden.', + details, + response.data.needsMyTischtennisReauth ? 'warning' : 'error' + ); + myTischtennisError.value = response.data.error || ''; } } catch (error) { console.error('Fehler beim Abrufen der Team-Daten:', error); + const isTimeout = error?.code === 'ECONNABORTED'; const errData = error?.response?.data || {}; - const errorMsg = errData.message || errData.error || error.message || 'Daten konnten nicht abgerufen werden.'; + const errorMsg = isTimeout ? 'Zeitüberschreitung beim Abruf (Timeout).' : (errData.message || errData.error || error.message || 'Daten konnten nicht abgerufen werden.'); myTischtennisError.value = errorMsg; // Spezielle Behandlung für Account-nicht-verknüpft Fehler - if (error.response?.status === 404) { + if (!isTimeout && error.response?.status === 404) { await showInfo( 'MyTischtennis-Account nicht verknüpft', errorMsg, 'Gehen Sie zu den MyTischtennis-Einstellungen, um Ihren Account zu verknüpfen.', 'warning' ); - } else if (error.response?.status === 401) { + } else if (!isTimeout && error.response?.status === 401) { await showInfo( 'Login erforderlich', errorMsg, @@ -1101,7 +1111,7 @@ export default { 'warning' ); } else { - const debugText = errData.debug ? JSON.stringify(errData.debug, null, 2) : ''; + const debugText = isTimeout ? 'Der Server hat nicht rechtzeitig geantwortet.' : (errData.debug ? JSON.stringify(errData.debug, null, 2) : ''); await showInfo('Fehler', errorMsg, debugText, 'error'); } } finally {