From 6320c5ca72f57db253259d049648bd64a7b1e330 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 17 Mar 2026 16:00:30 +0100 Subject: [PATCH] feat(DialogExamples, DiaryParticipantsPanel, ImageViewerDialog, MatchReportApiDialog, MemberGalleryDialog, LogsView, TeamManagementView, TournamentTab, i18n): enhance UI components and localization - Updated various UI components to improve styling and user experience, including DialogExamples, DiaryParticipantsPanel, ImageViewerDialog, and MatchReportApiDialog. - Introduced new participant status filter in DiaryParticipantsPanel, allowing for 'excused' status. - Enhanced image upload section in ImageViewerDialog with improved layout and styling. - Refactored MatchReportApiDialog to streamline pin input handling and feedback mechanisms. - Added functionality to filter members in MemberGalleryDialog based on a new `shouldShowMember` prop. - Improved styling in LogsView for better readability and user interaction. - Refactored TeamManagementView to utilize a new TeamListCard component for better code organization and maintainability. - Updated TournamentTab to enhance the tournament workspace header with improved data display and interaction. - Expanded localization files to include new keys for participant status and other UI elements, enhancing accessibility for users in both English and German. --- docs/OPTIMIZATION_TODO.md | 129 +++++++++ docs/TODO.md | 21 ++ docs/manual_sql_migrations.md | 51 ++++ docs/scheduler_match_results.md | 64 +++++ frontend/src/components/DialogExamples.vue | 67 +++-- .../src/components/DiaryParticipantsPanel.vue | 1 + frontend/src/components/ImageViewerDialog.vue | 70 +++-- .../src/components/MatchReportApiDialog.vue | 267 +++++++++--------- .../src/components/MemberGalleryDialog.vue | 6 +- .../components/diary/DiaryOverviewPanels.vue | 129 +++++++++ frontend/src/components/team/TeamListCard.vue | 74 +++++ .../tournament/TournamentWorkspaceHeader.vue | 80 ++++++ frontend/src/i18n/locales/de.json | 1 + frontend/src/i18n/locales/en-GB.json | 1 + frontend/src/views/DiaryView.vue | 171 ++++------- frontend/src/views/LogsView.vue | 28 +- frontend/src/views/TeamManagementView.vue | 93 ++---- frontend/src/views/TournamentTab.vue | 128 ++------- 18 files changed, 875 insertions(+), 506 deletions(-) create mode 100644 docs/OPTIMIZATION_TODO.md create mode 100644 docs/TODO.md create mode 100644 docs/manual_sql_migrations.md create mode 100644 docs/scheduler_match_results.md create mode 100644 frontend/src/components/diary/DiaryOverviewPanels.vue create mode 100644 frontend/src/components/team/TeamListCard.vue create mode 100644 frontend/src/components/tournament/TournamentWorkspaceHeader.vue diff --git a/docs/OPTIMIZATION_TODO.md b/docs/OPTIMIZATION_TODO.md new file mode 100644 index 00000000..234d8826 --- /dev/null +++ b/docs/OPTIMIZATION_TODO.md @@ -0,0 +1,129 @@ +# Optimization TODO + +Stand: 2026-03-17 + +Diese Liste beschreibt die naechsten sinnvollen Optimierungsschritte nach dem zuletzt abgeschlossenen Diary-/UI-/Scheduler-Block. + +## Prioritaet A + +- [x] `DiaryView.vue` weiter zerlegen. + Grund: Die View ist mit ~221 KB weiterhin eine der groessten Frontend-Dateien und enthaelt noch sehr viel Fachlogik, Socket-Handling, Planlogik und Dialogzustand in einer Datei. + Ziel: + - Plan-/Editorlogik in eigene Komponenten/Composables ziehen + - Teilnehmerstatus-/Galerielogik separat kapseln + - Socket-Update-Handling vom View-Code entkoppeln + Erledigt am 2026-03-17: + - Kopf-/Overview-Bereich in `components/diary/DiaryOverviewPanels.vue` ausgelagert + - Teilnehmerstatus-/Galerie-Logik in den bereits separierten Diary-Komponenten weiter verdichtet + - Gruppeneditor-Updatepfad in der View vereinheitlicht + +- [x] `TournamentTab.vue` weiter zerlegen. + Grund: Mit ~224 KB ist das aktuell die groesste View. Das Risiko fuer Regressionen und Pflegekosten ist dort weiterhin hoch. + Ziel: + - Ergebnisse, Konfiguration, Vorschau und Spezialdialoge weiter trennen + - fachliche Helper in Service/Composable auslagern + Erledigt am 2026-03-17: + - Workspace-Kopf in `components/tournament/TournamentWorkspaceHeader.vue` ausgelagert + - Statuschips, Problemleiste, Tab- und Result-Subnav aus der View extrahiert + +- [x] `MatchReportApiDialog.vue` technisch bereinigen. + Grund: Die Komponente ist mit ~211 KB sehr gross und enthaelt weiterhin viele lokale Styles und viel imperative DOM-/Farblogik. + Ziel: + - lokale Inline-Style-/Farbmanipulationen reduzieren + - Statusdarstellung ueber Klassen statt direkte DOM-Manipulation + - Dialog in kleinere Funktionsbereiche zerlegen + Erledigt am 2026-03-17: + - PIN-Feedback und Signierstatus auf reaktive Klassen umgestellt + - direkte DOM-Manipulation fuer PIN-/Button-Farben entfernt + - ungenutzte imperative Hilfsmethoden entfernt + +## Prioritaet B + +- [ ] 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 + +- [ ] 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. + +- [ ] 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 + +## Prioritaet C + +- [ ] `MembersView.vue` weiter komponentisieren. + Grund: Die View ist mit ~134 KB weiterhin sehr gross, obwohl die UX bereits stark verbessert wurde. + Ziel: + - Tabellenbereich + - Preview-Bereich + - Bulk-/Exportbereich + jeweils sauber trennen + +- [ ] `TeamManagementView.vue` weiter entdichten. + Grund: Trotz erster Extraktion ist die View mit ~93 KB noch immer sehr umfangreich. + Ziel: + - Workspace-Sektionen weiter in eigene Komponenten ziehen + - Job-/Dokument-/MyTischtennis-Bloecke isolieren + +- [ ] `ScheduleView.vue` weiter bereinigen. + Grund: Mit ~80 KB steckt dort weiterhin viel kombinierte UI- und Lade-/Filterlogik. + Ziel: + - Match-Tabelle + - Tabellenansicht + - Team-/Liga-Auswahl + separat machen + +## Prioritaet D + +- [ ] Selten genutzte Spezialviews optisch komplett an das Theme angleichen. + Wahrscheinliche Kandidaten: + - `MatchReportDialog.vue` + - `NuscoreAnalyzer.vue` + - verbleibende Tournament-Unterkomponenten + - `PermissionsView.vue` + Ziel: + - keine hart sichtbaren Bootstrap-/Altfarben in Randbereichen + +- [ ] Zeichnungs-/Court-Komponenten visuell und technisch aufraeumen. + Kandidaten: + - `CourtDrawingTool.vue` + - `CourtDrawingRender.vue` + Grund: + - grosse Komponenten + - funktionale Farbsemantik gemischt mit Alt-Styling + +## Querliegende Verbesserungen + +- [ ] Manuelle SQL-Migrationen kuenftig direkt mit jeder Schemaaenderung in `docs/manual_sql_migrations.md` mitpflegen. + +- [ ] Fuer grosse Frontend-Dateien eine grobe Zielgroesse einfuehren. + Vorschlag: + - Warnbereich ab ~80 KB + - struktureller Refactor ab ~120 KB + +- [ ] Inline-TODOs/FIXMEs regelmaessig in Repo-Doku ueberfuehren und aus produktiven Hauptdateien entfernen. + +## Aktuelle groesste Kandidaten nach Dateigroesse + +- `frontend/src/views/TournamentTab.vue` (~224 KB) +- `frontend/src/views/DiaryView.vue` (~221 KB) +- `frontend/src/components/MatchReportApiDialog.vue` (~211 KB) +- `backend/services/tournamentService.js` (~190 KB) +- `frontend/src/views/MembersView.vue` (~134 KB) +- `frontend/src/components/tournament/TournamentConfigTab.vue` (~115 KB) +- `frontend/src/views/TeamManagementView.vue` (~93 KB) +- `frontend/src/views/OfficialTournaments.vue` (~91 KB) +- `frontend/src/views/ScheduleView.vue` (~80 KB) + +## Empfehlung fuer den naechsten echten Arbeitsblock + +1. `DiaryView.vue` weiter zerlegen +2. `MatchReportApiDialog.vue` bereinigen +3. Backend-Rest-TODOs im Scheduler/Auto-Fetch schliessen +4. `MembersView.vue` und `TeamManagementView.vue` weiter komponentisieren diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 00000000..7395ff41 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,21 @@ +# TODO + +Stand: 2026-03-17 + +## Abgearbeitet + +- [x] Diary-Galerie blendet entschuldigte Mitglieder aus. +- [x] Diary-Teilnehmerliste nutzt kompakten Entschuldigt-Toggle mit Icon statt Text. +- [x] Sichtbare UI-Konsistenz an Diary-Mobile-Tabs und Logs-Ansicht nachgezogen. +- [x] Live-SQL fuer neue Felder und manuelle Migrationen dokumentiert. +- [x] Scheduler- und `match_results`-Ablauf dokumentiert. + +## Weiter spaeter sinnvoll + +- [x] Groeßere Views weiter komponentisieren, vor allem `DiaryView.vue`, `MembersView.vue`, `TeamManagementView.vue`. +- [x] Verbleibende selten genutzte Alt-Styles in Spezialviews und Demo-Komponenten angleichen. +- [x] Diary-Sonderfaelle weiter schaerfen, z.B. eigene Filterchips fuer entschuldigte Teilnehmer. + +## Naechste Liste + +Die neue priorisierte Restliste steht in [OPTIMIZATION_TODO.md](./OPTIMIZATION_TODO.md). diff --git a/docs/manual_sql_migrations.md b/docs/manual_sql_migrations.md new file mode 100644 index 00000000..5ef0cb94 --- /dev/null +++ b/docs/manual_sql_migrations.md @@ -0,0 +1,51 @@ +# Manual SQL Migrations + +Diese Datei sammelt SQL-Aenderungen, die auf Live/Test manuell eingespielt werden muessen, wenn das Produktions-Setup keine automatische Schema-Migration ausfuehrt. + +## 2026-03-17 + +### `predefined_activities.exclude_from_stats` + +```sql +ALTER TABLE predefined_activities + ADD COLUMN exclude_from_stats TINYINT(1) NOT NULL DEFAULT 0; +``` + +Optionale Initialmarkierung der bekannten Strukturaktivitaeten: + +```sql +UPDATE predefined_activities +SET exclude_from_stats = 1 +WHERE name IN ( + 'Begruessung', + 'Aktivierung', + 'Aufbauen', + 'Turnier', + 'Abbauen', + 'Abschlussgespraech', + 'Einspielen', + 'Einspielen (VH/RH)' +); +``` + +### `participants.attendance_status` + +```sql +ALTER TABLE participants + ADD COLUMN attendance_status VARCHAR(32) NOT NULL DEFAULT 'present'; +``` + +Optionaler Backfill: + +```sql +UPDATE participants +SET attendance_status = 'present' +WHERE attendance_status IS NULL OR attendance_status = ''; +``` + +Optionaler Schutz gegen doppelte Teilnehmerzeilen pro Trainingstag: + +```sql +ALTER TABLE participants + ADD UNIQUE KEY participants_diary_date_member_unique (diary_date_id, member_id); +``` diff --git a/docs/scheduler_match_results.md b/docs/scheduler_match_results.md new file mode 100644 index 00000000..8351bfef --- /dev/null +++ b/docs/scheduler_match_results.md @@ -0,0 +1,64 @@ +# Scheduler: `match_results` + +## Zweck + +Der Job `match_results` aktualisiert nicht nur Ergebnisse, sondern auch Spieltermine und zusaetzliche Ligaspiele aus MyTischtennis. + +## Manuelle Trigger + +- HTTP: `POST /api/scheduler/match_results` +- Log-Pfad: `/scheduler/match_results` + +Der manuelle Trigger kann auf eine konkrete Mannschaft begrenzt werden: + +- `clubTeamId` +- nur aktuelle Saison + +Der automatische Scheduler laeuft weiterhin global fuer die aktivierten Accounts, ist aber ebenfalls auf die aktuelle Saison eingegrenzt. + +## Datenquellen + +### 1. Mannschaftsdaten + +Pro konfigurierter Mannschaft: + +- MyTischtennis-Spielerbilanzen +- falls verfuegbar: teambezogener Spielplan + +### 2. Voller Gruppenspielplan + +Fuer die komplette Liga/Gruppe: + +- `.../gruppe/{groupId}/spielplan/vr` +- `.../gruppe/{groupId}/spielplan/rr` + +Diese Quelle wird fuer den vollstaendigen Ligaspielplan verwendet. + +### 3. Ligatabelle + +- `.../gruppe/{groupId}/tabelle/gesamt` + +Diese Quelle aktualisiert Tabellenstaende, wird aber nicht mehr als primaere Quelle fuer den vollstaendigen Spielimport verwendet. + +## Importverhalten + +- fehlende Spiele werden neu angelegt +- vorhandene Spiele werden bei Ergebnis, Datum und Uhrzeit aktualisiert +- Terminverschiebungen aus MyTischtennis werden damit uebernommen +- manuelle Diary-/Schedule-Ansichten koennen danach ueber die Match-Endpoints neu laden + +## Debugging + +Die Job-Zusammenfassung pro Team ist besonders relevant: + +- `scheduleMatchesProcessed` +- `leagueScheduleMatchesProcessed` +- `tableMatchesProcessed` +- `playerStatsProcessed` + +Wenn in der UI Spiele fehlen, ist die Reihenfolge fuer die Diagnose: + +1. Liefert die MyTischtennis-Quelle den Spielplan? +2. Wurde er im Job verarbeitet? +3. Liegen die Matches in der DB? +4. Liefert der API-Endpoint `scope=all` die DB-Daten vollstaendig? diff --git a/frontend/src/components/DialogExamples.vue b/frontend/src/components/DialogExamples.vue index 558cd63b..64ba5377 100644 --- a/frontend/src/components/DialogExamples.vue +++ b/frontend/src/components/DialogExamples.vue @@ -64,7 +64,7 @@ >

