From f1cfd1147d4ba109a834c9c3dabe7022f947e365 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 17 Mar 2026 09:04:45 +0100 Subject: [PATCH] feat(TeamManagementView, i18n): enhance team management interface and localization - Added new summary cards to display team statistics, including total teams, fully configured, partially configured, and teams without leagues. - Implemented search and filter functionality for teams, improving user navigation and management. - Expanded German localization file with new keys for team management features, enhancing the experience for German-speaking users. - Updated the layout for automatic job information, improving clarity and user interaction. --- frontend/src/i18n/locales/de.json | 62 +- frontend/src/views/TeamManagementView.vue | 932 +++++++++++++++------- 2 files changed, 700 insertions(+), 294 deletions(-) diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 40940ab0..6dd2431c 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1203,6 +1203,7 @@ "matchResults": "Spielergebnisse", "fetched": "abgerufen", "newTeam": "Neues Team", + "basicSettings": "Grundeinstellungen", "editTeam": "Team bearbeiten", "createNewTeam": "Neues Team anlegen", "teamName": "Team-Name", @@ -1234,8 +1235,15 @@ "myTischtennis": "MyTischtennis", "myTischtennisUrlPlaceholder": "MyTischtennis URL...", "teams": "Teams", + "activeTeam": "Aktives Team", + "searchTeams": "Team suchen", + "filterAll": "Alle", + "filterConfigured": "Konfiguriert", + "filterNeedsAttention": "Prüfen", + "filterNoLeague": "Ohne Liga", "seasonUnknown": "unbekannt", "noTeamsYet": "Noch keine Teams vorhanden. Erstellen Sie Ihr erstes Team!", + "noMatchingTeams": "Keine Teams passen zur aktuellen Suche oder Filterung.", "edit": "Bearbeiten", "delete": "Löschen", "noAssignment": "Keine Zuordnung", @@ -1246,7 +1254,59 @@ "notConfigured": "Nicht konfiguriert", "never": "Nie", "showCodeList": "Code-Liste anzeigen", - "showPinList": "Pin-Liste anzeigen" + "showPinList": "Pin-Liste anzeigen", + "deleteTeamTitle": "Club-Team löschen", + "deleteTeamConfirm": "Möchten Sie das Club-Team \"{name}\" wirklich löschen?", + "errorDeletingTeam": "Fehler beim Löschen des Club-Teams.", + "teamHasNoLeague": "Dieses Team ist keiner Liga zugeordnet.", + "assignLeagueBeforeDocuments": "Bitte ordnen Sie dem Team zuerst eine Liga zu, damit Dokumente verarbeitet werden können.", + "assignLeagueBeforeParsing": "Bitte ordnen Sie dem Team zuerst eine Liga zu, um PDF-Dateien zu parsen.", + "documentParsedSummary": "{label} erfolgreich hochgeladen und geparst!\n\nGefundene Spiele: {matchesFound}\nNeue Spiele erstellt: {created}\nSpiele aktualisiert: {updated}", + "errorsCount": "Fehler: {count}", + "moreErrors": "... und {count} weitere", + "noMatchesFoundTitle": "Keine Spiele gefunden", + "noMatchesFoundDetails": "Hinweis: Keine Spiele erkannt.\nZeilen im Dokument: {lines}", + "documentUploaded": "{label} \"{fileName}\" wurde erfolgreich hochgeladen!", + "errorUploadingDocument": "Fehler beim Hochladen und Parsen der Datei.", + "matchesSummary": "Gefundene Spiele: {matchesFound}\nNeue Spiele erstellt: {created}\nSpiele aktualisiert: {updated}", + "errorParsingPdf": "Fehler beim Parsen der PDF-Datei", + "documentNotFound": "Das ausgewählte Dokument wurde nicht gefunden.", + "missingLeagueForTeam": "Für das ausgewählte Team wurde keine Liga übermittelt.", + "pdfFileNotFound": "Die PDF-Datei konnte nicht gefunden werden.", + "reuploadFile": "Bitte laden Sie die Datei erneut hoch und versuchen Sie es noch einmal.", + "errorLoadingPdf": "Fehler beim Laden des PDFs.", + "errorParsingUrl": "URL konnte nicht geparst werden. Bitte überprüfen Sie das Format.", + "configureLeagueTitle": "Liga konfigurieren?", + "tableUrlDetected": "Tabellen-URL erkannt", + "configureLeagueDetails": "Verband: {association}\nSaison: {season}\nLiga: {league}\nGruppen-ID: {groupId}\n\nMöchten Sie diese Liga in der Datenbank konfigurieren? Dies ermöglicht es, Tabellendaten automatisch abzurufen.", + "selectTeamTitle": "Team auswählen", + "selectTeamFirst": "Bitte wählen Sie zuerst ein Team aus", + "selectTeamForConfiguration": "Um die MyTischtennis-Konfiguration zu aktivieren, müssen Sie zuerst ein Team aus der Liste auswählen.", + "teamConfiguredSuccess": "Team erfolgreich konfiguriert! Automatischer Datenabruf ist jetzt aktiv.", + "teamConfiguredDetails": "Liga: {league}\nSaison: {season}\nAutomatischer Datenabruf ist jetzt aktiv.", + "errorConfiguringTeam": "Team konnte nicht konfiguriert werden.", + "leagueConfiguredSuccess": "Liga erfolgreich konfiguriert! Tabellendaten können jetzt automatisch abgerufen werden.", + "leagueConfiguredDetails": "Liga: {league}\nSaison: {season}\nVerband: {association}\nGruppen-ID: {groupId}\n\nTabellendaten können jetzt automatisch abgerufen werden.", + "errorConfiguringLeague": "Liga konnte nicht konfiguriert werden.", + "notCreated": "Nicht erstellt", + "autoFetchEnabled": "Automatischer Datenabruf ist aktiviert", + "missingItems": "Fehlend: {items}", + "enterUrlForAutoConfig": "MyTischtennis-URL eingeben für automatische Konfiguration", + "errorLoadingStats": "Statistiken konnten nicht geladen werden.", + "asyncJobStartFailed": "Async-Job konnte nicht gestartet werden.", + "dataFetchFailed": "Daten konnten nicht abgerufen werden.", + "fetchTimedOut": "Zeitüberschreitung beim Abruf (Async-Job läuft zu lange).", + "teamDataFetched": "Teamdaten erfolgreich abgerufen.", + "unknownTeam": "Unbekanntes Team", + "teamDataFetchedDetails": "Team: {team}\nAbgerufene Datensätze: {count}", + "tableUpdateLabel": "Tabellenaktualisierung:", + "mytischtennisLoginRequired": "Login bei myTischtennis erforderlich", + "fetchTimeoutShort": "Zeitüberschreitung beim Abruf (Timeout).", + "invalidFileTypeTitle": "Ungültiger Dateityp", + "invalidFileExtension": "{label} muss eine der folgenden Endungen haben: {extensions}.", + "invalidMimeType": "{label} weist einen unerwarteten MIME-Typ auf: {type}.", + "fileTooLargeTitle": "Datei zu groß", + "fileTooLarge": "{label} darf maximal 10 MB groß sein." }, "pendingApprovals": { "title": "Ausstehende Benutzeranfragen", diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index 9f068d9c..2016d900 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -8,8 +8,20 @@ :show-current-season="true" /> - -
+
+
+ {{ t('teamManagement.automaticJobs') }} + + {{ t('teamManagement.lastRun') }}: + {{ formatJobDate(schedulerJobs.match_results?.lastRun || schedulerJobs.rating_updates?.lastRun) }} + +
+ +
+ +
🔄 {{ t('teamManagement.ratingUpdates') }}: @@ -41,6 +53,130 @@
+
+
+ {{ t('teamManagement.teams') }} + {{ teams.length }} +
+
+ {{ t('teamManagement.fullyConfigured') }} + {{ fullyConfiguredTeamsCount }} +
+
+ {{ t('teamManagement.partiallyConfigured') }} + {{ partiallyConfiguredTeamsCount }} +
+
+ {{ t('teamManagement.noLeague') }} + {{ teamsWithoutLeagueCount }} +
+
+ +
+
+

{{ t('teamManagement.teams') }} ({{ filteredTeams.length }}) - {{ t('teamManagement.season') }} {{ currentSeason?.season || t('teamManagement.seasonUnknown') }}

+
+ +
+ + + + +
+ +
+
+ +
+

{{ t('teamManagement.noTeamsYet') }}

+
+
+

{{ t('teamManagement.noMatchingTeams') }}

+
+ +
+
+
+
+

{{ team.name }}

+ + {{ team.league ? team.league.name : t('teamManagement.noLeague') }} + +
+
+ + +
+
+ +
+ + ✓ {{ t('teamManagement.fullyConfigured') }} + + + ⚠ {{ t('teamManagement.partiallyConfigured') }} + + + ✗ {{ t('teamManagement.notConfigured') }} + + + {{ team.season?.season || t('teamManagement.unknown') }} + +
+ +
+ + {{ t('teamManagement.created') }}: {{ formatDate(team.createdAt) }} + + + {{ t('teamManagement.lastUpdated') }}: + {{ getTeamJobInfo(team) && getTeamJobInfo(team).lastRun ? formatJobDate(getTeamJobInfo(team).lastRun) : t('teamManagement.never') }} + +
+ + +
+
{{ t('teamManagement.documents') }}
+
+ + +
+
+
+
+
+
@@ -51,9 +187,44 @@
-
- -
+
+
+
{{ teamToEdit ? t('teamManagement.activeTeam') : t('teamManagement.newTeam') }}
+

{{ teamToEdit ? teamToEdit.name : t('teamManagement.newTeam') }}

+
+
+ + {{ + getMyTischtennisStatus(teamToEdit).complete + ? t('teamManagement.fullyConfigured') + : (getMyTischtennisStatus(teamToEdit).partial + ? t('teamManagement.partiallyConfigured') + : t('teamManagement.notConfigured')) + }} + + {{ teamToEdit.league ? teamToEdit.league.name : t('teamManagement.noLeague') }} +
+
+ +
+ + + + + +
+ +
- - -
-
- 📊 {{ t('teamManagement.playerStats') }} - -
- -
{{ t('teamManagement.loadingStats') }}
- -
- {{ t('teamManagement.noPlayerStats') }} -
- - - - - - - - - - - - - - - - - - -
{{ t('teamManagement.player') }}(Q)TTR{{ t('teamManagement.season') }} - {{ isSecondHalf ? t('teamManagement.secondHalf') : t('teamManagement.firstHalf') }} -
{{ stat.firstName }} {{ stat.lastName }} - - {{ memberById[stat.memberId].qttr ?? memberById[stat.memberId].ttr ?? '–' }} - - - {{ stat.totalSeason }}{{ isSecondHalf ? stat.totalSecondHalf : stat.totalFirstHalf }}
-
+
+ +
+
+ 📊 {{ t('teamManagement.playerStats') }} +
- -
- +
{{ t('teamManagement.loadingStats') }}
+ +
+ {{ t('teamManagement.noPlayerStats') }} +
+ + + + + + + + + + + + + + + + + + +
{{ t('teamManagement.player') }}(Q)TTR{{ t('teamManagement.season') }} + {{ isSecondHalf ? t('teamManagement.secondHalf') : t('teamManagement.firstHalf') }} +
{{ stat.firstName }} {{ stat.lastName }} + + {{ memberById[stat.memberId].qttr ?? memberById[stat.memberId].ttr ?? '–' }} + + + {{ stat.totalSeason }}{{ isSecondHalf ? stat.totalSecondHalf : stat.totalFirstHalf }}
+
+ +
📋 {{ t('teamManagement.documents') }}
@@ -138,157 +306,61 @@
- - -
- 🔄 {{ t('teamManagement.automaticJobs') }} -
-
- {{ t('teamManagement.lastUpdated') }}: - {{ formatJobDate(getTeamJobInfo(teamToEdit).lastRun) }} -
-
- {{ t('teamManagement.status') }}: - ✓ {{ t('teamManagement.successful') }} - ✗ {{ t('teamManagement.error') }} -
+
+ +
+
+ 🔄 {{ t('teamManagement.automaticJobs') }} +
+
+ {{ t('teamManagement.lastUpdated') }}: + {{ formatJobDate(getTeamJobInfo(teamToEdit).lastRun) }}
-
- {{ t('teamManagement.noAutomaticUpdate') }} +
+ {{ t('teamManagement.status') }}: + ✓ {{ t('teamManagement.successful') }} + ✗ {{ t('teamManagement.error') }}
- - -
-
- 🏓 {{ t('teamManagement.myTischtennis') }} -
- - - -
- -
- -
- - -
- -
⚠️ {{ myTischtennisError }}
-
✅ {{ myTischtennisSuccess }}
+
+ {{ t('teamManagement.noAutomaticUpdate') }}
-
-
-
-

{{ t('teamManagement.teams') }} ({{ teams.length }}) - {{ t('teamManagement.season') }} {{ currentSeason?.season || t('teamManagement.seasonUnknown') }}

- -
-

{{ t('teamManagement.noTeamsYet') }}

-
- -
-
-
-

{{ team.name }}

-
- - -
-
- -
-
- {{ t('teamManagement.league') }}: - - {{ team.league ? team.league.name : t('teamManagement.noAssignment') }} - -
-
- {{ t('teamManagement.season') }}: - {{ team.season?.season || t('teamManagement.unknown') }} -
-
- {{ t('teamManagement.created') }}: - {{ formatDate(team.createdAt) }}
- -
- 🏓 MyTischtennis: - - ✓ {{ t('teamManagement.fullyConfigured') }} - - - ⚠ {{ t('teamManagement.partiallyConfigured') }} - - - ✗ {{ t('teamManagement.notConfigured') }} - +
+ +
- -
- 🔄 Automatische Jobs: - - - {{ formatJobDate(getTeamJobInfo(team).lastRun) }} - - {{ getTeamJobInfo(team).success ? '✓' : '✗' }} - - - {{ t('teamManagement.never') }} - -
-
- - -
-
{{ t('teamManagement.documents') }}:
-
- - -
+
⚠️ {{ myTischtennisError }}
+
✅ {{ myTischtennisSuccess }}
@@ -376,8 +448,8 @@ export default { message: '', details: '', type: 'info', - confirmText: 'OK', - cancelText: 'Abbrechen', + confirmText: t('common.confirm'), + cancelText: t('common.cancel'), showCancel: true, resolveCallback: null }); @@ -394,6 +466,10 @@ export default { const teamDocuments = ref([]); const parsingInProgress = ref(false); const parsingDocuments = ref({}); + const teamSearchQuery = ref(''); + const teamFilter = ref('all'); + const activeEditorSection = ref('basic'); + const showGlobalJobDetails = ref(false); // PDF-Dialog Variablen const showPDFViewer = ref(false); @@ -431,6 +507,27 @@ export default { const now = new Date(); return now.getMonth() >= 0 && now.getMonth() <= 5; // Januar (0) bis Juni (5) = Rückrunde }); + const fullyConfiguredTeamsCount = computed(() => teams.value.filter(team => getMyTischtennisStatus(team).complete).length); + const partiallyConfiguredTeamsCount = computed(() => teams.value.filter(team => getMyTischtennisStatus(team).partial).length); + const teamsWithoutLeagueCount = computed(() => teams.value.filter(team => !team.leagueId).length); + const filteredTeams = computed(() => { + const search = teamSearchQuery.value.trim().toLowerCase(); + return teams.value.filter(team => { + const status = getMyTischtennisStatus(team); + const matchesSearch = !search || [ + team.name, + team.league?.name, + team.season?.season + ].filter(Boolean).some(value => String(value).toLowerCase().includes(search)); + + if (!matchesSearch) return false; + + if (teamFilter.value === 'configured') return status.complete; + if (teamFilter.value === 'attention') return !status.complete; + if (teamFilter.value === 'noLeague') return !team.leagueId; + return true; + }); + }); // Methods const toggleNewTeam = () => { @@ -442,12 +539,14 @@ export default { const resetToNewTeam = () => { teamToEdit.value = null; + activeEditorSection.value = 'basic'; resetNewTeam(); }; const resetNewTeam = () => { newTeamName.value = ''; newLeagueId.value = ''; + activeEditorSection.value = 'basic'; }; const loadTeams = async () => { @@ -521,6 +620,7 @@ export default { newTeamName.value = team.name; newLeagueId.value = team.leagueId || ''; teamFormIsOpen.value = true; + activeEditorSection.value = 'basic'; await loadTeamDocuments(); // Aktualisiere Job-Informationen, damit Team-spezifische Daten aktuell sind await loadSchedulerJobsInfo(); @@ -528,8 +628,8 @@ export default { const deleteTeam = async (team) => { const confirmed = await showConfirm( - 'Club-Team löschen', - `Möchten Sie das Club-Team "${team.name}" wirklich löschen?`, + t('teamManagement.deleteTeamTitle'), + t('teamManagement.deleteTeamConfirm', { name: team.name }), '', 'danger' ); @@ -542,6 +642,7 @@ export default { await loadTeams(); } catch (error) { console.error('Fehler beim Löschen des Club-Teams:', error); + await showInfo(t('messages.error'), getSafeErrorMessage(error, t('teamManagement.errorDeletingTeam')), '', 'error'); } }; @@ -650,9 +751,9 @@ export default { const uploadAndParseDocument = async (file, documentType) => { if (!teamToEdit.value?.leagueId) { await showInfo( - 'Hinweis', - 'Dieses Team ist keiner Liga zugeordnet.', - 'Bitte ordnen Sie dem Team zuerst eine Liga zu, damit Dokumente verarbeitet werden können.', + t('messages.warning'), + t('teamManagement.teamHasNoLeague'), + t('teamManagement.assignLeagueBeforeDocuments'), 'warning' ); return; @@ -665,7 +766,7 @@ export default { parsingInProgress.value = true; try { - const documentLabel = documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste'; + const documentLabel = documentType === 'code_list' ? t('teamManagement.codeList') : t('teamManagement.pinList'); const isValid = await validateTeamDocumentFile(file, documentLabel); if (!isValid) { return; @@ -687,43 +788,45 @@ export default { const parseResponse = await apiClient.post(`/team-documents/${uploadResponse.data.id}/parse?leagueid=${teamToEdit.value.leagueId}`); const { parseResult, saveResult } = parseResponse.data; - let message = `${documentLabel} erfolgreich hochgeladen und geparst!\n\n`; - message += `Gefundene Spiele: ${parseResult.matchesFound}\n`; - message += `Neue Spiele erstellt: ${saveResult.created}\n`; - message += `Spiele aktualisiert: ${saveResult.updated}`; + let message = t('teamManagement.documentParsedSummary', { + label: documentLabel, + matchesFound: parseResult.matchesFound, + created: saveResult.created, + updated: saveResult.updated + }); if (saveResult.errors.length > 0) { - message += `\n\nFehler: ${saveResult.errors.length}\n`; + message += `\n\n${t('teamManagement.errorsCount', { count: saveResult.errors.length })}\n`; message += saveResult.errors.slice(0, 3).join('\n'); if (saveResult.errors.length > 3) { - message += `\n... und ${saveResult.errors.length - 3} weitere`; + message += `\n${t('teamManagement.moreErrors', { count: saveResult.errors.length - 3 })}`; } } - let dialogTitle = 'Erfolg'; + let dialogTitle = t('messages.success'); let dialogType = 'success'; if (parseResult.matchesFound === 0) { - dialogTitle = 'Keine Spiele gefunden'; + dialogTitle = t('teamManagement.noMatchesFoundTitle'); dialogType = 'warning'; if (parseResult.debugInfo) { - message += `\n\nHinweis: Keine Spiele erkannt.\nZeilen im Dokument: ${parseResult.debugInfo.totalLines}`; + message += `\n\n${t('teamManagement.noMatchesFoundDetails', { lines: parseResult.debugInfo.totalLines })}`; } } else if (saveResult.errors.length > 0) { - dialogTitle = 'Warnung'; + dialogTitle = t('messages.warning'); dialogType = 'warning'; } await showInfo(dialogTitle, message, '', dialogType); } else { - await showInfo('Information', `${documentLabel} "${file.name}" wurde erfolgreich hochgeladen!`, '', 'info'); + await showInfo(t('messages.info'), t('teamManagement.documentUploaded', { label: documentLabel, fileName: file.name }), '', 'info'); } await loadTeamDocuments(); } catch (error) { console.error('Fehler beim Hochladen und Parsen der Datei:', error); - const message = getSafeErrorMessage(error, 'Fehler beim Hochladen und Parsen der Datei.'); - await showInfo('Fehler', message, '', 'error'); + const message = getSafeErrorMessage(error, t('teamManagement.errorUploadingDocument')); + await showInfo(t('messages.error'), message, '', 'error'); } finally { parsingInProgress.value = false; } @@ -733,9 +836,9 @@ export default { const team = teams.value.find(t => t.id === document.clubTeamId); if (!team || !team.leagueId) { await showInfo( - 'Hinweis', - 'Dieses Team ist keiner Liga zugeordnet.', - 'Bitte ordnen Sie dem Team zuerst eine Liga zu, um PDF-Dateien zu parsen.', + t('messages.warning'), + t('teamManagement.teamHasNoLeague'), + t('teamManagement.assignLeagueBeforeParsing'), 'warning' ); return; @@ -754,26 +857,28 @@ export default { const response = await apiClient.post(`/team-documents/${document.id}/parse?leagueid=${team.leagueId}`); const { parseResult, saveResult } = response.data; - let message = `Gefundene Spiele: ${parseResult.matchesFound}\n`; - message += `Neue Spiele erstellt: ${saveResult.created}\n`; - message += `Spiele aktualisiert: ${saveResult.updated}`; + let message = t('teamManagement.matchesSummary', { + matchesFound: parseResult.matchesFound, + created: saveResult.created, + updated: saveResult.updated + }); - let dialogTitle = 'Erfolg'; + let dialogTitle = t('messages.success'); let dialogType = 'success'; if (parseResult.matchesFound === 0) { - dialogTitle = 'Keine Spiele gefunden'; + dialogTitle = t('teamManagement.noMatchesFoundTitle'); dialogType = 'warning'; if (parseResult.debugInfo) { - message += `\n\nHinweis: Keine Spiele erkannt.\nZeilen im Dokument: ${parseResult.debugInfo.totalLines}`; + message += `\n\n${t('teamManagement.noMatchesFoundDetails', { lines: parseResult.debugInfo.totalLines })}`; } } else if (saveResult.errors.length > 0) { - dialogTitle = 'Warnung'; + dialogTitle = t('messages.warning'); dialogType = 'warning'; - message += `\n\nFehler: ${saveResult.errors.length}\n`; + message += `\n\n${t('teamManagement.errorsCount', { count: saveResult.errors.length })}\n`; message += saveResult.errors.slice(0, 3).join('\n'); if (saveResult.errors.length > 3) { - message += `\n... und ${saveResult.errors.length - 3} weitere`; + message += `\n${t('teamManagement.moreErrors', { count: saveResult.errors.length - 3 })}`; } } @@ -783,19 +888,19 @@ export default { console.error('Fehler beim Parsen der PDF:', error); const responseData = error?.response?.data || {}; const status = error?.response?.status; - let errorMessage = getSafeErrorMessage(error, 'Fehler beim Parsen der PDF-Datei'); + let errorMessage = getSafeErrorMessage(error, t('teamManagement.errorParsingPdf')); let details = ''; if (status === 404 && responseData.error === 'documentnotfound') { - errorMessage = 'Das ausgewählte Dokument wurde nicht gefunden.'; + errorMessage = t('teamManagement.documentNotFound'); } else if (status === 400 && responseData.error === 'missingleagueid') { - errorMessage = 'Für das ausgewählte Team wurde keine Liga übermittelt.'; + errorMessage = t('teamManagement.missingLeagueForTeam'); } else if (error.code === 'ENOENT' || (typeof error?.message === 'string' && error.message.includes('ENOENT'))) { - errorMessage = 'Die PDF-Datei konnte nicht gefunden werden.'; - details = 'Bitte laden Sie die Datei erneut hoch und versuchen Sie es noch einmal.'; + errorMessage = t('teamManagement.pdfFileNotFound'); + details = t('teamManagement.reuploadFile'); } - await showInfo('Fehler', errorMessage, details, 'error'); + await showInfo(t('messages.error'), errorMessage, details, 'error'); } finally { const { [document.id]: _ignored, ...rest } = parsingDocuments.value; parsingDocuments.value = rest; @@ -883,7 +988,7 @@ export default { const document = documents[0]; // Nehme das erste Dokument const team = teams.value.find(t => t.id === teamId); - pdfDialogTitle.value = `${team?.name || 'Team'} - ${documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste'}`; + pdfDialogTitle.value = `${team?.name || t('teamManagement.team')} - ${documentType === 'code_list' ? t('teamManagement.codeList') : t('teamManagement.pinList')}`; try { // Lade das PDF über die API @@ -897,7 +1002,7 @@ export default { showPDFViewer.value = true; } catch (error) { console.error('Fehler beim Laden des PDFs:', error); - this.showInfo('Fehler', 'Fehler beim Laden des PDFs', '', 'error'); + await showInfo(t('messages.error'), t('teamManagement.errorLoadingPdf'), '', 'error'); } }; @@ -959,8 +1064,8 @@ export default { } } catch (error) { console.error('Fehler beim Parsen der URL:', error); - myTischtennisError.value = getSafeErrorMessage(error, 'URL konnte nicht geparst werden. Bitte überprüfen Sie das Format.'); - await showInfo('Fehler', myTischtennisError.value, '', 'error'); + myTischtennisError.value = getSafeErrorMessage(error, t('teamManagement.errorParsingUrl')); + await showInfo(t('messages.error'), myTischtennisError.value, '', 'error'); } finally { parsingUrl.value = false; } @@ -974,9 +1079,14 @@ export default { // 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.`, + t('teamManagement.configureLeagueTitle'), + t('teamManagement.tableUrlDetected'), + t('teamManagement.configureLeagueDetails', { + association: parsedMyTischtennisData.value.association, + season: parsedMyTischtennisData.value.season, + league: parsedMyTischtennisData.value.groupname, + groupId: parsedMyTischtennisData.value.groupId + }), 'info' ); @@ -989,9 +1099,9 @@ export default { // 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.', + t('teamManagement.selectTeamTitle'), + t('teamManagement.selectTeamFirst'), + t('teamManagement.selectTeamForConfiguration'), 'warning' ); return; @@ -1010,11 +1120,14 @@ export default { }); if (response.data.success) { - myTischtennisSuccess.value = 'Team erfolgreich konfiguriert! Automatischer Datenabruf ist jetzt aktiv.'; + myTischtennisSuccess.value = t('teamManagement.teamConfiguredSuccess'); await showInfo( - 'Erfolg', - 'Team erfolgreich konfiguriert!', - `Liga: ${response.data.data.league.name}\nSaison: ${response.data.data.season.name}\nAutomatischer Datenabruf ist jetzt aktiv.`, + t('messages.success'), + t('teamManagement.teamConfiguredSuccess'), + t('teamManagement.teamConfiguredDetails', { + league: response.data.data.league.name, + season: response.data.data.season.name + }), 'success' ); @@ -1037,8 +1150,8 @@ export default { } } catch (error) { console.error('Fehler bei der Konfiguration:', error); - myTischtennisError.value = getSafeErrorMessage(error, 'Team konnte nicht konfiguriert werden.'); - await showInfo('Fehler', myTischtennisError.value, '', 'error'); + myTischtennisError.value = getSafeErrorMessage(error, t('teamManagement.errorConfiguringTeam')); + await showInfo(t('messages.error'), myTischtennisError.value, '', 'error'); } finally { configuringTeam.value = false; } @@ -1060,11 +1173,16 @@ export default { }); if (response.data.success) { - myTischtennisSuccess.value = 'Liga erfolgreich konfiguriert! Tabellendaten können jetzt automatisch abgerufen werden.'; + myTischtennisSuccess.value = t('teamManagement.leagueConfiguredSuccess'); 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.`, + t('messages.success'), + t('teamManagement.leagueConfiguredSuccess'), + t('teamManagement.leagueConfiguredDetails', { + league: response.data.data.league.name, + season: response.data.data.season?.name || t('teamManagement.notCreated'), + association: response.data.data.league.association, + groupId: response.data.data.league.myTischtennisGroupId + }), 'success' ); @@ -1077,8 +1195,8 @@ export default { } } catch (error) { console.error('Fehler bei der Liga-Konfiguration:', error); - myTischtennisError.value = getSafeErrorMessage(error, 'Liga konnte nicht konfiguriert werden.'); - await showInfo('Fehler', myTischtennisError.value, '', 'error'); + myTischtennisError.value = getSafeErrorMessage(error, t('teamManagement.errorConfiguringLeague')); + await showInfo(t('messages.error'), myTischtennisError.value, '', 'error'); } finally { configuringTeam.value = false; } @@ -1104,28 +1222,28 @@ export default { !!team.league.groupname; const missingItems = []; - if (!hasTeamId) missingItems.push('Team-ID'); - if (!hasLeague) missingItems.push('Liga'); - if (hasLeague && !team.league.myTischtennisGroupId) missingItems.push('Gruppen-ID'); - if (hasLeague && !team.league.association) missingItems.push('Verband'); - if (hasLeague && !team.league.groupname) missingItems.push('Gruppenname'); + if (!hasTeamId) missingItems.push(t('teamManagement.teamId')); + if (!hasLeague) missingItems.push(t('teamManagement.league')); + if (hasLeague && !team.league.myTischtennisGroupId) missingItems.push(t('teamManagement.groupId')); + if (hasLeague && !team.league.association) missingItems.push(t('teamManagement.association')); + if (hasLeague && !team.league.groupName) missingItems.push(t('teamManagement.groupName')); const complete = hasTeamId && hasLeagueConfig; const partial = (hasTeamId || hasLeagueConfig) && !complete; let tooltip = ''; if (complete) { - tooltip = 'Automatischer Datenabruf ist aktiviert'; + tooltip = t('teamManagement.autoFetchEnabled'); } else if (partial) { - tooltip = `Fehlend: ${missingItems.join(', ')}`; + tooltip = t('teamManagement.missingItems', { items: missingItems.join(', ') }); } else { - tooltip = 'MyTischtennis-URL eingeben für automatische Konfiguration'; + tooltip = t('teamManagement.enterUrlForAutoConfig'); } return { complete, partial, - missing: missingItems.length > 0 ? `Fehlt: ${missingItems.join(', ')}` : '', + missing: missingItems.length > 0 ? t('teamManagement.missingItems', { items: missingItems.join(', ') }) : '', tooltip }; }; @@ -1160,7 +1278,7 @@ export default { playerStats.value = response.data; } catch (error) { console.error('Error loading player stats:', error); - await showInfo('Fehler', 'Statistiken konnten nicht geladen werden.', '', 'danger'); + await showInfo(t('messages.error'), t('teamManagement.errorLoadingStats'), '', 'danger'); } finally { loadingStats.value = false; } @@ -1183,7 +1301,7 @@ export default { const jobId = startResponse?.data?.jobId; if (!jobId) { - throw new Error('Async-Job konnte nicht gestartet werden.'); + throw new Error(t('teamManagement.asyncJobStartFailed')); } const maxPollAttempts = 120; // ~4 Minuten bei 2s Intervall @@ -1202,39 +1320,39 @@ export default { } if (job.status === 'failed') { - const failedMsg = getSafeMessage(job.error, 'Daten konnten nicht abgerufen werden.'); + const failedMsg = getSafeMessage(job.error, t('teamManagement.dataFetchFailed')); throw new Error(failedMsg); } } if (!completedResponse) { - throw new Error('Zeitüberschreitung beim Abruf (Async-Job läuft zu lange).'); + throw new Error(t('teamManagement.fetchTimedOut')); } if (completedResponse && completedResponse.success) { - const successMessage = getSafeMessage(completedResponse.message, 'Teamdaten erfolgreich abgerufen.'); + const successMessage = getSafeMessage(completedResponse.message, t('teamManagement.teamDataFetched')); myTischtennisSuccess.value = successMessage; - const teamName = getSafeMessage(completedResponse.data?.teamName, teamToEdit.value?.name || 'Unbekanntes Team'); + const teamName = getSafeMessage(completedResponse.data?.teamName, teamToEdit.value?.name || t('teamManagement.unknownTeam')); const fetchedCount = getSafeMessage(String(completedResponse.data?.fetchedCount ?? ''), '0'); - let detailsMessage = `Team: ${teamName}\nAbgerufene Datensätze: ${fetchedCount}`; + let detailsMessage = t('teamManagement.teamDataFetchedDetails', { team: teamName, count: fetchedCount }); if (completedResponse.data?.tableUpdate) { const tableUpdate = getSafeMessage(completedResponse.data.tableUpdate); if (tableUpdate) { - detailsMessage += `\n\nTabellenaktualisierung:\n${tableUpdate}`; + detailsMessage += `\n\n${t('teamManagement.tableUpdateLabel')}\n${tableUpdate}`; } } await showInfo( - 'Erfolg', + t('messages.success'), successMessage, detailsMessage, 'success' ); } else if (completedResponse && completedResponse.success === false) { - const errorTitle = completedResponse.needsMyTischtennisReauth ? 'Login bei myTischtennis erforderlich' : 'Fehler'; - const errorMessage = getSafeMessage(completedResponse.error, 'Daten konnten nicht abgerufen werden.'); + const errorTitle = completedResponse.needsMyTischtennisReauth ? t('teamManagement.mytischtennisLoginRequired') : t('messages.error'); + const errorMessage = getSafeMessage(completedResponse.error, t('teamManagement.dataFetchFailed')); const details = completedResponse.debug ? getSafeMessage(JSON.stringify(completedResponse.debug, null, 2)) : ''; await showInfo( errorTitle, @@ -1249,11 +1367,11 @@ export default { const isTimeout = error?.code === 'ECONNABORTED' || /Zeitüberschreitung/i.test(String(error?.message || '')); const errData = error?.response?.data || {}; const errorMsg = isTimeout - ? 'Zeitüberschreitung beim Abruf (Timeout).' - : getSafeMessage(error?.message || errData.message || errData.error, 'Daten konnten nicht abgerufen werden.'); + ? t('teamManagement.fetchTimeoutShort') + : getSafeMessage(error?.message || errData.message || errData.error, t('teamManagement.dataFetchFailed')); const details = errData.debug ? getSafeMessage(JSON.stringify(errData.debug, null, 2)) : ''; myTischtennisError.value = errorMsg; - await showInfo('Fehler', errorMsg, details, isTimeout ? 'warning' : 'error'); + await showInfo(t('messages.error'), errorMsg, details, isTimeout ? 'warning' : 'error'); } finally { fetchingTeamData.value = false; } @@ -1293,17 +1411,17 @@ export default { const extension = file.name.includes('.') ? file.name.split('.').pop().toLowerCase() : ''; if (!allowedExtensions.includes(extension)) { - await showInfo('Ungültiger Dateityp', `${label} muss eine der folgenden Endungen haben: ${allowedExtensions.join(', ')}.`, '', 'warning'); + await showInfo(t('teamManagement.invalidFileTypeTitle'), t('teamManagement.invalidFileExtension', { label, extensions: allowedExtensions.join(', ') }), '', 'warning'); return false; } if (file.type && !allowedMimeTypes.includes(file.type)) { - await showInfo('Ungültiger Dateityp', `${label} weist einen unerwarteten MIME-Typ auf: ${file.type}.`, '', 'warning'); + await showInfo(t('teamManagement.invalidFileTypeTitle'), t('teamManagement.invalidMimeType', { label, type: file.type }), '', 'warning'); return false; } if (file.size > maxSize) { - await showInfo('Datei zu groß', `${label} darf maximal 10 MB groß sein.`, '', 'warning'); + await showInfo(t('teamManagement.fileTooLargeTitle'), t('teamManagement.fileTooLarge', { label }), '', 'warning'); return false; } @@ -1326,6 +1444,11 @@ export default { selectedSeasonId, currentSeason, teamDocuments, + teamSearchQuery, + teamFilter, + activeEditorSection, + showGlobalJobDetails, + filteredTeams, parsingInProgress, parsingDocuments, showPDFViewer, @@ -1342,6 +1465,9 @@ export default { loadingStats, filteredLeagues, isSecondHalf, + fullyConfiguredTeamsCount, + partiallyConfiguredTeamsCount, + teamsWithoutLeagueCount, toggleNewTeam, resetToNewTeam, resetNewTeam, @@ -1378,6 +1504,68 @@ export default {