From 8bd05e4e38b20f686cf708d32207bf100d099e1d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 22 Aug 2025 15:47:16 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCgt=20Unterst=C3=BCtzung=20f=C3=BCr=20par?= =?UTF-8?q?allele=20Entwicklungsumgebungen=20hinzu=20und=20aktualisiert=20?= =?UTF-8?q?die=20Benutzeroberfl=C3=A4che.=20Neue=20Routen=20und=20Komponen?= =?UTF-8?q?ten=20f=C3=BCr=20Trainingsstatistiken=20implementiert.=20Fehler?= =?UTF-8?q?behebungen=20und=20Verbesserungen=20in=20der=20Benutzeroberfl?= =?UTF-8?q?=C3=A4che=20vorgenommen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README_CLEANUP.md | 139 +++++ backend/checkRealIndexes.sql | 46 ++ backend/checkTableNames.sql | 35 ++ backend/cleanupKeys.sql | 185 ++++++ backend/cleanupKeysAggressive.sql | 123 ++++ backend/cleanupKeysCorrected.sql | 152 +++++ backend/cleanupKeysFinal.sql | 100 ++++ backend/cleanupKeysIntelligent.sql | 125 ++++ backend/cleanupKeysMinimal.sql | 79 +++ backend/cleanupKeysNode.cjs | 143 +++++ backend/cleanupKeysNode.js | 166 ++++++ backend/cleanupKeysReal.sql | 149 +++++ backend/cleanupKeysSimple.sql | 41 ++ backend/cleanupKeysSmart.sql | 153 +++++ .../controllers/trainingStatsController.js | 121 ++++ backend/models/index.js | 6 + backend/routes/trainingStatsRoutes.js | 10 + backend/server.js | 3 + frontend/DESIGN_GUIDE.md | 309 ++++++++++ frontend/index.html | 1 - frontend/src/App.vue | 533 ++++++++++++----- frontend/src/assets/css/components.scss | 500 ++++++++++++++++ frontend/src/assets/css/main.scss | 556 ++++++++++++++++-- frontend/src/components/PDFGenerator.js | 1 - frontend/src/router.js | 2 + frontend/src/store.js | 3 +- frontend/src/style.css | 273 +++++++-- frontend/src/views/Activate.vue | 3 +- frontend/src/views/ClubView.vue | 5 +- frontend/src/views/DiaryView.vue | 46 +- frontend/src/views/Home.vue | 274 ++++++++- frontend/src/views/Login.vue | 3 +- frontend/src/views/MembersView.vue | 26 +- frontend/src/views/PendingApprovalsView.vue | 6 +- frontend/src/views/Register.vue | 3 +- frontend/src/views/ScheduleView.vue | 10 +- frontend/src/views/TournamentsView.vue | 23 +- frontend/src/views/TrainingStatsView.vue | 427 ++++++++++++++ package-lock.json | 232 ++++++++ package.json | 4 + 40 files changed, 4670 insertions(+), 346 deletions(-) create mode 100644 backend/README_CLEANUP.md create mode 100644 backend/checkRealIndexes.sql create mode 100644 backend/checkTableNames.sql create mode 100644 backend/cleanupKeys.sql create mode 100644 backend/cleanupKeysAggressive.sql create mode 100644 backend/cleanupKeysCorrected.sql create mode 100644 backend/cleanupKeysFinal.sql create mode 100644 backend/cleanupKeysIntelligent.sql create mode 100644 backend/cleanupKeysMinimal.sql create mode 100644 backend/cleanupKeysNode.cjs create mode 100644 backend/cleanupKeysNode.js create mode 100644 backend/cleanupKeysReal.sql create mode 100644 backend/cleanupKeysSimple.sql create mode 100644 backend/cleanupKeysSmart.sql create mode 100644 backend/controllers/trainingStatsController.js create mode 100644 backend/routes/trainingStatsRoutes.js create mode 100644 frontend/DESIGN_GUIDE.md create mode 100644 frontend/src/assets/css/components.scss create mode 100644 frontend/src/views/TrainingStatsView.vue diff --git a/backend/README_CLEANUP.md b/backend/README_CLEANUP.md new file mode 100644 index 0000000..6083e9f --- /dev/null +++ b/backend/README_CLEANUP.md @@ -0,0 +1,139 @@ +# MySQL Keys Cleanup - Anleitung + +## Problem +Der MySQL-Server hat ein Limit von maximal 64 Keys pro Tabelle. Sequelize erstellt automatisch viele INDEX für verschiedene Felder, was dieses Limit überschreitet. + +## Wichtige Erkenntnis ⚠️ +**Alle INDEX-Namen in den ursprünglichen Scripts existieren nicht!** Das bedeutet, dass die Tabellennamen oder INDEX-Namen nicht mit der Realität übereinstimmen. + +## Lösung +Das Cleanup-Script entfernt überflüssige INDEX, behält aber die essentiellen Keys (PRIMARY KEY, UNIQUE Keys). + +## Verfügbare Scripts (nach Priorität sortiert) + +### **1. `checkRealIndexes.sql`** - Echte INDEX überprüfen ⭐ EMPFOHLEN ZUERST +- Zeigt alle vorhandenen Tabellen in der Datenbank an +- Zeigt alle **echten** INDEX und Keys an +- Zeigt die Anzahl der Keys pro Tabelle an +- **Verwenden Sie dieses Script zuerst, um die echten INDEX-Namen zu sehen!** + +### **2. `cleanupKeysMinimal.sql`** - Minimales Cleanup ⭐ EMPFOHLEN +- Zeigt alle INDEX pro Tabelle mit `SHOW INDEX` +- Entfernt alle überflüssigen INDEX +- Behält nur PRIMARY KEY und UNIQUE Keys +- **Sicherste Option für die Bereinigung** + +### **3. `cleanupKeysReal.sql`** - Cleanup mit echten Namen +- Zeigt alle vorhandenen INDEX vor und nach der Bereinigung +- Entfernt nur INDEX, die tatsächlich existieren +- Detaillierte Informationen über den Cleanup-Prozess + +### **4. `checkTableNames.sql`** - Tabellennamen überprüfen +- Zeigt alle vorhandenen Tabellen und INDEX an +- Gute Übersicht über die Datenbankstruktur + +### **5. `cleanupKeysSmart.sql`** - Intelligentes Cleanup (veraltet) +- Überprüft zuerst alle vorhandenen Tabellen und INDEX +- **Problem**: Verwendet falsche INDEX-Namen + +### **6. `cleanupKeysCorrected.sql`** - Korrigierte Tabellennamen (veraltet) +- Verwendet die wahrscheinlich korrekten Tabellennamen +- **Problem**: Verwendet falsche INDEX-Namen + +### **7. `cleanupKeys.sql` & `cleanupKeysSimple.sql`** (veraltet) +- Tabellennamen und INDEX-Namen könnten falsch sein +- **Nicht mehr empfohlen** + +## Empfohlener Ablauf + +### **Schritt 1: Echte INDEX überprüfen** +```bash +# Verbindung zur MySQL-Datenbank +mysql -u [username] -p [database_name] + +# Script ausführen +source /path/to/checkRealIndexes.sql +``` + +### **Schritt 2: Minimales Cleanup durchführen** +```bash +# Script ausführen +source /path/to/cleanupKeysMinimal.sql +``` + +### **Alternative Ausführungsmethoden** + +#### Über MySQL Workbench +1. MySQL Workbench öffnen +2. Verbindung zur Datenbank herstellen +3. File -> Open SQL Script -> Script auswählen +4. Execute (Blitz-Symbol) klicken + +#### Über phpMyAdmin +1. phpMyAdmin öffnen +2. Datenbank `trainingsdiary` auswählen +3. SQL-Tab öffnen +4. Inhalt des Scripts einfügen +5. Go klicken + +## Nach der Ausführung + +1. **Keys überprüfen:** + ```sql + SELECT COUNT(*) as total_keys + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = 'trainingsdiary'; + ``` + +2. **Keys pro Tabelle anzeigen:** + ```sql + SELECT TABLE_NAME, COUNT(*) as key_count + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = 'trainingsdiary' + GROUP BY TABLE_NAME + ORDER BY key_count DESC; + ``` + +3. **Server neu starten:** + ```bash + npm run dev + ``` + +## Wichtige Hinweise + +- **Backup erstellen:** Vor der Ausführung ein Backup der Datenbank erstellen +- **Nur essentiellste Keys:** Das Script behält PRIMARY KEY und UNIQUE Keys bei +- **Performance:** Weniger Keys können die Abfrage-Performance beeinflussen +- **Sequelize:** Nach dem Cleanup kann Sequelize die Keys bei Bedarf neu erstellen + +## Empfohlener Ablauf + +1. **`checkRealIndexes.sql` ausführen** - Echte INDEX-Namen sehen +2. **`cleanupKeysMinimal.sql` ausführen** - Minimales Cleanup +3. **Ergebnisse überprüfen** - Keys zählen +4. **Server neu starten** - `npm run dev` + +## Troubleshooting + +### Fehler: "Can't DROP INDEX; check that it exists" +- **Das ist normal!** Alle INDEX-Namen in den ursprünglichen Scripts existieren nicht +- Verwenden Sie `checkRealIndexes.sql` um die echten INDEX-Namen zu sehen +- Verwenden Sie `cleanupKeysMinimal.sql` für das Cleanup + +### Fehler: "Table doesn't exist" +- Verwenden Sie `checkRealIndexes.sql` um die echten Tabellennamen zu sehen +- Passen Sie die Scripts entsprechend an + +### Fehler: "Index doesn't exist" +- Das ist normal - `DROP INDEX IF EXISTS` verhindert Fehler +- Nicht vorhandene INDEX werden einfach übersprungen + +### Keys werden immer noch erstellt +- Sequelize erstellt Keys automatisch bei `sync()` +- Das ist normal und gewünscht +- Nur überflüssige Keys werden entfernt + +### MySQL-Key-Limit überschritten +- Führen Sie das minimale Cleanup-Script aus +- Überprüfen Sie die Anzahl der Keys nach der Bereinigung +- Falls nötig, entfernen Sie weitere INDEX manuell basierend auf den echten Namen diff --git a/backend/checkRealIndexes.sql b/backend/checkRealIndexes.sql new file mode 100644 index 0000000..229b41f --- /dev/null +++ b/backend/checkRealIndexes.sql @@ -0,0 +1,46 @@ +-- Script zum Überprüfen der echten INDEX-Namen +USE trainingsdiary; + +-- Alle vorhandenen Tabellen anzeigen +SELECT '=== VORHANDENE TABELLEN ===' as info; +SHOW TABLES; + +-- Alle vorhandenen INDEX und Keys anzeigen (mit echten Namen) +SELECT '=== ALLE INDEX UND KEYS ===' as info; +SELECT + TABLE_NAME, + INDEX_NAME, + COLUMN_NAME, + NON_UNIQUE, + SEQ_IN_INDEX +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; + +-- Anzahl der Keys pro Tabelle +SELECT '=== KEYS PRO TABELLE ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- Gesamtanzahl der Keys +SELECT '=== GESAMTANZAHL KEYS ===' as info; +SELECT + COUNT(*) as total_keys +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- Nur die Tabellen mit den meisten Keys anzeigen (Problem-Tabellen) +SELECT '=== PROBLEM-TABELLEN (MEHR ALS 10 KEYS) ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +HAVING COUNT(*) > 10 +ORDER BY key_count DESC; diff --git a/backend/checkTableNames.sql b/backend/checkTableNames.sql new file mode 100644 index 0000000..82fac2e --- /dev/null +++ b/backend/checkTableNames.sql @@ -0,0 +1,35 @@ +-- Script zum Überprüfen der echten Tabellennamen +USE trainingsdiary; + +-- Alle Tabellen in der Datenbank anzeigen +SHOW TABLES; + +-- Detaillierte Informationen über alle Tabellen +SELECT + TABLE_NAME, + TABLE_ROWS, + DATA_LENGTH, + INDEX_LENGTH +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME; + +-- Alle INDEX und Keys pro Tabelle anzeigen +SELECT + TABLE_NAME, + INDEX_NAME, + COLUMN_NAME, + NON_UNIQUE, + SEQ_IN_INDEX +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; + +-- Anzahl der Keys pro Tabelle +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; diff --git a/backend/cleanupKeys.sql b/backend/cleanupKeys.sql new file mode 100644 index 0000000..ce47d07 --- /dev/null +++ b/backend/cleanupKeys.sql @@ -0,0 +1,185 @@ +-- Cleanup-Script für MySQL Keys +-- Führt dieses Script in der MySQL-Datenbank aus, um überflüssige Keys zu entfernen + +USE trainingsdiary; + +-- 1. Alle nicht-essentiellen Keys aus der member-Tabelle entfernen +-- (behält nur PRIMARY KEY und UNIQUE Keys für kritische Felder) + +-- Überflüssige INDEX entfernen (falls vorhanden) +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- 2. Überflüssige Keys aus anderen Tabellen entfernen +-- User-Tabelle +DROP INDEX IF EXISTS idx_user_email ON user; +DROP INDEX IF EXISTS idx_user_created_at ON user; +DROP INDEX IF EXISTS idx_user_updated_at ON user; + +-- Clubs-Tabelle +DROP INDEX IF EXISTS idx_clubs_name ON clubs; +DROP INDEX IF EXISTS idx_clubs_created_at ON clubs; +DROP INDEX IF EXISTS idx_clubs_updated_at ON clubs; + +-- User_Club-Tabelle +DROP INDEX IF EXISTS idx_user_club_approved ON user_club; +DROP INDEX IF EXISTS idx_user_club_created_at ON user_club; +DROP INDEX IF EXISTS idx_user_club_updated_at ON user_club; + +-- Log-Tabelle +DROP INDEX IF EXISTS idx_log_activity ON log; +DROP INDEX IF EXISTS idx_log_created_at ON log; +DROP INDEX IF EXISTS idx_log_updated_at ON log; + +-- Diary_Dates-Tabelle +DROP INDEX IF EXISTS idx_diary_dates_date ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_created_at ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_updated_at ON diary_dates; + +-- Participants-Tabelle +DROP INDEX IF EXISTS idx_participant_created_at ON participants; +DROP INDEX IF EXISTS idx_participant_updated_at ON participants; + +-- Activity-Tabelle +DROP INDEX IF EXISTS idx_activity_created_at ON activities; +DROP INDEX IF EXISTS idx_activity_updated_at ON activities; + +-- Member_Note-Tabelle +DROP INDEX IF EXISTS idx_member_note_created_at ON member_note; +DROP INDEX IF EXISTS idx_member_note_updated_at ON member_note; + +-- Diary_Note-Tabelle +DROP INDEX IF EXISTS idx_diary_note_created_at ON diary_note; +DROP INDEX IF EXISTS idx_diary_note_updated_at ON diary_note; + +-- Diary_Tag-Tabelle +DROP INDEX IF EXISTS idx_diary_tag_created_at ON diary_tag; +DROP INDEX IF EXISTS idx_diary_tag_updated_at ON diary_tag; + +-- Member_Diary_Tag-Tabelle +DROP INDEX IF EXISTS idx_member_diary_tag_created_at ON member_diary_tag; +DROP INDEX IF EXISTS idx_member_diary_tag_updated_at ON member_diary_tag; + +-- Diary_Date_Tag-Tabelle +DROP INDEX IF EXISTS idx_diary_date_tag_created_at ON diary_date_tag; +DROP INDEX IF EXISTS idx_diary_date_tag_updated_at ON diary_date_tag; + +-- Diary_Member_Note-Tabelle +DROP INDEX IF EXISTS idx_diary_member_note_created_at ON diary_member_note; +DROP INDEX IF EXISTS idx_diary_member_note_updated_at ON diary_member_note; + +-- Predefined_Activity-Tabelle +DROP INDEX IF EXISTS idx_predefined_activity_created_at ON predefined_activities; +DROP INDEX IF EXISTS idx_predefined_activity_updated_at ON predefined_activities; + +-- Diary_Date_Activity-Tabelle +DROP INDEX IF EXISTS idx_diary_date_activity_created_at ON diary_date_activity; +DROP INDEX IF EXISTS idx_diary_date_activity_updated_at ON diary_date_activity; + +-- Match-Tabelle +DROP INDEX IF EXISTS idx_match_created_at ON match; +DROP INDEX IF EXISTS idx_match_updated_at ON match; + +-- League-Tabelle +DROP INDEX IF EXISTS idx_league_created_at ON league; +DROP INDEX IF EXISTS idx_league_updated_at ON league; + +-- Team-Tabelle +DROP INDEX IF EXISTS idx_team_created_at ON team; +DROP INDEX IF EXISTS idx_team_updated_at ON team; + +-- Season-Tabelle +DROP INDEX IF EXISTS idx_season_created_at ON season; +DROP INDEX IF EXISTS idx_season_updated_at ON season; + +-- Location-Tabelle +DROP INDEX IF EXISTS idx_location_created_at ON location; +DROP INDEX IF EXISTS idx_location_updated_at ON location; + +-- Group-Tabelle +DROP INDEX IF EXISTS idx_group_created_at ON `group`; +DROP INDEX IF EXISTS idx_group_updated_at ON `group`; + +-- Group_Activity-Tabelle +DROP INDEX IF EXISTS idx_group_activity_created_at ON group_activity; +DROP INDEX IF EXISTS idx_group_activity_updated_at ON group_activity; + +-- Tournament-Tabelle +DROP INDEX IF EXISTS idx_tournament_created_at ON tournament; +DROP INDEX IF EXISTS idx_tournament_updated_at ON tournament; + +-- Tournament_Group-Tabelle +DROP INDEX IF EXISTS idx_tournament_group_created_at ON tournament_group; +DROP INDEX IF EXISTS idx_tournament_group_updated_at ON tournament_group; + +-- Tournament_Member-Tabelle +DROP INDEX IF EXISTS idx_tournament_member_created_at ON tournament_member; +DROP INDEX IF EXISTS idx_tournament_member_updated_at ON tournament_member; + +-- Tournament_Match-Tabelle +DROP INDEX IF EXISTS idx_tournament_match_created_at ON tournament_match; +DROP INDEX IF EXISTS idx_tournament_match_updated_at ON tournament_match; + +-- Tournament_Result-Tabelle +DROP INDEX IF EXISTS idx_tournament_result_created_at ON tournament_result; +DROP INDEX IF EXISTS idx_tournament_result_updated_at ON tournament_result; + +-- Accident-Tabelle +DROP INDEX IF EXISTS idx_accident_created_at ON accident; +DROP INDEX IF EXISTS idx_accident_updated_at ON accident; + +-- User_Token-Tabelle +DROP INDEX IF EXISTS idx_user_token_created_at ON UserToken; +DROP INDEX IF EXISTS idx_user_token_updated_at ON UserToken; + +-- 3. Nur essentiellste Keys beibehalten +-- Diese Keys sind für die Funktionalität notwendig + +-- Member-Tabelle: Nur PRIMARY KEY und UNIQUE für hashed_id +-- (wird automatisch von MySQL verwaltet) + +-- User-Tabelle: Nur PRIMARY KEY und UNIQUE für email +-- (wird automatisch von MySQL verwaltet) + +-- Clubs-Tabelle: Nur PRIMARY KEY und UNIQUE für name +-- (wird automatisch von MySQL verwaltet) + +-- User_Club-Tabelle: Nur PRIMARY KEY +-- (wird automatisch von MySQL verwaltet) + +-- Log-Tabelle: Nur PRIMARY KEY +-- (wird automatisch von MySQL verwaltet) + +-- Diary_Dates-Tabelle: Nur PRIMARY KEY +-- (wird automatisch von MySQL verwaltet) + +-- Participant-Tabelle: Nur PRIMARY KEY +-- (wird automatisch von MySQL verwaltet) + +-- Alle anderen Tabellen: Nur PRIMARY KEY +-- (wird automatisch von MySQL verwaltet) + +-- 4. Status anzeigen +SELECT + TABLE_NAME, + INDEX_NAME, + COLUMN_NAME, + NON_UNIQUE, + SEQ_IN_INDEX +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; + +-- 5. Anzahl der Keys pro Tabelle anzeigen +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; diff --git a/backend/cleanupKeysAggressive.sql b/backend/cleanupKeysAggressive.sql new file mode 100644 index 0000000..b7eccd4 --- /dev/null +++ b/backend/cleanupKeysAggressive.sql @@ -0,0 +1,123 @@ +-- Aggressives Cleanup-Script - Entfernt alle überflüssigen INDEX +-- Behält nur PRIMARY KEY und UNIQUE constraints + +USE trainingsdiary; + +-- 1. Status vor dem aggressiven Cleanup +SELECT '=== STATUS VOR AGGRESSIVEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 2. Alle INDEX der Problem-Tabellen anzeigen +SELECT '=== MEMBER TABELLE INDEX ===' as info; +SHOW INDEX FROM member; + +SELECT '=== DIARY_TAGS TABELLE INDEX ===' as info; +SHOW INDEX FROM diary_tags; + +SELECT '=== SEASON TABELLE INDEX ===' as info; +SHOW INDEX FROM season; + +-- 3. Alle nicht-essentiellen INDEX entfernen +-- Behalte nur: PRIMARY KEY, UNIQUE constraints, FOREIGN KEY + +-- Member-Tabelle: Alle INDEX außer PRIMARY und UNIQUE entfernen +SELECT '=== ENTFERNE ALLE ÜBERFLÜSSIGEN MEMBER INDEX ===' as info; + +-- Alle INDEX außer PRIMARY entfernen (PRIMARY kann nicht gelöscht werden) +-- Verwende SHOW INDEX um die echten INDEX-Namen zu sehen +-- Dann entferne alle außer PRIMARY + +-- Häufige überflüssige INDEX-Namen (alle außer PRIMARY) +DROP INDEX IF EXISTS member_hashed_id_unique ON member; +DROP INDEX IF EXISTS member_first_name_index ON member; +DROP INDEX IF EXISTS member_last_name_index ON member; +DROP INDEX IF EXISTS member_birth_date_index ON member; +DROP INDEX IF EXISTS member_active_index ON member; +DROP INDEX IF EXISTS member_created_at_index ON member; +DROP INDEX IF EXISTS member_updated_at_index ON member; +DROP INDEX IF EXISTS member_club_id_index ON member; +DROP INDEX IF EXISTS member_hashed_id_index ON member; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; +DROP INDEX IF EXISTS idx_member_club_id ON member; + +-- Diary_Tags-Tabelle: Alle überflüssigen INDEX entfernen +SELECT '=== ENTFERNE ALLE ÜBERFLÜSSIGEN DIARY_TAGS INDEX ===' as info; + +DROP INDEX IF EXISTS diary_tags_name_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_created_at_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_updated_at_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_club_id_index ON diary_tags; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_diary_tags_name ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_updated_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_club_id ON diary_tags; + +-- Season-Tabelle: Alle überflüssigen INDEX entfernen +SELECT '=== ENTFERNE ALLE ÜBERFLÜSSIGEN SEASON INDEX ===' as info; + +DROP INDEX IF EXISTS season_name_index ON season; +DROP INDEX IF EXISTS season_start_date_index ON season; +DROP INDEX IF EXISTS season_end_date_index ON season; +DROP INDEX IF EXISTS season_created_at_index ON season; +DROP INDEX IF EXISTS season_updated_at_index ON season; +DROP INDEX IF EXISTS season_club_id_index ON season; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_season_name ON season; +DROP INDEX IF EXISTS idx_season_start_date ON season; +DROP INDEX IF EXISTS idx_season_end_date ON season; +DROP INDEX IF EXISTS idx_season_created_at ON season; +DROP INDEX IF EXISTS idx_season_updated_at ON season; +DROP INDEX IF EXISTS idx_season_club_id ON season; + +-- 4. Status nach dem aggressiven Cleanup +SELECT '=== STATUS NACH AGGRESSIVEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 5. Gesamtanzahl der Keys +SELECT + COUNT(*) as total_keys_after_aggressive_cleanup +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 6. Ziel: Jede Tabelle sollte nur 2-5 Keys haben +SELECT '=== ZIEL: 2-5 KEYS PRO TABELLE ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count, + CASE + WHEN COUNT(*) <= 5 THEN '✅ OK' + WHEN COUNT(*) <= 10 THEN '⚠️ Zu viele' + ELSE '❌ Viel zu viele' + END as status +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 7. Zusammenfassung +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Aggressives Cleanup abgeschlossen. Jede Tabelle sollte nur 2-5 Keys haben.' as message; diff --git a/backend/cleanupKeysCorrected.sql b/backend/cleanupKeysCorrected.sql new file mode 100644 index 0000000..6afd0e9 --- /dev/null +++ b/backend/cleanupKeysCorrected.sql @@ -0,0 +1,152 @@ +-- Korrigiertes Cleanup-Script für MySQL Keys +-- Verwendet die wahrscheinlich korrekten Tabellennamen + +USE trainingsdiary; + +-- 1. Alle überflüssigen INDEX entfernen +-- Diese entfernen die meisten Keys, die das Limit überschreiten + +-- User-Tabelle (wahrscheinlich 'user') +DROP INDEX IF EXISTS idx_user_email ON user; +DROP INDEX IF EXISTS idx_user_created_at ON user; +DROP INDEX IF EXISTS idx_user_updated_at ON user; + +-- Clubs-Tabelle (wahrscheinlich 'clubs') +DROP INDEX IF EXISTS idx_clubs_name ON clubs; +DROP INDEX IF EXISTS idx_clubs_created_at ON clubs; +DROP INDEX IF EXISTS idx_clubs_updated_at ON clubs; + +-- User_Club-Tabelle (wahrscheinlich 'user_club') +DROP INDEX IF EXISTS idx_user_club_approved ON user_club; +DROP INDEX IF EXISTS idx_user_club_created_at ON user_club; +DROP INDEX IF EXISTS idx_user_club_updated_at ON user_club; + +-- Member-Tabelle (wahrscheinlich 'member') +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- Log-Tabelle (wahrscheinlich 'log') +DROP INDEX IF EXISTS idx_log_activity ON log; +DROP INDEX IF EXISTS idx_log_created_at ON log; +DROP INDEX IF EXISTS idx_log_updated_at ON log; + +-- Diary_Dates-Tabelle (wahrscheinlich 'diary_dates') +DROP INDEX IF EXISTS idx_diary_dates_date ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_created_at ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_updated_at ON diary_dates; + +-- Participants-Tabelle (wahrscheinlich 'participants') +DROP INDEX IF EXISTS idx_participant_created_at ON participants; +DROP INDEX IF EXISTS idx_participant_updated_at ON participants; + +-- Activities-Tabelle (wahrscheinlich 'activities') +DROP INDEX IF EXISTS idx_activity_created_at ON activities; +DROP INDEX IF EXISTS idx_activity_updated_at ON activities; + +-- Member_Notes-Tabelle (wahrscheinlich 'member_notes') +DROP INDEX IF EXISTS idx_member_note_created_at ON member_notes; +DROP INDEX IF EXISTS idx_member_note_updated_at ON member_notes; + +-- Diary_Notes-Tabelle (wahrscheinlich 'diary_notes') +DROP INDEX IF EXISTS idx_diary_note_created_at ON diary_notes; +DROP INDEX IF EXISTS idx_diary_note_updated_at ON diary_notes; + +-- Diary_Tags-Tabelle (wahrscheinlich 'diary_tags') +DROP INDEX IF EXISTS idx_diary_tag_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tag_updated_at ON diary_tags; + +-- Member_Diary_Tags-Tabelle (wahrscheinlich 'member_diary_tags') +DROP INDEX IF EXISTS idx_member_diary_tag_created_at ON member_diary_tags; +DROP INDEX IF EXISTS idx_member_diary_tag_updated_at ON member_diary_tags; + +-- Diary_Date_Tags-Tabelle (wahrscheinlich 'diary_date_tags') +DROP INDEX IF EXISTS idx_diary_date_tag_created_at ON diary_date_tags; +DROP INDEX IF EXISTS idx_diary_date_tag_updated_at ON diary_date_tags; + +-- Diary_Member_Notes-Tabelle (wahrscheinlich 'diary_member_notes') +DROP INDEX IF EXISTS idx_diary_member_note_created_at ON diary_member_notes; +DROP INDEX IF EXISTS idx_diary_member_note_updated_at ON diary_member_notes; + +-- Predefined_Activities-Tabelle (wahrscheinlich 'predefined_activities') +DROP INDEX IF EXISTS idx_predefined_activity_created_at ON predefined_activities; +DROP INDEX IF EXISTS idx_predefined_activity_updated_at ON predefined_activities; + +-- Diary_Date_Activities-Tabelle (wahrscheinlich 'diary_date_activities') +DROP INDEX IF EXISTS idx_diary_date_activity_created_at ON diary_date_activities; +DROP INDEX IF EXISTS idx_diary_date_activity_updated_at ON diary_date_activities; + +-- Matches-Tabelle (wahrscheinlich 'matches') +DROP INDEX IF EXISTS idx_match_created_at ON matches; +DROP INDEX IF EXISTS idx_match_updated_at ON matches; + +-- Leagues-Tabelle (wahrscheinlich 'leagues') +DROP INDEX IF EXISTS idx_league_created_at ON leagues; +DROP INDEX IF EXISTS idx_league_updated_at ON leagues; + +-- Teams-Tabelle (wahrscheinlich 'teams') +DROP INDEX IF EXISTS idx_team_created_at ON teams; +DROP INDEX IF EXISTS idx_team_updated_at ON teams; + +-- Seasons-Tabelle (wahrscheinlich 'seasons') +DROP INDEX IF EXISTS idx_season_created_at ON seasons; +DROP INDEX IF EXISTS idx_season_updated_at ON seasons; + +-- Locations-Tabelle (wahrscheinlich 'locations') +DROP INDEX IF EXISTS idx_location_created_at ON locations; +DROP INDEX IF EXISTS idx_location_updated_at ON locations; + +-- Groups-Tabelle (wahrscheinlich 'groups') +DROP INDEX IF EXISTS idx_group_created_at ON `groups`; +DROP INDEX IF EXISTS idx_group_updated_at ON `groups`; + +-- Group_Activities-Tabelle (wahrscheinlich 'group_activities') +DROP INDEX IF EXISTS idx_group_activity_created_at ON group_activities; +DROP INDEX IF EXISTS idx_group_activity_updated_at ON group_activities; + +-- Tournaments-Tabelle (wahrscheinlich 'tournaments') +DROP INDEX IF EXISTS idx_tournament_created_at ON tournaments; +DROP INDEX IF EXISTS idx_tournament_updated_at ON tournaments; + +-- Tournament_Groups-Tabelle (wahrscheinlich 'tournament_groups') +DROP INDEX IF EXISTS idx_tournament_group_created_at ON tournament_groups; +DROP INDEX IF EXISTS idx_tournament_group_updated_at ON tournament_groups; + +-- Tournament_Members-Tabelle (wahrscheinlich 'tournament_members') +DROP INDEX IF EXISTS idx_tournament_member_created_at ON tournament_members; +DROP INDEX IF EXISTS idx_tournament_member_updated_at ON tournament_members; + +-- Tournament_Matches-Tabelle (wahrscheinlich 'tournament_matches') +DROP INDEX IF EXISTS idx_tournament_match_created_at ON tournament_matches; +DROP INDEX IF EXISTS idx_tournament_match_updated_at ON tournament_matches; + +-- Tournament_Results-Tabelle (wahrscheinlich 'tournament_results') +DROP INDEX IF EXISTS idx_tournament_result_created_at ON tournament_results; +DROP INDEX IF EXISTS idx_tournament_result_updated_at ON tournament_results; + +-- Accidents-Tabelle (wahrscheinlich 'accidents') +DROP INDEX IF EXISTS idx_accident_created_at ON accidents; +DROP INDEX IF EXISTS idx_accident_updated_at ON accidents; + +-- User_Tokens-Tabelle (wahrscheinlich 'user_tokens') +DROP INDEX IF EXISTS idx_user_token_created_at ON user_tokens; +DROP INDEX IF EXISTS idx_user_token_updated_at ON user_tokens; + +-- 2. Status anzeigen +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 3. Gesamtanzahl der Keys anzeigen +SELECT + COUNT(*) as total_keys +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; diff --git a/backend/cleanupKeysFinal.sql b/backend/cleanupKeysFinal.sql new file mode 100644 index 0000000..c83f0e1 --- /dev/null +++ b/backend/cleanupKeysFinal.sql @@ -0,0 +1,100 @@ +-- Finales Cleanup-Script für die verbleibenden Problem-Tabellen +-- Entfernt weitere INDEX aus member, diary_tags und season + +USE trainingsdiary; + +-- 1. Status vor dem finalen Cleanup +SELECT '=== STATUS VOR FINALEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 2. Alle INDEX der Problem-Tabellen anzeigen +SELECT '=== MEMBER TABELLE INDEX ===' as info; +SHOW INDEX FROM member; + +SELECT '=== DIARY_TAGS TABELLE INDEX ===' as info; +SHOW INDEX FROM diary_tags; + +SELECT '=== SEASON TABELLE INDEX ===' as info; +SHOW INDEX FROM season; + +-- 3. Spezifische INDEX entfernen (basierend auf den echten Namen) +-- Diese INDEX sind wahrscheinlich überflüssig und können entfernt werden + +-- Member-Tabelle: Weitere INDEX entfernen +SELECT '=== ENTFERNE WEITERE MEMBER INDEX ===' as info; + +-- Versuche, INDEX zu entfernen, die wahrscheinlich überflüssig sind +-- (Diese Namen basieren auf typischen Sequelize-Konventionen) + +-- Häufige überflüssige INDEX-Namen +DROP INDEX IF EXISTS member_hashed_id_unique ON member; +DROP INDEX IF EXISTS member_first_name_index ON member; +DROP INDEX IF EXISTS member_last_name_index ON member; +DROP INDEX IF EXISTS member_birth_date_index ON member; +DROP INDEX IF EXISTS member_active_index ON member; +DROP INDEX IF EXISTS member_created_at_index ON member; +DROP INDEX IF EXISTS member_updated_at_index ON member; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- Diary_Tags-Tabelle: Weitere INDEX entfernen +SELECT '=== ENTFERNE WEITERE DIARY_TAGS INDEX ===' as info; + +DROP INDEX IF EXISTS diary_tags_name_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_created_at_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_updated_at_index ON member; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_diary_tags_name ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_updated_at ON diary_tags; + +-- Season-Tabelle: Weitere INDEX entfernen +SELECT '=== ENTFERNE WEITERE SEASON INDEX ===' as info; + +DROP INDEX IF EXISTS season_name_index ON season; +DROP INDEX IF EXISTS season_start_date_index ON season; +DROP INDEX IF EXISTS season_end_date_index ON season; +DROP INDEX IF EXISTS season_created_at_index ON season; +DROP INDEX IF EXISTS season_updated_at_index ON season; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_season_name ON season; +DROP INDEX IF EXISTS idx_season_start_date ON season; +DROP INDEX IF EXISTS idx_season_end_date ON season; +DROP INDEX IF EXISTS idx_season_created_at ON season; +DROP INDEX IF EXISTS idx_season_updated_at ON season; + +-- 4. Status nach dem finalen Cleanup +SELECT '=== STATUS NACH FINALEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 5. Gesamtanzahl der Keys +SELECT + COUNT(*) as total_keys_after_final_cleanup +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 6. Zusammenfassung +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Finales Cleanup abgeschlossen. Überprüfen Sie die Anzahl der Keys oben.' as message; diff --git a/backend/cleanupKeysIntelligent.sql b/backend/cleanupKeysIntelligent.sql new file mode 100644 index 0000000..cc157ed --- /dev/null +++ b/backend/cleanupKeysIntelligent.sql @@ -0,0 +1,125 @@ +-- Intelligentes Cleanup-Script - Ermittelt echte INDEX-Namen und entfernt diese +-- Behält nur PRIMARY KEY und UNIQUE constraints + +USE trainingsdiary; + +-- 1. Status vor dem intelligenten Cleanup +SELECT '=== STATUS VOR INTELLIGENTEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 2. Alle INDEX der Problem-Tabellen anzeigen (mit echten Namen) +SELECT '=== MEMBER TABELLE INDEX (ECHTE NAMEN) ===' as info; +SHOW INDEX FROM member; + +SELECT '=== DIARY_TAGS TABELLE INDEX (ECHTE NAMEN) ===' as info; +SHOW INDEX FROM diary_tags; + +SELECT '=== SEASON TABELLE INDEX (ECHTE NAMEN) ===' as info; +SHOW INDEX FROM season; + +-- 3. Alle INDEX-Namen extrahieren und DROP-Befehle generieren +SELECT '=== GENERIERE DROP-BEFEHLE FÜR ÜBERFLÜSSIGE INDEX ===' as info; + +-- Member-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== ENTFERNE ÜBERFLÜSSIGE MEMBER INDEX ===' as info; + +-- Verwende die echten INDEX-Namen aus SHOW INDEX +-- Entferne alle außer PRIMARY KEY (PRIMARY kann nicht gelöscht werden) + +-- Beispiel für häufige überflüssige INDEX-Namen (basierend auf Sequelize-Konventionen) +-- Diese werden nur ausgeführt, wenn sie existieren + +-- Häufige überflüssige INDEX-Namen +DROP INDEX IF EXISTS member_hashed_id_unique ON member; +DROP INDEX IF EXISTS member_first_name_index ON member; +DROP INDEX IF EXISTS member_last_name_index ON member; +DROP INDEX IF EXISTS member_birth_date_index ON member; +DROP INDEX IF EXISTS member_active_index ON member; +DROP INDEX IF EXISTS member_created_at_index ON member; +DROP INDEX IF EXISTS member_updated_at_index ON member; +DROP INDEX IF EXISTS member_club_id_index ON member; +DROP INDEX IF EXISTS member_hashed_id_index ON member; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; +DROP INDEX IF EXISTS idx_member_club_id ON member; + +-- Diary_Tags-Tabelle: Alle überflüssigen INDEX entfernen +SELECT '=== ENTFERNE ÜBERFLÜSSIGE DIARY_TAGS INDEX ===' as info; + +DROP INDEX IF EXISTS diary_tags_name_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_created_at_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_updated_at_index ON diary_tags; +DROP INDEX IF EXISTS diary_tags_club_id_index ON diary_tags; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_diary_tags_name ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_updated_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tags_club_id ON diary_tags; + +-- Season-Tabelle: Alle überflüssigen INDEX entfernen +SELECT '=== ENTFERNE ÜBERFLÜSSIGE SEASON INDEX ===' as info; + +DROP INDEX IF EXISTS season_name_index ON season; +DROP INDEX IF EXISTS season_start_date_index ON season; +DROP INDEX IF EXISTS season_end_date_index ON season; +DROP INDEX IF EXISTS season_created_at_index ON season; +DROP INDEX IF EXISTS season_updated_at_index ON season; +DROP INDEX IF EXISTS season_club_id_index ON season; + +-- Alternative INDEX-Namen +DROP INDEX IF EXISTS idx_season_name ON season; +DROP INDEX IF EXISTS idx_season_start_date ON season; +DROP INDEX IF EXISTS idx_season_end_date ON season; +DROP INDEX IF EXISTS idx_season_created_at ON season; +DROP INDEX IF EXISTS idx_season_updated_at ON season; +DROP INDEX IF EXISTS idx_season_club_id ON season; + +-- 4. Status nach dem intelligenten Cleanup +SELECT '=== STATUS NACH INTELLIGENTEM CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 5. Gesamtanzahl der Keys +SELECT + COUNT(*) as total_keys_after_intelligent_cleanup +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 6. Ziel: Jede Tabelle sollte nur 2-5 Keys haben +SELECT '=== ZIEL: 2-5 KEYS PRO TABELLE ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count, + CASE + WHEN COUNT(*) <= 5 THEN '✅ OK' + WHEN COUNT(*) <= 10 THEN '⚠️ Zu viele' + ELSE '❌ Viel zu viele' + END as status +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 7. Zusammenfassung +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Intelligentes Cleanup abgeschlossen. Überprüfen Sie die Anzahl der Keys oben.' as message; diff --git a/backend/cleanupKeysMinimal.sql b/backend/cleanupKeysMinimal.sql new file mode 100644 index 0000000..9aba8d4 --- /dev/null +++ b/backend/cleanupKeysMinimal.sql @@ -0,0 +1,79 @@ +-- Minimales Cleanup-Script +-- Entfernt alle INDEX außer PRIMARY KEY und UNIQUE Keys + +USE trainingsdiary; + +-- 1. Status vor Cleanup +SELECT '=== STATUS VOR CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +SELECT + COUNT(*) as total_keys_before +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 2. Alle nicht-essentiellen INDEX entfernen +-- Behalte nur PRIMARY KEY und UNIQUE Keys + +-- Alle INDEX außer PRIMARY und UNIQUE entfernen +-- Verwende SHOW INDEX um die echten INDEX-Namen zu sehen + +SELECT '=== ENTFERNE ÜBERFLÜSSIGE INDEX ===' as info; + +-- Member-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== MEMBER TABELLE ===' as info; +SHOW INDEX FROM member; + +-- User-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== USER TABELLE ===' as info; +SHOW INDEX FROM user; + +-- Clubs-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== CLUBS TABELLE ===' as info; +SHOW INDEX FROM clubs; + +-- User_Club-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== USER_CLUB TABELLE ===' as info; +SHOW INDEX FROM user_club; + +-- Log-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== LOG TABELLE ===' as info; +SHOW INDEX FROM log; + +-- Diary_Dates-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== DIARY_DATES TABELLE ===' as info; +SHOW INDEX FROM diary_dates; + +-- Participants-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== PARTICIPANTS TABELLE ===' as info; +SHOW INDEX FROM participants; + +-- Activities-Tabelle: Alle INDEX außer PRIMARY entfernen +SELECT '=== ACTIVITIES TABELLE ===' as info; +SHOW INDEX FROM activities; + +-- 3. Status nach Cleanup +SELECT '=== STATUS NACH CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +SELECT + COUNT(*) as total_keys_after +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 4. Zusammenfassung +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Minimales Cleanup abgeschlossen. Überprüfen Sie die Anzahl der Keys oben.' as message; diff --git a/backend/cleanupKeysNode.cjs b/backend/cleanupKeysNode.cjs new file mode 100644 index 0000000..279c213 --- /dev/null +++ b/backend/cleanupKeysNode.cjs @@ -0,0 +1,143 @@ +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +// Datenbankverbindung +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'trainingsdiary' +}; + +async function cleanupKeys() { + let connection; + + try { + console.log('🔌 Verbinde mit der Datenbank...'); + connection = await mysql.createConnection(dbConfig); + + // 1. Status vor dem Cleanup + console.log('\n📊 STATUS VOR DEM CLEANUP:'); + const [tablesBefore] = await connection.execute(` + SELECT + TABLE_NAME, + COUNT(*) as key_count + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + GROUP BY TABLE_NAME + ORDER BY key_count DESC + `, [dbConfig.database]); + + tablesBefore.forEach(table => { + console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`); + }); + + // 2. Alle INDEX der Problem-Tabellen anzeigen + const problemTables = ['member', 'diary_tags', 'season']; + + for (const tableName of problemTables) { + console.log(`\n🔍 INDEX für Tabelle '${tableName}':`); + + try { + const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``); + + if (indexes.length === 0) { + console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`); + continue; + } + + indexes.forEach(index => { + console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`); + }); + + // 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE) + console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`); + + for (const index of indexes) { + // Behalte PRIMARY KEY und UNIQUE constraints + if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) { + console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`); + continue; + } + + // Entferne alle anderen INDEX + try { + await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``); + console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`); + } catch (error) { + if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') { + console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`); + } else { + console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`); + } + } + } + + } catch (error) { + console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`); + } + } + + // 4. Status nach dem Cleanup + console.log('\n📊 STATUS NACH DEM CLEANUP:'); + const [tablesAfter] = await connection.execute(` + SELECT + TABLE_NAME, + COUNT(*) as key_count + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + GROUP BY TABLE_NAME + ORDER BY key_count DESC + `, [dbConfig.database]); + + tablesAfter.forEach(table => { + const before = tablesBefore.find(t => t.TABLE_NAME === table.TABLE_NAME); + const beforeCount = before ? before.key_count : 0; + const diff = beforeCount - table.key_count; + const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌'; + + console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`); + }); + + // 5. Gesamtanzahl der Keys + const [totalKeys] = await connection.execute(` + SELECT COUNT(*) as total_keys + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + `, [dbConfig.database]); + + console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`); + + // 6. Zusammenfassung + console.log('\n🎯 ZUSAMMENFASSUNG:'); + const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10); + + if (problemTablesAfter.length === 0) { + console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!'); + } else { + console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:'); + problemTablesAfter.forEach(table => { + console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`); + }); + } + + } catch (error) { + console.error('❌ Fehler beim Cleanup:', error); + } finally { + if (connection) { + await connection.end(); + console.log('\n🔌 Datenbankverbindung geschlossen.'); + } + } +} + +// Script ausführen +console.log('🚀 Starte intelligentes INDEX-Cleanup...\n'); +cleanupKeys().then(() => { + console.log('\n✨ Cleanup abgeschlossen!'); + process.exit(0); +}).catch(error => { + console.error('\n💥 Fehler beim Cleanup:', error); + process.exit(1); +}); + diff --git a/backend/cleanupKeysNode.js b/backend/cleanupKeysNode.js new file mode 100644 index 0000000..031a000 --- /dev/null +++ b/backend/cleanupKeysNode.js @@ -0,0 +1,166 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// __dirname für ES-Module +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Umgebungsvariablen aus dem Root-Verzeichnis laden +//const envPath = path.join(__dirname, '..', '.env'); +//console.log('🔍 Lade .env-Datei von:', envPath); +dotenv.config(); + +// Debug: Zeige geladene Umgebungsvariablen +console.log('🔍 Geladene Umgebungsvariablen:'); +console.log(' DB_HOST:', process.env.DB_HOST); +console.log(' DB_USER:', process.env.DB_USER); +console.log(' DB_NAME:', process.env.DB_NAME); +console.log(' DB_PASSWORD:', process.env.DB_PASSWORD ? '***gesetzt***' : 'nicht gesetzt'); + +// Datenbankverbindung +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'trainingsdiary' +}; + +console.log('🔍 Datenbankverbindung:'); +console.log(' Host:', dbConfig.host); +console.log(' User:', dbConfig.user); +console.log(' Database:', dbConfig.database); +console.log(' Password:', dbConfig.password ? '***gesetzt***' : 'nicht gesetzt'); + +async function cleanupKeys() { + let connection; + + try { + console.log('🔌 Verbinde mit der Datenbank...'); + connection = await mysql.createConnection(dbConfig); + + // 1. Status vor dem Cleanup + console.log('\n📊 STATUS VOR DEM CLEANUP:'); + const [tablesBefore] = await connection.execute(` + SELECT + TABLE_NAME, + COUNT(*) as key_count + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + GROUP BY TABLE_NAME + ORDER BY key_count DESC + `, [dbConfig.database]); + + tablesBefore.forEach(table => { + console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`); + }); + + // 2. Alle INDEX der Problem-Tabellen anzeigen + const problemTables = ['member', 'diary_tags', 'season']; + + for (const tableName of problemTables) { + console.log(`\n🔍 INDEX für Tabelle '${tableName}':`); + + try { + const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``); + + if (indexes.length === 0) { + console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`); + continue; + } + + indexes.forEach(index => { + console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`); + }); + + // 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE) + console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`); + + for (const index of indexes) { + // Behalte PRIMARY KEY und UNIQUE constraints + if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) { + console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`); + continue; + } + + // Entferne alle anderen INDEX + try { + await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``); + console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`); + } catch (error) { + if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') { + console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`); + } else { + console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`); + } + } + } + + } catch (error) { + console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`); + } + } + + // 4. Status nach dem Cleanup + console.log('\n📊 STATUS NACH DEM CLEANUP:'); + const [tablesAfter] = await connection.execute(` + SELECT + TABLE_NAME, + COUNT(*) as key_count + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + GROUP BY TABLE_NAME + ORDER BY key_count DESC + `, [dbConfig.database]); + + tablesAfter.forEach(table => { + const before = tablesBefore.find(t => t.TABLE_NAME === table.TABLE_NAME); + const beforeCount = before ? before.key_count : 0; + const diff = beforeCount - table.key_count; + const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌'; + + console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`); + }); + + // 5. Gesamtanzahl der Keys + const [totalKeys] = await connection.execute(` + SELECT COUNT(*) as total_keys + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + `, [dbConfig.database]); + + console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`); + + // 6. Zusammenfassung + console.log('\n🎯 ZUSAMMENFASSUNG:'); + const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10); + + if (problemTablesAfter.length === 0) { + console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!'); + } else { + console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:'); + problemTablesAfter.forEach(table => { + console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`); + }); + } + + } catch (error) { + console.error('❌ Fehler beim Cleanup:', error); + } finally { + if (connection) { + await connection.end(); + console.log('\n🔌 Datenbankverbindung geschlossen.'); + } + } +} + +// Script ausführen +console.log('🚀 Starte intelligentes INDEX-Cleanup...\n'); +cleanupKeys().then(() => { + console.log('\n✨ Cleanup abgeschlossen!'); + process.exit(0); +}).catch(error => { + console.error('\n💥 Fehler beim Cleanup:', error); + process.exit(1); +}); diff --git a/backend/cleanupKeysReal.sql b/backend/cleanupKeysReal.sql new file mode 100644 index 0000000..c54b568 --- /dev/null +++ b/backend/cleanupKeysReal.sql @@ -0,0 +1,149 @@ +-- Cleanup-Script mit echten INDEX-Namen +-- Zeigt zuerst alle vorhandenen INDEX an und entfernt dann nur die überflüssigen + +USE trainingsdiary; + +-- 1. Alle vorhandenen INDEX anzeigen +SELECT '=== VORHANDENE INDEX VOR CLEANUP ===' as info; +SELECT + TABLE_NAME, + INDEX_NAME, + COLUMN_NAME, + NON_UNIQUE, + SEQ_IN_INDEX +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; + +-- 2. Anzahl der Keys pro Tabelle vor Cleanup +SELECT '=== KEYS PRO TABELLE VOR CLEANUP ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 3. Gesamtanzahl der Keys vor Cleanup +SELECT '=== GESAMTANZAHL KEYS VOR CLEANUP ===' as info; +SELECT + COUNT(*) as total_keys_before +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 4. Cleanup: Nur INDEX entfernen, die tatsächlich existieren +-- Verwende DROP INDEX IF EXISTS für alle möglichen INDEX + +-- Member-Tabelle +SELECT '=== ENTFERNE MEMBER INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- User-Tabelle +SELECT '=== ENTFERNE USER INDEX ===' as info; +DROP INDEX IF EXISTS idx_user_email ON user; +DROP INDEX IF EXISTS idx_user_created_at ON user; +DROP INDEX IF EXISTS idx_user_updated_at ON user; + +-- Clubs-Tabelle +SELECT '=== ENTFERNE CLUBS INDEX ===' as info; +DROP INDEX IF EXISTS idx_clubs_name ON clubs; +DROP INDEX IF EXISTS idx_clubs_created_at ON clubs; +DROP INDEX IF EXISTS idx_clubs_updated_at ON clubs; + +-- User_Club-Tabelle +SELECT '=== ENTFERNE USER_CLUB INDEX ===' as info; +DROP INDEX IF EXISTS idx_user_club_approved ON user_club; +DROP INDEX IF EXISTS idx_user_club_created_at ON user_club; +DROP INDEX IF EXISTS idx_user_club_updated_at ON user_club; + +-- Log-Tabelle +SELECT '=== ENTFERNE LOG INDEX ===' as info; +DROP INDEX IF EXISTS idx_log_activity ON log; +DROP INDEX IF EXISTS idx_log_created_at ON log; +DROP INDEX IF EXISTS idx_log_updated_at ON log; + +-- Diary_Dates-Tabelle +SELECT '=== ENTFERNE DIARY_DATES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_dates_date ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_created_at ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_updated_at ON diary_dates; + +-- Participants-Tabelle +SELECT '=== ENTFERNE PARTICIPANTS INDEX ===' as info; +DROP INDEX IF EXISTS idx_participant_created_at ON participants; +DROP INDEX IF EXISTS idx_participant_updated_at ON participants; + +-- Activities-Tabelle +SELECT '=== ENTFERNE ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_activity_created_at ON activities; +DROP INDEX IF EXISTS idx_activity_updated_at ON activities; + +-- Member_Notes-Tabelle +SELECT '=== ENTFERNE MEMBER_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_note_created_at ON member_notes; +DROP INDEX IF EXISTS idx_member_note_updated_at ON member_notes; + +-- Diary_Notes-Tabelle +SELECT '=== ENTFERNE DIARY_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_note_created_at ON diary_notes; +DROP INDEX IF EXISTS idx_diary_note_updated_at ON diary_notes; + +-- Diary_Tags-Tabelle +SELECT '=== ENTFERNE DIARY_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_tag_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tag_updated_at ON diary_tags; + +-- Member_Diary_Tags-Tabelle +SELECT '=== ENTFERNE MEMBER_DIARY_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_diary_tag_created_at ON member_diary_tags; +DROP INDEX IF EXISTS idx_member_diary_tag_updated_at ON member_diary_tags; + +-- Diary_Date_Tags-Tabelle +SELECT '=== ENTFERNE DIARY_DATE_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_date_tag_created_at ON diary_date_tags; +DROP INDEX IF EXISTS idx_diary_date_tag_updated_at ON diary_date_tags; + +-- Diary_Member_Notes-Tabelle +SELECT '=== ENTFERNE DIARY_MEMBER_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_member_note_created_at ON diary_member_notes; +DROP INDEX IF EXISTS idx_diary_member_note_updated_at ON diary_member_notes; + +-- Predefined_Activities-Tabelle +SELECT '=== ENTFERNE PREDEFINED_ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_predefined_activity_created_at ON predefined_activities; +DROP INDEX IF EXISTS idx_predefined_activity_updated_at ON predefined_activities; + +-- Diary_Date_Activities-Tabelle +SELECT '=== ENTFERNE DIARY_DATE_ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_date_activity_created_at ON diary_date_activities; +DROP INDEX IF EXISTS idx_diary_date_activity_updated_at ON diary_date_activities; + +-- 5. Nach der Bereinigung: Status anzeigen +SELECT '=== STATUS NACH BEREINIGUNG ===' as info; + +-- Anzahl der Keys pro Tabelle nach der Bereinigung +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- Gesamtanzahl der Keys nach der Bereinigung +SELECT + COUNT(*) as total_keys_after +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 6. Zusammenfassung der Änderungen +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Cleanup abgeschlossen. Überprüfen Sie die Anzahl der Keys oben.' as message; diff --git a/backend/cleanupKeysSimple.sql b/backend/cleanupKeysSimple.sql new file mode 100644 index 0000000..d99deb5 --- /dev/null +++ b/backend/cleanupKeysSimple.sql @@ -0,0 +1,41 @@ +-- Vereinfachtes Cleanup-Script für MySQL Keys +-- Entfernt nur die problematischsten Keys + +USE trainingsdiary; + +-- 1. Alle überflüssigen INDEX entfernen (die meisten werden von Sequelize automatisch erstellt) +-- Diese entfernen die meisten Keys, die das Limit überschreiten + +-- Member-Tabelle (Hauptproblem) +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- User-Tabelle +DROP INDEX IF EXISTS idx_user_email ON user; +DROP INDEX IF EXISTS idx_user_created_at ON user; +DROP INDEX IF EXISTS idx_user_updated_at ON user; + +-- Clubs-Tabelle +DROP INDEX IF EXISTS idx_clubs_name ON clubs; +DROP INDEX IF EXISTS idx_clubs_created_at ON clubs; +DROP INDEX IF EXISTS idx_clubs_updated_at ON clubs; + +-- 2. Status anzeigen +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 3. Gesamtanzahl der Keys anzeigen +SELECT + COUNT(*) as total_keys +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; diff --git a/backend/cleanupKeysSmart.sql b/backend/cleanupKeysSmart.sql new file mode 100644 index 0000000..c860067 --- /dev/null +++ b/backend/cleanupKeysSmart.sql @@ -0,0 +1,153 @@ +-- Intelligentes Cleanup-Script für MySQL Keys +-- Überprüft zuerst die echten Tabellennamen und entfernt nur vorhandene INDEX + +USE trainingsdiary; + +-- 1. Alle vorhandenen Tabellen anzeigen +SELECT '=== VORHANDENE TABELLEN ===' as info; +SHOW TABLES; + +-- 2. Alle vorhandenen INDEX anzeigen +SELECT '=== VORHANDENE INDEX ===' as info; +SELECT + TABLE_NAME, + INDEX_NAME, + COLUMN_NAME, + NON_UNIQUE, + SEQ_IN_INDEX +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; + +-- 3. Anzahl der Keys pro Tabelle anzeigen +SELECT '=== KEYS PRO TABELLE ===' as info; +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- 4. Gesamtanzahl der Keys anzeigen +SELECT '=== GESAMTANZAHL KEYS ===' as info; +SELECT + COUNT(*) as total_keys +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 5. Intelligente INDEX-Entfernung basierend auf vorhandenen Tabellen +-- Nur INDEX entfernen, die tatsächlich existieren + +-- Member-Tabelle (Hauptproblem) +SELECT '=== ENTFERNE MEMBER INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_hashed_id ON member; +DROP INDEX IF EXISTS idx_member_first_name ON member; +DROP INDEX IF EXISTS idx_member_last_name ON member; +DROP INDEX IF EXISTS idx_member_birth_date ON member; +DROP INDEX IF EXISTS idx_member_active ON member; +DROP INDEX IF EXISTS idx_member_created_at ON member; +DROP INDEX IF EXISTS idx_member_updated_at ON member; + +-- User-Tabelle +SELECT '=== ENTFERNE USER INDEX ===' as info; +DROP INDEX IF EXISTS idx_user_email ON user; +DROP INDEX IF EXISTS idx_user_created_at ON user; +DROP INDEX IF EXISTS idx_user_updated_at ON user; + +-- Clubs-Tabelle +SELECT '=== ENTFERNE CLUBS INDEX ===' as info; +DROP INDEX IF EXISTS idx_clubs_name ON clubs; +DROP INDEX IF EXISTS idx_clubs_created_at ON clubs; +DROP INDEX IF EXISTS idx_clubs_updated_at ON clubs; + +-- User_Club-Tabelle +SELECT '=== ENTFERNE USER_CLUB INDEX ===' as info; +DROP INDEX IF EXISTS idx_user_club_approved ON user_club; +DROP INDEX IF EXISTS idx_user_club_created_at ON user_club; +DROP INDEX IF EXISTS idx_user_club_updated_at ON user_club; + +-- Log-Tabelle +SELECT '=== ENTFERNE LOG INDEX ===' as info; +DROP INDEX IF EXISTS idx_log_activity ON log; +DROP INDEX IF EXISTS idx_log_created_at ON log; +DROP INDEX IF EXISTS idx_log_updated_at ON log; + +-- Diary_Dates-Tabelle +SELECT '=== ENTFERNE DIARY_DATES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_dates_date ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_created_at ON diary_dates; +DROP INDEX IF EXISTS idx_diary_dates_updated_at ON diary_dates; + +-- Participants-Tabelle +SELECT '=== ENTFERNE PARTICIPANTS INDEX ===' as info; +DROP INDEX IF EXISTS idx_participant_created_at ON participants; +DROP INDEX IF EXISTS idx_participant_updated_at ON participants; + +-- Activities-Tabelle +SELECT '=== ENTFERNE ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_activity_created_at ON activities; +DROP INDEX IF EXISTS idx_activity_updated_at ON activities; + +-- Member_Notes-Tabelle +SELECT '=== ENTFERNE MEMBER_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_note_created_at ON member_notes; +DROP INDEX IF EXISTS idx_member_note_updated_at ON member_notes; + +-- Diary_Notes-Tabelle +SELECT '=== ENTFERNE DIARY_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_note_created_at ON diary_notes; +DROP INDEX IF EXISTS idx_diary_note_updated_at ON diary_notes; + +-- Diary_Tags-Tabelle +SELECT '=== ENTFERNE DIARY_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_tag_created_at ON diary_tags; +DROP INDEX IF EXISTS idx_diary_tag_updated_at ON diary_tags; + +-- Member_Diary_Tags-Tabelle +SELECT '=== ENTFERNE MEMBER_DIARY_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_member_diary_tag_created_at ON member_diary_tags; +DROP INDEX IF EXISTS idx_member_diary_tag_updated_at ON member_diary_tags; + +-- Diary_Date_Tags-Tabelle +SELECT '=== ENTFERNE DIARY_DATE_TAGS INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_date_tag_created_at ON diary_date_tags; +DROP INDEX IF EXISTS idx_diary_date_tag_updated_at ON diary_date_tags; + +-- Diary_Member_Notes-Tabelle +SELECT '=== ENTFERNE DIARY_MEMBER_NOTES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_member_note_created_at ON diary_member_notes; +DROP INDEX IF EXISTS idx_diary_member_note_updated_at ON diary_member_notes; + +-- Predefined_Activities-Tabelle +SELECT '=== ENTFERNE PREDEFINED_ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_predefined_activity_created_at ON predefined_activities; +DROP INDEX IF EXISTS idx_predefined_activity_updated_at ON predefined_activities; + +-- Diary_Date_Activities-Tabelle +SELECT '=== ENTFERNE DIARY_DATE_ACTIVITIES INDEX ===' as info; +DROP INDEX IF EXISTS idx_diary_date_activity_created_at ON diary_date_activities; +DROP INDEX IF EXISTS idx_diary_date_activity_updated_at ON diary_date_activities; + +-- 6. Nach der Bereinigung: Status anzeigen +SELECT '=== STATUS NACH BEREINIGUNG ===' as info; + +-- Anzahl der Keys pro Tabelle nach der Bereinigung +SELECT + TABLE_NAME, + COUNT(*) as key_count +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary' +GROUP BY TABLE_NAME +ORDER BY key_count DESC; + +-- Gesamtanzahl der Keys nach der Bereinigung +SELECT + COUNT(*) as total_keys_after_cleanup +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'trainingsdiary'; + +-- 7. Zusammenfassung +SELECT '=== ZUSAMMENFASSUNG ===' as info; +SELECT + 'Cleanup abgeschlossen. Überprüfen Sie die Anzahl der Keys oben.' as message; diff --git a/backend/controllers/trainingStatsController.js b/backend/controllers/trainingStatsController.js new file mode 100644 index 0000000..99a8823 --- /dev/null +++ b/backend/controllers/trainingStatsController.js @@ -0,0 +1,121 @@ +import { DiaryDate, Member, Participant } from '../models/index.js'; +import { Op } from 'sequelize'; + +class TrainingStatsController { + async getTrainingStats(req, res) { + try { + const { clubId } = req.params; + + // Aktuelle Datum für Berechnungen + const now = new Date(); + const twelveMonthsAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); + const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()); + + // Alle aktiven Mitglieder des Vereins laden + const members = await Member.findAll({ + where: { + active: true + } + }); + + const stats = []; + + for (const member of members) { + // Trainingsteilnahmen der letzten 12 Monate über Participant-Model + const participation12Months = await Participant.count({ + include: [{ + model: DiaryDate, + as: 'diaryDate', + where: { + clubId: parseInt(clubId), + date: { + [Op.gte]: twelveMonthsAgo + } + } + }], + where: { + memberId: member.id + } + }); + + // Trainingsteilnahmen der letzten 3 Monate über Participant-Model + const participation3Months = await Participant.count({ + include: [{ + model: DiaryDate, + as: 'diaryDate', + where: { + clubId: parseInt(clubId), + date: { + [Op.gte]: threeMonthsAgo + } + } + }], + where: { + memberId: member.id + } + }); + + // Trainingsteilnahmen insgesamt über Participant-Model + const participationTotal = await Participant.count({ + include: [{ + model: DiaryDate, + as: 'diaryDate', + where: { + clubId: parseInt(clubId) + } + }], + where: { + memberId: member.id + } + }); + + // Detaillierte Trainingsdaten (absteigend sortiert) über Participant-Model + const trainingDetails = await Participant.findAll({ + include: [{ + model: DiaryDate, + as: 'diaryDate', + where: { + clubId: parseInt(clubId) + } + }], + where: { + memberId: member.id + }, + order: [['diaryDate', 'date', 'DESC']], + limit: 50 // Begrenzen auf die letzten 50 Trainingseinheiten + }); + + // Trainingsteilnahmen für den Member formatieren + const formattedTrainingDetails = trainingDetails.map(participation => ({ + id: participation.id, + date: participation.diaryDate.date, + activityName: 'Training', + startTime: '--:--', + endTime: '--:--' + })); + + stats.push({ + id: member.id, + firstName: member.firstName, + lastName: member.lastName, + birthDate: member.birthDate, + participation12Months, + participation3Months, + participationTotal, + trainingDetails: formattedTrainingDetails + }); + } + + // Nach Gesamtteilnahme absteigend sortieren + stats.sort((a, b) => b.participationTotal - a.participationTotal); + + res.json(stats); + + } catch (error) { + console.error('Fehler beim Laden der Trainings-Statistik:', error); + res.status(500).json({ error: 'Fehler beim Laden der Trainings-Statistik' }); + } + } +} + +export default new TrainingStatsController(); diff --git a/backend/models/index.js b/backend/models/index.js index 974cec8..3f74910 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -41,6 +41,12 @@ Club.hasMany(DiaryDate, { foreignKey: 'clubId' }); DiaryDate.belongsToMany(Member, { through: Participant, as: 'participants', foreignKey: 'diaryDateId' }); Member.belongsToMany(DiaryDate, { through: Participant, as: 'diaryDates', foreignKey: 'memberId' }); +// Explizite Assoziationen für Participant +Participant.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDate' }); +Participant.belongsTo(Member, { foreignKey: 'memberId', as: 'member' }); +DiaryDate.hasMany(Participant, { foreignKey: 'diaryDateId', as: 'participantList' }); +Member.hasMany(Participant, { foreignKey: 'memberId', as: 'participantList' }); + DiaryDate.hasMany(Activity, { as: 'activities', foreignKey: 'diaryDateId' }); Activity.belongsTo(DiaryDate, { as: 'diaryDate', foreignKey: 'diaryDateId' }); diff --git a/backend/routes/trainingStatsRoutes.js b/backend/routes/trainingStatsRoutes.js new file mode 100644 index 0000000..abb2a8b --- /dev/null +++ b/backend/routes/trainingStatsRoutes.js @@ -0,0 +1,10 @@ +import express from 'express'; +import trainingStatsController from '../controllers/trainingStatsController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; +const router = express.Router(); + +router.use(authenticate); + +router.get('/:clubId', trainingStatsController.getTrainingStats); + +export default router; diff --git a/backend/server.js b/backend/server.js index 6bd8f3d..28f28f4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -30,6 +30,7 @@ import diaryDateTagRoutes from './routes/diaryDateTagRoutes.js'; import sessionRoutes from './routes/sessionRoutes.js'; import tournamentRoutes from './routes/tournamentRoutes.js'; import accidentRoutes from './routes/accidentRoutes.js'; +import trainingStatsRoutes from './routes/trainingStatsRoutes.js'; const app = express(); const port = process.env.PORT || 3000; @@ -58,9 +59,11 @@ app.use('/api/diarydatetags', diaryDateTagRoutes); app.use('/api/session', sessionRoutes); app.use('/api/tournament', tournamentRoutes); app.use('/api/accident', accidentRoutes); +app.use('/api/training-stats', trainingStatsRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); +// Catch-All Handler für Frontend-Routen (muss nach den API-Routen stehen) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); }); diff --git a/frontend/DESIGN_GUIDE.md b/frontend/DESIGN_GUIDE.md new file mode 100644 index 0000000..2210dd5 --- /dev/null +++ b/frontend/DESIGN_GUIDE.md @@ -0,0 +1,309 @@ +# TrainingsTagebuch Design Guide + +## Übersicht + +Das TrainingsTagebuch verwendet ein modernes, frisches Design mit einer konsistenten Farbpalette und modernen UI-Elementen. Alle bestehenden Farben wurden beibehalten, aber mit verbesserten Schatten, Abständen und Animationen versehen. + +## Farbpalette + +### Primärfarben +- **Primärgrün**: `#4CAF50` - Hauptfarbe für Buttons und Akzente +- **Primärgrün Hover**: `#45a049` - Hover-Zustand für Primärfarbe +- **Sekundärbraun**: `#a07040` - Sekundärfarbe für Navigation und Menüs +- **Sekundärbraun Hover**: `#804b29` - Hover-Zustand für Sekundärfarbe + +### Neutrale Farben +- **Text Primär**: `#333` - Haupttextfarbe +- **Text Sekundär**: `#666` - Sekundärtextfarbe +- **Text Hell**: `#999` - Helle Textfarbe +- **Hintergrund Hell**: `#f8f9fa` - Haupthintergrundfarbe +- **Rahmenfarbe**: `#e9ecef` - Rahmen und Trennlinien + +## Design-Tokens + +### Schatten +- **Schatten Hell**: `0 2px 8px rgba(0, 0, 0, 0.1)` +- **Schatten Mittel**: `0 4px 16px rgba(0, 0, 0, 0.15)` +- **Schatten Schwer**: `0 8px 32px rgba(0, 0, 0, 0.2)` + +### Abstände +- **Border Radius**: `8px` (Standard), `12px` (Groß) +- **Transition**: `all 0.3s cubic-bezier(0.4, 0, 0.2, 1)` + +## Typografie + +### Schriftarten +- **Primär**: Inter (mit Fallbacks zu System-Schriften) +- **Größen**: + - Basis: `16px` + - Klein: `14px` (Mobile) + - Überschriften: `1.5rem` - `2rem` + +### Zeilenhöhe +- **Standard**: `1.6` für optimale Lesbarkeit + +## Komponenten + +### Buttons + +#### Primärer Button +```html + +``` + +#### Sekundärer Button +```html + +``` + +#### Button-Varianten +- `.btn-primary` - Primärer Button (Grün) +- `.btn-secondary` - Sekundärer Button (Braun) +- `.btn-primary.small` - Kleiner Button +- `.btn-primary.large` - Großer Button +- `.btn-primary.icon` - Icon-Button (rund) + +### Karten + +#### Standard-Karte +```html +
+
+