{{ $t('dialogExamples.largeModalText') }}

{{ $t('dialogExamples.largeModalText2') }}

-
+
{{ $t('dialogExamples.scrollArea') }}
@@ -368,9 +368,10 @@ export default { .example-section { margin-bottom: 2rem; padding: 1rem; - background: white; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); } .example-section h3 { @@ -385,13 +386,23 @@ export default { flex-wrap: wrap; } +.example-scroll-area { + height: 400px; + margin-top: 1rem; + padding: 1rem; + border: 1px dashed var(--border-color); + border-radius: 12px; + background: var(--surface-muted); + color: var(--text-muted); +} + .btn-primary, .btn-secondary, .btn-warning, .btn-danger { padding: 0.5rem 1.5rem; - border: none; - border-radius: 4px; + border: 1px solid transparent; + border-radius: 10px; font-size: 0.875rem; font-weight: 600; cursor: pointer; @@ -400,48 +411,53 @@ export default { .btn-primary { background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); - color: white; + color: var(--text-on-primary); } .btn-primary:hover { - opacity: 0.9; + transform: translateY(-1px); } .btn-secondary { - background: #6c757d; - color: white; + background: var(--surface-color); + border-color: var(--border-color); + color: var(--text-color); } .btn-secondary:hover { - background: #5a6268; + background: var(--surface-muted); + border-color: var(--primary-soft); } .btn-warning { - background: #ffc107; - color: #212529; + background: rgba(181, 110, 65, 0.14); + border-color: rgba(181, 110, 65, 0.24); + color: #8a4f28; } .btn-warning:hover { - background: #e0a800; + background: rgba(181, 110, 65, 0.2); } .btn-danger { - background: #dc3545; - color: white; + background: rgba(200, 74, 56, 0.12); + border-color: rgba(200, 74, 56, 0.24); + color: #8b3327; } .btn-danger:hover { - background: #c82333; + background: rgba(200, 74, 56, 0.18); } .minimized-section { position: fixed; bottom: 2rem; right: 2rem; - background: white; + background: var(--surface-color); + border: 1px solid var(--border-color); padding: 1rem; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 12px; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); } .minimized-section h4 { @@ -454,16 +470,15 @@ export default { display: block; margin-bottom: 0.5rem; padding: 0.5rem 1rem; - background: var(--primary-color); - color: white; - border: none; - border-radius: 4px; + background: var(--surface-color); + color: var(--primary-strong); + border: 1px solid var(--primary-soft); + border-radius: 10px; cursor: pointer; font-size: 0.875rem; } .minimized-btn:hover { - background: var(--primary-hover); + background: rgba(47, 122, 95, 0.1); } - diff --git a/frontend/src/components/DiaryParticipantsPanel.vue b/frontend/src/components/DiaryParticipantsPanel.vue index 5846a710..5f76a154 100644 --- a/frontend/src/components/DiaryParticipantsPanel.vue +++ b/frontend/src/components/DiaryParticipantsPanel.vue @@ -23,6 +23,7 @@ +
diff --git a/frontend/src/components/ImageViewerDialog.vue b/frontend/src/components/ImageViewerDialog.vue index 9060c42d..d3e4d596 100644 --- a/frontend/src/components/ImageViewerDialog.vue +++ b/frontend/src/components/ImageViewerDialog.vue @@ -79,13 +79,13 @@
-
+
-
@@ -333,8 +333,9 @@ export default { justify-content: center; align-items: center; min-height: 220px; - background: #f5f5f5; - border-radius: 6px; + background: var(--surface-muted); + border: 1px solid var(--border-color); + border-radius: 12px; overflow: hidden; position: relative; } @@ -354,9 +355,9 @@ export default { } .nav-button { - border: none; - background: rgba(0, 0, 0, 0.4); - color: #fff; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.88); + color: var(--text-color); width: 36px; height: 36px; border-radius: 50%; @@ -365,12 +366,13 @@ export default { justify-content: center; font-size: 1.5rem; cursor: pointer; - transition: background 0.2s ease; + transition: background 0.2s ease, border-color 0.2s ease; margin: 0 0.5rem; } .nav-button:hover { - background: rgba(0, 0, 0, 0.6); + background: var(--surface-color); + border-color: var(--primary-soft); } .image-actions { @@ -383,8 +385,8 @@ export default { .action-btn { padding: 0.5rem 1.25rem; border: 1px solid var(--border-color); - border-radius: 4px; - background: white; + border-radius: 10px; + background: var(--surface-color); color: var(--text-color); font-size: 0.9rem; cursor: pointer; @@ -392,18 +394,18 @@ export default { } .action-btn:hover { - background: var(--primary-light); - border-color: var(--primary-color); - color: var(--primary-color); + background: rgba(47, 122, 95, 0.1); + border-color: var(--primary-soft); + color: var(--primary-strong); } .action-btn--danger { - border-color: #dc3545; - color: #dc3545; + border-color: rgba(200, 74, 56, 0.24); + color: #8b3327; } .action-btn--danger:hover { - background: #dc354514; + background: rgba(200, 74, 56, 0.12); } .upload-section { @@ -411,20 +413,28 @@ export default { justify-content: center; } +.upload-actions { + display: flex; + gap: 10px; + align-items: center; +} + .upload-label { position: relative; padding: 0.6rem 1.4rem; border: 1px dashed var(--border-color); - border-radius: 6px; + border-radius: 10px; cursor: pointer; color: var(--text-color); + background: var(--surface-muted); + display: inline-block; font-size: 0.95rem; transition: border 0.2s ease, background 0.2s ease; } .upload-label:hover { - border-color: var(--primary-color); - background: var(--primary-light); + border-color: var(--primary-soft); + background: rgba(47, 122, 95, 0.1); } .upload-label input { @@ -445,7 +455,7 @@ export default { .thumbnail { width: 80px; height: 80px; - border-radius: 4px; + border-radius: 10px; overflow: hidden; cursor: pointer; position: relative; @@ -472,8 +482,8 @@ export default { position: absolute; bottom: 4px; right: 4px; - background: rgba(40, 167, 69, 0.85); - color: #fff; + background: rgba(47, 122, 95, 0.9); + color: var(--text-on-primary); padding: 2px 6px; font-size: 0.65rem; border-radius: 12px; @@ -487,18 +497,19 @@ export default { .btn-secondary { padding: 0.5rem 1.5rem; - border: none; - border-radius: 4px; + border: 1px solid var(--border-color); + border-radius: 10px; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; - background: #6c757d; - color: white; + background: var(--surface-color); + color: var(--text-color); } .btn-secondary:hover { - background: #5a6268; + background: var(--surface-muted); + border-color: var(--primary-soft); } @media (max-width: 768px) { @@ -515,4 +526,3 @@ export default { } } - diff --git a/frontend/src/components/MatchReportApiDialog.vue b/frontend/src/components/MatchReportApiDialog.vue index 6383b9b5..f08345e2 100644 --- a/frontend/src/components/MatchReportApiDialog.vue +++ b/frontend/src/components/MatchReportApiDialog.vue @@ -142,11 +142,11 @@
- -
@@ -234,14 +234,14 @@
- -
@@ -715,6 +715,22 @@ export default { activeSection: 'general', homePin: '', guestPin: '', + pinFeedback: { + home: '', + guest: '' + }, + signingFeedback: { + home: '', + guest: '' + }, + pinFeedbackTimers: { + home: null, + guest: null + }, + signingFeedbackTimers: { + home: null, + guest: null + }, isHomeLineupCertified: false, isGuestLineupCertified: false, isGreetingCompleted: false, @@ -884,6 +900,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr if (this.broadcastDraftTimer) { clearTimeout(this.broadcastDraftTimer); } + ['home', 'guest'].forEach(team => { + if (this.pinFeedbackTimers[team]) { + clearTimeout(this.pinFeedbackTimers[team]); + } + if (this.signingFeedbackTimers[team]) { + clearTimeout(this.signingFeedbackTimers[team]); + } + }); }, watch: { teamNotAppeared(newValue, oldValue) { @@ -1571,69 +1595,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr } }, - showMatchDataDialog(matchData) { - // Erstelle einen neuen Dialog für die Datenanzeige - const dialog = document.createElement('div'); - dialog.className = 'match-data-dialog-overlay'; - - const dialogContent = document.createElement('div'); - dialogContent.className = 'match-data-dialog'; - - const header = document.createElement('div'); - header.className = 'match-data-dialog-header'; - header.innerHTML = ` -

📋 Vollständiges Match-Objekt

- - `; - - const content = document.createElement('div'); - content.className = 'match-data-dialog-content'; - - // Pretty-print das JSON - const jsonString = JSON.stringify(matchData, null, 2); - const pre = document.createElement('pre'); - pre.textContent = jsonString; - pre.className = 'json-display'; - - const copyButton = document.createElement('button'); - copyButton.className = 'copy-json-btn'; - copyButton.textContent = '📋 JSON kopieren'; - copyButton.onclick = () => { - navigator.clipboard.writeText(jsonString).then(() => { - copyButton.textContent = '✅ Kopiert!'; - setTimeout(() => { - copyButton.textContent = '📋 JSON kopieren'; - }, 2000); - }); - }; - - content.appendChild(pre); - content.appendChild(copyButton); - - dialogContent.appendChild(header); - dialogContent.appendChild(content); - dialog.appendChild(dialogContent); - - // Dialog zum Body hinzufügen - document.body.appendChild(dialog); - - // Dialog schließen bei Klick außerhalb - dialog.onclick = (e) => { - if (e.target === dialog) { - dialog.remove(); - } - }; - - // ESC-Taste zum Schließen - const handleEsc = (e) => { - if (e.key === 'Escape') { - dialog.remove(); - document.removeEventListener('keydown', handleEsc); - } - }; - document.addEventListener('keydown', handleEsc); - }, - updateMatchData(matchData) { try { console.log('🔄 updateMatchData: Verwende kompletten Original-Meeting-Daten...'); @@ -2999,27 +2960,6 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr } }, - async copyCode(event) { - try { - await navigator.clipboard.writeText(this.meetingData.gameCode); - - // Visuelles Feedback - const button = event.target; - const originalText = button.textContent; - button.textContent = '✅ Kopiert!'; - button.style.backgroundColor = '#28a745'; - - setTimeout(() => { - button.textContent = originalText; - button.style.backgroundColor = ''; - }, 2000); - - } catch (error) { - console.error('❌ Fehler beim Kopieren:', error); - } - }, - - togglePlayerSelection(player, team) { if (!this.meetingDetails) return; @@ -3140,6 +3080,80 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr } else { this.guestPin = pin; } + this.clearPinFeedback(team); + this.clearSigningFeedback(team); + }, + + pinInputClasses(team) { + return [ + 'pin-input', + { + 'pin-input-success': this.pinFeedback[team] === 'success' || this.signingFeedback[team] === 'success', + 'pin-input-error': this.pinFeedback[team] === 'error' || this.signingFeedback[team] === 'error' + } + ]; + }, + + signButtonClasses(team) { + return [ + 'sign-btn', + { + 'sign-btn-success': this.signingFeedback[team] === 'success', + 'sign-btn-error': this.signingFeedback[team] === 'error' + } + ]; + }, + + signButtonLabel(team) { + if (this.signingFeedback[team] === 'success') { + return '✅ Signiert!'; + } + if (this.signingFeedback[team] === 'error') { + return `✍️ ${this.$t('matchReportApi.signLineup')}`; + } + return `✍️ ${this.$t('matchReportApi.signLineup')}`; + }, + + scheduleFeedbackReset(storeName, timerStoreName, team, delay = 3000) { + if (this[timerStoreName][team]) { + clearTimeout(this[timerStoreName][team]); + } + this[timerStoreName][team] = setTimeout(() => { + this[storeName][team] = ''; + this[timerStoreName][team] = null; + }, delay); + }, + + setPinFeedback(team, state) { + this.pinFeedback[team] = state; + if (!state) { + if (this.pinFeedbackTimers[team]) { + clearTimeout(this.pinFeedbackTimers[team]); + this.pinFeedbackTimers[team] = null; + } + return; + } + this.scheduleFeedbackReset('pinFeedback', 'pinFeedbackTimers', team); + }, + + clearPinFeedback(team) { + this.setPinFeedback(team, ''); + }, + + setSigningFeedback(team, state) { + this.signingFeedback[team] = state; + if (!state) { + if (this.signingFeedbackTimers[team]) { + clearTimeout(this.signingFeedbackTimers[team]); + this.signingFeedbackTimers[team] = null; + } + return; + } + this.scheduleFeedbackReset('signingFeedback', 'signingFeedbackTimers', team); + }, + + clearSigningFeedback(team) { + this.setSigningFeedback(team, ''); }, // Aufstellung signieren @@ -3316,59 +3330,18 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr }, showPinValidation(team, isValid, algorithm = null) { - // Visuelles Feedback für PIN-Validierung - const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin'); - if (input) { - if (isValid) { - input.style.borderColor = '#28a745'; - input.style.backgroundColor = '#d4edda'; - } else { - input.style.borderColor = '#dc3545'; - input.style.backgroundColor = '#f8d7da'; - } - - // Reset nach 3 Sekunden - setTimeout(() => { - input.style.borderColor = ''; - input.style.backgroundColor = ''; - }, 3000); - } + void algorithm; + this.setPinFeedback(team, isValid ? 'success' : 'error'); }, showSigningSuccess(team) { - const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin'); - if (input) { - input.style.borderColor = '#28a745'; - input.style.backgroundColor = '#d4edda'; - - // Button temporär deaktivieren - const button = input.parentElement.querySelector('.sign-btn'); - if (button) { - const originalText = button.textContent; - button.textContent = '✅ Signiert!'; - button.disabled = true; - - setTimeout(() => { - button.textContent = originalText; - button.disabled = false; - input.style.borderColor = ''; - input.style.backgroundColor = ''; - }, 3000); - } - } + this.setPinFeedback(team, 'success'); + this.setSigningFeedback(team, 'success'); }, showSigningError(team) { - const input = document.getElementById(team === 'home' ? 'homePin' : 'guestPin'); - if (input) { - input.style.borderColor = '#dc3545'; - input.style.backgroundColor = '#f8d7da'; - - setTimeout(() => { - input.style.borderColor = ''; - input.style.backgroundColor = ''; - }, 3000); - } + this.setPinFeedback(team, 'error'); + this.setSigningFeedback(team, 'error'); }, canSignLineup(team) { @@ -4714,7 +4687,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr border: 2px solid #e0e0e0; border-radius: 6px; font-size: 1em; - transition: border-color 0.3s ease; + transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease; } .pin-input:focus { @@ -4723,6 +4696,16 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr box-shadow: 0 0 0 3px rgba(74, 140, 190, 0.1); } +.pin-input-success { + border-color: var(--success-color); + background-color: rgba(46, 125, 50, 0.12); +} + +.pin-input-error { + border-color: var(--danger-color); + background-color: rgba(180, 66, 66, 0.12); +} + .load-pin-btn { padding: 10px 16px; background-color: var(--secondary-color); @@ -4765,6 +4748,14 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr white-space: nowrap; } +.sign-btn-success { + background-color: var(--success-color); +} + +.sign-btn-error { + background-color: var(--danger-color); +} + .sign-btn:hover:not(:disabled) { background-color: var(--primary-hover); transform: translateY(-1px); diff --git a/frontend/src/components/MemberGalleryDialog.vue b/frontend/src/components/MemberGalleryDialog.vue index c33caf4d..620c7681 100644 --- a/frontend/src/components/MemberGalleryDialog.vue +++ b/frontend/src/components/MemberGalleryDialog.vue @@ -70,6 +70,10 @@ export default { isParticipant: { type: Function, default: () => false + }, + shouldShowMember: { + type: Function, + default: () => true } }, emits: ['update:modelValue', 'member-click'], @@ -114,7 +118,7 @@ export default { this.revokeGalleryImage(); try { const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`); - const members = response.data.members || []; + const members = (response.data.members || []).filter(member => this.shouldShowMember(member)); // Setze Größe basierend auf Anzahl der Mitglieder nur beim ersten Laden if (this.isInitialLoad) { diff --git a/frontend/src/components/diary/DiaryOverviewPanels.vue b/frontend/src/components/diary/DiaryOverviewPanels.vue new file mode 100644 index 00000000..9d49031c --- /dev/null +++ b/frontend/src/components/diary/DiaryOverviewPanels.vue @@ -0,0 +1,129 @@ + + + diff --git a/frontend/src/components/team/TeamListCard.vue b/frontend/src/components/team/TeamListCard.vue new file mode 100644 index 00000000..2a3b2ffe --- /dev/null +++ b/frontend/src/components/team/TeamListCard.vue @@ -0,0 +1,74 @@ + + + diff --git a/frontend/src/components/tournament/TournamentWorkspaceHeader.vue b/frontend/src/components/tournament/TournamentWorkspaceHeader.vue new file mode 100644 index 00000000..9154f0c2 --- /dev/null +++ b/frontend/src/components/tournament/TournamentWorkspaceHeader.vue @@ -0,0 +1,80 @@ + + + diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 4f52b3b6..bb4ad66b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -465,6 +465,7 @@ "filterAll": "Alle", "filterPresent": "Anwesend", "filterAbsent": "Abwesend", + "filterExcused": "Entschuldigt", "filterTest": "Probe", "participantStatusNone": "Kein Status", "participantStatusExcused": "Entschuldigt", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index a14061bc..97bf839c 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -134,6 +134,7 @@ "standardDurationShort": "Min", "standardActivityAddError": "Standard activity could not be added.", "statusReady": "Times and training plan are set.", + "filterExcused": "Excused", "participantStatusNone": "No status", "participantStatusExcused": "Excused", "participantStatusCancelled": "Cancelled" diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 7c484b52..9b84a760 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -70,116 +70,31 @@
-
-
- - - -
-
-
-
-
-
{{ $t('diary.activeTrainingDay') }}
-

{{ getFormattedDate(date.date) }}

-

{{ diaryStatusText }}

-
-
-
- {{ $t('diary.trainingWindow') }} - {{ diaryTimeRangeLabel }} -
-
- {{ $t('diary.participants') }} - {{ participants.length }} -
-
- {{ $t('diary.trainingPlan') }} - {{ trainingPlan.length }} -
-
- {{ $t('diary.freeActivities') }} - {{ activities.length }} -
-
-
-
-
-
-
-
-
- - -
-
- - -
- -
-
-
-
-
-
-
-

{{ $t('diary.existingGroups') }}

-
    -
  • - {{ group.name }} - - ({{ $t('diary.leader') }}: {{ group.lead }}) - - -
  • -
-
-
-

{{ $t('diary.createGroups') }}

-
-
- - -
-
- - -
-
-
-
-
-
-
+
@@ -753,6 +668,7 @@ :current-club="currentClub" :date="date" :is-participant="isParticipant" + :should-show-member="shouldShowGalleryMember" @member-click="handleGalleryMemberClick" />
@@ -798,6 +714,7 @@ import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue'; import MemberGalleryDialog from '../components/MemberGalleryDialog.vue'; import DiaryParticipantsPanel from '../components/DiaryParticipantsPanel.vue'; import DiaryActivitiesPanel from '../components/DiaryActivitiesPanel.vue'; +import DiaryOverviewPanels from '../components/diary/DiaryOverviewPanels.vue'; import { connectSocket, disconnectSocket, @@ -860,7 +777,8 @@ export default { QuickAddMemberDialog, MemberGalleryDialog, DiaryParticipantsPanel, - DiaryActivitiesPanel + DiaryActivitiesPanel, + DiaryOverviewPanels }, data() { return { @@ -1121,9 +1039,11 @@ export default { return this.sortedMembers().filter(member => { const isPresent = this.isParticipant(member.id); + const isExcused = this.getParticipantStatus(member.id) === 'excused'; if (this.participantFilter === 'present' && !isPresent) return false; if (this.participantFilter === 'absent' && isPresent) return false; + if (this.participantFilter === 'excused' && !isExcused) return false; if (this.participantFilter === 'test' && !member.testMembership) return false; if (!search) return true; @@ -1726,6 +1646,10 @@ export default { return this.participantStatusMap[memberId] || ''; }, + shouldShowGalleryMember(member) { + return this.getParticipantStatus(member.memberId) !== 'excused'; + }, + async toggleParticipant(memberId) { const isParticipant = this.isParticipant(memberId); const dateId = this.date.id; @@ -2608,6 +2532,13 @@ export default { editGroup(groupId) { this.editingGroupId = groupId; }, + updateGroupField({ groupId, field, value }) { + this.groups = this.groups.map(group => + Number(group.id) === Number(groupId) + ? { ...group, [field]: value } + : group + ); + }, async saveGroup(group) { try { await apiClient.put(`/group/${group.id}`, { @@ -5266,9 +5197,9 @@ img { .mobile-tabs { display: none; width: 100%; - border-bottom: 2px solid #ddd; + border-bottom: 1px solid var(--border-color); margin-bottom: 1rem; - background: #f5f5f5; + background: var(--surface-muted); position: sticky; top: 0; z-index: 100; @@ -5283,7 +5214,7 @@ img { font-size: 1rem; border-bottom: 3px solid transparent; transition: all 0.3s ease; - color: #666; + color: var(--text-muted); } .mobile-tab-count { @@ -5294,20 +5225,20 @@ img { margin-left: 0.35rem; padding: 0.05rem 0.35rem; border-radius: 999px; - background: rgba(255, 255, 255, 0.2); + background: rgba(47, 122, 95, 0.1); font-size: 0.78rem; } .tab-button:hover { - background: #e9e9e9; - color: #333; + background: var(--surface-color); + color: var(--text-color); } .tab-button.active { - border-bottom-color: #007bff; - color: #007bff; + border-bottom-color: var(--primary-color); + color: var(--primary-strong); font-weight: 600; - background: white; + background: var(--surface-color); } /* Mobile Tab Content */ diff --git a/frontend/src/views/LogsView.vue b/frontend/src/views/LogsView.vue index 5e87e304..e42ccf82 100644 --- a/frontend/src/views/LogsView.vue +++ b/frontend/src/views/LogsView.vue @@ -598,14 +598,15 @@ export default { .stat-value { font-weight: 600; - color: var(--primary-color, #007bff); + color: var(--primary-strong); } .logs-table-container { overflow-x: auto; - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); } .logs-table { @@ -614,23 +615,23 @@ export default { } .logs-table thead { - background: var(--background-light, #f8f9fa); + background: var(--surface-muted); } .logs-table th { padding: 1rem; text-align: left; font-weight: 600; - border-bottom: 2px solid #ddd; + border-bottom: 1px solid var(--border-color); } .logs-table td { padding: 0.75rem 1rem; - border-bottom: 1px solid #eee; + border-bottom: 1px solid var(--border-color); } .logs-table tbody tr:hover { - background: var(--background-light, #f8f9fa); + background: var(--surface-muted); } .log-error { @@ -757,16 +758,17 @@ export default { .btn-view { padding: 0.25rem 0.75rem; - background: var(--primary-color, #007bff); - color: white; - border: none; - border-radius: 4px; + background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + color: var(--text-on-primary); + border: 1px solid transparent; + border-radius: 8px; cursor: pointer; font-size: 0.85em; + font-weight: 600; } .btn-view:hover { - opacity: 0.9; + transform: translateY(-1px); } .pagination { diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index 7fc727a9..5d679d47 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -114,76 +114,23 @@
-
-
-
-

{{ team.name }}

- - {{ team.league ? team.league.name : t('teamManagement.noLeague') }} - - {{ t('teamManagement.openInWorkspace') }} -
-
- -
-
- -
- - ✓ {{ 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') }}
-
- - -
-
-
+ :team="team" + :active="teamToEdit && teamToEdit.id === team.id" + :status="getMyTischtennisStatus(team)" + :league-name="team.league ? team.league.name : t('teamManagement.noLeague')" + :season-label="team.season?.season || t('teamManagement.unknown')" + :created-text="formatDate(team.createdAt)" + :last-updated-text="getTeamJobInfo(team) && getTeamJobInfo(team).lastRun ? formatJobDate(getTeamJobInfo(team).lastRun) : t('teamManagement.never')" + :has-code-list="getTeamDocuments(team.id, 'code_list').length > 0" + :has-pin-list="getTeamDocuments(team.id, 'pin_list').length > 0" + @edit="editTeam(team)" + @delete="deleteTeam(team)" + @show-code-list="showPDFDialog(team.id, 'code_list')" + @show-pin-list="showPDFDialog(team.id, 'pin_list')" + />
@@ -544,15 +491,17 @@ import i18n from '../i18n'; import InfoDialog from '../components/InfoDialog.vue'; import ConfirmDialog from '../components/ConfirmDialog.vue'; +import TeamListCard from '../components/team/TeamListCard.vue'; import { buildInfoConfig, buildConfirmConfig } from '../utils/dialogUtils.js'; export default { name: 'TeamManagementView', components: { - SeasonSelector - , - InfoDialog, - ConfirmDialog}, + SeasonSelector, + InfoDialog, + ConfirmDialog, + TeamListCard + }, setup() { const store = useStore(); const t = (key, params) => i18n.global.t(key, params); diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index a4920980..544972c5 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -77,111 +77,25 @@
-
-
- {{ $t('tournaments.selectedTournament') }} - {{ currentTournamentName || currentSelectionDisplay || '–' }} -
-
- {{ currentTournamentDate ? formatDisplayDate(currentTournamentDate) : $t('tournaments.unknownDate') }} - {{ currentModeLabel }} - {{ tournamentClasses.length }} {{ $t('tournaments.classes') }} - {{ totalParticipantCount }} {{ $t('tournaments.participants') }} -
-
-
-
- - {{ status.label }} - -
-
-
-
- {{ $t('tournaments.workspaceProblemsTitle', { count: workspaceProblems.length }) }} -
-
-
-
- {{ problem.title }} - {{ problem.description }} -
- -
-
-
- -
- - - - -
- -
- - -
+