From 23708b99b5df4cb079d847e40b2c7305348c514a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 3 Nov 2025 10:45:04 +0100 Subject: [PATCH] Refactor error handling in MyTischtennisUrlController and improve memberService indexing Refactored error handling in MyTischtennisUrlController to standardize error messages and ensure consistent status codes. Enhanced memberService by implementing a more efficient indexing system for member data retrieval, improving performance and accuracy in TTR and QTTR updates. Updated TeamManagementView to handle timeout errors and provide detailed user feedback, enhancing overall user experience. --- .../controllers/myTischtennisUrlController.js | 38 +++++++++++------- backend/services/memberService.js | 40 +++++++++++++------ frontend/src/views/TeamManagementView.vue | 22 +++++++--- 3 files changed, 68 insertions(+), 32 deletions(-) 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 {