Kartentitel

+
+
+

Karteninhalt

+
+
+``` + +### Formularelemente + +#### Input-Felder +```html + + + +``` + +### Tabellen + +#### Standard-Tabelle +```html + + + + + + + + + + + + + +
Spalte 1Spalte 2
Daten 1Daten 2
+``` + +### Alerts + +#### Verschiedene Alert-Typen +```html +
+ +
+
Erfolg!
+
Operation erfolgreich abgeschlossen.
+
+
+ +
+ +
+
Fehler!
+
Ein Fehler ist aufgetreten.
+
+
+``` + +### Badges + +#### Verschiedene Badge-Typen +```html +Primär +Sekundär +Erfolg +Gefahr +Warnung +Info +``` + +### Progress-Bars + +#### Standard-Progress-Bar +```html +
+
75%
+
+``` + +### Tabs + +#### Tab-Navigation +```html +
+ +
+ +
+

Inhalt von Tab 1

+
+``` + +### Accordion + +#### Akkordeon-Element +```html +
+
+ +
+
+

Akkordeon-Inhalt

+
+
+
+
+``` + +### Breadcrumbs + +#### Navigationspfad +```html + +``` + +### Pagination + +#### Seitenzahlen +```html + +``` + +## Utility-Klassen + +### Abstände +- `.mb-1` bis `.mb-5` - Margin Bottom +- `.mt-1` bis `.mt-5` - Margin Top +- `.p-1` bis `.p-5` - Padding +- `.gap-1` bis `.gap-5` - Gap zwischen Flexbox-Elementen + +### Ausrichtung +- `.text-center` - Text zentrieren +- `.text-left` - Text links ausrichten +- `.text-right` - Text rechts ausrichten + +### Flexbox +- `.d-flex` - Display Flex +- `.flex-column` - Flex Direction Column +- `.justify-content-center` - Zentrieren +- `.align-items-center` - Vertikal zentrieren + +### Farben +- `.text-primary` - Primärfarbe für Text +- `.text-secondary` - Sekundärfarbe für Text +- `.bg-primary` - Primärfarbe für Hintergrund +- `.bg-secondary` - Sekundärfarbe für Hintergrund + +### Schatten +- `.shadow-sm` - Kleiner Schatten +- `.shadow` - Standard-Schatten +- `.shadow-md` - Mittlerer Schatten +- `.shadow-lg` - Großer Schatten +- `.shadow-xl` - Sehr großer Schatten + +## Responsive Design + +### Breakpoints +- **Desktop**: `> 1024px` +- **Tablet**: `768px - 1024px` +- **Mobile**: `< 768px` +- **Small Mobile**: `< 480px` + +### Mobile-First Ansatz +Alle Komponenten sind mobile-first entwickelt und werden für größere Bildschirme erweitert. + +## Animationen + +### Standard-Animationen +- **Fade In**: `.fade-in` - Sanftes Einblenden +- **Slide In**: `.slide-in` - Von links einschieben +- **Hover-Effekte**: Alle interaktiven Elemente haben sanfte Hover-Übergänge + +### Transition-Zeiten +- **Standard**: `0.3s` für alle Übergänge +- **Easing**: `cubic-bezier(0.4, 0, 0.2, 1)` für natürliche Bewegungen + +## Verwendung + +### CSS-Import +```scss +// In Ihrer Vue-Komponente + +``` + +### Komponenten verwenden +Alle Komponenten sind sofort verfügbar, sobald die CSS-Dateien importiert sind. Es sind keine zusätzlichen JavaScript-Bibliotheken erforderlich. + +## Best Practices + +1. **Konsistenz**: Verwenden Sie immer die vordefinierten Farben und Abstände +2. **Accessibility**: Alle interaktiven Elemente haben Hover- und Focus-States +3. **Performance**: Animationen verwenden CSS-Transforms und Opacity für optimale Performance +4. **Mobile**: Testen Sie alle Komponenten auf verschiedenen Bildschirmgrößen + +## Browser-Support + +- **Modern**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ +- **Fallbacks**: Ältere Browser erhalten eine funktionale, aber weniger animierte Version diff --git a/frontend/index.html b/frontend/index.html index c922af3..ffd6d36 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,6 @@ Trainingstagebuch -

Trainingstagebuch

diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5221c58..ba02e5b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,41 +1,80 @@ @@ -48,7 +87,6 @@ export default { data() { return { selectedClub: null, - isMenuOpen: false, sessionInterval: null, }; }, @@ -75,10 +113,6 @@ export default { this.$router.push(`/showclub/${this.currentClub}`); }, - toggleMenu() { - this.isMenuOpen = !this.isMenuOpen; - }, - async checkSession() { try { const response = await apiClient.get('/session/status'); @@ -86,8 +120,9 @@ export default { this.handleLogout(); } } catch (error) { - console.error('Session check failed:', error); - this.handleLogout(); + this.isAuthenticated = false; + this.username = ''; + this.currentClub = ''; } }, @@ -119,159 +154,343 @@ export default { diff --git a/frontend/src/assets/css/components.scss b/frontend/src/assets/css/components.scss new file mode 100644 index 0000000..2e28291 --- /dev/null +++ b/frontend/src/assets/css/components.scss @@ -0,0 +1,500 @@ +/* Moderne UI-Komponenten für TrainingsTagebuch */ + +/* Alert-Komponenten */ +.alert { + padding: 0.75rem 1.25rem; + margin: 0.75rem 0; + border: 1px solid transparent; + border-radius: var(--border-radius); + position: relative; + display: flex; + align-items: flex-start; + gap: 0.625rem; +} + +.alert-icon { + font-size: 1.125rem; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.alert-content { + flex: 1; +} + +.alert-title { + font-weight: 600; + margin: 0 0 0.125rem 0; + font-size: 0.9rem; +} + +.alert-message { + margin: 0; + line-height: 1.4; +} + +.alert-success { + background-color: rgba(40, 167, 69, 0.08); + border-color: rgba(40, 167, 69, 0.25); + color: #155724; +} + +.alert-info { + background-color: rgba(23, 162, 184, 0.08); + border-color: rgba(23, 162, 184, 0.25); + color: #0c5460; +} + +.alert-warning { + background-color: rgba(255, 193, 7, 0.08); + border-color: rgba(255, 193, 7, 0.25); + color: #856404; +} + +.alert-danger { + background-color: rgba(220, 53, 69, 0.08); + border-color: rgba(220, 53, 69, 0.25); + color: #721c24; +} + +/* Badge-Komponenten */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.625rem; + font-size: 0.7rem; + font-weight: 500; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 9999px; + background-color: var(--text-light); + color: white; +} + +.badge-primary { + background-color: var(--primary-color); +} + +.badge-secondary { + background-color: var(--secondary-color); +} + +.badge-success { + background-color: #28a745; +} + +.badge-danger { + background-color: #dc3545; +} + +.badge-warning { + background-color: #ffc107; + color: #212529; +} + +.badge-info { + background-color: #17a2b8; +} + +.badge-light { + background-color: #f8f9fa; + color: #212529; +} + +.badge-dark { + background-color: #343a40; +} + +/* Progress-Bar */ +.progress { + height: 0.625rem; + background-color: #e9ecef; + border-radius: var(--border-radius); + overflow: hidden; + margin: 0.75rem 0; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); + transition: width 0.5s ease; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 0.7rem; + font-weight: 500; +} + +/* Tooltip */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .tooltip-text { + visibility: hidden; + width: 180px; + background-color: #333; + color: white; + text-align: center; + border-radius: var(--border-radius); + padding: 0.375rem; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -90px; + opacity: 0; + transition: opacity 0.25s; + font-size: 0.8rem; + line-height: 1.3; +} + +.tooltip .tooltip-text::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -4px; + border-width: 4px; + border-style: solid; + border-color: #333 transparent transparent transparent; +} + +.tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* Modal-ähnliche Overlays */ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + opacity: 0; + visibility: hidden; + transition: all 0.25s ease; +} + +.overlay.active { + opacity: 1; + visibility: visible; +} + +.overlay-content { + background: white; + border-radius: var(--border-radius-large); + padding: 1.5rem; + max-width: 90vw; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-heavy); + transform: scale(0.9); + transition: transform 0.25s ease; +} + +.overlay.active .overlay-content { + transform: scale(1); +} + +/* Skeleton Loading */ +.skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: var(--border-radius); +} + +.skeleton-text { + height: 0.875rem; + margin-bottom: 0.375rem; +} + +.skeleton-text:last-child { + width: 60%; +} + +.skeleton-avatar { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; +} + +.skeleton-button { + height: 2.25rem; + width: 7rem; +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* Tabs */ +.tabs { + border-bottom: 1px solid var(--border-color); + margin-bottom: 1.5rem; +} + +.tab-list { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 0.375rem; +} + +.tab-item { + margin: 0; +} + +.tab-button { + background: none; + border: none; + padding: 0.625rem 1.25rem; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: var(--transition); + font-weight: 500; + min-height: auto; + margin: 0; + box-shadow: none; +} + +.tab-button:hover { + color: var(--primary-color); + transform: none; + box-shadow: none; +} + +.tab-button.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Accordion */ +.accordion { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + overflow: hidden; +} + +.accordion-item { + border-bottom: 1px solid var(--border-color); +} + +.accordion-item:last-child { + border-bottom: none; +} + +.accordion-header { + background: none; + border: none; + width: 100%; + padding: 0.875rem 1.25rem; + text-align: left; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 500; + color: var(--text-primary); + transition: var(--transition); + min-height: auto; + margin: 0; + box-shadow: none; +} + +.accordion-header:hover { + background-color: rgba(76, 175, 80, 0.04); + transform: none; + box-shadow: none; +} + +.accordion-icon { + transition: transform 0.25s ease; + font-size: 1.125rem; + color: var(--text-light); +} + +.accordion-header.active .accordion-icon { + transform: rotate(180deg); +} + +.accordion-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease; +} + +.accordion-content.active { + max-height: 400px; +} + +.accordion-body { + padding: 0 1.25rem 1.25rem; + color: var(--text-secondary); + line-height: 1.5; +} + +/* Breadcrumb */ +.breadcrumb { + display: flex; + align-items: center; + gap: 0.375rem; + margin: 0.75rem 0; + font-size: 0.8rem; + color: var(--text-light); +} + +.breadcrumb-item { + display: flex; + align-items: center; +} + +.breadcrumb-separator { + margin: 0 0.375rem; + color: var(--text-light); +} + +.breadcrumb-link { + color: var(--primary-color); + text-decoration: none; + transition: var(--transition); +} + +.breadcrumb-link:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +.breadcrumb-current { + color: var(--text-secondary); + font-weight: 500; +} + +/* Pagination */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 0.125rem; + margin: 1.5rem 0; +} + +.page-item { + list-style: none; + margin: 0; +} + +.page-link { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border: 1px solid var(--border-color); + background: white; + color: var(--text-primary); + text-decoration: none; + border-radius: var(--border-radius); + transition: var(--transition); + font-weight: 500; +} + +.page-link:hover { + background-color: var(--primary-color); + color: white; + border-color: var(--primary-color); + transform: translateY(-1px); + box-shadow: var(--shadow-light); +} + +.page-link.active { + background-color: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.page-link.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* Responsive Design für Komponenten */ +@media (max-width: 768px) { + .overlay-content { + margin: 0.75rem; + padding: 1.25rem; + } + + .tab-list { + flex-wrap: wrap; + } + + .tab-button { + padding: 0.5rem 0.875rem; + font-size: 0.8rem; + } + + .accordion-header { + padding: 0.75rem 1rem; + } + + .accordion-body { + padding: 0 1rem 1rem; + } + + .pagination { + gap: 0.125rem; + } + + .page-link { + width: 2rem; + height: 2rem; + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .alert { + padding: 0.625rem 0.875rem; + flex-direction: column; + align-items: flex-start; + gap: 0.375rem; + } + + .tooltip .tooltip-text { + width: 140px; + margin-left: -70px; + } + + .overlay-content { + margin: 0.375rem; + padding: 0.875rem; + } +} diff --git a/frontend/src/assets/css/main.scss b/frontend/src/assets/css/main.scss index c90c0f0..52b7fe1 100644 --- a/frontend/src/assets/css/main.scss +++ b/frontend/src/assets/css/main.scss @@ -1,69 +1,529 @@ +/* Import der Komponenten */ +@import './components.scss'; + +/* Modernes, frisches Design für TrainingsTagebuch */ +:root { + /* Bestehende Farben beibehalten */ + --primary-color: #4CAF50; + --primary-hover: #45a049; + --secondary-color: #a07040; + --secondary-hover: #804b29; + --danger-color: #dc3545; + --danger-hover: #c82333; + --nav-bg: #e0f0e8; + --text-primary: #333; + --text-secondary: #666; + --text-light: #999; + --text-muted: #888; + --bg-light: #f8f9fa; + --border-color: #e9ecef; + --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-medium: 0 2px 8px rgba(0, 0, 0, 0.12); + --shadow-heavy: 0 4px 16px rgba(0, 0, 0, 0.15); + --border-radius: 6px; + --border-radius-large: 8px; + --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + html, body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); + background-color: var(--bg-light); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + body { - display: flex; - flex-direction: column; -} -h1 { - margin: 0; - height: 3rem; - padding: 0 0.5rem; - text-align: center; - background-color: #f0f0f0; - color: #4CAF50; - text-shadow: 2px 2px 3px #a0a0a0; + display: flex; + flex-direction: column; } + h2 { - margin: 0; + margin: 0 0 0.75rem 0; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary); + position: relative; + padding-bottom: 0.375rem; } + +h2::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 2.5rem; + height: 2px; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + border-radius: 1px; +} + +h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 1.25rem 0 0.5rem 0; +} + #app { - flex: 1; - width: 100%; - height: 100%; - overflow: hidden; + flex: 1; + width: 100%; + height: 100%; + overflow: hidden; } + +/* Kompaktere Button-Styles */ button { - background-color: #4CAF50; - color: white; - border: 1px solid #4CAF50; - border-radius: 0; - padding: 2px 5px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; - cursor: pointer; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s ease; - margin: 2px; + background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + color: white; + border: none; + border-radius: var(--border-radius); + padding: 0.5rem 1rem; + text-align: center; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + box-shadow: var(--shadow-light); + transition: var(--transition); + margin: 0.125rem; + position: relative; + overflow: hidden; + min-height: 2.25rem; +} + +button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + transition: left 0.4s; +} + +button:hover::before { + left: 100%; } button:hover { - background-color: #45a049; + transform: translateY(-1px); + box-shadow: var(--shadow-medium); +} + +button:active { + transform: translateY(0); + box-shadow: var(--shadow-light); } button.cancel-action { - background-color: white; - color: #4CAF50; - border: 1px solid #4CAF50; - border-radius: 0; - padding: 2px 5px; text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; - cursor: pointer; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s ease, color 0.3s ease; + background: white; + color: var(--primary-color); + border: 1.5px solid var(--primary-color); + box-shadow: var(--shadow-light); } button.cancel-action:hover { - background-color: #f2f2f2; - color: #45a049; + background: var(--primary-color); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-medium); } + +/* Mülleimer-Buttons (Delete-Buttons) */ +button.delete-btn, +button[onclick*="delete"], +button[onclick*="remove"] { + background: white; + color: var(--danger-color); + border: 1.5px solid var(--danger-color); + box-shadow: var(--shadow-light); + transition: var(--transition); +} + +button.delete-btn:hover, +button[onclick*="delete"]:hover, +button[onclick*="remove"]:hover { + background: var(--danger-color); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-medium); +} + +/* Spezielle Styles für Mülleimer-Symbol */ +button.trash-btn { + background: white !important; + color: var(--danger-color) !important; + border: 1.5px solid var(--danger-color) !important; + box-shadow: var(--shadow-light); + transition: var(--transition); + min-width: 2rem; + min-height: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1rem; +} + +button.trash-btn:hover { + background: var(--danger-color) !important; + color: white !important; + transform: translateY(-1px); + box-shadow: var(--shadow-medium); +} + +/* Sekundäre Buttons */ +button.secondary { + background: linear-gradient(135deg, var(--secondary-color), var(--secondary-hover)); +} + +button.secondary:hover { + box-shadow: var(--shadow-medium); +} + +/* Kleine Buttons */ +button.small { + padding: 0.375rem 0.75rem; + font-size: 0.8rem; + min-height: 1.875rem; +} + +/* Große Buttons */ +button.large { + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + min-height: 2.75rem; +} + +/* Icon-Buttons */ +button.icon { + width: 2.25rem; + height: 2.25rem; + padding: 0; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Kompaktere Form-Elemente */ +input, select, textarea { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1.5px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 0.85rem; + transition: var(--transition); + background: white; + color: var(--text-primary); + box-sizing: border-box; +} + +/* Spezielle Styles für Checkboxen */ +input[type="checkbox"] { + width: auto; + min-width: 1rem; + height: 1rem; + margin: 0 0.5rem 0 0; + padding: 0; + border: 1.5px solid var(--border-color); + border-radius: 3px; + background: white; + cursor: pointer; + flex-shrink: 0; + vertical-align: middle; +} + +input[type="checkbox"]:checked { + background: var(--primary-color); + border-color: var(--primary-color); + position: relative; +} + +input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 0.75rem; + font-weight: bold; +} + +/* Label-Styles für Checkboxen */ +label { + display: inline-flex; + align-items: center; + cursor: pointer; + margin: 0.25rem 0; + font-size: 0.85rem; + color: var(--text-primary); + line-height: 1.4; + vertical-align: middle; +} + +label span { + margin-right: 0.5rem; +} + +/* Checkbox-Container für bessere Ausrichtung */ +.checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; +} + +.checkbox-item { + display: inline-flex; + align-items: center; + margin: 0.25rem 0; + vertical-align: middle; +} + +/* Spezielle Anpassungen für Listen mit Checkboxen */ +ul li.checkbox-item { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.25rem 0; +} + +ul li.checkbox-item label { + margin: 0; + flex: 1; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1); +} + +input:hover, select:hover, textarea:hover { + border-color: var(--primary-color); +} + +/* Kompaktere Karten */ +.card { + background: white; + border-radius: var(--border-radius-large); + padding: 1rem; + margin: 0.75rem 0; + box-shadow: var(--shadow-light); + border: 1px solid var(--border-color); + transition: var(--transition); +} + +.card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); +} + +.card-header { + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.75rem; + margin-bottom: 0.75rem; +} + +.card-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.card-body { + color: var(--text-secondary); + line-height: 1.5; +} + +/* Kompaktere Tabellen */ +table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow-light); + margin: 0.75rem 0; +} + +th, td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + color: white; + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.4px; +} + +tr:hover { + background-color: rgba(76, 175, 80, 0.03); +} + +/* Kompaktere Listen */ +ul, ol { + padding-left: 1.25rem; +} + +li { + margin: 0.375rem 0; + color: var(--text-secondary); +} + +/* Links */ +a { + color: var(--primary-color); + text-decoration: none; + transition: var(--transition); + position: relative; +} + +a:hover { + color: var(--primary-hover); +} + +a::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 0; + height: 1px; + background: var(--primary-color); + transition: width 0.25s ease; +} + +a:hover::after { + width: 100%; +} + +/* Utility-Klassen */ .pointer { - cursor: pointer; + cursor: pointer; +} + +.text-center { + text-align: center; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.mb-1 { margin-bottom: 0.375rem; } +.mb-2 { margin-bottom: 0.75rem; } +.mb-3 { margin-bottom: 1.125rem; } +.mb-4 { margin-bottom: 1.5rem; } + +.mt-1 { margin-top: 0.375rem; } +.mt-2 { margin-top: 0.75rem; } +.mt-3 { margin-top: 1.125rem; } +.mt-4 { margin-top: 1.5rem; } + +.p-1 { padding: 0.375rem; } +.p-2 { padding: 0.75rem; } +.p-3 { padding: 1.125rem; } +.p-4 { padding: 1.5rem; } + +/* Responsive Design */ +@media (max-width: 768px) { + html { + font-size: 13px; + } + + button { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + } + + .card { + padding: 0.75rem; + margin: 0.5rem 0; + } + + h1 { + height: 2.75rem; + font-size: 1.125rem; + } + + h2 { + font-size: 1.375rem; + } +} + +/* Animationen */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.5s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +.slide-in { + animation: slideIn 0.3s ease-out; +} + +/* Loading-States */ +.loading { + opacity: 0.6; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + border: 2px solid var(--primary-color); + border-top: 2px solid transparent; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } \ No newline at end of file diff --git a/frontend/src/components/PDFGenerator.js b/frontend/src/components/PDFGenerator.js index 8f7a0aa..63e3273 100644 --- a/frontend/src/components/PDFGenerator.js +++ b/frontend/src/components/PDFGenerator.js @@ -243,7 +243,6 @@ class PDFGenerator { addTable(tableId, highlightName = '') { this.pdf.setFontSize(11); - console.log(highlightName); autoTable(this.pdf, { html: `#${tableId}`, startY: this.cursorY, diff --git a/frontend/src/router.js b/frontend/src/router.js index f966c87..cace9c4 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -10,6 +10,7 @@ import DiaryView from './views/DiaryView.vue'; import PendingApprovalsView from './views/PendingApprovalsView.vue'; import ScheduleView from './views/ScheduleView.vue'; import TournamentsView from './views/TournamentsView.vue'; +import TrainingStatsView from './views/TrainingStatsView.vue'; const routes = [ { path: '/register', component: Register }, @@ -23,6 +24,7 @@ const routes = [ { path: '/pending-approvals', component: PendingApprovalsView}, { path: '/schedule', component: ScheduleView}, { path: '/tournaments', component: TournamentsView }, + { path: '/training-stats', component: TrainingStatsView }, ]; const router = createRouter({ diff --git a/frontend/src/store.js b/frontend/src/store.js index 6f4537c..e71fabc 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -11,8 +11,7 @@ const store = createStore({ try { return JSON.parse(localStorage.getItem('clubs')) || []; } catch (e) { - console.error('Error parsing clubs from localStorage:', e); - return []; + this.clubs = []; } })(), }, diff --git a/frontend/src/style.css b/frontend/src/style.css index bb131d6..533af43 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,79 +1,232 @@ +/* Globale Styles für TrainingsTagebuch */ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - + color-scheme: light; + color: #333; + background-color: #f8f9fa; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +* { + box-sizing: border-box; } body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; + padding: 0; font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; + background-color: inherit; + color: inherit; + line-height: inherit; } #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + width: 100%; + height: 100vh; + margin: 0; + padding: 0; + text-align: left; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +/* Utility-Klassen für das neue Design */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.mb-0 { margin-bottom: 0; } +.mb-1 { margin-bottom: 0.25rem; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-3 { margin-bottom: 1rem; } +.mb-4 { margin-bottom: 1.5rem; } +.mb-5 { margin-bottom: 3rem; } + +.mt-0 { margin-top: 0; } +.mt-1 { margin-top: 0.25rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-3 { margin-top: 1rem; } +.mt-4 { margin-top: 1.5rem; } +.mt-5 { margin-top: 3rem; } + +.p-0 { padding: 0; } +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 1rem; } +.p-4 { padding: 1.5rem; } +.p-5 { padding: 3rem; } + +/* Responsive Container */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.container-fluid { + width: 100%; + padding: 0 1rem; +} + +/* Grid System */ +.row { + display: flex; + flex-wrap: wrap; + margin: 0 -0.5rem; +} + +.col { + flex: 1; + padding: 0 0.5rem; +} + +.col-12 { flex: 0 0 100%; } +.col-6 { flex: 0 0 50%; } +.col-4 { flex: 0 0 33.333333%; } +.col-3 { flex: 0 0 25%; } + +@media (max-width: 768px) { + .col-md-12 { flex: 0 0 100%; } + .col-md-6 { flex: 0 0 50%; } + .col-md-4 { flex: 0 0 33.333333%; } + .col-md-3 { flex: 0 0 25%; } +} + +@media (max-width: 480px) { + .col-sm-12 { flex: 0 0 100%; } + .col-sm-6 { flex: 0 0 50%; } + .col-sm-4 { flex: 0 0 33.333333%; } + .col-sm-3 { flex: 0 0 25%; } +} + +/* Flexbox Utilities */ +.d-flex { display: flex; } +.d-inline-flex { display: inline-flex; } +.d-block { display: block; } +.d-inline-block { display: inline-block; } +.d-none { display: none; } + +.flex-row { flex-direction: row; } +.flex-column { flex-direction: column; } +.flex-wrap { flex-wrap: wrap; } +.flex-nowrap { flex-wrap: nowrap; } + +.justify-content-start { justify-content: flex-start; } +.justify-content-center { justify-content: center; } +.justify-content-end { justify-content: flex-end; } +.justify-content-between { justify-content: space-between; } +.justify-content-around { justify-content: space-around; } + +.align-items-start { align-items: flex-start; } +.align-items-center { align-items: center; } +.align-items-end { align-items: flex-end; } +.align-items-stretch { align-items: stretch; } + +.flex-1 { flex: 1; } +.flex-auto { flex: auto; } +.flex-none { flex: none; } + +/* Spacing Utilities */ +.gap-0 { gap: 0; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-3 { gap: 1rem; } +.gap-4 { gap: 1.5rem; } +.gap-5 { gap: 3rem; } + +/* Border Utilities */ +.border { border: 1px solid #e9ecef; } +.border-top { border-top: 1px solid #e9ecef; } +.border-right { border-right: 1px solid #e9ecef; } +.border-bottom { border-bottom: 1px solid #e9ecef; } +.border-left { border-left: 1px solid #e9ecef; } + +.border-0 { border: 0; } +.rounded { border-radius: 0.375rem; } +.rounded-sm { border-radius: 0.25rem; } +.rounded-lg { border-radius: 0.5rem; } +.rounded-xl { border-radius: 0.75rem; } + +/* Shadow Utilities */ +.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } +.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } +.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } +.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } +.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } + +/* Position Utilities */ +.position-relative { position: relative; } +.position-absolute { position: absolute; } +.position-fixed { position: fixed; } +.position-sticky { position: sticky; } + +/* Overflow Utilities */ +.overflow-hidden { overflow: hidden; } +.overflow-auto { overflow: auto; } +.overflow-scroll { overflow: scroll; } + +/* Cursor Utilities */ +.cursor-pointer { cursor: pointer; } +.cursor-default { cursor: default; } +.cursor-not-allowed { cursor: not-allowed; } + +/* Text Utilities */ +.text-primary { color: #4CAF50; } +.text-secondary { color: #a07040; } +.text-success { color: #28a745; } +.text-danger { color: #dc3545; } +.text-warning { color: #ffc107; } +.text-info { color: #17a2b8; } +.text-light { color: #6c757d; } +.text-dark { color: #343a40; } +.text-muted { color: #6c757d; } + +.text-white { color: #ffffff; } +.text-black { color: #000000; } + +.font-weight-light { font-weight: 300; } +.font-weight-normal { font-weight: 400; } +.font-weight-medium { font-weight: 500; } +.font-weight-semibold { font-weight: 600; } +.font-weight-bold { font-weight: 700; } + +.font-size-sm { font-size: 0.875rem; } +.font-size-base { font-size: 1rem; } +.font-size-lg { font-size: 1.125rem; } +.font-size-xl { font-size: 1.25rem; } + +/* Background Utilities */ +.bg-primary { background-color: #4CAF50; } +.bg-secondary { background-color: #a07040; } +.bg-success { background-color: #28a745; } +.bg-danger { background-color: #dc3545; } +.bg-warning { background-color: #ffc107; } +.bg-info { background-color: #17a2b8; } +.bg-light { background-color: #f8f9fa; } +.bg-dark { background-color: #343a40; } +.bg-white { background-color: #ffffff; } +.bg-transparent { background-color: transparent; } + +/* Responsive Utilities */ +@media (max-width: 768px) { + .d-md-none { display: none; } + .d-md-block { display: block; } + .d-md-flex { display: flex; } +} + +@media (max-width: 480px) { + .d-sm-none { display: none; } + .d-sm-block { display: block; } + .d-sm-flex { display: flex; } +} + +/* Print Utilities */ +@media print { + .d-print-none { display: none; } + .d-print-block { display: block; } + .d-print-flex { display: flex; } } diff --git a/frontend/src/views/Activate.vue b/frontend/src/views/Activate.vue index 0f946e3..48aade3 100644 --- a/frontend/src/views/Activate.vue +++ b/frontend/src/views/Activate.vue @@ -17,8 +17,7 @@ alert('Account activated! You can now log in.'); this.$router.push('/login'); } catch (error) { - console.error(error); - alert('Activation failed.'); + alert('Aktivierung fehlgeschlagen'); } }, }, diff --git a/frontend/src/views/ClubView.vue b/frontend/src/views/ClubView.vue index d582e69..ef2691a 100644 --- a/frontend/src/views/ClubView.vue +++ b/frontend/src/views/ClubView.vue @@ -48,8 +48,7 @@ export default { this.club = response.data; this.accessAllowed = true; } catch (error) { - console.error("Zugriff auf den Verein nicht gestattet", error); - this.accessAllowed = false; + alert('Zugriff auf den Verein nicht gestattet'); } }, async loadOpenRequests() { @@ -57,7 +56,7 @@ export default { const response = await apiClient.get(`/clubmembers/notapproved/${this.currentClub}`); this.openRequests = response.data; } catch (error) { - console.error("Fehler beim Laden der offenen Anfragen", error); + alert('Fehler beim Laden der offenen Anfragen'); } }, async requestAccess() { diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 0f6f24e..b153d5c 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -112,7 +112,7 @@ v-if="item.durationText && item.durationText.trim() !== ''"> ({{ item.durationText }}) - + - @@ -102,8 +102,8 @@ - @@ -190,8 +190,8 @@
-
@@ -448,13 +448,11 @@ export default { ) { return `${x}:${y}`; } - console.warn('Ungültiges Satz-Ergebnis:', s); return null; } const num = Number(s); if (isNaN(num)) { - console.warn('Ungültiges Ergebnisformat:', raw); return null; } @@ -594,7 +592,7 @@ export default { ); const updated = allRes.data.find(m2 => m2.id === match.id); if (!updated) { - console.error('Konnte aktualisiertes Match nicht finden'); + alert('Fehler beim Aktualisieren des Matches'); return; } match.tournamentResults = updated.tournamentResults || []; @@ -785,8 +783,7 @@ export default { }); await this.loadTournamentData(); } catch (err) { - console.error('Reset KO failed:', err); - alert(err.response?.data?.error || err.message); + alert('Fehler beim Zurücksetzen der K.o.-Runde'); } } } diff --git a/frontend/src/views/TrainingStatsView.vue b/frontend/src/views/TrainingStatsView.vue new file mode 100644 index 0000000..75609db --- /dev/null +++ b/frontend/src/views/TrainingStatsView.vue @@ -0,0 +1,427 @@ + + + + + diff --git a/package-lock.json b/package-lock.json index aa0232e..be8b2c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@babel/cli": "^7.24.8", "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", + "concurrently": "^8.2.2", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1" @@ -2269,6 +2270,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2303,6 +2319,50 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -2398,6 +2458,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2490,6 +2567,13 @@ "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==", "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -3198,6 +3282,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3668,6 +3762,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -4566,6 +4670,16 @@ "node": ">=6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4645,6 +4759,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -4779,6 +4903,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -4865,6 +5002,27 @@ "node": ">=6" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -5003,6 +5161,16 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -5029,6 +5197,13 @@ "json5": "lib/cli.js" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5428,18 +5603,75 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index f1d30b5..e7f3845 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "dev": "concurrently \"cd backend && npm run dev\" \"cd frontend && npm run dev\"", + "dev:backend": "cd backend && npm run dev", + "dev:frontend": "cd frontend && npm run dev", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -13,6 +16,7 @@ "@babel/cli": "^7.24.8", "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", + "concurrently": "^8.2.2", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1"