Compare commits
77 Commits
accident
...
spielplaen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
144034a305 | ||
|
|
09ffd1db3d | ||
|
|
d90acf43e1 | ||
|
|
adb93af906 | ||
|
|
a36f0ea446 | ||
|
|
e4fcf2eca2 | ||
|
|
0ee16c7766 | ||
|
|
21c19298da | ||
|
|
3c65fed994 | ||
|
|
66046ddccd | ||
|
|
561d8186d3 | ||
|
|
312f8f24ab | ||
|
|
ba4b56360d | ||
|
|
02732a01da | ||
|
|
4307fa7d82 | ||
|
|
a1dc6afb2c | ||
|
|
92ce64b807 | ||
|
|
296939d1a0 | ||
|
|
dc8a5778d6 | ||
|
|
cf04e5bfe8 | ||
|
|
ace15ae1d3 | ||
|
|
d4b82a3a6f | ||
|
|
48cd0921df | ||
|
|
df02e48cfd | ||
|
|
4a6d868820 | ||
|
|
52556a4292 | ||
|
|
3a02ffb3e3 | ||
|
|
c4b9a7d782 | ||
|
|
5e8b221541 | ||
|
|
26720c8df3 | ||
|
|
a1ab742126 | ||
|
|
f21ad3d8a3 | ||
|
|
51d3087006 | ||
|
|
a08588a075 | ||
|
|
5d67a52b45 | ||
|
|
f29425c987 | ||
|
|
e3b8488d2b | ||
|
|
f49e1896b9 | ||
|
|
2092473cf3 | ||
|
|
c00849a154 | ||
|
|
8069946154 | ||
|
|
975800c1ab | ||
|
|
b82a80a11d | ||
|
|
244b61c901 | ||
|
|
c7325ac982 | ||
|
|
8fbdc68016 | ||
|
|
455b2c94cd | ||
|
|
c9a1026b50 | ||
|
|
f6f1ea0403 | ||
|
|
a636b32510 | ||
|
|
8ee1203ec6 | ||
|
|
bce5150757 | ||
|
|
117f6b4c93 | ||
|
|
6a8b0e35d7 | ||
|
|
ed96fc5f27 | ||
|
|
8bd05e4e38 | ||
|
|
e827964688 | ||
|
|
353b8386ee | ||
|
|
ad2ab3cae8 | ||
|
|
f5deb343a8 | ||
|
|
4122868ab0 | ||
|
|
eba160c43d | ||
|
|
39089a70d3 | ||
|
|
d0544da1ba | ||
|
|
b6dd39dda3 | ||
|
|
f3a4159536 | ||
|
|
69b4302e23 | ||
|
|
68725af630 | ||
|
|
f753d45e17 | ||
|
|
549147cfb3 | ||
|
|
81cf94cebc | ||
|
|
9f17f2399a | ||
|
|
9ba39f9f47 | ||
|
|
f935c72f56 | ||
|
|
f29185dd33 | ||
|
|
821f9d24f5 | ||
|
|
df41720b50 |
139
backend/README_CLEANUP.md
Normal file
139
backend/README_CLEANUP.md
Normal file
@@ -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
|
||||
46
backend/checkRealIndexes.sql
Normal file
46
backend/checkRealIndexes.sql
Normal file
@@ -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;
|
||||
35
backend/checkTableNames.sql
Normal file
35
backend/checkTableNames.sql
Normal file
@@ -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;
|
||||
185
backend/cleanupKeys.sql
Normal file
185
backend/cleanupKeys.sql
Normal file
@@ -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;
|
||||
123
backend/cleanupKeysAggressive.sql
Normal file
123
backend/cleanupKeysAggressive.sql
Normal file
@@ -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;
|
||||
152
backend/cleanupKeysCorrected.sql
Normal file
152
backend/cleanupKeysCorrected.sql
Normal file
@@ -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';
|
||||
100
backend/cleanupKeysFinal.sql
Normal file
100
backend/cleanupKeysFinal.sql
Normal file
@@ -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;
|
||||
125
backend/cleanupKeysIntelligent.sql
Normal file
125
backend/cleanupKeysIntelligent.sql
Normal file
@@ -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;
|
||||
79
backend/cleanupKeysMinimal.sql
Normal file
79
backend/cleanupKeysMinimal.sql
Normal file
@@ -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;
|
||||
143
backend/cleanupKeysNode.cjs
Normal file
143
backend/cleanupKeysNode.cjs
Normal file
@@ -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);
|
||||
});
|
||||
|
||||
166
backend/cleanupKeysNode.js
Normal file
166
backend/cleanupKeysNode.js
Normal file
@@ -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);
|
||||
});
|
||||
149
backend/cleanupKeysReal.sql
Normal file
149
backend/cleanupKeysReal.sql
Normal file
@@ -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;
|
||||
41
backend/cleanupKeysSimple.sql
Normal file
41
backend/cleanupKeysSimple.sql
Normal file
@@ -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';
|
||||
153
backend/cleanupKeysSmart.sql
Normal file
153
backend/cleanupKeysSmart.sql
Normal file
@@ -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;
|
||||
@@ -1,4 +1,7 @@
|
||||
import { register, activateUser, login, logout } from '../services/authService.js';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import User from '../models/User.js'; // ggf. Pfad anpassen
|
||||
|
||||
const registerUser = async (req, res, next) => {
|
||||
try {
|
||||
@@ -30,14 +33,14 @@ const loginUser = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logoutUser = async(req, res) => {
|
||||
const { userid: userId, authtoken: authToken } = req.headers;
|
||||
const logoutUser = async (req, res, next) => {
|
||||
try {
|
||||
logout(userId, authToken);
|
||||
const token = req.headers['authorization']?.split(' ')[1];
|
||||
const result = await logout(token);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
res.status(401).json({ msg: 'not found' });
|
||||
next(error);
|
||||
}
|
||||
res.status(200).json({ msg: 'loggedout' });
|
||||
}
|
||||
};
|
||||
|
||||
export { registerUser, activate, loginUser, logoutUser };
|
||||
|
||||
@@ -116,3 +116,15 @@ const deleteTagFromDiaryDate = async (req, res) => {
|
||||
|
||||
export { getDatesForClub, createDateForClub, updateTrainingTimes, addDiaryNote, deleteDiaryNote, addDiaryTag,
|
||||
addTagToDiaryDate, deleteTagFromDiaryDate };
|
||||
|
||||
export const deleteDateForClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId, dateId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const result = await diaryService.removeDateForClub(userToken, clubId, dateId);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[deleteDateForClub] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message || 'systemerror' });
|
||||
}
|
||||
};
|
||||
|
||||
52
backend/controllers/diaryMemberActivityController.js
Normal file
52
backend/controllers/diaryMemberActivityController.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
|
||||
import Participant from '../models/Participant.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
|
||||
export const getMembersForActivity = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, diaryDateActivityId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const list = await DiaryMemberActivity.findAll({ where: { diaryDateActivityId } });
|
||||
res.status(200).json(list);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error fetching members for activity' });
|
||||
}
|
||||
};
|
||||
|
||||
export const addMembersToActivity = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, diaryDateActivityId } = req.params;
|
||||
const { participantIds } = req.body; // array of participant ids
|
||||
await checkAccess(userToken, clubId);
|
||||
const validParticipants = await Participant.findAll({ where: { id: participantIds } });
|
||||
const validIds = new Set(validParticipants.map(p => p.id));
|
||||
const created = [];
|
||||
for (const pid of participantIds) {
|
||||
if (!validIds.has(pid)) continue;
|
||||
const existing = await DiaryMemberActivity.findOne({ where: { diaryDateActivityId, participantId: pid } });
|
||||
if (!existing) {
|
||||
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
|
||||
created.push(rec);
|
||||
}
|
||||
}
|
||||
res.status(201).json(created);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error adding members to activity' });
|
||||
}
|
||||
};
|
||||
|
||||
export const removeMemberFromActivity = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, diaryDateActivityId, participantId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
|
||||
res.status(200).json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error removing member from activity' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ const getWaitingApprovals = async(req, res) => {
|
||||
const setClubMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
|
||||
testMembership, picsInInternetAllowed } = req.body;
|
||||
testMembership, picsInInternetAllowed, gender } = req.body;
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
|
||||
phone, email, active, testMembership, picsInInternetAllowed);
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender);
|
||||
res.status(addResult.status || 500).json(addResult.response);
|
||||
} catch (error) {
|
||||
console.error('[setClubMembers] - Error:', error);
|
||||
|
||||
619
backend/controllers/officialTournamentController.js
Normal file
619
backend/controllers/officialTournamentController.js
Normal file
@@ -0,0 +1,619 @@
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import OfficialTournament from '../models/OfficialTournament.js';
|
||||
import OfficialCompetition from '../models/OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js';
|
||||
import Member from '../models/Member.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// In-Memory Store (einfacher Start); später DB-Modell
|
||||
const parsedTournaments = new Map(); // key: id, value: { id, clubId, rawText, parsedData }
|
||||
let seq = 1;
|
||||
|
||||
export const uploadTournamentPdf = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
if (!req.file || !req.file.buffer) return res.status(400).json({ error: 'No pdf provided' });
|
||||
const data = await pdfParse(req.file.buffer);
|
||||
const parsed = parseTournamentText(data.text);
|
||||
const t = await OfficialTournament.create({
|
||||
clubId,
|
||||
title: parsed.title || null,
|
||||
eventDate: parsed.termin || null,
|
||||
organizer: null,
|
||||
host: null,
|
||||
venues: JSON.stringify(parsed.austragungsorte || []),
|
||||
competitionTypes: JSON.stringify(parsed.konkurrenztypen || []),
|
||||
registrationDeadlines: JSON.stringify(parsed.meldeschluesse || []),
|
||||
entryFees: JSON.stringify(parsed.entryFees || {}),
|
||||
});
|
||||
// competitions persistieren
|
||||
for (const c of parsed.competitions || []) {
|
||||
// Korrigiere Fehlzuordnung: Wenn die Zeile mit "Stichtag" fälschlich in performanceClass steht
|
||||
let performanceClass = c.leistungsklasse || c.performanceClass || null;
|
||||
let cutoffDate = c.stichtag || c.cutoffDate || null;
|
||||
if (performanceClass && /^stichtag\b/i.test(performanceClass)) {
|
||||
cutoffDate = performanceClass.replace(/^stichtag\s*:?\s*/i, '').trim();
|
||||
performanceClass = null;
|
||||
}
|
||||
await OfficialCompetition.create({
|
||||
tournamentId: t.id,
|
||||
ageClassCompetition: c.altersklasseWettbewerb || c.ageClassCompetition || null,
|
||||
performanceClass,
|
||||
startTime: c.startzeit || c.startTime || null,
|
||||
registrationDeadlineDate: c.meldeschlussDatum || c.registrationDeadlineDate || null,
|
||||
registrationDeadlineOnline: c.meldeschlussOnline || c.registrationDeadlineOnline || null,
|
||||
cutoffDate,
|
||||
ttrRelevant: c.ttrRelevant || null,
|
||||
openTo: c.offenFuer || c.openTo || null,
|
||||
preliminaryRound: c.vorrunde || c.preliminaryRound || null,
|
||||
finalRound: c.endrunde || c.finalRound || null,
|
||||
maxParticipants: c.maxTeilnehmer || c.maxParticipants || null,
|
||||
entryFee: c.startgeld || c.entryFee || null,
|
||||
});
|
||||
}
|
||||
res.status(201).json({ id: String(t.id) });
|
||||
} catch (e) {
|
||||
console.error('[uploadTournamentPdf] Error:', e);
|
||||
res.status(500).json({ error: 'Failed to parse pdf' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getParsedTournament = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await OfficialTournament.findOne({ where: { id, clubId } });
|
||||
if (!t) return res.status(404).json({ error: 'not found' });
|
||||
const comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
|
||||
const entries = await OfficialCompetitionMember.findAll({ where: { tournamentId: id } });
|
||||
const competitions = comps.map((c) => {
|
||||
const j = c.toJSON();
|
||||
return {
|
||||
id: j.id,
|
||||
tournamentId: j.tournamentId,
|
||||
ageClassCompetition: j.ageClassCompetition || null,
|
||||
performanceClass: j.performanceClass || null,
|
||||
startTime: j.startTime || null,
|
||||
registrationDeadlineDate: j.registrationDeadlineDate || null,
|
||||
registrationDeadlineOnline: j.registrationDeadlineOnline || null,
|
||||
cutoffDate: j.cutoffDate || null,
|
||||
ttrRelevant: j.ttrRelevant || null,
|
||||
openTo: j.openTo || null,
|
||||
preliminaryRound: j.preliminaryRound || null,
|
||||
finalRound: j.finalRound || null,
|
||||
maxParticipants: j.maxParticipants || null,
|
||||
entryFee: j.entryFee || null,
|
||||
// Legacy Felder zusätzlich, falls Frontend sie noch nutzt
|
||||
altersklasseWettbewerb: j.ageClassCompetition || null,
|
||||
leistungsklasse: j.performanceClass || null,
|
||||
startzeit: j.startTime || null,
|
||||
meldeschlussDatum: j.registrationDeadlineDate || null,
|
||||
meldeschlussOnline: j.registrationDeadlineOnline || null,
|
||||
stichtag: j.cutoffDate || null,
|
||||
offenFuer: j.openTo || null,
|
||||
vorrunde: j.preliminaryRound || null,
|
||||
endrunde: j.finalRound || null,
|
||||
maxTeilnehmer: j.maxParticipants || null,
|
||||
startgeld: j.entryFee || null,
|
||||
};
|
||||
});
|
||||
res.status(200).json({
|
||||
id: String(t.id),
|
||||
clubId: String(t.clubId),
|
||||
parsedData: {
|
||||
title: t.title,
|
||||
termin: t.eventDate,
|
||||
austragungsorte: JSON.parse(t.venues || '[]'),
|
||||
konkurrenztypen: JSON.parse(t.competitionTypes || '[]'),
|
||||
meldeschluesse: JSON.parse(t.registrationDeadlines || '[]'),
|
||||
entryFees: JSON.parse(t.entryFees || '{}'),
|
||||
competitions,
|
||||
},
|
||||
participation: entries.map(e => ({
|
||||
id: e.id,
|
||||
tournamentId: e.tournamentId,
|
||||
competitionId: e.competitionId,
|
||||
memberId: e.memberId,
|
||||
wants: !!e.wants,
|
||||
registered: !!e.registered,
|
||||
participated: !!e.participated,
|
||||
placement: e.placement || null,
|
||||
})),
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to fetch parsed tournament' });
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertCompetitionMember = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params; // id = tournamentId
|
||||
await checkAccess(userToken, clubId);
|
||||
const { competitionId, memberId, wants, registered, participated, placement } = req.body;
|
||||
if (!competitionId || !memberId) return res.status(400).json({ error: 'competitionId and memberId required' });
|
||||
const [row] = await OfficialCompetitionMember.findOrCreate({
|
||||
where: { competitionId, memberId },
|
||||
defaults: {
|
||||
tournamentId: id,
|
||||
competitionId,
|
||||
memberId,
|
||||
wants: !!wants,
|
||||
registered: !!registered,
|
||||
participated: !!participated,
|
||||
placement: placement || null,
|
||||
}
|
||||
});
|
||||
row.wants = wants !== undefined ? !!wants : row.wants;
|
||||
row.registered = registered !== undefined ? !!registered : row.registered;
|
||||
row.participated = participated !== undefined ? !!participated : row.participated;
|
||||
if (placement !== undefined) row.placement = placement;
|
||||
await row.save();
|
||||
return res.status(200).json({ success: true, id: row.id });
|
||||
} catch (e) {
|
||||
console.error('[upsertCompetitionMember] Error:', e);
|
||||
res.status(500).json({ error: 'Failed to save participation' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantStatus = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params; // id = tournamentId
|
||||
await checkAccess(userToken, clubId);
|
||||
const { competitionId, memberId, action } = req.body;
|
||||
|
||||
if (!competitionId || !memberId || !action) {
|
||||
return res.status(400).json({ error: 'competitionId, memberId and action required' });
|
||||
}
|
||||
|
||||
const [row] = await OfficialCompetitionMember.findOrCreate({
|
||||
where: { competitionId, memberId },
|
||||
defaults: {
|
||||
tournamentId: id,
|
||||
competitionId,
|
||||
memberId,
|
||||
wants: false,
|
||||
registered: false,
|
||||
participated: false,
|
||||
placement: null,
|
||||
}
|
||||
});
|
||||
|
||||
// Status-Update basierend auf Aktion
|
||||
switch (action) {
|
||||
case 'register':
|
||||
// Von "möchte teilnehmen" zu "angemeldet"
|
||||
row.wants = true;
|
||||
row.registered = true;
|
||||
row.participated = false;
|
||||
break;
|
||||
case 'participate':
|
||||
// Von "angemeldet" zu "hat gespielt"
|
||||
row.wants = true;
|
||||
row.registered = true;
|
||||
row.participated = true;
|
||||
break;
|
||||
case 'reset':
|
||||
// Zurück zu "möchte teilnehmen"
|
||||
row.wants = true;
|
||||
row.registered = false;
|
||||
row.participated = false;
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: 'Invalid action. Use: register, participate, or reset' });
|
||||
}
|
||||
|
||||
await row.save();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
id: row.id,
|
||||
status: {
|
||||
wants: row.wants,
|
||||
registered: row.registered,
|
||||
participated: row.participated,
|
||||
placement: row.placement
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[updateParticipantStatus] Error:', e);
|
||||
res.status(500).json({ error: 'Failed to update participant status' });
|
||||
}
|
||||
};
|
||||
|
||||
export const listOfficialTournaments = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const list = await OfficialTournament.findAll({ where: { clubId } });
|
||||
res.status(200).json(list);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to list tournaments' });
|
||||
}
|
||||
};
|
||||
|
||||
export const listClubParticipations = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournaments = await OfficialTournament.findAll({ where: { clubId } });
|
||||
if (!tournaments || tournaments.length === 0) return res.status(200).json([]);
|
||||
const tournamentIds = tournaments.map(t => t.id);
|
||||
|
||||
const rows = await OfficialCompetitionMember.findAll({
|
||||
where: { tournamentId: { [Op.in]: tournamentIds }, participated: true },
|
||||
include: [
|
||||
{ model: OfficialCompetition, as: 'competition', attributes: ['id', 'tournamentId', 'ageClassCompetition', 'startTime'] },
|
||||
{ model: OfficialTournament, as: 'tournament', attributes: ['id', 'title', 'eventDate'] },
|
||||
{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] },
|
||||
]
|
||||
});
|
||||
|
||||
const parseDmy = (s) => {
|
||||
if (!s) return null;
|
||||
const m = String(s).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
||||
if (!m) return null;
|
||||
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
const fmtDmy = (d) => {
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = d.getFullYear();
|
||||
return `${dd}.${mm}.${yyyy}`;
|
||||
};
|
||||
|
||||
const byTournament = new Map();
|
||||
for (const r of rows) {
|
||||
const t = r.tournament;
|
||||
const c = r.competition;
|
||||
const m = r.member;
|
||||
if (!t || !c || !m) continue;
|
||||
if (!byTournament.has(t.id)) {
|
||||
byTournament.set(t.id, {
|
||||
tournamentId: String(t.id),
|
||||
title: t.title || null,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
entries: [],
|
||||
_dates: [],
|
||||
_eventDate: t.eventDate || null,
|
||||
});
|
||||
}
|
||||
const bucket = byTournament.get(t.id);
|
||||
const compDate = parseDmy(c.startTime || '') || null;
|
||||
if (compDate) bucket._dates.push(compDate);
|
||||
bucket.entries.push({
|
||||
memberId: m.id,
|
||||
memberName: `${m.firstName || ''} ${m.lastName || ''}`.trim(),
|
||||
competitionId: c.id,
|
||||
competitionName: c.ageClassCompetition || '',
|
||||
placement: r.placement || null,
|
||||
date: compDate ? fmtDmy(compDate) : null,
|
||||
});
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const t of tournaments) {
|
||||
const bucket = byTournament.get(t.id) || {
|
||||
tournamentId: String(t.id),
|
||||
title: t.title || null,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
entries: [],
|
||||
_dates: [],
|
||||
_eventDate: t.eventDate || null,
|
||||
};
|
||||
// Ableiten Start/Ende
|
||||
if (bucket._dates.length) {
|
||||
bucket._dates.sort((a, b) => a - b);
|
||||
bucket.startDate = fmtDmy(bucket._dates[0]);
|
||||
bucket.endDate = fmtDmy(bucket._dates[bucket._dates.length - 1]);
|
||||
} else if (bucket._eventDate) {
|
||||
const all = String(bucket._eventDate).match(/(\d{1,2}\.\d{1,2}\.\d{4})/g) || [];
|
||||
if (all.length >= 1) {
|
||||
const d1 = parseDmy(all[0]);
|
||||
const d2 = all.length >= 2 ? parseDmy(all[1]) : d1;
|
||||
if (d1) bucket.startDate = fmtDmy(d1);
|
||||
if (d2) bucket.endDate = fmtDmy(d2);
|
||||
}
|
||||
}
|
||||
// Sort entries: Mitglied, dann Konkurrenz
|
||||
bucket.entries.sort((a, b) => {
|
||||
const mcmp = (a.memberName || '').localeCompare(b.memberName || '', 'de', { sensitivity: 'base' });
|
||||
if (mcmp !== 0) return mcmp;
|
||||
return (a.competitionName || '').localeCompare(b.competitionName || '', 'de', { sensitivity: 'base' });
|
||||
});
|
||||
delete bucket._dates;
|
||||
delete bucket._eventDate;
|
||||
out.push(bucket);
|
||||
}
|
||||
|
||||
res.status(200).json(out);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to list club participations' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteOfficialTournament = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await OfficialTournament.findOne({ where: { id, clubId } });
|
||||
if (!t) return res.status(404).json({ error: 'not found' });
|
||||
await OfficialCompetition.destroy({ where: { tournamentId: id } });
|
||||
await OfficialTournament.destroy({ where: { id } });
|
||||
res.status(204).send();
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to delete tournament' });
|
||||
}
|
||||
};
|
||||
|
||||
function parseTournamentText(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const normLines = lines.map(l => l.replace(/\s+/g, ' ').trim());
|
||||
|
||||
const findTitle = () => {
|
||||
const idx = normLines.findIndex(l => /Kreiseinzelmeisterschaften/i.test(l));
|
||||
return idx >= 0 ? normLines[idx] : null;
|
||||
};
|
||||
|
||||
// Neue Funktion: Teilnahmegebühren pro Spielklasse extrahieren
|
||||
const extractEntryFees = () => {
|
||||
const entryFees = {};
|
||||
|
||||
// Verschiedene Patterns für Teilnahmegebühren suchen
|
||||
const feePatterns = [
|
||||
// Pattern 1: "Startgeld: U12: 5€, U14: 7€, U16: 10€"
|
||||
/startgeld\s*:?\s*(.+)/i,
|
||||
// Pattern 2: "Teilnahmegebühr: U12: 5€, U14: 7€"
|
||||
/teilnahmegebühr\s*:?\s*(.+)/i,
|
||||
// Pattern 3: "Gebühr: U12: 5€, U14: 7€"
|
||||
/gebühr\s*:?\s*(.+)/i,
|
||||
// Pattern 4: "Einschreibegebühr: U12: 5€, U14: 7€"
|
||||
/einschreibegebühr\s*:?\s*(.+)/i,
|
||||
// Pattern 5: "Anmeldegebühr: U12: 5€, U14: 7€"
|
||||
/anmeldegebühr\s*:?\s*(.+)/i
|
||||
];
|
||||
|
||||
for (const pattern of feePatterns) {
|
||||
for (let i = 0; i < normLines.length; i++) {
|
||||
const line = normLines[i];
|
||||
const match = line.match(pattern);
|
||||
if (match) {
|
||||
const feeText = match[1];
|
||||
|
||||
// Extrahiere Gebühren aus dem Text
|
||||
// Unterstützt verschiedene Formate:
|
||||
// "U12: 5€, U14: 7€, U16: 10€"
|
||||
// "U12: 5 Euro, U14: 7 Euro"
|
||||
// "U12 5€, U14 7€"
|
||||
// "U12: 5,00€, U14: 7,00€"
|
||||
const feeMatches = feeText.matchAll(/(U\d+|AK\s*\d+)\s*:?\s*(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/gi);
|
||||
|
||||
for (const feeMatch of feeMatches) {
|
||||
const ageClass = feeMatch[1].toUpperCase().replace(/\s+/g, '');
|
||||
const amount = feeMatch[2].replace(',', '.');
|
||||
const numericAmount = parseFloat(amount);
|
||||
|
||||
if (!isNaN(numericAmount)) {
|
||||
entryFees[ageClass] = {
|
||||
amount: numericAmount,
|
||||
currency: '€',
|
||||
rawText: feeMatch[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn wir Gebühren gefunden haben, brechen wir ab
|
||||
if (Object.keys(entryFees).length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(entryFees).length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entryFees;
|
||||
};
|
||||
|
||||
const extractBlockAfter = (labels, multiline = false) => {
|
||||
const idx = normLines.findIndex(l => labels.some(lb => l.toLowerCase().startsWith(lb)));
|
||||
if (idx === -1) return multiline ? [] : null;
|
||||
const line = normLines[idx];
|
||||
const afterColon = line.includes(':') ? line.split(':').slice(1).join(':').trim() : '';
|
||||
if (!multiline) {
|
||||
if (afterColon) return afterColon;
|
||||
// sonst nächste nicht-leere Zeile
|
||||
for (let i = idx + 1; i < normLines.length; i++) {
|
||||
if (normLines[i]) return normLines[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// multiline bis zur nächsten Leerzeile oder nächsten bekannten Section
|
||||
const out = [];
|
||||
if (afterColon) out.push(afterColon);
|
||||
for (let i = idx + 1; i < normLines.length; i++) {
|
||||
const ln = normLines[i];
|
||||
if (!ln) break;
|
||||
if (/^(termin|austragungsort|austragungsorte|konkurrenz|konkurrenzen|konkurrenztypen|meldeschluss|altersklassen|startzeiten)/i.test(ln)) break;
|
||||
out.push(ln);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const extractAllMatches = (regex) => {
|
||||
const results = [];
|
||||
for (const l of normLines) {
|
||||
const m = l.match(regex);
|
||||
if (m) results.push(m);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const title = findTitle();
|
||||
const termin = extractBlockAfter(['termin', 'termin '], false);
|
||||
const austragungsorte = extractBlockAfter(['austragungsort', 'austragungsorte'], true);
|
||||
let konkurrenzRaw = extractBlockAfter(['konkurrenz', 'konkurrenzen', 'konkurrenztypen'], true);
|
||||
if (konkurrenzRaw && !Array.isArray(konkurrenzRaw)) konkurrenzRaw = [konkurrenzRaw];
|
||||
const konkurrenztypen = (konkurrenzRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
|
||||
|
||||
// Meldeschlüsse mit Position und Zuordnung zu AK ermitteln
|
||||
const meldeschluesseRaw = [];
|
||||
for (let i = 0; i < normLines.length; i++) {
|
||||
const l = normLines[i];
|
||||
const m = l.match(/meldeschluss\s*:?\s*(.+)$/i);
|
||||
if (m) meldeschluesseRaw.push({ line: i, value: m[1].trim() });
|
||||
}
|
||||
|
||||
let altersRaw = extractBlockAfter(['altersklassen', 'altersklasse'], true);
|
||||
if (altersRaw && !Array.isArray(altersRaw)) altersRaw = [altersRaw];
|
||||
const altersklassen = (altersRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
|
||||
|
||||
// Wettbewerbe/Konkurrenzen parsen (Block ab "3. Konkurrenzen")
|
||||
const competitions = [];
|
||||
const konkIdx = normLines.findIndex(l => /^\s*3\.?\s+Konkurrenzen/i.test(l) || /^Konkurrenzen\b/i.test(l));
|
||||
// Bestimme Start-Sektionsnummer (z. B. 3 bei "3. Konkurrenzen"), fallback 3
|
||||
const startSectionNum = (() => {
|
||||
if (konkIdx === -1) return 3;
|
||||
const m = normLines[konkIdx].match(/^\s*(\d+)\./);
|
||||
return m ? parseInt(m[1], 10) : 3;
|
||||
})();
|
||||
const nextSectionIdx = () => {
|
||||
for (let i = konkIdx + 1; i < normLines.length; i++) {
|
||||
const m = normLines[i].match(/^\s*(\d+)\.\s+/);
|
||||
if (m) {
|
||||
const num = parseInt(m[1], 10);
|
||||
if (!Number.isNaN(num) && num > startSectionNum) return i;
|
||||
}
|
||||
// Hinweis: Seitenfußzeilen wie "nu.Dokument ..." ignorieren wir, damit mehrseitige Blöcke nicht abbrechen
|
||||
}
|
||||
return normLines.length;
|
||||
};
|
||||
if (konkIdx !== -1) {
|
||||
const endIdx = nextSectionIdx();
|
||||
let i = konkIdx + 1;
|
||||
while (i < endIdx) {
|
||||
const line = normLines[i];
|
||||
if (/^Altersklasse\/Wettbewerb\s*:/i.test(line)) {
|
||||
const comp = {};
|
||||
comp.altersklasseWettbewerb = line.split(':').slice(1).join(':').trim();
|
||||
i++;
|
||||
while (i < endIdx && !/^Altersklasse\/Wettbewerb\s*:/i.test(normLines[i])) {
|
||||
const ln = normLines[i];
|
||||
const m = ln.match(/^([^:]+):\s*(.*)$/);
|
||||
if (m) {
|
||||
const key = m[1].trim().toLowerCase();
|
||||
const val = m[2].trim();
|
||||
if (key.startsWith('leistungsklasse')) comp.leistungsklasse = val;
|
||||
else if (key === 'startzeit') {
|
||||
// Erwartet: 20.09.2025 13:30 Uhr -> wir extrahieren Datum+Zeit
|
||||
const sm = val.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{1,2}:\d{2})/);
|
||||
comp.startzeit = sm ? `${sm[1]} ${sm[2]}` : val;
|
||||
}
|
||||
else if (key.startsWith('meldeschluss datum')) comp.meldeschlussDatum = val;
|
||||
else if (key.startsWith('meldeschluss online')) comp.meldeschlussOnline = val;
|
||||
else if (key === 'stichtag') comp.stichtag = val;
|
||||
else if (key === 'ttr-relevant') comp.ttrRelevant = val;
|
||||
else if (key === 'offen für') comp.offenFuer = val;
|
||||
else if (key.startsWith('austragungssys. vorrunde')) comp.vorrunde = val;
|
||||
else if (key.startsWith('austragungssys. endrunde')) comp.endrunde = val;
|
||||
else if (key.startsWith('max. teilnehmerzahl')) comp.maxTeilnehmer = val;
|
||||
else if (key === 'startgeld') {
|
||||
comp.startgeld = val;
|
||||
// Versuche auch spezifische Gebühren für diese Altersklasse zu extrahieren
|
||||
const ageClassMatch = comp.altersklasseWettbewerb?.match(/(U\d+|AK\s*\d+)/i);
|
||||
if (ageClassMatch) {
|
||||
const ageClass = ageClassMatch[1].toUpperCase().replace(/\s+/g, '');
|
||||
const feeMatch = val.match(/(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/);
|
||||
if (feeMatch) {
|
||||
const amount = feeMatch[1].replace(',', '.');
|
||||
const numericAmount = parseFloat(amount);
|
||||
if (!isNaN(numericAmount)) {
|
||||
comp.entryFeeDetails = {
|
||||
amount: numericAmount,
|
||||
currency: '€',
|
||||
ageClass: ageClass
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
competitions.push(comp);
|
||||
continue; // schon auf nächster Zeile
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Altersklassen-Positionen im Text (zur Zuordnung von Meldeschlüssen)
|
||||
const akPositions = [];
|
||||
for (let i = 0; i < normLines.length; i++) {
|
||||
const l = normLines[i];
|
||||
const m = l.match(/\b(U\d+|AK\s*\d+)\b/i);
|
||||
if (m) akPositions.push({ line: i, ak: m[1].toUpperCase().replace(/\s+/g, '') });
|
||||
}
|
||||
|
||||
const meldeschluesseByAk = {};
|
||||
for (const ms of meldeschluesseRaw) {
|
||||
// Nächste AK im Umkreis von 3 Zeilen suchen
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
for (const ak of akPositions) {
|
||||
const dist = Math.abs(ak.line - ms.line);
|
||||
if (dist < bestDist && dist <= 3) { best = ak; bestDist = dist; }
|
||||
}
|
||||
if (best) {
|
||||
if (!meldeschluesseByAk[best.ak]) meldeschluesseByAk[best.ak] = new Set();
|
||||
meldeschluesseByAk[best.ak].add(ms.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Dedup global
|
||||
const meldeschluesse = Array.from(new Set(meldeschluesseRaw.map(x => x.value)));
|
||||
// Sets zu Arrays
|
||||
const meldeschluesseByAkOut = Object.fromEntries(Object.entries(meldeschluesseByAk).map(([k,v]) => [k, Array.from(v)]));
|
||||
|
||||
// Vorhandene einfache Personenerkennung (optional, zu Analysezwecken)
|
||||
const entries = [];
|
||||
for (const l of normLines) {
|
||||
const m = l.match(/^([A-Za-zÄÖÜäöüß\-\s']{3,})(?:\s+\((m|w|d)\))?$/i);
|
||||
if (m && /\s/.test(m[1])) {
|
||||
entries.push({ name: m[1].trim(), genderHint: m[2] || null });
|
||||
}
|
||||
}
|
||||
|
||||
// Extrahiere Teilnahmegebühren
|
||||
const entryFees = extractEntryFees();
|
||||
|
||||
return {
|
||||
title,
|
||||
termin,
|
||||
austragungsorte,
|
||||
konkurrenztypen,
|
||||
meldeschluesse,
|
||||
meldeschluesseByAk: meldeschluesseByAkOut,
|
||||
altersklassen,
|
||||
startzeiten: {},
|
||||
competitions,
|
||||
entries,
|
||||
entryFees, // Neue: Teilnahmegebühren pro Spielklasse
|
||||
debug: { normLines },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import predefinedActivityService from '../services/predefinedActivityService.js';
|
||||
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export const createPredefinedActivity = async (req, res) => {
|
||||
try {
|
||||
const { name, description, durationText, duration } = req.body;
|
||||
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, description, durationText, duration });
|
||||
const { name, code, description, durationText, duration, imageLink } = req.body;
|
||||
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink });
|
||||
res.status(201).json(predefinedActivity);
|
||||
} catch (error) {
|
||||
console.error('[createPredefinedActivity] - Error:', error);
|
||||
@@ -25,10 +28,11 @@ export const getPredefinedActivityById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const predefinedActivity = await predefinedActivityService.getPredefinedActivityById(id);
|
||||
const images = await PredefinedActivityImage.findAll({ where: { predefinedActivityId: id } });
|
||||
if (!predefinedActivity) {
|
||||
return res.status(404).json({ error: 'Predefined activity not found' });
|
||||
}
|
||||
res.status(200).json(predefinedActivity);
|
||||
res.status(200).json({ ...predefinedActivity.toJSON(), images });
|
||||
} catch (error) {
|
||||
console.error('[getPredefinedActivityById] - Error:', error);
|
||||
res.status(500).json({ error: 'Error fetching predefined activity' });
|
||||
@@ -38,11 +42,43 @@ export const getPredefinedActivityById = async (req, res) => {
|
||||
export const updatePredefinedActivity = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, durationText, duration } = req.body;
|
||||
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, description, durationText, duration });
|
||||
const { name, code, description, durationText, duration, imageLink } = req.body;
|
||||
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink });
|
||||
res.status(200).json(updatedActivity);
|
||||
} catch (error) {
|
||||
console.error('[updatePredefinedActivity] - Error:', error);
|
||||
res.status(500).json({ error: 'Error updating predefined activity' });
|
||||
}
|
||||
};
|
||||
|
||||
export const searchPredefinedActivities = async (req, res) => {
|
||||
try {
|
||||
const { q, limit } = req.query;
|
||||
const result = await predefinedActivityService.searchPredefinedActivities(q, limit);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[searchPredefinedActivities] - Error:', error);
|
||||
res.status(500).json({ error: 'Error searching predefined activities' });
|
||||
}
|
||||
};
|
||||
|
||||
export const mergePredefinedActivities = async (req, res) => {
|
||||
try {
|
||||
const { sourceId, targetId } = req.body;
|
||||
await predefinedActivityService.mergeActivities(sourceId, targetId);
|
||||
res.status(200).json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('[mergePredefinedActivities] - Error:', error);
|
||||
res.status(500).json({ error: 'Error merging predefined activities' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deduplicatePredefinedActivities = async (req, res) => {
|
||||
try {
|
||||
const result = await predefinedActivityService.deduplicateActivities();
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[deduplicatePredefinedActivities] - Error:', error);
|
||||
res.status(500).json({ error: 'Error deduplicating predefined activities' });
|
||||
}
|
||||
};
|
||||
|
||||
92
backend/controllers/predefinedActivityImageController.js
Normal file
92
backend/controllers/predefinedActivityImageController.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import PredefinedActivity from '../models/PredefinedActivity.js';
|
||||
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import { checkGlobalAccess } from '../utils/userUtils.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const uploadPredefinedActivityImage = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params; // predefinedActivityId
|
||||
const { authcode: userToken } = req.headers;
|
||||
await checkGlobalAccess(userToken); // Predefined Activities sind global, keine Club-Zugriffskontrolle nötig
|
||||
|
||||
const activity = await PredefinedActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
return res.status(404).json({ error: 'Predefined activity not found' });
|
||||
}
|
||||
|
||||
if (!req.file || !req.file.buffer) {
|
||||
return res.status(400).json({ error: 'No image uploaded' });
|
||||
}
|
||||
|
||||
const imagesDir = path.join('images', 'predefined');
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `${id}-${Date.now()}.jpg`;
|
||||
const filePath = path.join(imagesDir, fileName);
|
||||
|
||||
await sharp(req.file.buffer)
|
||||
.resize(800, 800, { fit: 'inside' })
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(filePath);
|
||||
|
||||
const imageRecord = await PredefinedActivityImage.create({
|
||||
predefinedActivityId: id,
|
||||
imagePath: filePath,
|
||||
mimeType: 'image/jpeg',
|
||||
});
|
||||
|
||||
// Optional: als imageLink am Activity-Datensatz setzen
|
||||
activity.imageLink = `/api/predefined-activities/${id}/image/${imageRecord.id}`;
|
||||
await activity.save();
|
||||
|
||||
res.status(201).json({ id: imageRecord.id, imageLink: activity.imageLink });
|
||||
} catch (error) {
|
||||
console.error('[uploadPredefinedActivityImage] - Error:', error);
|
||||
res.status(500).json({ error: 'Failed to upload image' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePredefinedActivityImage = async (req, res) => {
|
||||
try {
|
||||
const { id, imageId } = req.params; // predefinedActivityId, imageId
|
||||
const { authcode: userToken } = req.headers;
|
||||
await checkGlobalAccess(userToken);
|
||||
|
||||
const activity = await PredefinedActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
return res.status(404).json({ error: 'Predefined activity not found' });
|
||||
}
|
||||
|
||||
const image = await PredefinedActivityImage.findOne({
|
||||
where: { id: imageId, predefinedActivityId: id }
|
||||
});
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Image not found' });
|
||||
}
|
||||
|
||||
// Datei vom Dateisystem löschen
|
||||
if (fs.existsSync(image.imagePath)) {
|
||||
fs.unlinkSync(image.imagePath);
|
||||
}
|
||||
|
||||
// Datensatz aus der Datenbank löschen
|
||||
await image.destroy();
|
||||
|
||||
// Falls das gelöschte Bild der aktuelle imageLink war, diesen zurücksetzen
|
||||
if (activity.imageLink === `/api/predefined-activities/${id}/image/${imageId}`) {
|
||||
activity.imageLink = null;
|
||||
await activity.save();
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Image deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('[deletePredefinedActivityImage] - Error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete image' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
280
backend/controllers/tournamentController.js
Normal file
280
backend/controllers/tournamentController.js
Normal file
@@ -0,0 +1,280 @@
|
||||
// controllers/tournamentController.js
|
||||
import tournamentService from "../services/tournamentService.js";
|
||||
|
||||
// 1. Alle Turniere eines Vereins
|
||||
export const getTournaments = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
try {
|
||||
const tournaments = await tournamentService.getTournaments(token, clubId);
|
||||
res.status(200).json(tournaments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Neues Turnier anlegen
|
||||
export const addTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentName, date } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date);
|
||||
res.status(201).json(tournament);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Teilnehmer hinzufügen
|
||||
export const addParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participant: participantId } = req.body;
|
||||
try {
|
||||
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Teilnehmerliste abrufen
|
||||
export const getParticipants = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Turniermodus (Gruppen/K.O.) setzen
|
||||
export const setModus = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body;
|
||||
try {
|
||||
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Gruppen-Strukturen anlegen (leere Gruppen)
|
||||
export const createGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroups(token, clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Teilnehmer zufällig auf Gruppen verteilen & Gruppenspiele anlegen
|
||||
export const fillGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
|
||||
res.status(200).json(updatedMembers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 8. Gruppen mit ihren Teilnehmern abfragen
|
||||
export const getGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.query;
|
||||
try {
|
||||
const groups = await tournamentService.getGroupsWithParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 9. Einzelnes Turnier abrufen
|
||||
export const getTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
try {
|
||||
const tournament = await tournamentService.getTournament(token, clubId, tournamentId);
|
||||
res.status(200).json(tournament);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 10. Alle Spiele eines Turniers abfragen
|
||||
export const getTournamentMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
try {
|
||||
const matches = await tournamentService.getTournamentMatches(token, clubId, tournamentId);
|
||||
res.status(200).json(matches);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 11. Satz-Ergebnis speichern
|
||||
export const addMatchResult = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId, set, result } = req.body;
|
||||
try {
|
||||
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result);
|
||||
res.status(200).json({ message: "Result added successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 12. Spiel abschließen (Endergebnis ermitteln)
|
||||
export const finishMatch = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId } = req.body;
|
||||
try {
|
||||
await tournamentService.finishMatch(token, clubId, tournamentId, matchId);
|
||||
res.status(200).json({ message: "Match finished successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const startKnockout = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
|
||||
try {
|
||||
await tournamentService.startKnockout(token, clubId, tournamentId);
|
||||
res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" });
|
||||
} catch (error) {
|
||||
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const manualAssignGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const {
|
||||
clubId,
|
||||
tournamentId,
|
||||
assignments, // [{ participantId, groupNumber }]
|
||||
numberOfGroups, // optional
|
||||
maxGroupSize // optional
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const groupsWithParts = await tournamentService.manualAssignGroups(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
assignments,
|
||||
numberOfGroups, // neu
|
||||
maxGroupSize // neu
|
||||
);
|
||||
res.status(200).json(groupsWithParts);
|
||||
} catch (error) {
|
||||
console.error('Error in manualAssignGroups:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetGroups(token, clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const removeParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.body;
|
||||
try {
|
||||
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMatchResult = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId, set } = req.body;
|
||||
try {
|
||||
await tournamentService.deleteMatchResult(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
matchId,
|
||||
set
|
||||
);
|
||||
res.status(200).json({ message: 'Einzelsatz gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteMatchResult:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const reopenMatch = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId } = req.body;
|
||||
try {
|
||||
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
|
||||
// Gib optional das aktualisierte Match zurück
|
||||
res.status(200).json({ message: "Match reopened" });
|
||||
} catch (error) {
|
||||
console.error("Error in reopenMatch:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteKnockoutMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetKnockout(token, clubId, tournamentId);
|
||||
res.status(200).json({ message: "K.o.-Runde gelöscht" });
|
||||
} catch (error) {
|
||||
console.error("Error in deleteKnockoutMatches:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
128
backend/controllers/trainingStatsController.js
Normal file
128
backend/controllers/trainingStatsController.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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 spezifischen Vereins laden
|
||||
const members = await Member.findAll({
|
||||
where: {
|
||||
active: true,
|
||||
clubId: parseInt(clubId)
|
||||
}
|
||||
});
|
||||
|
||||
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: '--:--'
|
||||
}));
|
||||
|
||||
// Letztes Training
|
||||
const lastTrainingDate = trainingDetails.length ? trainingDetails[0].diaryDate.date : null;
|
||||
const lastTrainingTs = lastTrainingDate ? new Date(lastTrainingDate).getTime() : 0;
|
||||
|
||||
stats.push({
|
||||
id: member.id,
|
||||
firstName: member.firstName,
|
||||
lastName: member.lastName,
|
||||
birthDate: member.birthDate,
|
||||
participation12Months,
|
||||
participation3Months,
|
||||
participationTotal,
|
||||
lastTraining: lastTrainingDate,
|
||||
lastTrainingTs,
|
||||
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();
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { development } from './config.js';
|
||||
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
development.database,
|
||||
development.database,
|
||||
development.username,
|
||||
development.password,
|
||||
{
|
||||
host: development.host,
|
||||
dialect: development.dialect,
|
||||
define: development.define,
|
||||
logging: false, // SQL-Logging deaktivieren
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import User from '../models/User.js';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
|
||||
export const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
const { userid: userId, authcode: authCode } = req.headers;
|
||||
if (!userId || !authCode) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Missing credentials' });
|
||||
}
|
||||
const user = await User.findOne({ where: { email: userId, authCode: authCode } });
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||
}
|
||||
next();
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
return res.status(500).json({ error: 'Internal Server Error at auth' });
|
||||
let token = req.headers['authorization']?.split(' ')[1];
|
||||
if (!token) {
|
||||
token = req.headers['authcode'];
|
||||
}
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Token fehlt' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const tokenRecord = await UserToken.findOne({ where: { token } });
|
||||
if (!tokenRecord || tokenRecord.expiresAt < new Date()) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||
}
|
||||
req.user = { id: decoded.userId };
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||
}
|
||||
};
|
||||
26
backend/models/DiaryMemberActivity.js
Normal file
26
backend/models/DiaryMemberActivity.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const DiaryMemberActivity = sequelize.define('DiaryMemberActivity', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
diaryDateActivityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
participantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
tableName: 'diary_member_activities',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default DiaryMemberActivity;
|
||||
|
||||
|
||||
@@ -122,6 +122,12 @@ const Member = sequelize.define('Member', {
|
||||
allowNull: false,
|
||||
default: false,
|
||||
}
|
||||
,
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male','female','diverse','unknown'),
|
||||
allowNull: true,
|
||||
defaultValue: 'unknown'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
sequelize,
|
||||
|
||||
28
backend/models/OfficialCompetition.js
Normal file
28
backend/models/OfficialCompetition.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const OfficialCompetition = sequelize.define('OfficialCompetition', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
tournamentId: { type: DataTypes.INTEGER, allowNull: false },
|
||||
// Englische Attributnamen, gemappt auf bestehende DB-Spalten
|
||||
ageClassCompetition: { type: DataTypes.STRING, allowNull: true, field: 'age_class_competition' },
|
||||
performanceClass: { type: DataTypes.STRING, allowNull: true, field: 'performance_class' },
|
||||
startTime: { type: DataTypes.STRING, allowNull: true, field: 'start_time' },
|
||||
registrationDeadlineDate: { type: DataTypes.STRING, allowNull: true, field: 'registration_deadline_date' },
|
||||
registrationDeadlineOnline: { type: DataTypes.STRING, allowNull: true, field: 'registration_deadline_online' },
|
||||
cutoffDate: { type: DataTypes.STRING, allowNull: true, field: 'cutoff_date' },
|
||||
ttrRelevant: { type: DataTypes.STRING, allowNull: true },
|
||||
openTo: { type: DataTypes.STRING, allowNull: true, field: 'open_to' },
|
||||
preliminaryRound: { type: DataTypes.STRING, allowNull: true, field: 'preliminary_round' },
|
||||
finalRound: { type: DataTypes.STRING, allowNull: true, field: 'final_round' },
|
||||
maxParticipants: { type: DataTypes.STRING, allowNull: true, field: 'max_participants' },
|
||||
entryFee: { type: DataTypes.STRING, allowNull: true, field: 'entry_fee' },
|
||||
}, {
|
||||
tableName: 'official_competitions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default OfficialCompetition;
|
||||
|
||||
|
||||
25
backend/models/OfficialCompetitionMember.js
Normal file
25
backend/models/OfficialCompetitionMember.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const OfficialCompetitionMember = sequelize.define('OfficialCompetitionMember', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
tournamentId: { type: DataTypes.INTEGER, allowNull: false },
|
||||
competitionId: { type: DataTypes.INTEGER, allowNull: false },
|
||||
memberId: { type: DataTypes.INTEGER, allowNull: false },
|
||||
wants: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
registered: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
participated: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
placement: { type: DataTypes.STRING, allowNull: true },
|
||||
}, {
|
||||
tableName: 'official_competition_members',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['competition_id', 'member_id'] },
|
||||
{ fields: ['tournament_id'] },
|
||||
],
|
||||
});
|
||||
|
||||
export default OfficialCompetitionMember;
|
||||
|
||||
|
||||
23
backend/models/OfficialTournament.js
Normal file
23
backend/models/OfficialTournament.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const OfficialTournament = sequelize.define('OfficialTournament', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
clubId: { type: DataTypes.INTEGER, allowNull: false },
|
||||
title: { type: DataTypes.STRING, allowNull: true },
|
||||
eventDate: { type: DataTypes.STRING, allowNull: true },
|
||||
organizer: { type: DataTypes.STRING, allowNull: true },
|
||||
host: { type: DataTypes.STRING, allowNull: true },
|
||||
venues: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
|
||||
competitionTypes: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
|
||||
registrationDeadlines: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
|
||||
entryFees: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Object) - Teilnahmegebühren pro Spielklasse
|
||||
}, {
|
||||
tableName: 'official_tournaments',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default OfficialTournament;
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
@@ -23,6 +27,10 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
imageLink: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
tableName: 'predefined_activities',
|
||||
timestamps: true,
|
||||
|
||||
30
backend/models/PredefinedActivityImage.js
Normal file
30
backend/models/PredefinedActivityImage.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const PredefinedActivityImage = sequelize.define('PredefinedActivityImage', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
predefinedActivityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
imagePath: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
mimeType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
tableName: 'predefined_activity_images',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default PredefinedActivityImage;
|
||||
|
||||
|
||||
38
backend/models/Tournament.js
Normal file
38
backend/models/Tournament.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const Tournament = sequelize.define('Tournament', {
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
advancingPerGroup: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
numberOfGroups: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
},
|
||||
advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default Tournament;
|
||||
21
backend/models/TournamentGroup.js
Normal file
21
backend/models/TournamentGroup.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentGroup = sequelize.define('TournamentGroup', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
tournamentId : {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_group',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentGroup;
|
||||
59
backend/models/TournamentMatch.js
Normal file
59
backend/models/TournamentMatch.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// models/TournamentMatch.js
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Tournament from './Tournament.js';
|
||||
import TournamentGroup from './TournamentGroup.js';
|
||||
|
||||
const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Tournament,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: TournamentGroup,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
groupRound: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
round: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
player1Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
player2Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
isFinished: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
result: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_match',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentMatch;
|
||||
26
backend/models/TournamentMember.js
Normal file
26
backend/models/TournamentMember.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentMember = sequelize.define('TournamentMember', {
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: true
|
||||
},
|
||||
groupId : {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: true
|
||||
},
|
||||
clubMemberId: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_member',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentMember;
|
||||
24
backend/models/TournamentResult.js
Normal file
24
backend/models/TournamentResult.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentResult = sequelize.define('TournamentResult', {
|
||||
matchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
set: {
|
||||
type: DataTypes.INTEGER,
|
||||
},
|
||||
pointsPlayer1: {
|
||||
type: DataTypes.INTEGER,
|
||||
},
|
||||
pointsPlayer2: {
|
||||
type: DataTypes.INTEGER,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_result',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentResult;
|
||||
@@ -63,4 +63,8 @@ const User = sequelize.define('User', {
|
||||
},
|
||||
});
|
||||
|
||||
User.prototype.validatePassword = function(password) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
20
backend/models/UserToken.js
Normal file
20
backend/models/UserToken.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js'; // Korrigierter Pfad
|
||||
|
||||
const UserToken = sequelize.define('UserToken', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
expiresAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default UserToken;
|
||||
@@ -13,6 +13,8 @@ import DiaryDateTag from './DiaryDateTag.js';
|
||||
import DiaryMemberNote from './DiaryMemberNote.js';
|
||||
import DiaryMemberTag from './DiaryMemberTag.js';
|
||||
import PredefinedActivity from './PredefinedActivity.js';
|
||||
import DiaryMemberActivity from './DiaryMemberActivity.js';
|
||||
import PredefinedActivityImage from './PredefinedActivityImage.js';
|
||||
import DiaryDateActivity from './DiaryDateActivity.js';
|
||||
import Match from './Match.js';
|
||||
import League from './League.js';
|
||||
@@ -21,7 +23,26 @@ import Season from './Season.js';
|
||||
import Location from './Location.js';
|
||||
import Group from './Group.js';
|
||||
import GroupActivity from './GroupActivity.js';
|
||||
import Tournament from './Tournament.js';
|
||||
import TournamentGroup from './TournamentGroup.js';
|
||||
import TournamentMember from './TournamentMember.js';
|
||||
import TournamentMatch from './TournamentMatch.js';
|
||||
import TournamentResult from './TournamentResult.js';
|
||||
import Accident from './Accident.js';
|
||||
import UserToken from './UserToken.js';
|
||||
import OfficialTournament from './OfficialTournament.js';
|
||||
import OfficialCompetition from './OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
// Official competition participations
|
||||
OfficialCompetition.hasMany(OfficialCompetitionMember, { foreignKey: 'competitionId', as: 'members' });
|
||||
OfficialCompetitionMember.belongsTo(OfficialCompetition, { foreignKey: 'competitionId', as: 'competition' });
|
||||
OfficialTournament.hasMany(OfficialCompetitionMember, { foreignKey: 'tournamentId', as: 'competitionMembers' });
|
||||
OfficialCompetitionMember.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Member.hasMany(OfficialCompetitionMember, { foreignKey: 'memberId', as: 'officialCompetitionEntries' });
|
||||
OfficialCompetitionMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
|
||||
User.hasMany(Log, { foreignKey: 'userId' });
|
||||
Log.belongsTo(User, { foreignKey: 'userId' });
|
||||
@@ -35,6 +56,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' });
|
||||
|
||||
@@ -64,6 +91,14 @@ DiaryDateActivity.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDa
|
||||
|
||||
PredefinedActivity.hasMany(DiaryDateActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivities' });
|
||||
DiaryDateActivity.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
|
||||
// DiaryMemberActivity links a Participant to a DiaryDateActivity
|
||||
DiaryMemberActivity.belongsTo(DiaryDateActivity, { foreignKey: 'diaryDateActivityId', as: 'activity' });
|
||||
DiaryDateActivity.hasMany(DiaryMemberActivity, { foreignKey: 'diaryDateActivityId', as: 'activityMembers' });
|
||||
DiaryMemberActivity.belongsTo(Participant, { foreignKey: 'participantId', as: 'participant' });
|
||||
Participant.hasMany(DiaryMemberActivity, { foreignKey: 'participantId', as: 'memberActivities' });
|
||||
// PredefinedActivity Images
|
||||
PredefinedActivity.hasMany(PredefinedActivityImage, { foreignKey: 'predefinedActivityId', as: 'images' });
|
||||
PredefinedActivityImage.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
|
||||
|
||||
Club.hasMany(Match, { foreignKey: 'clubId', as: 'matches' });
|
||||
Match.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
@@ -115,6 +150,54 @@ DiaryDateTag.belongsTo(DiaryTag, { foreignKey: 'tagId', as: 'tag' });
|
||||
DiaryMemberTag.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
|
||||
DiaryDate.hasMany(DiaryMemberTag, { foreignKey: 'diaryDateId', as: 'diaryMemberTags' });
|
||||
|
||||
Tournament.belongsTo(Club, { foreignKey: 'clubId', as: 'tournamentclub' });
|
||||
Club.hasMany(Tournament, { foreignKey: 'clubId', as: 'tournaments' });
|
||||
|
||||
TournamentGroup.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournaments' });
|
||||
Tournament.hasMany(TournamentGroup, { foreignKey: 'tournamentId', as: 'tournamentGroups' });
|
||||
|
||||
TournamentMember.belongsTo(TournamentGroup, {
|
||||
foreignKey: 'groupId',
|
||||
targetKey: 'id',
|
||||
as: 'group',
|
||||
constraints: false
|
||||
});
|
||||
TournamentGroup.hasMany(TournamentMember, {
|
||||
foreignKey: 'groupId',
|
||||
as: 'tournamentGroupMembers'
|
||||
});
|
||||
|
||||
TournamentMember.belongsTo(Member, { foreignKey: 'clubMemberId', as: 'member' });
|
||||
Member.hasMany(TournamentMember, { foreignKey: 'clubMemberId', as: 'tournamentGroupMembers' });
|
||||
|
||||
TournamentMember.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMember, { foreignKey: 'tournamentId', as: 'tournamentMembers' });
|
||||
|
||||
TournamentMatch.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamentMatches' });
|
||||
|
||||
TournamentMatch.belongsTo(TournamentGroup, { foreignKey: 'groupId', as: 'group' });
|
||||
TournamentGroup.hasMany(TournamentMatch, { foreignKey: 'groupId', as: 'tournamentMatches' });
|
||||
|
||||
TournamentMatch.hasMany(TournamentResult, {
|
||||
foreignKey: 'matchId',
|
||||
as: 'tournamentResults',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
|
||||
TournamentResult.belongsTo(TournamentMatch, {
|
||||
foreignKey: 'matchId',
|
||||
as: 'match',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
|
||||
TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player1Id', as: 'player1' });
|
||||
TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'player2' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
|
||||
|
||||
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
|
||||
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
|
||||
|
||||
@@ -138,11 +221,22 @@ export {
|
||||
DiaryMemberNote,
|
||||
DiaryMemberTag,
|
||||
PredefinedActivity,
|
||||
DiaryMemberActivity,
|
||||
PredefinedActivityImage,
|
||||
DiaryDateActivity,
|
||||
Match,
|
||||
League,
|
||||
Team,
|
||||
Group,
|
||||
GroupActivity,
|
||||
Tournament,
|
||||
TournamentGroup,
|
||||
TournamentMember,
|
||||
TournamentMatch,
|
||||
TournamentResult,
|
||||
Accident,
|
||||
UserToken,
|
||||
OfficialTournament,
|
||||
OfficialCompetition,
|
||||
OfficialCompetitionMember,
|
||||
};
|
||||
|
||||
410
backend/node_modules/.package-lock.json
generated
vendored
410
backend/node_modules/.package-lock.json
generated
vendored
@@ -4,6 +4,17 @@
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
|
||||
"ideallyInert": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
@@ -180,6 +191,137 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
@@ -196,6 +338,23 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
@@ -212,6 +371,75 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
@@ -234,6 +462,29 @@
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
@@ -256,6 +507,66 @@
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"ideallyInert": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
@@ -573,9 +884,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -848,9 +1160,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -879,10 +1192,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -1265,16 +1579,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -1288,7 +1603,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@@ -1303,6 +1618,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
@@ -1477,6 +1796,21 @@
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"ideallyInert": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -2302,6 +2636,12 @@
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -2554,9 +2894,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf-parse": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
|
||||
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.1.0",
|
||||
"node-ensure": "^0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
@@ -3240,6 +3608,14 @@
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"ideallyInert": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
2
backend/node_modules/brace-expansion/index.js
generated
vendored
2
backend/node_modules/brace-expansion/index.js
generated
vendored
@@ -109,7 +109,7 @@ function expand(str, isTop) {
|
||||
var isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
if (m.post.match(/,.*\}/)) {
|
||||
if (m.post.match(/,(?!,).*\}/)) {
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
return expand(str);
|
||||
}
|
||||
|
||||
5
backend/node_modules/brace-expansion/package.json
generated
vendored
5
backend/node_modules/brace-expansion/package.json
generated
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "brace-expansion",
|
||||
"description": "Brace expansion as known from sh/bash",
|
||||
"version": "1.1.11",
|
||||
"version": "1.1.12",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/juliangruber/brace-expansion.git"
|
||||
@@ -43,5 +43,8 @@
|
||||
"iphone/6.0..latest",
|
||||
"android-browser/4.2..latest"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"tag": "1.x"
|
||||
}
|
||||
}
|
||||
|
||||
147
backend/node_modules/cookie/HISTORY.md
generated
vendored
147
backend/node_modules/cookie/HISTORY.md
generated
vendored
@@ -1,147 +0,0 @@
|
||||
0.6.0 / 2023-11-06
|
||||
==================
|
||||
|
||||
* Add `partitioned` option
|
||||
|
||||
0.5.0 / 2022-04-11
|
||||
==================
|
||||
|
||||
* Add `priority` option
|
||||
* Fix `expires` option to reject invalid dates
|
||||
* perf: improve default decode speed
|
||||
* perf: remove slow string split in parse
|
||||
|
||||
0.4.2 / 2022-02-02
|
||||
==================
|
||||
|
||||
* perf: read value only when assigning in parse
|
||||
* perf: remove unnecessary regexp in parse
|
||||
|
||||
0.4.1 / 2020-04-21
|
||||
==================
|
||||
|
||||
* Fix `maxAge` option to reject invalid values
|
||||
|
||||
0.4.0 / 2019-05-15
|
||||
==================
|
||||
|
||||
* Add `SameSite=None` support
|
||||
|
||||
0.3.1 / 2016-05-26
|
||||
==================
|
||||
|
||||
* Fix `sameSite: true` to work with draft-7 clients
|
||||
- `true` now sends `SameSite=Strict` instead of `SameSite`
|
||||
|
||||
0.3.0 / 2016-05-26
|
||||
==================
|
||||
|
||||
* Add `sameSite` option
|
||||
- Replaces `firstPartyOnly` option, never implemented by browsers
|
||||
* Improve error message when `encode` is not a function
|
||||
* Improve error message when `expires` is not a `Date`
|
||||
|
||||
0.2.4 / 2016-05-20
|
||||
==================
|
||||
|
||||
* perf: enable strict mode
|
||||
* perf: use for loop in parse
|
||||
* perf: use string concatenation for serialization
|
||||
|
||||
0.2.3 / 2015-10-25
|
||||
==================
|
||||
|
||||
* Fix cookie `Max-Age` to never be a floating point number
|
||||
|
||||
0.2.2 / 2015-09-17
|
||||
==================
|
||||
|
||||
* Fix regression when setting empty cookie value
|
||||
- Ease the new restriction, which is just basic header-level validation
|
||||
* Fix typo in invalid value errors
|
||||
|
||||
0.2.1 / 2015-09-17
|
||||
==================
|
||||
|
||||
* Throw on invalid values provided to `serialize`
|
||||
- Ensures the resulting string is a valid HTTP header value
|
||||
|
||||
0.2.0 / 2015-08-13
|
||||
==================
|
||||
|
||||
* Add `firstPartyOnly` option
|
||||
* Throw better error for invalid argument to parse
|
||||
* perf: hoist regular expression
|
||||
|
||||
0.1.5 / 2015-09-17
|
||||
==================
|
||||
|
||||
* Fix regression when setting empty cookie value
|
||||
- Ease the new restriction, which is just basic header-level validation
|
||||
* Fix typo in invalid value errors
|
||||
|
||||
0.1.4 / 2015-09-17
|
||||
==================
|
||||
|
||||
* Throw better error for invalid argument to parse
|
||||
* Throw on invalid values provided to `serialize`
|
||||
- Ensures the resulting string is a valid HTTP header value
|
||||
|
||||
0.1.3 / 2015-05-19
|
||||
==================
|
||||
|
||||
* Reduce the scope of try-catch deopt
|
||||
* Remove argument reassignments
|
||||
|
||||
0.1.2 / 2014-04-16
|
||||
==================
|
||||
|
||||
* Remove unnecessary files from npm package
|
||||
|
||||
0.1.1 / 2014-02-23
|
||||
==================
|
||||
|
||||
* Fix bad parse when cookie value contained a comma
|
||||
* Fix support for `maxAge` of `0`
|
||||
|
||||
0.1.0 / 2013-05-01
|
||||
==================
|
||||
|
||||
* Add `decode` option
|
||||
* Add `encode` option
|
||||
|
||||
0.0.6 / 2013-04-08
|
||||
==================
|
||||
|
||||
* Ignore cookie parts missing `=`
|
||||
|
||||
0.0.5 / 2012-10-29
|
||||
==================
|
||||
|
||||
* Return raw cookie value if value unescape errors
|
||||
|
||||
0.0.4 / 2012-06-21
|
||||
==================
|
||||
|
||||
* Use encode/decodeURIComponent for cookie encoding/decoding
|
||||
- Improve server/client interoperability
|
||||
|
||||
0.0.3 / 2012-06-06
|
||||
==================
|
||||
|
||||
* Only escape special characters per the cookie RFC
|
||||
|
||||
0.0.2 / 2012-06-01
|
||||
==================
|
||||
|
||||
* Fix `maxAge` option to not throw error
|
||||
|
||||
0.0.1 / 2012-05-28
|
||||
==================
|
||||
|
||||
* Add more tests
|
||||
|
||||
0.0.0 / 2012-05-28
|
||||
==================
|
||||
|
||||
* Initial release
|
||||
174
backend/node_modules/cookie/index.js
generated
vendored
174
backend/node_modules/cookie/index.js
generated
vendored
@@ -23,14 +23,66 @@ exports.serialize = serialize;
|
||||
var __toString = Object.prototype.toString
|
||||
|
||||
/**
|
||||
* RegExp to match field-content in RFC 7230 sec 3.2
|
||||
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
|
||||
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
|
||||
* which has been replaced by the token definition in RFC 7230 appendix B.
|
||||
*
|
||||
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
* field-vchar = VCHAR / obs-text
|
||||
* obs-text = %x80-FF
|
||||
* cookie-name = token
|
||||
* token = 1*tchar
|
||||
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
|
||||
* "*" / "+" / "-" / "." / "^" / "_" /
|
||||
* "`" / "|" / "~" / DIGIT / ALPHA
|
||||
*/
|
||||
|
||||
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
|
||||
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
||||
|
||||
/**
|
||||
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
||||
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
* ; US-ASCII characters excluding CTLs,
|
||||
* ; whitespace DQUOTE, comma, semicolon,
|
||||
* ; and backslash
|
||||
*/
|
||||
|
||||
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
||||
|
||||
/**
|
||||
* RegExp to match domain-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* domain-value = <subdomain>
|
||||
* ; defined in [RFC1034], Section 3.5, as
|
||||
* ; enhanced by [RFC1123], Section 2.1
|
||||
* <subdomain> = <label> | <subdomain> "." <label>
|
||||
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||
* Labels must be 63 characters or less.
|
||||
* 'let-dig' not 'letter' in the first char, per RFC1123
|
||||
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||
* <let-dig-hyp> = <let-dig> | "-"
|
||||
* <let-dig> = <letter> | <digit>
|
||||
* <letter> = any one of the 52 alphabetic characters A through Z in
|
||||
* upper case and a through z in lower case
|
||||
* <digit> = any one of the ten digits 0 through 9
|
||||
*
|
||||
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
|
||||
*
|
||||
* > (Note that a leading %x2E ("."), if present, is ignored even though that
|
||||
* character is not permitted, but a trailing %x2E ("."), if present, will
|
||||
* cause the user agent to ignore the attribute.)
|
||||
*/
|
||||
|
||||
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
||||
|
||||
/**
|
||||
* RegExp to match path-value in RFC 6265 sec 4.1.1
|
||||
*
|
||||
* path-value = <any CHAR except CTLs or ";">
|
||||
* CHAR = %x01-7F
|
||||
* ; defined in RFC 5234 appendix B.1
|
||||
*/
|
||||
|
||||
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
||||
|
||||
/**
|
||||
* Parse a cookie header.
|
||||
@@ -39,107 +91,128 @@ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
|
||||
* The object has the various cookies as keys(names) => values
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {object} [options]
|
||||
* @param {object} [opt]
|
||||
* @return {object}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function parse(str, options) {
|
||||
function parse(str, opt) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new TypeError('argument str must be a string');
|
||||
}
|
||||
|
||||
var obj = {}
|
||||
var opt = options || {};
|
||||
var dec = opt.decode || decode;
|
||||
var obj = {};
|
||||
var len = str.length;
|
||||
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
|
||||
if (len < 2) return obj;
|
||||
|
||||
var index = 0
|
||||
while (index < str.length) {
|
||||
var eqIdx = str.indexOf('=', index)
|
||||
var dec = (opt && opt.decode) || decode;
|
||||
var index = 0;
|
||||
var eqIdx = 0;
|
||||
var endIdx = 0;
|
||||
|
||||
// no more cookie pairs
|
||||
if (eqIdx === -1) {
|
||||
break
|
||||
}
|
||||
do {
|
||||
eqIdx = str.indexOf('=', index);
|
||||
if (eqIdx === -1) break; // No more cookie pairs.
|
||||
|
||||
var endIdx = str.indexOf(';', index)
|
||||
endIdx = str.indexOf(';', index);
|
||||
|
||||
if (endIdx === -1) {
|
||||
endIdx = str.length
|
||||
} else if (endIdx < eqIdx) {
|
||||
endIdx = len;
|
||||
} else if (eqIdx > endIdx) {
|
||||
// backtrack on prior semicolon
|
||||
index = str.lastIndexOf(';', eqIdx - 1) + 1
|
||||
continue
|
||||
index = str.lastIndexOf(';', eqIdx - 1) + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = str.slice(index, eqIdx).trim()
|
||||
var keyStartIdx = startIndex(str, index, eqIdx);
|
||||
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
||||
var key = str.slice(keyStartIdx, keyEndIdx);
|
||||
|
||||
// only assign once
|
||||
if (undefined === obj[key]) {
|
||||
var val = str.slice(eqIdx + 1, endIdx).trim()
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
||||
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
||||
|
||||
// quoted values
|
||||
if (val.charCodeAt(0) === 0x22) {
|
||||
val = val.slice(1, -1)
|
||||
if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
|
||||
valStartIdx++;
|
||||
valEndIdx--;
|
||||
}
|
||||
|
||||
var val = str.slice(valStartIdx, valEndIdx);
|
||||
obj[key] = tryDecode(val, dec);
|
||||
}
|
||||
|
||||
index = endIdx + 1
|
||||
}
|
||||
} while (index < len);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function startIndex(str, index, max) {
|
||||
do {
|
||||
var code = str.charCodeAt(index);
|
||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index;
|
||||
} while (++index < max);
|
||||
return max;
|
||||
}
|
||||
|
||||
function endIndex(str, index, min) {
|
||||
while (index > min) {
|
||||
var code = str.charCodeAt(--index);
|
||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data into a cookie header.
|
||||
*
|
||||
* Serialize the a name value pair into a cookie string suitable for
|
||||
* http headers. An optional options object specified cookie parameters.
|
||||
* Serialize a name value pair into a cookie string suitable for
|
||||
* http headers. An optional options object specifies cookie parameters.
|
||||
*
|
||||
* serialize('foo', 'bar', { httpOnly: true })
|
||||
* => "foo=bar; httpOnly"
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} val
|
||||
* @param {object} [options]
|
||||
* @param {object} [opt]
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function serialize(name, val, options) {
|
||||
var opt = options || {};
|
||||
var enc = opt.encode || encode;
|
||||
function serialize(name, val, opt) {
|
||||
var enc = (opt && opt.encode) || encodeURIComponent;
|
||||
|
||||
if (typeof enc !== 'function') {
|
||||
throw new TypeError('option encode is invalid');
|
||||
}
|
||||
|
||||
if (!fieldContentRegExp.test(name)) {
|
||||
if (!cookieNameRegExp.test(name)) {
|
||||
throw new TypeError('argument name is invalid');
|
||||
}
|
||||
|
||||
var value = enc(val);
|
||||
|
||||
if (value && !fieldContentRegExp.test(value)) {
|
||||
if (!cookieValueRegExp.test(value)) {
|
||||
throw new TypeError('argument val is invalid');
|
||||
}
|
||||
|
||||
var str = name + '=' + value;
|
||||
if (!opt) return str;
|
||||
|
||||
if (null != opt.maxAge) {
|
||||
var maxAge = opt.maxAge - 0;
|
||||
var maxAge = Math.floor(opt.maxAge);
|
||||
|
||||
if (isNaN(maxAge) || !isFinite(maxAge)) {
|
||||
if (!isFinite(maxAge)) {
|
||||
throw new TypeError('option maxAge is invalid')
|
||||
}
|
||||
|
||||
str += '; Max-Age=' + Math.floor(maxAge);
|
||||
str += '; Max-Age=' + maxAge;
|
||||
}
|
||||
|
||||
if (opt.domain) {
|
||||
if (!fieldContentRegExp.test(opt.domain)) {
|
||||
if (!domainValueRegExp.test(opt.domain)) {
|
||||
throw new TypeError('option domain is invalid');
|
||||
}
|
||||
|
||||
@@ -147,7 +220,7 @@ function serialize(name, val, options) {
|
||||
}
|
||||
|
||||
if (opt.path) {
|
||||
if (!fieldContentRegExp.test(opt.path)) {
|
||||
if (!pathValueRegExp.test(opt.path)) {
|
||||
throw new TypeError('option path is invalid');
|
||||
}
|
||||
|
||||
@@ -178,8 +251,7 @@ function serialize(name, val, options) {
|
||||
|
||||
if (opt.priority) {
|
||||
var priority = typeof opt.priority === 'string'
|
||||
? opt.priority.toLowerCase()
|
||||
: opt.priority
|
||||
? opt.priority.toLowerCase() : opt.priority;
|
||||
|
||||
switch (priority) {
|
||||
case 'low':
|
||||
@@ -234,17 +306,6 @@ function decode (str) {
|
||||
: str
|
||||
}
|
||||
|
||||
/**
|
||||
* URL-encode value.
|
||||
*
|
||||
* @param {string} val
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
function encode (val) {
|
||||
return encodeURIComponent(val)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if value is a Date.
|
||||
*
|
||||
@@ -253,8 +314,7 @@ function encode (val) {
|
||||
*/
|
||||
|
||||
function isDate (val) {
|
||||
return __toString.call(val) === '[object Date]' ||
|
||||
val instanceof Date
|
||||
return __toString.call(val) === '[object Date]';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
6
backend/node_modules/cookie/package.json
generated
vendored
6
backend/node_modules/cookie/package.json
generated
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cookie",
|
||||
"description": "HTTP server cookie parsing and serialization",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>"
|
||||
@@ -29,6 +29,7 @@
|
||||
"SECURITY.md",
|
||||
"index.js"
|
||||
],
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
@@ -38,7 +39,6 @@
|
||||
"test": "mocha --reporter spec --bail --check-leaks test/",
|
||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||
"update-bench": "node scripts/update-benchmark.js",
|
||||
"version": "node scripts/version-history.js && git add HISTORY.md"
|
||||
"update-bench": "node scripts/update-benchmark.js"
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/node_modules/express/History.md
generated
vendored
14
backend/node_modules/express/History.md
generated
vendored
@@ -1,3 +1,17 @@
|
||||
4.21.2 / 2024-11-06
|
||||
==========
|
||||
|
||||
* deps: path-to-regexp@0.1.12
|
||||
- Fix backtracking protection
|
||||
* deps: path-to-regexp@0.1.11
|
||||
- Throws an error on invalid path values
|
||||
|
||||
4.21.1 / 2024-10-08
|
||||
==========
|
||||
|
||||
* Backported a fix for [CVE-2024-47764](https://nvd.nist.gov/vuln/detail/CVE-2024-47764)
|
||||
|
||||
|
||||
4.21.0 / 2024-09-11
|
||||
==========
|
||||
|
||||
|
||||
10
backend/node_modules/express/package.json
generated
vendored
10
backend/node_modules/express/package.json
generated
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "express",
|
||||
"description": "Fast, unopinionated, minimalist web framework",
|
||||
"version": "4.21.0",
|
||||
"version": "4.21.2",
|
||||
"author": "TJ Holowaychuk <tj@vision-media.ca>",
|
||||
"contributors": [
|
||||
"Aaron Heckmann <aaron.heckmann+github@gmail.com>",
|
||||
@@ -15,6 +15,10 @@
|
||||
"license": "MIT",
|
||||
"repository": "expressjs/express",
|
||||
"homepage": "http://expressjs.com/",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
},
|
||||
"keywords": [
|
||||
"express",
|
||||
"framework",
|
||||
@@ -33,7 +37,7 @@
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -47,7 +51,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
|
||||
16
backend/node_modules/path-to-regexp/index.js
generated
vendored
16
backend/node_modules/path-to-regexp/index.js
generated
vendored
@@ -65,23 +65,33 @@ function pathToRegexp(path, keys, options) {
|
||||
return new RegExp(path.join('|'), flags);
|
||||
}
|
||||
|
||||
if (typeof path !== 'string') {
|
||||
throw new TypeError('path must be a string, array of strings, or regular expression');
|
||||
}
|
||||
|
||||
path = path.replace(
|
||||
/\\.|(\/)?(\.)?:(\w+)(\(.*?\))?(\*)?(\?)?|[.*]|\/\(/g,
|
||||
function (match, slash, format, key, capture, star, optional, offset) {
|
||||
pos = offset + match.length;
|
||||
|
||||
if (match[0] === '\\') {
|
||||
backtrack += match;
|
||||
pos += 2;
|
||||
return match;
|
||||
}
|
||||
|
||||
if (match === '.') {
|
||||
backtrack += '\\.';
|
||||
extraOffset += 1;
|
||||
pos += 1;
|
||||
return '\\.';
|
||||
}
|
||||
|
||||
backtrack = slash || format ? '' : path.slice(pos, offset);
|
||||
if (slash || format) {
|
||||
backtrack = '';
|
||||
} else {
|
||||
backtrack += path.slice(pos, offset);
|
||||
}
|
||||
|
||||
pos = offset + match.length;
|
||||
|
||||
if (match === '*') {
|
||||
extraOffset += 3;
|
||||
|
||||
2
backend/node_modules/path-to-regexp/package.json
generated
vendored
2
backend/node_modules/path-to-regexp/package.json
generated
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "path-to-regexp",
|
||||
"description": "Express style path to RegExp utility",
|
||||
"version": "0.1.10",
|
||||
"version": "0.1.12",
|
||||
"files": [
|
||||
"index.js",
|
||||
"LICENSE"
|
||||
|
||||
77
backend/package-lock.json
generated
77
backend/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
@@ -893,9 +894,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -1168,9 +1170,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -1199,10 +1202,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -1585,16 +1589,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -1608,7 +1613,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@@ -1623,6 +1628,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
@@ -2636,6 +2645,12 @@
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -2888,9 +2903,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf-parse": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
|
||||
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.1.0",
|
||||
"node-ensure": "^0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"postinstall": "cd ../frontend && npm install && npm run build",
|
||||
"dev": "nodemon server.js"
|
||||
"dev": "nodemon server.js",
|
||||
"cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js",
|
||||
"cleanup:indexes": "node ./scripts/cleanupAllIndexes.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -16,6 +18,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"csv-parser": "^3.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@@ -23,6 +26,7 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,6 @@ const router = express.Router();
|
||||
router.post('/register', registerUser);
|
||||
router.get('/activate/:activationCode', activate);
|
||||
router.post('/login', loginUser);
|
||||
router.get('/logout', logoutUser);
|
||||
router.post('/logout', logoutUser); // Ändere GET zu POST
|
||||
|
||||
export default router;
|
||||
|
||||
15
backend/routes/diaryMemberActivityRoutes.js
Normal file
15
backend/routes/diaryMemberActivityRoutes.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { addMembersToActivity, removeMemberFromActivity, getMembersForActivity } from '../controllers/diaryMemberActivityController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/:clubId/:diaryDateActivityId', getMembersForActivity);
|
||||
router.post('/:clubId/:diaryDateActivityId', addMembersToActivity);
|
||||
router.delete('/:clubId/:diaryDateActivityId/:participantId', removeMemberFromActivity);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
deleteDiaryNote,
|
||||
addDiaryTag,
|
||||
addTagToDiaryDate,
|
||||
deleteTagFromDiaryDate
|
||||
deleteTagFromDiaryDate,
|
||||
deleteDateForClub,
|
||||
} from '../controllers/diaryController.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -21,5 +22,6 @@ router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate);
|
||||
router.get('/:clubId', authenticate, getDatesForClub);
|
||||
router.post('/:clubId', authenticate, createDateForClub);
|
||||
router.put('/:clubId', authenticate, updateTrainingTimes);
|
||||
router.delete('/:clubId/:dateId', authenticate, deleteDateForClub);
|
||||
|
||||
export default router;
|
||||
|
||||
21
backend/routes/officialTournamentRoutes.js
Normal file
21
backend/routes/officialTournamentRoutes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember, listClubParticipations, updateParticipantStatus } from '../controllers/officialTournamentController.js';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/:clubId', listOfficialTournaments);
|
||||
router.get('/:clubId/participations/summary', listClubParticipations);
|
||||
router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf);
|
||||
router.get('/:clubId/:id', getParsedTournament);
|
||||
router.delete('/:clubId/:id', deleteOfficialTournament);
|
||||
router.post('/:clubId/:id/participation', upsertCompetitionMember);
|
||||
router.post('/:clubId/:id/status', updateParticipantStatus);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
@@ -4,13 +4,40 @@ import {
|
||||
getAllPredefinedActivities,
|
||||
getPredefinedActivityById,
|
||||
updatePredefinedActivity,
|
||||
searchPredefinedActivities,
|
||||
mergePredefinedActivities,
|
||||
deduplicatePredefinedActivities,
|
||||
} from '../controllers/predefinedActivityController.js';
|
||||
import multer from 'multer';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { uploadPredefinedActivityImage, deletePredefinedActivityImage } from '../controllers/predefinedActivityImageController.js';
|
||||
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.post('/', createPredefinedActivity);
|
||||
router.get('/', getAllPredefinedActivities);
|
||||
router.get('/:id', getPredefinedActivityById);
|
||||
router.put('/:id', updatePredefinedActivity);
|
||||
router.post('/', authenticate, createPredefinedActivity);
|
||||
router.get('/', authenticate, getAllPredefinedActivities);
|
||||
router.get('/:id', authenticate, getPredefinedActivityById);
|
||||
router.put('/:id', authenticate, updatePredefinedActivity);
|
||||
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
|
||||
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
|
||||
router.get('/search/query', authenticate, searchPredefinedActivities);
|
||||
router.post('/merge', authenticate, mergePredefinedActivities);
|
||||
router.post('/deduplicate', authenticate, deduplicatePredefinedActivities);
|
||||
router.get('/:id/image/:imageId', async (req, res) => {
|
||||
try {
|
||||
const { id, imageId } = req.params;
|
||||
const image = await PredefinedActivityImage.findOne({ where: { id: imageId, predefinedActivityId: id } });
|
||||
if (!image) return res.status(404).json({ error: 'Image not found' });
|
||||
if (!fs.existsSync(image.imagePath)) return res.status(404).json({ error: 'Image file missing' });
|
||||
res.sendFile(path.resolve(image.imagePath));
|
||||
} catch (e) {
|
||||
console.error('[getPredefinedActivityImage] - Error:', e);
|
||||
res.status(500).json({ error: 'Failed to fetch image' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
49
backend/routes/tournamentRoutes.js
Normal file
49
backend/routes/tournamentRoutes.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
getTournaments,
|
||||
addTournament,
|
||||
addParticipant,
|
||||
getParticipants,
|
||||
setModus,
|
||||
createGroups,
|
||||
fillGroups,
|
||||
getGroups,
|
||||
getTournament,
|
||||
getTournamentMatches,
|
||||
addMatchResult,
|
||||
finishMatch,
|
||||
startKnockout,
|
||||
manualAssignGroups,
|
||||
resetGroups,
|
||||
resetMatches,
|
||||
removeParticipant,
|
||||
deleteMatchResult,
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
} from '../controllers/tournamentController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/participant', authenticate, addParticipant);
|
||||
router.post('/participants', authenticate, getParticipants);
|
||||
router.delete('/participant', authenticate, removeParticipant);
|
||||
router.post('/modus', authenticate, setModus);
|
||||
router.post('/groups/reset', authenticate, resetGroups);
|
||||
router.post('/matches/reset', authenticate, resetMatches);
|
||||
router.put('/groups', authenticate, createGroups);
|
||||
router.post('/groups', authenticate, fillGroups);
|
||||
router.get('/groups', authenticate, getGroups);
|
||||
router.post('/match/result', authenticate, addMatchResult);
|
||||
router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
router.post("/match/reopen", reopenMatch);
|
||||
router.post('/match/finish', authenticate, finishMatch);
|
||||
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/knockout', authenticate, startKnockout);
|
||||
router.delete("/matches/knockout", deleteKnockoutMatches);
|
||||
router.post('/groups/manual', authenticate, manualAssignGroups);
|
||||
router.post('/', authenticate, addTournament);
|
||||
|
||||
export default router;
|
||||
10
backend/routes/trainingStatsRoutes.js
Normal file
10
backend/routes/trainingStatsRoutes.js
Normal file
@@ -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;
|
||||
100
backend/scripts/cleanupAllIndexes.js
Normal file
100
backend/scripts/cleanupAllIndexes.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'trainingdiary',
|
||||
};
|
||||
|
||||
async function getTables(connection) {
|
||||
const [rows] = await connection.execute(
|
||||
`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE='BASE TABLE'`,
|
||||
[dbConfig.database]
|
||||
);
|
||||
return rows.map(r => r.TABLE_NAME);
|
||||
}
|
||||
|
||||
async function getIndexSummary(connection, table) {
|
||||
const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
|
||||
const byName = rows.reduce((acc, r) => {
|
||||
const key = r.Key_name;
|
||||
if (!acc[key]) acc[key] = { nonUnique: r.Non_unique === 1, seqMap: {}, columns: [] };
|
||||
acc[key].seqMap[r.Seq_in_index] = r.Column_name;
|
||||
return acc;
|
||||
}, {});
|
||||
// normalize columns order by seq
|
||||
for (const name of Object.keys(byName)) {
|
||||
const cols = Object.keys(byName[name].seqMap)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map(k => byName[name].seqMap[k]);
|
||||
byName[name].columns = cols;
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
|
||||
async function cleanupDuplicates(connection, table) {
|
||||
const before = await getIndexSummary(connection, table);
|
||||
const keepSignatureToName = new Map();
|
||||
const dropNames = [];
|
||||
|
||||
for (const [name, info] of Object.entries(before)) {
|
||||
if (name === 'PRIMARY') continue; // niemals Primary droppen
|
||||
const uniqueFlag = info.nonUnique ? 'N' : 'U';
|
||||
const sig = `${uniqueFlag}|${info.columns.join(',')}`;
|
||||
if (!keepSignatureToName.has(sig)) {
|
||||
keepSignatureToName.set(sig, name);
|
||||
} else {
|
||||
// doppelter Index mit gleicher Spaltenliste und gleicher Einzigartigkeit
|
||||
dropNames.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const idxName of dropNames) {
|
||||
try {
|
||||
await connection.execute(`DROP INDEX \`${idxName}\` ON \`${table}\``);
|
||||
console.log(`[drop] ${table}: ${idxName}`);
|
||||
} catch (e) {
|
||||
console.warn(`[warn] ${table}: konnte Index ${idxName} nicht löschen: ${e.code || e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const after = await getIndexSummary(connection, table);
|
||||
return { beforeCount: Object.keys(before).length, afterCount: Object.keys(after).length, dropped: dropNames.length };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let connection;
|
||||
try {
|
||||
console.log('Connecting to DB:', dbConfig);
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
const tables = await getTables(connection);
|
||||
console.log(`Found ${tables.length} tables`);
|
||||
|
||||
let totalBefore = 0;
|
||||
let totalAfter = 0;
|
||||
let totalDropped = 0;
|
||||
|
||||
for (const table of tables) {
|
||||
const { beforeCount, afterCount, dropped } = await cleanupDuplicates(connection, table);
|
||||
totalBefore += beforeCount;
|
||||
totalAfter += afterCount;
|
||||
totalDropped += dropped;
|
||||
}
|
||||
|
||||
console.log('Summary:', { totalBefore, totalAfter, totalDropped });
|
||||
} catch (e) {
|
||||
console.error('Cleanup failed:', e);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (connection) await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
90
backend/scripts/cleanupUserTokenKeys.js
Normal file
90
backend/scripts/cleanupUserTokenKeys.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'trainingdiary',
|
||||
};
|
||||
|
||||
async function getIndexSummary(connection, table) {
|
||||
const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
|
||||
const summary = rows.reduce((acc, r) => {
|
||||
const key = r.Key_name;
|
||||
acc[key] = acc[key] || { unique: r.Non_unique === 0, columns: [] };
|
||||
acc[key].columns.push(r.Column_name);
|
||||
return acc;
|
||||
}, {});
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function cleanupUserTokenKeys() {
|
||||
let connection;
|
||||
const table = 'UserToken';
|
||||
|
||||
try {
|
||||
console.log('Connecting to DB:', dbConfig);
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
console.log(`\nBefore cleanup (indexes on ${table}):`);
|
||||
let before = await getIndexSummary(connection, table);
|
||||
Object.entries(before).forEach(([name, info]) => {
|
||||
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
|
||||
});
|
||||
|
||||
// Drop all non-PRIMARY indexes on UserToken
|
||||
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
|
||||
const keyNames = Array.from(new Set(indexes.map(i => i.Key_name))).filter(k => k !== 'PRIMARY');
|
||||
|
||||
for (const keyName of keyNames) {
|
||||
try {
|
||||
await connection.execute(`DROP INDEX \`${keyName}\` ON \`${table}\``);
|
||||
console.log(`Dropped index: ${keyName}`);
|
||||
} catch (err) {
|
||||
console.warn(`Could not drop ${keyName}: ${err.code || err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-create minimal, deterministic indexes
|
||||
// Unique on token (column is 'token')
|
||||
try {
|
||||
await connection.execute(`CREATE UNIQUE INDEX \`uniq_UserToken_token\` ON \`${table}\` (\`token\`)`);
|
||||
console.log('Created UNIQUE index: uniq_UserToken_token (token)');
|
||||
} catch (err) {
|
||||
console.warn('Could not create uniq_UserToken_token:', err.code || err.message);
|
||||
}
|
||||
|
||||
// Helpful index on user_id if column exists
|
||||
try {
|
||||
const [cols] = await connection.execute(`SHOW COLUMNS FROM \`${table}\` LIKE 'user_id'`);
|
||||
if (cols && cols.length > 0) {
|
||||
await connection.execute(`CREATE INDEX \`idx_UserToken_user_id\` ON \`${table}\` (\`user_id\`)`);
|
||||
console.log('Created INDEX: idx_UserToken_user_id (user_id)');
|
||||
} else {
|
||||
console.log('Column user_id not found, skip creating idx_UserToken_user_id');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not create idx_UserToken_user_id:', err.code || err.message);
|
||||
}
|
||||
|
||||
console.log(`\nAfter cleanup (indexes on ${table}):`);
|
||||
const after = await getIndexSummary(connection, table);
|
||||
Object.entries(after).forEach(([name, info]) => {
|
||||
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
|
||||
});
|
||||
|
||||
console.log('\nDone.');
|
||||
} catch (err) {
|
||||
console.error('Cleanup failed:', err);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (connection) await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
cleanupUserTokenKeys();
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import cors from 'cors';
|
||||
import {
|
||||
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, DiaryDateActivity, Match, League, Team, Group,
|
||||
GroupActivity,
|
||||
Accident
|
||||
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -22,13 +22,17 @@ import diaryNoteRoutes from './routes/diaryNoteRoutes.js';
|
||||
import diaryMemberRoutes from './routes/diaryMemberRoutes.js';
|
||||
import predefinedActivityRoutes from './routes/predefinedActivityRoutes.js';
|
||||
import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js';
|
||||
import diaryMemberActivityRoutes from './routes/diaryMemberActivityRoutes.js';
|
||||
import matchRoutes from './routes/matchRoutes.js';
|
||||
import Season from './models/Season.js';
|
||||
import Location from './models/Location.js';
|
||||
import groupRoutes from './routes/groupRoutes.js';
|
||||
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';
|
||||
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
@@ -39,6 +43,14 @@ const __dirname = path.dirname(__filename);
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Globale Fehlerbehandlung, damit der Server bei unerwarteten Fehlern nicht hart abstürzt
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[uncaughtException]', err);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[unhandledRejection]', reason);
|
||||
});
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/clubs', clubRoutes);
|
||||
app.use('/api/clubmembers', memberRoutes);
|
||||
@@ -51,14 +63,19 @@ app.use('/api/tags', diaryTagRoutes);
|
||||
app.use('/api/diarymember', diaryMemberRoutes);
|
||||
app.use('/api/predefined-activities', predefinedActivityRoutes);
|
||||
app.use('/api/diary-date-activities', diaryDateActivityRoutes);
|
||||
app.use('/api/diary-member-activities', diaryMemberActivityRoutes);
|
||||
app.use('/api/matches', matchRoutes);
|
||||
app.use('/api/group', groupRoutes);
|
||||
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('/api/official-tournaments', officialTournamentRoutes);
|
||||
|
||||
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'));
|
||||
});
|
||||
@@ -67,31 +84,93 @@ app.get('*', (req, res) => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
|
||||
await User.sync({ alter: true });
|
||||
await Club.sync({ alter: true });
|
||||
await UserClub.sync({ alter: true });
|
||||
await Log.sync({ alter: true });
|
||||
await Member.sync({ alter: true });
|
||||
await DiaryDate.sync({ alter: true });
|
||||
await Participant.sync({ alter: true });
|
||||
await Activity.sync({ alter: true });
|
||||
await MemberNote.sync({ alter: true });
|
||||
await DiaryNote.sync({ alter: true });
|
||||
await DiaryTag.sync({ alter: true });
|
||||
await MemberDiaryTag.sync({ alter: true });
|
||||
await DiaryDateTag.sync({ alter: true });
|
||||
await DiaryMemberTag.sync({ alter: true });
|
||||
await DiaryMemberNote.sync({ alter: true });
|
||||
await PredefinedActivity.sync({ alter: true });
|
||||
await DiaryDateActivity.sync({ alter: true });
|
||||
await Season.sync({ alter: true });
|
||||
await League.sync({ alter: true });
|
||||
await Team.sync({ alter: true });
|
||||
await Location.sync({ alter: true });
|
||||
await Match.sync({ alter: true });
|
||||
await Group.sync({ alter: true });
|
||||
await GroupActivity.sync({ alter: true });
|
||||
await Accident.sync({ alter: true });
|
||||
// Einmalige Migration: deutsche Spaltennamen -> englische
|
||||
const renameColumnIfExists = async (table, from, to, typeSql) => {
|
||||
try {
|
||||
const [rows] = await sequelize.query(`SHOW COLUMNS FROM \`${table}\` LIKE :col`, { replacements: { col: from } });
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
await sequelize.query(`ALTER TABLE \`${table}\` CHANGE \`${from}\` \`${to}\` ${typeSql}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[migration] Failed to rename ${table}.${from} -> ${to}:`, e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// official_competitions
|
||||
await renameColumnIfExists('official_competitions', 'altersklasse_wettbewerb', 'age_class_competition', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'leistungsklasse', 'performance_class', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'startzeit', 'start_time', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'meldeschluss_datum', 'registration_deadline_date', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'meldeschluss_online', 'registration_deadline_online', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'stichtag', 'cutoff_date', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'offen_fuer', 'open_to', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'vorrunde', 'preliminary_round', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'endrunde', 'final_round', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'max_teilnehmer', 'max_participants', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_competitions', 'startgeld', 'entry_fee', 'VARCHAR(255) NULL');
|
||||
|
||||
// official_tournaments
|
||||
await renameColumnIfExists('official_tournaments', 'termin', 'event_date', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_tournaments', 'veranstalter', 'organizer', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_tournaments', 'ausrichter', 'host', 'VARCHAR(255) NULL');
|
||||
await renameColumnIfExists('official_tournaments', 'austragungsorte', 'venues', 'TEXT NULL');
|
||||
await renameColumnIfExists('official_tournaments', 'konkurrenztypen', 'competition_types', 'TEXT NULL');
|
||||
await renameColumnIfExists('official_tournaments', 'meldeschluesse', 'registration_deadlines', 'TEXT NULL');
|
||||
|
||||
const isDev = process.env.STAGE === 'dev';
|
||||
const safeSync = async (model) => {
|
||||
try {
|
||||
if (isDev) {
|
||||
await model.sync({ alter: true });
|
||||
} else {
|
||||
await model.sync();
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
console.error(`[sync] ${model?.name || 'model'} alter failed:`, e?.message || e);
|
||||
await model.sync();
|
||||
} catch (e2) {
|
||||
console.error(`[sync] fallback failed for ${model?.name || 'model'}:`, e2?.message || e2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await safeSync(User);
|
||||
await safeSync(Club);
|
||||
await safeSync(UserClub);
|
||||
await safeSync(Log);
|
||||
await safeSync(Member);
|
||||
await safeSync(DiaryDate);
|
||||
await safeSync(Participant);
|
||||
await safeSync(Activity);
|
||||
await safeSync(MemberNote);
|
||||
await safeSync(DiaryNote);
|
||||
await safeSync(DiaryTag);
|
||||
await safeSync(MemberDiaryTag);
|
||||
await safeSync(DiaryDateTag);
|
||||
await safeSync(DiaryMemberTag);
|
||||
await safeSync(DiaryMemberNote);
|
||||
await safeSync(PredefinedActivity);
|
||||
await safeSync(PredefinedActivityImage);
|
||||
await safeSync(DiaryDateActivity);
|
||||
await safeSync(DiaryMemberActivity);
|
||||
await safeSync(OfficialTournament);
|
||||
await safeSync(OfficialCompetition);
|
||||
await safeSync(OfficialCompetitionMember);
|
||||
await safeSync(Season);
|
||||
await safeSync(League);
|
||||
await safeSync(Team);
|
||||
await safeSync(Location);
|
||||
await safeSync(Match);
|
||||
await safeSync(Group);
|
||||
await safeSync(GroupActivity);
|
||||
await safeSync(Tournament);
|
||||
await safeSync(TournamentGroup);
|
||||
await safeSync(TournamentMember);
|
||||
await safeSync(TournamentMatch);
|
||||
await safeSync(TournamentResult);
|
||||
await safeSync(Accident);
|
||||
await safeSync(UserToken);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
|
||||
@@ -8,8 +8,7 @@ class AccidentService {
|
||||
await checkAccess(userToken, clubId);
|
||||
const user = await getUserByToken(userToken);
|
||||
if (!user) {
|
||||
console.log('---------------');
|
||||
throw new Error('User not found');
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const member = await Member.findByPk(memberId);
|
||||
if (!member || member.clubId != clubId) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '../models/User.js';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import { sendActivationEmail } from './emailService.js';
|
||||
|
||||
const register = async (email, password) => {
|
||||
@@ -25,22 +26,28 @@ const activateUser = async (activationCode) => {
|
||||
};
|
||||
|
||||
const login = async (email, password) => {
|
||||
if (!email || !password) {
|
||||
throw { status: 400, message: 'Email und Passwort sind erforderlich.' };
|
||||
}
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user || !user.isActive) throw new Error('Invalid email or password.');
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) throw new Error('Invalid email or password!');
|
||||
const token = jwt.sign({ userId: user.hashedId }, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
user.authCode = token;
|
||||
await user.save();
|
||||
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||
throw { status: 401, message: 'Ungültige Anmeldedaten' };
|
||||
}
|
||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
await UserToken.create({
|
||||
userId: user.id,
|
||||
token,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
});
|
||||
return { token };
|
||||
};
|
||||
|
||||
const logout = async(userId, authToken) => {
|
||||
const user = await User.findOne({ where: { id: userId, authToken: authToken }});
|
||||
if (!user) {
|
||||
throw new Error('not found');
|
||||
const logout = async (token) => {
|
||||
if (!token) {
|
||||
throw { status: 400, message: 'Token fehlt' };
|
||||
}
|
||||
user.update({ authToken: null });
|
||||
}
|
||||
await UserToken.destroy({ where: { token } });
|
||||
return { message: 'Logout erfolgreich' };
|
||||
};
|
||||
|
||||
export { register, activateUser, login, logout };
|
||||
|
||||
@@ -20,9 +20,9 @@ class DiaryDateActivityService {
|
||||
duration: data.duration
|
||||
});
|
||||
}
|
||||
restData.predefinedActivityId = predefinedActivity.id;
|
||||
restData.predefinedActivityId = predefinedActivity.id;
|
||||
const maxOrderId = await DiaryDateActivity.max('orderId', {
|
||||
where: { diaryDateId: data.diaryDateId }
|
||||
where: { diaryDateId: data.diaryDateId }
|
||||
});
|
||||
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
|
||||
restData.orderId = newOrderId;
|
||||
@@ -39,7 +39,34 @@ class DiaryDateActivityService {
|
||||
console.log('[DiaryDateActivityService::updateActivity] - activity not found');
|
||||
throw new Error('Activity not found');
|
||||
}
|
||||
console.log('[DiaryDateActivityService::updateActivity] - update activity');
|
||||
|
||||
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
|
||||
if (data.customActivityName) {
|
||||
console.log('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
|
||||
|
||||
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
|
||||
let predefinedActivity = await PredefinedActivity.findOne({
|
||||
where: { name: data.customActivityName }
|
||||
});
|
||||
|
||||
if (!predefinedActivity) {
|
||||
// Erstelle eine neue PredefinedActivity
|
||||
console.log('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
|
||||
predefinedActivity = await PredefinedActivity.create({
|
||||
name: data.customActivityName,
|
||||
description: '',
|
||||
duration: data.duration || activity.duration
|
||||
});
|
||||
}
|
||||
|
||||
// Setze die predefinedActivityId
|
||||
data.predefinedActivityId = predefinedActivity.id;
|
||||
|
||||
// Entferne customActivityName aus den zu aktualisierenden Daten
|
||||
delete data.customActivityName;
|
||||
}
|
||||
|
||||
console.log('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
|
||||
return await activity.update(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DiaryDate from '../models/DiaryDates.js';
|
||||
import DiaryDateActivity from '../models/DiaryDateActivity.js';
|
||||
import Club from '../models/Club.js';
|
||||
import DiaryNote from '../models/DiaryNote.js';
|
||||
import { DiaryTag } from '../models/DiaryTag.js';
|
||||
@@ -151,6 +152,23 @@ class DiaryService {
|
||||
await DiaryDateTag.destroy({ where: { tagId } });
|
||||
}
|
||||
|
||||
async removeDateForClub(userToken, clubId, dateId) {
|
||||
console.log('[DiaryService::removeDateForClub] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryService::removeDateForClub] - Validate date');
|
||||
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
|
||||
if (!diaryDate) {
|
||||
throw new HttpError('Diary entry not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::removeDateForClub] - Check for activities');
|
||||
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
|
||||
if (activityCount > 0) {
|
||||
throw new HttpError('Cannot delete date with activities', 409);
|
||||
}
|
||||
console.log('[DiaryService::removeDateForClub] - Delete diary date');
|
||||
await diaryDate.destroy();
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new DiaryService();
|
||||
|
||||
@@ -31,7 +31,7 @@ class MatchService {
|
||||
const matches = [];
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath)
|
||||
.pipe(iconv.decodeStream('ISO-8859-15'))
|
||||
.pipe(iconv.decodeStream('utf8'))
|
||||
.pipe(csv({ separator: ';' }));
|
||||
for await (const row of fileStream) {
|
||||
const parsedDate = parse(row['Termin'], 'dd.MM.yyyy HH:mm', new Date());
|
||||
@@ -67,15 +67,11 @@ class MatchService {
|
||||
clubId: clubId,
|
||||
});
|
||||
}
|
||||
let season = null;
|
||||
if (seasonString) {
|
||||
const season = await Season.findOne({ where: { season: seasonString } });
|
||||
season = await Season.findOne({ where: { season: seasonString } });
|
||||
if (season) {
|
||||
await Match.destroy({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
seasonId: season.id,
|
||||
}
|
||||
});
|
||||
await Match.destroy({ where: { clubId, seasonId: season.id } });
|
||||
}
|
||||
}
|
||||
const result = await Match.bulkCreate(matches);
|
||||
|
||||
@@ -54,7 +54,7 @@ class MemberService {
|
||||
}
|
||||
|
||||
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
|
||||
picsInInternetAllowed = false) {
|
||||
picsInInternetAllowed = false, gender = 'unknown') {
|
||||
try {
|
||||
console.log('[setClubMembers] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -76,6 +76,7 @@ class MemberService {
|
||||
member.active = active;
|
||||
member.testMembership = testMembership;
|
||||
member.picsInInternetAllowed = picsInInternetAllowed;
|
||||
if (gender) member.gender = gender;
|
||||
await member.save();
|
||||
} else {
|
||||
await Member.create({
|
||||
@@ -90,6 +91,7 @@ class MemberService {
|
||||
active: active,
|
||||
testMembership: testMembership,
|
||||
picsInInternetAllowed: picsInInternetAllowed,
|
||||
gender: gender || 'unknown',
|
||||
});
|
||||
}
|
||||
console.log('[setClubMembers] - return response');
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import PredefinedActivity from '../models/PredefinedActivity.js';
|
||||
import DiaryDateActivity from '../models/DiaryDateActivity.js';
|
||||
import GroupActivity from '../models/GroupActivity.js';
|
||||
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import sequelize from '../database.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
class PredefinedActivityService {
|
||||
async createPredefinedActivity(data) {
|
||||
console.log('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
|
||||
return await PredefinedActivity.create({
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
durationText: data.durationText,
|
||||
duration: data.duration,
|
||||
imageLink: data.imageLink,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,15 +27,23 @@ class PredefinedActivityService {
|
||||
}
|
||||
return await activity.update({
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
durationText: data.durationText,
|
||||
duration: data.duration,
|
||||
imageLink: data.imageLink,
|
||||
});
|
||||
}
|
||||
|
||||
async getAllPredefinedActivities() {
|
||||
console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
|
||||
return await PredefinedActivity.findAll();
|
||||
return await PredefinedActivity.findAll({
|
||||
order: [
|
||||
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
|
||||
['code', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getPredefinedActivityById(id) {
|
||||
@@ -40,6 +55,94 @@ class PredefinedActivityService {
|
||||
}
|
||||
return activity;
|
||||
}
|
||||
|
||||
async searchPredefinedActivities(query, limit = 20) {
|
||||
const q = (query || '').trim();
|
||||
if (!q || q.length < 2) {
|
||||
return [];
|
||||
}
|
||||
return await PredefinedActivity.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ name: { [Op.like]: `%${q}%` } },
|
||||
{ code: { [Op.like]: `%${q}%` } },
|
||||
],
|
||||
},
|
||||
order: [
|
||||
[sequelize.literal('code IS NULL'), 'ASC'],
|
||||
['code', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
limit: Math.min(parseInt(limit || 20, 10), 50),
|
||||
});
|
||||
}
|
||||
|
||||
async mergeActivities(sourceId, targetId) {
|
||||
console.log(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
|
||||
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
|
||||
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
|
||||
|
||||
const tx = await sequelize.transaction();
|
||||
try {
|
||||
const source = await PredefinedActivity.findByPk(sourceId, { transaction: tx });
|
||||
const target = await PredefinedActivity.findByPk(targetId, { transaction: tx });
|
||||
if (!source) throw new Error('Source activity not found');
|
||||
if (!target) throw new Error('Target activity not found');
|
||||
|
||||
// Reassign references
|
||||
await DiaryDateActivity.update(
|
||||
{ predefinedActivityId: targetId },
|
||||
{ where: { predefinedActivityId: sourceId }, transaction: tx }
|
||||
);
|
||||
|
||||
await GroupActivity.update(
|
||||
{ customActivity: targetId },
|
||||
{ where: { customActivity: sourceId }, transaction: tx }
|
||||
);
|
||||
|
||||
await PredefinedActivityImage.update(
|
||||
{ predefinedActivityId: targetId },
|
||||
{ where: { predefinedActivityId: sourceId }, transaction: tx }
|
||||
);
|
||||
|
||||
// Finally delete source
|
||||
await source.destroy({ transaction: tx });
|
||||
|
||||
await tx.commit();
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
await tx.rollback();
|
||||
console.error('[PredefinedActivityService::mergeActivities] - Error:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deduplicateActivities() {
|
||||
console.log('[PredefinedActivityService::deduplicateActivities] - Start');
|
||||
const all = await PredefinedActivity.findAll();
|
||||
const nameToActivities = new Map();
|
||||
for (const activity of all) {
|
||||
const key = (activity.name || '').trim().toLowerCase();
|
||||
if (!key) continue;
|
||||
if (!nameToActivities.has(key)) nameToActivities.set(key, []);
|
||||
nameToActivities.get(key).push(activity);
|
||||
}
|
||||
let mergedCount = 0;
|
||||
let groupCount = 0;
|
||||
for (const list of nameToActivities.values()) {
|
||||
if (!list || list.length <= 1) continue;
|
||||
groupCount++;
|
||||
// Stable target: kleinste ID
|
||||
list.sort((a, b) => a.id - b.id);
|
||||
const target = list[0];
|
||||
for (const src of list.slice(1)) {
|
||||
await this.mergeActivities(src.id, target.id);
|
||||
mergedCount++;
|
||||
}
|
||||
}
|
||||
console.log('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
|
||||
return { mergedCount, groupCount };
|
||||
}
|
||||
}
|
||||
|
||||
export default new PredefinedActivityService();
|
||||
|
||||
614
backend/services/tournamentService.js
Normal file
614
backend/services/tournamentService.js
Normal file
@@ -0,0 +1,614 @@
|
||||
import Club from "../models/Club.js";
|
||||
import Member from "../models/Member.js";
|
||||
import Tournament from "../models/Tournament.js";
|
||||
import TournamentGroup from "../models/TournamentGroup.js";
|
||||
import TournamentMatch from "../models/TournamentMatch.js";
|
||||
import TournamentMember from "../models/TournamentMember.js";
|
||||
import TournamentResult from "../models/TournamentResult.js";
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op, literal } from 'sequelize';
|
||||
|
||||
|
||||
function getRoundName(size) {
|
||||
switch (size) {
|
||||
case 2: return "Finale";
|
||||
case 4: return "Halbfinale";
|
||||
case 8: return "Viertelfinale";
|
||||
case 16: return "Achtelfinale";
|
||||
default: return `Runde der ${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
function nextRoundName(currentName) {
|
||||
switch (currentName) {
|
||||
case "Achtelfinale": return "Viertelfinale";
|
||||
case "Viertelfinale": return "Halbfinale";
|
||||
case "Halbfinale": return "Finale";
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
class TournamentService {
|
||||
// 1. Turniere listen
|
||||
async getTournaments(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournaments = await Tournament.findAll({
|
||||
where: { clubId },
|
||||
order: [['date', 'DESC']],
|
||||
attributes: ['id', 'name', 'date']
|
||||
});
|
||||
return JSON.parse(JSON.stringify(tournaments));
|
||||
}
|
||||
|
||||
// 2. Neues Turnier anlegen (prüft Duplikat)
|
||||
async addTournament(userToken, clubId, tournamentName, date) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const existing = await Tournament.findOne({ where: { clubId, date } });
|
||||
if (existing) {
|
||||
throw new Error('Ein Turnier mit diesem Datum existiert bereits');
|
||||
}
|
||||
const t = await Tournament.create({
|
||||
name: tournamentName,
|
||||
date,
|
||||
clubId: +clubId,
|
||||
bestOfEndroundSize: 0,
|
||||
type: ''
|
||||
});
|
||||
return JSON.parse(JSON.stringify(t));
|
||||
}
|
||||
|
||||
// 3. Teilnehmer hinzufügen (kein Duplikat)
|
||||
async addParticipant(userToken, clubId, tournamentId, participantId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const exists = await TournamentMember.findOne({
|
||||
where: { tournamentId, clubMemberId: participantId }
|
||||
});
|
||||
if (exists) {
|
||||
throw new Error('Teilnehmer bereits hinzugefügt');
|
||||
}
|
||||
await TournamentMember.create({
|
||||
tournamentId,
|
||||
clubMemberId: participantId,
|
||||
groupId: null
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Teilnehmerliste
|
||||
async getParticipants(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
return await TournamentMember.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
model: Member,
|
||||
as: 'member',
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
}],
|
||||
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Modus setzen (Gruppen / KO‑Runde)
|
||||
async setModus(userToken, clubId, tournamentId, type, numberOfGroups, advancingPerGroup) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
await tournament.update({ type, numberOfGroups, advancingPerGroup });
|
||||
}
|
||||
|
||||
// 6. Leere Gruppen anlegen
|
||||
async createGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const existing = await TournamentGroup.findAll({ where: { tournamentId } });
|
||||
const desired = tournament.numberOfGroups;
|
||||
// zu viele Gruppen löschen
|
||||
if (existing.length > desired) {
|
||||
const toRemove = existing.slice(desired);
|
||||
await Promise.all(toRemove.map(g => g.destroy()));
|
||||
}
|
||||
// fehlende Gruppen anlegen
|
||||
for (let i = existing.length; i < desired; i++) {
|
||||
await TournamentGroup.create({ tournamentId });
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Gruppen zufällig füllen & Spiele anlegen
|
||||
generateRoundRobinSchedule(players) {
|
||||
const list = [...players];
|
||||
const n = list.length;
|
||||
const hasBye = n % 2 === 1;
|
||||
if (hasBye) list.push(null); // füge Bye hinzu
|
||||
const total = list.length; // jetzt gerade Zahl
|
||||
const rounds = [];
|
||||
|
||||
for (let round = 0; round < total - 1; round++) {
|
||||
const pairs = [];
|
||||
for (let i = 0; i < total / 2; i++) {
|
||||
const p1 = list[i];
|
||||
const p2 = list[total - 1 - i];
|
||||
if (p1 && p2) {
|
||||
pairs.push([p1.id, p2.id]);
|
||||
}
|
||||
}
|
||||
rounds.push(pairs);
|
||||
// Rotation (Fixpunkt list[0]):
|
||||
list.splice(1, 0, list.pop());
|
||||
}
|
||||
|
||||
return rounds;
|
||||
}
|
||||
|
||||
|
||||
// services/tournamentService.js
|
||||
async fillGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// 1) Hole vorhandene Gruppen
|
||||
let groups = await TournamentGroup.findAll({ where: { tournamentId } });
|
||||
|
||||
// **Neu**: Falls noch keine Gruppen existieren, lege sie nach numberOfGroups an
|
||||
if (!groups.length) {
|
||||
const desired = tournament.numberOfGroups || 1; // Fallback auf 1, wenn undefiniert
|
||||
for (let i = 0; i < desired; i++) {
|
||||
await TournamentGroup.create({ tournamentId });
|
||||
}
|
||||
groups = await TournamentGroup.findAll({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
const members = await TournamentMember.findAll({ where: { tournamentId } });
|
||||
if (!members.length) {
|
||||
throw new Error('Keine Teilnehmer vorhanden.');
|
||||
}
|
||||
|
||||
// 2) Alte Matches löschen
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
|
||||
// 3) Prüfe, ob Spieler bereits manuell zugeordnet wurden
|
||||
const alreadyAssigned = members.filter(m => m.groupId !== null);
|
||||
const unassigned = members.filter(m => m.groupId === null);
|
||||
|
||||
if (alreadyAssigned.length > 0) {
|
||||
// Spieler sind bereits manuell zugeordnet - nicht neu verteilen
|
||||
console.log(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
|
||||
} else {
|
||||
// Keine manuellen Zuordnungen - zufällig verteilen
|
||||
const shuffled = members.slice();
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
groups.forEach((g, idx) => {
|
||||
shuffled
|
||||
.filter((_, i) => i % groups.length === idx)
|
||||
.forEach(m => m.update({ groupId: g.id }));
|
||||
});
|
||||
}
|
||||
|
||||
// 4) Round‑Robin anlegen wie gehabt - NUR innerhalb jeder Gruppe
|
||||
console.log(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
|
||||
for (const g of groups) {
|
||||
console.log(`[fillGroups] Verarbeite Gruppe ${g.id}`);
|
||||
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
|
||||
console.log(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
|
||||
|
||||
if (gm.length < 2) {
|
||||
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const rounds = this.generateRoundRobinSchedule(gm);
|
||||
console.log(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
|
||||
|
||||
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
|
||||
console.log(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
|
||||
for (const [p1Id, p2Id] of rounds[roundIndex]) {
|
||||
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
|
||||
const p1 = gm.find(p => p.id === p1Id);
|
||||
const p2 = gm.find(p => p.id === p2Id);
|
||||
if (p1 && p2 && p1.groupId === p2.groupId && p1.groupId === g.id) {
|
||||
const match = await TournamentMatch.create({
|
||||
tournamentId,
|
||||
groupId: g.id,
|
||||
round: 'group',
|
||||
player1Id: p1Id,
|
||||
player2Id: p2Id,
|
||||
groupRound: roundIndex + 1
|
||||
});
|
||||
console.log(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
|
||||
} else {
|
||||
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Teilnehmer mit Gruppen zurückgeben
|
||||
return await TournamentMember.findAll({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
async getGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
return await TournamentGroup.findAll({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
// 9. Gruppen mit ihren Teilnehmern
|
||||
// services/tournamentService.js
|
||||
|
||||
async getGroupsWithParticipants(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const groups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
model: TournamentMember,
|
||||
as: 'tournamentGroupMembers',
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }]
|
||||
}],
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
// hier den Index mit aufnehmen:
|
||||
return groups.map((g, idx) => ({
|
||||
groupId: g.id,
|
||||
groupNumber: idx + 1, // jetzt definiert
|
||||
participants: g.tournamentGroupMembers.map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.member.firstName} ${m.member.lastName}`
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// 10. Einzelnes Turnier
|
||||
async getTournament(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
|
||||
if (!t) throw new Error('Turnier nicht gefunden');
|
||||
return t;
|
||||
}
|
||||
|
||||
// 11. Spiele eines Turniers
|
||||
async getTournamentMatches(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
|
||||
if (!t) throw new Error('Turnier nicht gefunden');
|
||||
return await TournamentMatch.findAll({
|
||||
where: { tournamentId },
|
||||
include: [
|
||||
{ model: TournamentMember, as: 'player1', include: [{ model: Member, as: 'member' }] },
|
||||
{ model: TournamentMember, as: 'player2', include: [{ model: Member, as: 'member' }] },
|
||||
{ model: TournamentResult, as: 'tournamentResults' }
|
||||
],
|
||||
order: [
|
||||
['group_id', 'ASC'],
|
||||
['group_round', 'ASC'],
|
||||
['id', 'ASC'],
|
||||
[{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC']
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 12. Satz-Ergebnis hinzufügen/überschreiben
|
||||
async addMatchResult(userToken, clubId, tournamentId, matchId, set, result) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const [match] = await TournamentMatch.findAll({ where: { id: matchId, tournamentId } });
|
||||
if (!match) throw new Error('Match nicht gefunden');
|
||||
const existing = await TournamentResult.findOne({ where: { matchId, set } });
|
||||
if (existing) {
|
||||
existing.pointsPlayer1 = +result.split(':')[0];
|
||||
existing.pointsPlayer2 = +result.split(':')[1];
|
||||
await existing.save();
|
||||
} else {
|
||||
const [p1, p2] = result.split(':').map(Number);
|
||||
await TournamentResult.create({ matchId, set, pointsPlayer1: p1, pointsPlayer2: p2 });
|
||||
}
|
||||
}
|
||||
|
||||
async finishMatch(userToken, clubId, tournamentId, matchId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await TournamentMatch.findByPk(matchId, {
|
||||
include: [{ model: TournamentResult, as: "tournamentResults" }]
|
||||
});
|
||||
if (!match) throw new Error("Match nicht gefunden");
|
||||
|
||||
let win = 0, lose = 0;
|
||||
for (const r of match.tournamentResults) {
|
||||
if (r.pointsPlayer1 > r.pointsPlayer2) win++;
|
||||
else lose++;
|
||||
}
|
||||
match.isFinished = true;
|
||||
match.result = `${win}:${lose}`;
|
||||
await match.save();
|
||||
|
||||
const allFinished = await TournamentMatch.count({
|
||||
where: { tournamentId, round: match.round, isFinished: false }
|
||||
}) === 0;
|
||||
|
||||
if (allFinished) {
|
||||
const sameRound = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: match.round }
|
||||
});
|
||||
const winners = sameRound.map(m => {
|
||||
const [w1, w2] = m.result.split(":").map(n => +n);
|
||||
return w1 > w2 ? m.player1Id : m.player2Id;
|
||||
});
|
||||
|
||||
const nextName = nextRoundName(match.round);
|
||||
if (nextName) {
|
||||
for (let i = 0; i < winners.length / 2; i++) {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
round: nextName,
|
||||
player1Id: winners[i],
|
||||
player2Id: winners[winners.length - 1 - i]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _determineQualifiers(tournamentId, tournament) {
|
||||
const groups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{ model: TournamentMember, as: "tournamentGroupMembers" }]
|
||||
});
|
||||
const groupMatches = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: "group", isFinished: true }
|
||||
});
|
||||
|
||||
const qualifiers = [];
|
||||
for (const g of groups) {
|
||||
const stats = {};
|
||||
for (const tm of g.tournamentGroupMembers) {
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0 };
|
||||
}
|
||||
for (const m of groupMatches.filter(m => m.groupId === g.id)) {
|
||||
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
|
||||
const [p1, p2] = m.result.split(":").map(n => parseInt(n, 10));
|
||||
if (p1 > p2) {
|
||||
stats[m.player1Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player2Id].points -= 1; // Verlierer bekommt -1
|
||||
} else {
|
||||
stats[m.player2Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player1Id].points -= 1; // Verlierer bekommt -1
|
||||
}
|
||||
stats[m.player1Id].setsWon += p1;
|
||||
stats[m.player1Id].setsLost += p2;
|
||||
stats[m.player2Id].setsWon += p2;
|
||||
stats[m.player2Id].setsLost += p1;
|
||||
}
|
||||
const ranked = Object.values(stats).sort((a, b) => {
|
||||
const diffA = a.setsWon - a.setsLost;
|
||||
const diffB = b.setsWon - b.setsLost;
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
if (diffB !== diffA) return diffB - diffA;
|
||||
if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
|
||||
return a.member.id - b.member.id;
|
||||
});
|
||||
qualifiers.push(...ranked.slice(0, tournament.advancingPerGroup).map(r => r.member));
|
||||
}
|
||||
return qualifiers;
|
||||
}
|
||||
|
||||
async startKnockout(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.findByPk(tournamentId);
|
||||
if (!t || t.clubId != clubId) throw new Error("Tournament not found");
|
||||
|
||||
if (t.type === "groups") {
|
||||
const unfinished = await TournamentMatch.count({
|
||||
where: { tournamentId, round: "group", isFinished: false }
|
||||
});
|
||||
if (unfinished > 0) {
|
||||
throw new Error(
|
||||
"Turnier ist im Gruppenmodus, K.o.-Runde kann erst nach Abschluss aller Gruppenspiele gestartet werden."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const qualifiers = await this._determineQualifiers(tournamentId, t);
|
||||
if (qualifiers.length < 2) throw new Error("Zu wenige Qualifikanten für K.O.-Runde");
|
||||
|
||||
await TournamentMatch.destroy({
|
||||
where: { tournamentId, round: { [Op.ne]: "group" } }
|
||||
});
|
||||
|
||||
const roundSize = qualifiers.length;
|
||||
const rn = getRoundName(roundSize);
|
||||
for (let i = 0; i < roundSize / 2; i++) {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
round: rn,
|
||||
player1Id: qualifiers[i].id,
|
||||
player2Id: qualifiers[roundSize - 1 - i].id
|
||||
});
|
||||
}
|
||||
}
|
||||
async manualAssignGroups(
|
||||
userToken,
|
||||
clubId,
|
||||
tournamentId,
|
||||
assignments,
|
||||
numberOfGroups,
|
||||
maxGroupSize
|
||||
) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// 1) Turnier und Teilnehmerzahl validieren
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const totalMembers = assignments.length;
|
||||
if (totalMembers === 0) {
|
||||
throw new Error('Keine Teilnehmer zum Verteilen');
|
||||
}
|
||||
|
||||
// 2) Bestimme, wie viele Gruppen wir anlegen
|
||||
let groupCount;
|
||||
if (numberOfGroups != null) {
|
||||
groupCount = Number(numberOfGroups);
|
||||
if (isNaN(groupCount) || groupCount < 1) {
|
||||
throw new Error('Ungültige Anzahl Gruppen');
|
||||
}
|
||||
} else if (maxGroupSize != null) {
|
||||
const sz = Number(maxGroupSize);
|
||||
if (isNaN(sz) || sz < 1) {
|
||||
throw new Error('Ungültige maximale Gruppengröße');
|
||||
}
|
||||
groupCount = Math.ceil(totalMembers / sz);
|
||||
} else {
|
||||
// Fallback auf im Turnier gespeicherte Anzahl
|
||||
groupCount = tournament.numberOfGroups;
|
||||
if (!groupCount || groupCount < 1) {
|
||||
throw new Error('Anzahl Gruppen nicht definiert');
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Alte Gruppen löschen und neue anlegen
|
||||
await TournamentGroup.destroy({ where: { tournamentId } });
|
||||
const createdGroups = [];
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
const grp = await TournamentGroup.create({ tournamentId });
|
||||
createdGroups.push(grp);
|
||||
}
|
||||
|
||||
// 4) Mapping von UI‑Nummer (1…groupCount) auf reale DB‑ID
|
||||
const groupMap = {};
|
||||
createdGroups.forEach((grp, idx) => {
|
||||
groupMap[idx + 1] = grp.id;
|
||||
});
|
||||
|
||||
// 5) Teilnehmer updaten
|
||||
await Promise.all(
|
||||
assignments.map(({ participantId, groupNumber }) => {
|
||||
const dbGroupId = groupMap[groupNumber];
|
||||
if (!dbGroupId) {
|
||||
throw new Error(`Ungültige Gruppen‑Nummer: ${groupNumber}`);
|
||||
}
|
||||
return TournamentMember.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId } }
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// 6) Ergebnis zurückliefern wie getGroupsWithParticipants
|
||||
const groups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
model: TournamentMember,
|
||||
as: 'tournamentGroupMembers',
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }]
|
||||
}],
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
return groups.map(g => ({
|
||||
groupId: g.id,
|
||||
participants: g.tournamentGroupMembers.map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.member.firstName} ${m.member.lastName}`
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
// services/tournamentService.js
|
||||
async resetGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
// löscht alle Gruppen … (inkl. CASCADE oder manuell TournamentMatch.destroy)
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
await TournamentGroup.destroy({ where: { tournamentId } });
|
||||
}
|
||||
async resetMatches(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
async removeParticipant(userToken, clubId, tournamentId, participantId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
await TournamentMember.destroy({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
}
|
||||
|
||||
// services/tournamentService.js
|
||||
async deleteMatchResult(userToken, clubId, tournamentId, matchId, setToDelete) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Match existiert?
|
||||
const match = await TournamentMatch.findOne({ where: { id: matchId, tournamentId } });
|
||||
if (!match) throw new Error('Match nicht gefunden');
|
||||
|
||||
// Satz löschen
|
||||
await TournamentResult.destroy({ where: { matchId, set: setToDelete } });
|
||||
|
||||
// verbleibende Sätze neu durchnummerieren
|
||||
const remaining = await TournamentResult.findAll({
|
||||
where: { matchId },
|
||||
order: [['set', 'ASC']]
|
||||
});
|
||||
for (let i = 0; i < remaining.length; i++) {
|
||||
const r = remaining[i];
|
||||
const newSet = i + 1;
|
||||
if (r.set !== newSet) {
|
||||
r.set = newSet;
|
||||
await r.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async reopenMatch(userToken, clubId, tournamentId, matchId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const match = await TournamentMatch.findOne({
|
||||
where: { id: matchId, tournamentId }
|
||||
});
|
||||
if (!match) {
|
||||
throw new Error("Match nicht gefunden");
|
||||
}
|
||||
|
||||
// Nur den Abschluss‑Status zurücksetzen, nicht die Einzelsätze
|
||||
match.isFinished = false;
|
||||
match.result = null; // optional: entfernt die zusammengefasste Ergebnis‑Spalte
|
||||
await match.save();
|
||||
}
|
||||
|
||||
async resetKnockout(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
// lösche alle Matches außer Gruppenphase
|
||||
await TournamentMatch.destroy({
|
||||
where: {
|
||||
tournamentId,
|
||||
round: { [Op.ne]: "group" }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new TournamentService();
|
||||
@@ -1,24 +1,45 @@
|
||||
import User from '../models/User.js'
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Op } from 'sequelize';
|
||||
import User from '../models/User.js';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import UserClub from '../models/UserClub.js'; // <-- hier hinzufügen
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { config } from 'dotenv';
|
||||
config(); // sorgt dafür, dass process.env.JWT_SECRET geladen wird
|
||||
|
||||
export const getUserByToken = async(token) => {
|
||||
export const getUserByToken = async (token) => {
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
where: [
|
||||
{auth_code: token}
|
||||
]
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const err = new HttpError('noaccess', 403);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// 1. JWT validieren
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
export const hasUserClubAccess = async(userId, clubId) => {
|
||||
// 2. Token-Eintrag prüfen (existiert und nicht abgelaufen)
|
||||
const stored = await UserToken.findOne({
|
||||
where: {
|
||||
token,
|
||||
expiresAt: { [Op.gt]: new Date() }
|
||||
}
|
||||
});
|
||||
if (!stored) {
|
||||
throw new HttpError('Token abgelaufen oder ungültig', 401);
|
||||
}
|
||||
|
||||
// 3. User laden
|
||||
const user = await User.findByPk(payload.userId);
|
||||
if (!user) {
|
||||
throw new HttpError('Benutzer nicht gefunden', 404);
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// Falls es ein HttpError ist, einfach weiterwerfen
|
||||
if (err instanceof HttpError) throw err;
|
||||
// ansonsten pauschal „noaccess“
|
||||
throw new HttpError('noaccess', 403);
|
||||
}
|
||||
};
|
||||
|
||||
export const hasUserClubAccess = async (userId, clubId) => {
|
||||
try {
|
||||
console.log('[hasUserClubAccess]');
|
||||
const userClub = await UserClub.findOne({
|
||||
@@ -29,23 +50,33 @@ export const hasUserClubAccess = async(userId, clubId) => {
|
||||
}
|
||||
});
|
||||
return userClub !== null;
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new HttpError('notfound', 500);
|
||||
}
|
||||
console.log('---- no user found');
|
||||
}
|
||||
|
||||
export const checkAccess = async(userToken, clubId) => {
|
||||
export const checkAccess = async (userToken, clubId) => {
|
||||
try {
|
||||
const user = await getUserByToken(userToken);
|
||||
if (!await hasUserClubAccess(user.id, clubId)) {
|
||||
const hasAccess = await hasUserClubAccess(user.id, clubId);
|
||||
if (!hasAccess) {
|
||||
console.log('no club access');
|
||||
const err = new HttpError('noaccess', 403);
|
||||
throw err;
|
||||
throw new HttpError('noaccess', 403);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkGlobalAccess = async (userToken) => {
|
||||
try {
|
||||
const user = await getUserByToken(userToken);
|
||||
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
309
frontend/DESIGN_GUIDE.md
Normal file
309
frontend/DESIGN_GUIDE.md
Normal file
@@ -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
|
||||
<button class="btn-primary">Primärer Button</button>
|
||||
```
|
||||
|
||||
#### Sekundärer Button
|
||||
```html
|
||||
<button class="btn-secondary">Sekundärer Button</button>
|
||||
```
|
||||
|
||||
#### 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
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Kartentitel</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Karteninhalt</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Formularelemente
|
||||
|
||||
#### Input-Felder
|
||||
```html
|
||||
<input type="text" placeholder="Text eingeben">
|
||||
<select>
|
||||
<option>Option wählen</option>
|
||||
</select>
|
||||
<textarea placeholder="Mehrzeiliger Text"></textarea>
|
||||
```
|
||||
|
||||
### Tabellen
|
||||
|
||||
#### Standard-Tabelle
|
||||
```html
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Spalte 1</th>
|
||||
<th>Spalte 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Daten 1</td>
|
||||
<td>Daten 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
### Alerts
|
||||
|
||||
#### Verschiedene Alert-Typen
|
||||
```html
|
||||
<div class="alert alert-success">
|
||||
<span class="alert-icon">✅</span>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">Erfolg!</div>
|
||||
<div class="alert-message">Operation erfolgreich abgeschlossen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<span class="alert-icon">❌</span>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">Fehler!</div>
|
||||
<div class="alert-message">Ein Fehler ist aufgetreten.</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Badges
|
||||
|
||||
#### Verschiedene Badge-Typen
|
||||
```html
|
||||
<span class="badge badge-primary">Primär</span>
|
||||
<span class="badge badge-secondary">Sekundär</span>
|
||||
<span class="badge badge-success">Erfolg</span>
|
||||
<span class="badge badge-danger">Gefahr</span>
|
||||
<span class="badge badge-warning">Warnung</span>
|
||||
<span class="badge badge-info">Info</span>
|
||||
```
|
||||
|
||||
### Progress-Bars
|
||||
|
||||
#### Standard-Progress-Bar
|
||||
```html
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 75%">75%</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Tabs
|
||||
|
||||
#### Tab-Navigation
|
||||
```html
|
||||
<div class="tabs">
|
||||
<ul class="tab-list">
|
||||
<li class="tab-item">
|
||||
<button class="tab-button active">Tab 1</button>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<button class="tab-button">Tab 2</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active">
|
||||
<p>Inhalt von Tab 1</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Accordion
|
||||
|
||||
#### Akkordeon-Element
|
||||
```html
|
||||
<div class="accordion">
|
||||
<div class="accordion-item">
|
||||
<button class="accordion-header">
|
||||
<span>Akkordeon-Titel</span>
|
||||
<span class="accordion-icon">▼</span>
|
||||
</button>
|
||||
<div class="accordion-content">
|
||||
<div class="accordion-body">
|
||||
<p>Akkordeon-Inhalt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
#### Navigationspfad
|
||||
```html
|
||||
<nav class="breadcrumb">
|
||||
<div class="breadcrumb-item">
|
||||
<a href="/" class="breadcrumb-link">Start</a>
|
||||
</div>
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<div class="breadcrumb-item">
|
||||
<a href="/verein" class="breadcrumb-link">Verein</a>
|
||||
</div>
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<div class="breadcrumb-item">
|
||||
<span class="breadcrumb-current">Mitglieder</span>
|
||||
</div>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
#### Seitenzahlen
|
||||
```html
|
||||
<nav class="pagination">
|
||||
<ul>
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link">1</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link active">2</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link">3</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
## 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
|
||||
<style lang="scss">
|
||||
@import '@/assets/css/main.scss';
|
||||
|
||||
// Ihre spezifischen Styles hier
|
||||
</style>
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -1,13 +1,59 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Trainingstagebuch</title>
|
||||
<title>Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere</title>
|
||||
<meta name="description" content="Das TrainingsTagebuch hilft Vereinen und Trainer:innen, Mitglieder zu verwalten, Trainings zu dokumentieren, Spielpläne zu organisieren und Statistiken auszuwerten – alles in einer modernen Web‑App." />
|
||||
<link rel="canonical" href="https://tt-tagebuch.de/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Trainingstagebuch" />
|
||||
<meta property="og:title" content="Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere" />
|
||||
<meta property="og:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel‑ und Turnierorganisation sowie Statistiken – DSGVO‑freundlich und einfach." />
|
||||
<meta property="og:url" content="https://tt-tagebuch.de/" />
|
||||
<meta property="og:image" content="https://tt-tagebuch.de/vite.svg" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere" />
|
||||
<meta name="twitter:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel‑ und Turnierorganisation sowie Statistiken – DSGVO‑freundlich und einfach." />
|
||||
<meta name="twitter:image" content="https://tt-tagebuch.de/vite.svg" />
|
||||
|
||||
<!-- JSON-LD: Website + Organization -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "Trainingstagebuch",
|
||||
"url": "https://tt-tagebuch.de/",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "https://tt-tagebuch.de/?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Trainingstagebuch",
|
||||
"applicationCategory": "SportsApplication",
|
||||
"operatingSystem": "Web",
|
||||
"description": "Mitgliederverwaltung, Trainingstagebuch, Spiel- und Turnierorganisation sowie Statistiken – DSGVO-freundlich und einfach.",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR"
|
||||
},
|
||||
"url": "https://tt-tagebuch.de/"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Trainingstagebuch</h1>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
1338
frontend/package-lock.json
generated
1338
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"axios": "^1.7.3",
|
||||
"core-js": "^3.8.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"sortablejs": "^1.15.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-multiselect": "^3.0.0",
|
||||
|
||||
4
frontend/public/robots.txt
Normal file
4
frontend/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://tt-tagebuch.de/sitemap.xml
|
||||
28
frontend/public/sitemap.xml
Normal file
28
frontend/public/sitemap.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://tt-tagebuch.de/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tt-tagebuch.de/register</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tt-tagebuch.de/login</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tt-tagebuch.de/impressum</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tt-tagebuch.de/datenschutz</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -1,54 +1,115 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<button class="menu-toggle" @click="toggleMenu">
|
||||
{{ isMenuOpen ? 'Menü schließen' : 'Menü öffnen' }}
|
||||
</button>
|
||||
<header class="app-header">
|
||||
<h1>
|
||||
<router-link to="/" class="home-link">
|
||||
<img :src="logoUrl" alt="Logo" class="home-logo" width="24" height="24" loading="lazy" />
|
||||
<span>Trainingstagebuch</span>
|
||||
</router-link>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div v-if="isAuthenticated" class="navigation" :class="{ 'menu-open': isMenuOpen }">
|
||||
<div class="club-selector">
|
||||
<div>
|
||||
Verein:
|
||||
<select v-model="selectedClub">
|
||||
<option value="">---</option>
|
||||
<option value="new">Neuer Verein</option>
|
||||
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
|
||||
</select>
|
||||
<button @click="loadClub">-></button>
|
||||
<div class="app-container">
|
||||
<aside v-if="isAuthenticated" class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
<div class="club-selector card">
|
||||
<h3 class="card-title">Verein auswählen</h3>
|
||||
<div class="select-group">
|
||||
<select v-model="selectedClub" class="club-select">
|
||||
<option value="">Verein wählen...</option>
|
||||
<option value="new">Neuer Verein</option>
|
||||
<option v-for="club in clubs" :key="club.id" :value="club.id">{{ club.name }}</option>
|
||||
</select>
|
||||
<button @click="loadClub" class="btn-primary" :disabled="!selectedClub">
|
||||
<span>Laden</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav v-if="selectedClub" class="nav-menu">
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Verwaltung</h4>
|
||||
<a href="/members" class="nav-link">
|
||||
<span class="nav-icon">👥</span>
|
||||
Mitglieder
|
||||
</a>
|
||||
<a href="/diary" class="nav-link">
|
||||
<span class="nav-icon">📝</span>
|
||||
Tagebuch
|
||||
</a>
|
||||
<a href="/pending-approvals" class="nav-link">
|
||||
<span class="nav-icon">⏳</span>
|
||||
Freigaben
|
||||
</a>
|
||||
<a href="/training-stats" class="nav-link">
|
||||
<span class="nav-icon">📊</span>
|
||||
Trainings-Statistik
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Organisation</h4>
|
||||
<a href="/schedule" class="nav-link">
|
||||
<span class="nav-icon">📅</span>
|
||||
Spielpläne
|
||||
</a>
|
||||
<a href="/tournaments" class="nav-link">
|
||||
<span class="nav-icon">🏆</span>
|
||||
Interne Turniere
|
||||
</a>
|
||||
<a href="/official-tournaments" class="nav-link">
|
||||
<span class="nav-icon">📄</span>
|
||||
Offizielle Turniere
|
||||
</a>
|
||||
<a href="/predefined-activities" class="nav-link">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vordefinierte Aktivitäten
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button @click="logout()" class="btn-secondary logout-btn">
|
||||
<span class="nav-icon">🚪</span>
|
||||
Ausloggen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div v-else class="auth-nav">
|
||||
<div class="auth-links">
|
||||
<a href="/login" class="btn-primary">Einloggen</a>
|
||||
<a href="/register" class="btn-secondary">Registrieren</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedClub" class="nav-links">
|
||||
<a href="/members">Mitglieder</a>
|
||||
<a href="/diary">Tagebuch</a>
|
||||
<a href="/pending-approvals">Freigaben</a>
|
||||
<a href="/schedule">Spielpläne</a>
|
||||
</div>
|
||||
|
||||
<div class="logout-btn">
|
||||
<button @click="logout()">Ausloggen</button>
|
||||
</div>
|
||||
<main class="main-content">
|
||||
<router-view class="content fade-in"></router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div v-else class="navigation">
|
||||
<a href="/login">Einloggen</a>
|
||||
<a href="/register">Registrieren</a>
|
||||
</div>
|
||||
|
||||
<router-view class="content"></router-view>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<router-link to="/impressum" class="footer-link">Impressum</router-link>
|
||||
<span class="footer-sep">·</span>
|
||||
<router-link to="/datenschutz" class="footer-link">Datenschutzerklärung</router-link>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import apiClient from './apiClient.js';
|
||||
import logoUrl from './assets/logo.png';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
selectedClub: null,
|
||||
isMenuOpen: false,
|
||||
sessionInterval: null,
|
||||
logoUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -65,19 +126,44 @@ export default {
|
||||
this.$router.push(`/showclub/${newVal}`);
|
||||
}
|
||||
},
|
||||
isAuthenticated(newVal) {
|
||||
if (newVal) {
|
||||
// Benutzer hat sich eingeloggt - Daten laden
|
||||
this.loadUserData();
|
||||
} else {
|
||||
// Benutzer hat sich ausgeloggt - Daten zurücksetzen
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
if (this.sessionInterval) {
|
||||
clearInterval(this.sessionInterval);
|
||||
this.sessionInterval = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setCurrentClub', 'setClubs', 'logout']),
|
||||
|
||||
async loadUserData() {
|
||||
try {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
this.checkSession();
|
||||
this.sessionInterval = setInterval(this.checkSession, 5000);
|
||||
} catch (error) {
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
}
|
||||
},
|
||||
|
||||
loadClub() {
|
||||
this.setCurrentClub(this.currentClub);
|
||||
this.$router.push(`/showclub/${this.currentClub}`);
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
},
|
||||
|
||||
async checkSession() {
|
||||
try {
|
||||
const response = await apiClient.get('/session/status');
|
||||
@@ -85,8 +171,9 @@ export default {
|
||||
this.handleLogout();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session check failed:', error);
|
||||
this.handleLogout();
|
||||
this.isAuthenticated = false;
|
||||
this.username = '';
|
||||
this.currentClub = '';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -98,17 +185,20 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
// Nur Daten laden, wenn der Benutzer authentifiziert ist
|
||||
if (this.isAuthenticated) {
|
||||
try {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
this.checkSession();
|
||||
this.sessionInterval = setInterval(this.checkSession, 5000);
|
||||
} catch (error) {
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
}
|
||||
this.checkSession();
|
||||
this.sessionInterval = setInterval(this.checkSession, 5000);
|
||||
} catch (error) {
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -118,159 +208,381 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Main Container */
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
background-color: #e0f0e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.club-selector,
|
||||
.logout-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.navigation>a {
|
||||
text-decoration: none;
|
||||
margin: 0.3em 0;
|
||||
color: #a07040;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.3em 0.6em;
|
||||
background-color: #a07040;
|
||||
/* Header */
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-medium);
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #804b29;
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 0.75rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.3em;
|
||||
border: 1px solid #ccc;
|
||||
/* Styling für das erste h1 (aus main.scss) - Design vom zweiten, aber ursprüngliche Schriftgröße */
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-align: center;
|
||||
/* Schriftgröße bleibt wie in der main.scss definiert */
|
||||
}
|
||||
|
||||
/* Menü-Toggle-Button nur auf kleinen Bildschirmen anzeigen */
|
||||
.menu-toggle {
|
||||
.home-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.home-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* App Container */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: white;
|
||||
border-right: 1px solid var(--border-color);
|
||||
box-shadow: var(--shadow-small);
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.club-selector {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.club-selector .card-title {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.select-group {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.club-select {
|
||||
flex: 1;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-size: 0.75rem;
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.select-group .btn-primary {
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Navigation Menu */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
margin-bottom: 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
border-radius: var(--border-radius-small);
|
||||
transition: all var(--transition-fast);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
transform: translateX(0.125rem);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 0.875rem;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Auth Navigation */
|
||||
.auth-nav {
|
||||
width: 260px;
|
||||
background: white;
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.auth-links a:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.auth-links a::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--background-light);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
background: white;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (min-width: 768px) {
|
||||
.main {
|
||||
flex-direction: row;
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 13em;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.content {
|
||||
padding: 1rem;
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-links>a {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.main {
|
||||
flex-direction: column;
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Das Menü anzeigen, wenn es geöffnet ist */
|
||||
.menu-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-links>a {
|
||||
display: block;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
/* Menü-Toggle-Button nur auf kleinen Bildschirmen */
|
||||
.menu-toggle {
|
||||
display: block;
|
||||
background-color: #a07040;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-toggle:hover {
|
||||
background-color: #804b29;
|
||||
|
||||
.main-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.navigation {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
select {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 3rem;
|
||||
left: 0;
|
||||
height: calc(100vh - 3rem);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.content {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.nav-links>a {
|
||||
font-size: 0.85em;
|
||||
padding: 0.5em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
.header-content {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button-Varianten */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: var(--transition);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-medium);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--secondary-color);
|
||||
border: 1.5px solid var(--secondary-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: var(--transition);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,4 +19,16 @@ apiClient.interceptors.request.use(config => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response-Interceptor für automatische Logout-Behandlung bei 401
|
||||
apiClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// Automatisch ausloggen und zur Login-Seite weiterleiten
|
||||
store.dispatch('logout');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
|
||||
531
frontend/src/assets/css/components.scss
Normal file
531
frontend/src/assets/css/components.scss
Normal file
@@ -0,0 +1,531 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Login und Registrierungslinks */
|
||||
.login-link,
|
||||
.register-link {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-light);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.login-link p,
|
||||
.register-link p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-link a,
|
||||
.register-link a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.login-link a:hover,
|
||||
.register-link a:hover {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,69 +1,529 @@
|
||||
/* Import der Komponenten */
|
||||
@use './components.scss' as *;
|
||||
|
||||
/* 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: 700;
|
||||
text-transform: none;
|
||||
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); }
|
||||
}
|
||||
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1,4 +1,5 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
class PDFGenerator {
|
||||
@@ -6,7 +7,7 @@ class PDFGenerator {
|
||||
this.pdf = new jsPDF('p', 'mm', 'a4');
|
||||
this.margin = margin;
|
||||
this.columnGap = columnGap;
|
||||
this.pageHeight = 297 - margin * 2;
|
||||
this.pageHeight = 297 - margin * 2;
|
||||
this.columnWidth = (210 - margin * 2 - columnGap) / 2;
|
||||
this.position = margin;
|
||||
this.yPos = this.position;
|
||||
@@ -17,16 +18,33 @@ class PDFGenerator {
|
||||
this.COLUMN_GROUP = margin + 100;
|
||||
this.COLUMN_DURATION = margin + 150;
|
||||
this.LINE_HEIGHT = 7;
|
||||
this.cursorY = margin;
|
||||
}
|
||||
|
||||
async addSchedule(element) {
|
||||
const canvas = await html2canvas(element, { scale: 2 });
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
onclone: (clonedDoc) => {
|
||||
const clonedEl = clonedDoc.getElementById('schedule');
|
||||
if (clonedEl) clonedEl.style.fontSize = '12pt';
|
||||
// Klon des Wurzel-Elements
|
||||
const tbl = clonedDoc.getElementById(element.id);
|
||||
if (!tbl) return;
|
||||
|
||||
// Alle Zellen und Überschriften-Elemente auswählen
|
||||
const cells = tbl.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
cell.style.fontSize = '12pt';
|
||||
});
|
||||
}
|
||||
});
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgWidth = 210 - this.margin * 2;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = this.margin;
|
||||
this.pdf.addImage(imgData, 'PNG', this.margin, position, imgWidth, imgHeight);
|
||||
this.pdf.setFontSize(12);
|
||||
heightLeft -= this.pageHeight;
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight + this.margin;
|
||||
@@ -43,6 +61,23 @@ class PDFGenerator {
|
||||
this.isLeftColumn = true;
|
||||
}
|
||||
|
||||
addTitle(text) {
|
||||
// remember old settings
|
||||
const oldFont = this.pdf.getFont();
|
||||
const oldStyle = this.pdf.getFont().fontStyle;
|
||||
const oldSize = this.pdf.getFontSize();
|
||||
|
||||
// set new
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(text, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// restore
|
||||
this.pdf.setFont(oldFont.fontName, oldStyle);
|
||||
this.pdf.setFontSize(oldSize);
|
||||
}
|
||||
|
||||
addHeader(clubName, formattedDate, formattedStartTime, formattedEndTime) {
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
@@ -175,6 +210,280 @@ class PDFGenerator {
|
||||
this.pdf.text(phoneNumber, this.margin + 120, this.yPos);
|
||||
this.yPos += this.LINE_HEIGHT;
|
||||
}
|
||||
|
||||
addAddress(clubName, addressLines) {
|
||||
if (!this.addressY) {
|
||||
this.addressY = 30;
|
||||
}
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.setFont(undefined, 'bold');
|
||||
this.pdf.text(clubName, 20, this.addressY);
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.setFont(undefined, 'normal');
|
||||
addressLines.forEach(line => {
|
||||
this.addressY += 7;
|
||||
this.pdf.text(line, 20, this.addressY);
|
||||
});
|
||||
|
||||
this.addressY += 10; // Abstand zur nächsten Adresse
|
||||
}
|
||||
|
||||
async addScreenshot(element) {
|
||||
const canvas = await html2canvas(element, { scale: 2 });
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgWidth = this.pageWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
this.pdf.addImage(imgData, 'PNG', this.margin, this.cursorY, imgWidth, imgHeight);
|
||||
this.cursorY += imgHeight + 10;
|
||||
if (this.cursorY > this.pdf.internal.pageSize.height - this.margin) {
|
||||
this.pdf.addPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
}
|
||||
|
||||
addTable(tableId, highlightName = '') {
|
||||
this.pdf.setFontSize(11);
|
||||
autoTable(this.pdf, {
|
||||
html: `#${tableId}`,
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
styles: { fontSize: this.pdf.getFontSize() },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left' },
|
||||
theme: 'grid',
|
||||
didParseCell: (data) => {
|
||||
const cellText = Array.isArray(data.cell.text)
|
||||
? data.cell.text.join(' ')
|
||||
: String(data.cell.text);
|
||||
if (highlightName && cellText.includes(highlightName)) {
|
||||
data.cell.styles.fontStyle = 'bold';
|
||||
}
|
||||
},
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addParticipantsSummary(tournamentTitle, tournamentDateText, groups) {
|
||||
// Header
|
||||
const title = tournamentTitle || 'Offizielles Turnier';
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
this.cursorY += 8;
|
||||
if (tournamentDateText) {
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.text(String(tournamentDateText), this.margin, this.cursorY);
|
||||
this.cursorY += 8;
|
||||
}
|
||||
|
||||
// Tabelle mit Gruppierung
|
||||
const head = [['Mitglied', 'Konkurrenz', 'Startzeit', 'Status', 'Platzierung']];
|
||||
const body = [];
|
||||
const rowStyles = [];
|
||||
|
||||
for (const group of groups) {
|
||||
for (let i = 0; i < group.items.length; i++) {
|
||||
const item = group.items[i];
|
||||
const rowData = [
|
||||
i === 0 ? group.memberName : '', // Name nur in erster Zeile
|
||||
item.competitionName,
|
||||
item.start || '–',
|
||||
item.statusText || '',
|
||||
item.placement || ''
|
||||
];
|
||||
body.push(rowData);
|
||||
rowStyles.push({
|
||||
isFirstRow: i === 0,
|
||||
memberStyle: group.memberStyle,
|
||||
competitionName: item.competitionName,
|
||||
statusStyle: item.statusStyle
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.pdf.setFontSize(11);
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 11 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left' },
|
||||
didParseCell: (data) => {
|
||||
if (data.section !== 'body') return;
|
||||
const rowStyle = rowStyles[data.row.index];
|
||||
|
||||
// Formatierung für Mitgliedsname (erste Spalte, erste Zeile der Gruppe)
|
||||
if (data.column.index === 0 && rowStyle.isFirstRow) {
|
||||
if (rowStyle.memberStyle === 'bold') data.cell.styles.fontStyle = 'bold';
|
||||
else if (rowStyle.memberStyle === 'italic') data.cell.styles.fontStyle = 'italic';
|
||||
else data.cell.styles.fontStyle = 'normal';
|
||||
}
|
||||
// Formatierung für Konkurrenzname (zweite Spalte)
|
||||
else if (data.column.index === 1) {
|
||||
if (rowStyle.statusStyle === 'bold') data.cell.styles.fontStyle = 'bold';
|
||||
else if (rowStyle.statusStyle === 'italic') data.cell.styles.fontStyle = 'italic';
|
||||
else data.cell.styles.fontStyle = 'normal';
|
||||
}
|
||||
},
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = [], venues = []) {
|
||||
let y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(tournamentTitle || 'Offizielles Turnier', this.margin, y);
|
||||
y += 9;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.text(`Mitglied: ${memberName}`, this.margin, y);
|
||||
y += 8;
|
||||
// Empfehlungen (fett)
|
||||
if (recommendedRows && recommendedRows.length) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Empfehlungen', this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.text('Wettbewerb', this.margin, y);
|
||||
this.pdf.text('Datum', this.margin + 80, y);
|
||||
this.pdf.text('Startzeit', this.margin + 120, y);
|
||||
this.pdf.text('Gebühr', this.margin + 160, y);
|
||||
y += 7;
|
||||
for (const r of recommendedRows) {
|
||||
this.pdf.text(r.name || '', this.margin, y);
|
||||
this.pdf.text(r.date || '–', this.margin + 80, y);
|
||||
this.pdf.text(r.time || '–', this.margin + 120, y);
|
||||
this.pdf.text(r.entryFee || '–', this.margin + 160, y);
|
||||
y += 7;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weitere spielbare Wettbewerbe (normal)
|
||||
if (otherRows && otherRows.length) {
|
||||
y += 5;
|
||||
if (y > this.pageHeight) { this.addNewPage(); y = this.margin; }
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Ebenfalls spielbar', this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
for (const r of otherRows) {
|
||||
this.pdf.text(r.name || '', this.margin, y);
|
||||
this.pdf.text(r.date || '–', this.margin + 80, y);
|
||||
this.pdf.text(r.time || '–', this.margin + 120, y);
|
||||
this.pdf.text(r.entryFee || '–', this.margin + 160, y);
|
||||
y += 7;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Austragungsort(e) direkt vor den Hinweisen
|
||||
const venueLines = Array.isArray(venues) ? venues.filter(Boolean) : [];
|
||||
if (venueLines.length) {
|
||||
const heading = venueLines.length === 1 ? 'Austragungsort' : 'Austragungsorte';
|
||||
const maxWidth = 210 - this.margin * 2;
|
||||
if (y + 20 + venueLines.length * 6 > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
} else {
|
||||
y += 6;
|
||||
}
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text(`${heading}:`, this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
for (const v of venueLines) {
|
||||
const wrapped = this.pdf.splitTextToSize(String(v), maxWidth);
|
||||
for (const line of wrapped) {
|
||||
this.pdf.text(line, this.margin, y);
|
||||
y += 6;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hinweise-Sektion
|
||||
const remainingForHints = 60; // Platz für Überschrift + Liste abschätzen
|
||||
if (y + remainingForHints > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
} else {
|
||||
y += 6;
|
||||
}
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text('Hinweise:', this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
const maxWidth = 210 - this.margin * 2;
|
||||
const bullets = [
|
||||
'Eine Stunde vor Beginn der Konkurrenz in der Halle sein',
|
||||
'Kein weißes Trikot',
|
||||
'Sportshorts (oder Sportröckchen), am besten auch nicht weiß',
|
||||
'Hallenschuhe (dürfen auf Boden nicht abfärben)',
|
||||
'Eine Flasche Wasser dabei haben',
|
||||
'Da der Verein die Meldung übernehmen möchte, die Trainer mind. eine Woche vor dem Turnier über die Teilnahme informieren',
|
||||
];
|
||||
for (const b of bullets) {
|
||||
const lines = this.pdf.splitTextToSize(`- ${b}`, maxWidth);
|
||||
for (const line of lines) {
|
||||
this.pdf.text(line, this.margin, y);
|
||||
y += 6;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Leerzeile vor dem Abschlusssatz
|
||||
if (y + 6 > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
} else {
|
||||
y += 6;
|
||||
}
|
||||
const finalLine = 'Die Trainer probieren bei allen Turnieren anwesend zu sein.';
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
const finalLines = this.pdf.splitTextToSize(finalLine, maxWidth);
|
||||
for (const line of finalLines) {
|
||||
this.pdf.text(line, this.margin, y);
|
||||
y += 6;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
}
|
||||
}
|
||||
this.cursorY = y + 10;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PDFGenerator;
|
||||
|
||||
@@ -9,6 +9,12 @@ import MembersView from './views/MembersView.vue';
|
||||
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';
|
||||
import PredefinedActivities from './views/PredefinedActivities.vue';
|
||||
import OfficialTournaments from './views/OfficialTournaments.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/register', component: Register },
|
||||
@@ -21,6 +27,12 @@ const routes = [
|
||||
{ path: '/diary', component: DiaryView },
|
||||
{ path: '/pending-approvals', component: PendingApprovalsView},
|
||||
{ path: '/schedule', component: ScheduleView},
|
||||
{ path: '/tournaments', component: TournamentsView },
|
||||
{ path: '/training-stats', component: TrainingStatsView },
|
||||
{ path: '/predefined-activities', component: PredefinedActivities },
|
||||
{ path: '/official-tournaments', component: OfficialTournaments },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
})(),
|
||||
},
|
||||
@@ -55,8 +54,8 @@ const store = createStore({
|
||||
logout({ commit }) {
|
||||
commit('clearToken');
|
||||
commit('clearUsername');
|
||||
router.push("/");
|
||||
window.location.reload();
|
||||
router.push('/login'); // Leitet den Benutzer zur Login-Seite um
|
||||
// window.location.reload() entfernt, um Endlos-Neuladeschleife zu verhindern
|
||||
},
|
||||
|
||||
setCurrentClub({ commit }, club) {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<h3>Mitglieder</h3>
|
||||
<ul>
|
||||
<li v-for="member in club.members" :key="member.id">{{ member.lastName }}, {{ member.firstName }}</li>
|
||||
<ul class="members">
|
||||
<li v-for="member in displayedMembers" :key="member.id" class="member-item">
|
||||
<span class="gender-symbol" :class="'gender-' + (member.gender || 'unknown')" :title="labelGender(member.gender)">{{ genderSymbol(member.gender) }}</span>
|
||||
<span class="gender-name" :class="['gender-' + (member.gender || 'unknown'), { 'is-test': member.testMembership }]">
|
||||
{{ member.lastName }}, {{ member.firstName }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -48,8 +53,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 +61,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() {
|
||||
@@ -65,6 +69,40 @@ export default {
|
||||
if (response.status === 200) {
|
||||
alert('Zugriff wurde angefragt');
|
||||
}
|
||||
},
|
||||
labelGender(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return 'Männlich';
|
||||
if (v === 'female') return 'Weiblich';
|
||||
if (v === 'diverse') return 'Divers';
|
||||
return 'Unbekannt';
|
||||
},
|
||||
genderSymbol(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return '♂';
|
||||
if (v === 'female') return '♀';
|
||||
if (v === 'diverse') return '⚧';
|
||||
return '';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs']),
|
||||
displayedMembers() {
|
||||
const members = Array.isArray(this.club.members) ? this.club.members : [];
|
||||
const onlyActive = members.filter(m => m && (m.active === true));
|
||||
return onlyActive.sort((a, b) => {
|
||||
const lnA = (a.lastName || '').toLowerCase();
|
||||
const lnB = (b.lastName || '').toLowerCase();
|
||||
if (lnA && !lnB) return -1;
|
||||
if (!lnA && lnB) return 1;
|
||||
if (lnA && lnB) {
|
||||
const cmp = lnA.localeCompare(lnB, 'de-DE');
|
||||
if (cmp !== 0) return cmp;
|
||||
}
|
||||
const fnA = (a.firstName || '').toLowerCase();
|
||||
const fnB = (b.firstName || '').toLowerCase();
|
||||
return fnA.localeCompare(fnB, 'de-DE');
|
||||
});
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -84,4 +122,18 @@ ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.members { margin-top: .25rem; }
|
||||
.member-item { padding: .15rem 0; }
|
||||
.gender-symbol, .gender-name { background: transparent; border: none; }
|
||||
.gender-name.gender-male { color: #1a73e8; }
|
||||
.gender-name.gender-female { color: #d81b60; }
|
||||
.gender-name.gender-diverse { color: #6a1b9a; }
|
||||
.gender-name.gender-unknown { color: #444; }
|
||||
.gender-symbol.gender-male { color: #1a73e8; }
|
||||
.gender-symbol.gender-female { color: #d81b60; }
|
||||
.gender-symbol.gender-diverse { color: #6a1b9a; }
|
||||
.gender-symbol.gender-unknown { color: #444; }
|
||||
.gender-symbol { margin-right: .35rem; opacity: .9; font-size: 1.05em; display: inline-block; width: 1.1em; text-align: center; }
|
||||
.is-test { font-style: italic; }
|
||||
</style>
|
||||
|
||||
119
frontend/src/views/Datenschutz.vue
Normal file
119
frontend/src/views/Datenschutz.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p class="back-home"><router-link to="/">Zur Startseite</router-link></p>
|
||||
|
||||
<section>
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
<p>
|
||||
Torsten Schulz<br/>
|
||||
Friedrich-Stampfer-Str. 21<br/>
|
||||
60437 Frankfurt, Deutschland<br/>
|
||||
E-Mail: <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Zwecke und Rechtsgrundlagen der Verarbeitung</h2>
|
||||
<ul>
|
||||
<li><strong>Bereitstellung der Website</strong> (Server-Logs, Sicherheit, Stabilität) – Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.</li>
|
||||
<li><strong>Nutzung des TrainingsTagebuchs</strong> (Registrierung, Login, Vereinsverwaltung) – Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO (Vertrag/vertragsähnliches Verhältnis).</li>
|
||||
<li><strong>Einwilligungsbasierte Vorgänge</strong> (z. B. optionale Funktionen) – Rechtsgrundlage: Art. 6 Abs. 1 lit. a DSGVO.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Kategorien personenbezogener Daten</h2>
|
||||
<ul>
|
||||
<li><strong>Nutzungsdaten</strong>: IP-Adresse, Datum/Uhrzeit, abgerufene Inhalte, User-Agent (Server-Logfiles).</li>
|
||||
<li><strong>Registrierungs-/Profildaten</strong>: Benutzername, E-Mail-Adresse (und ggf. weitere durch den Nutzer bereitgestellte Angaben).</li>
|
||||
<li><strong>Vereins-/Aktivitätsdaten</strong>: Inhalte, die Nutzer im Rahmen der Anwendung anlegen (z. B. Mitglieder-/Trainingsdaten).</li>
|
||||
<li><strong>Cookies/Local Storage</strong>: technisch notwendige Informationen (z. B. Session-/Auth-Token).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Empfänger</h2>
|
||||
<p>
|
||||
Eine Weitergabe erfolgt nur, soweit dies zur Bereitstellung der Website und Funktionen notwendig ist (z. B. Hosting/Technik) oder eine rechtliche Verpflichtung besteht.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Drittlandübermittlung</h2>
|
||||
<p>
|
||||
Eine Übermittlung in Drittländer findet grundsätzlich nicht statt, es sei denn, dies ist zur Nutzung einzelner Dienste technisch erforderlich. In solchen Fällen wird auf geeignete Garantien geachtet.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Speicherdauer</h2>
|
||||
<p>
|
||||
Personenbezogene Daten werden nur so lange gespeichert, wie es für die jeweiligen Zwecke erforderlich ist bzw. gesetzliche Aufbewahrungspflichten bestehen. Server-Logdaten werden in der Regel kurzfristig gelöscht.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Rechte der betroffenen Personen</h2>
|
||||
<ul>
|
||||
<li>Auskunft (Art. 15 DSGVO)</li>
|
||||
<li>Berichtigung (Art. 16 DSGVO)</li>
|
||||
<li>Löschung (Art. 17 DSGVO)</li>
|
||||
<li>Einschränkung (Art. 18 DSGVO)</li>
|
||||
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
|
||||
<li>Widerspruch (Art. 21 DSGVO)</li>
|
||||
<li>Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Zudem besteht ein Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO), z. B. beim HBDI in Hessen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. Erforderlichkeit der Bereitstellung</h2>
|
||||
<p>
|
||||
Für die Nutzung des TrainingsTagebuchs sind bestimmte Angaben erforderlich (z. B. E-Mail und Login-Daten). Ohne diese ist eine Registrierung/Anmeldung nicht möglich.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Cookies</h2>
|
||||
<p>
|
||||
Es werden vorwiegend technisch notwendige Cookies bzw. Webspeicher (Local Storage/Session Storage) verwendet, um die Anmeldung und Sitzungen zu ermöglichen. Eine Nutzung findet ohne Tracking zu Werbezwecken statt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Stand</h2>
|
||||
<p>
|
||||
Diese Datenschutzerklärung ist aktuell und wird bei Bedarf angepasst.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Datenschutz',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
section + section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.back-home {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,203 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Home</h2>
|
||||
<p v-if="!isAuthenticated">Du bist nicht eingeloggt.<router-link to="/login">Einloggen</router-link> oder <router-link to="/register">Registrieren</router-link></p>
|
||||
<p v-else>Herzlich Willkommen <button @click="logout">Ausloggen</button></p>
|
||||
<div class="home-container">
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-card card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Willkommen im TrainingsTagebuch</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="!isAuthenticated" class="marketing">
|
||||
<section class="hero">
|
||||
<h1 class="hero-title">Vereinsverwaltung, Trainingsplanung und Turniere – alles an einem Ort</h1>
|
||||
<p class="hero-subtitle">
|
||||
Das TrainingsTagebuch hilft Vereinen und Trainerinnen/Trainern, Mitglieder zu verwalten, Trainings zu dokumentieren,
|
||||
Spielpläne zu organisieren und Ergebnisse auszuwerten – DSGVO‑konform und einfach zu bedienen.
|
||||
</p>
|
||||
<div class="auth-actions">
|
||||
<router-link to="/register" class="btn-primary">
|
||||
<span class="btn-icon">🚀</span>
|
||||
Kostenlos starten
|
||||
</router-link>
|
||||
<router-link to="/login" class="btn-secondary">
|
||||
<span class="btn-icon">🔐</span>
|
||||
Einloggen
|
||||
</router-link>
|
||||
</div>
|
||||
<ul class="hero-bullets">
|
||||
<li>✔️ Mitglieder- und Gruppenverwaltung</li>
|
||||
<li>✔️ Trainings‑ und Turnierplanung</li>
|
||||
<li>✔️ Trainingsstatistiken und Auswertungen</li>
|
||||
<li>✔️ Rollen, Freigaben und sichere Zugriffe</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<h3 class="section-title">Was kannst du mit dem TrainingsTagebuch machen?</h3>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Mitglieder verwalten</h4>
|
||||
<p class="feature-description">
|
||||
Erstelle Mitgliedsprofile, bilde Gruppen und halte Kontakt‑ und Freigabestände aktuell.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📝</div>
|
||||
<h4 class="feature-title">Trainingstagebuch führen</h4>
|
||||
<p class="feature-description">
|
||||
Dokumentiere Inhalte, Umfang und Anwesenheiten jeder Einheit – nachvollziehbar und strukturiert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📅</div>
|
||||
<h4 class="feature-title">Spielpläne organisieren</h4>
|
||||
<p class="feature-description">
|
||||
Plane Spiele, Turniere und Veranstaltungen inklusive Gruppen, Runden und Ergebnissen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h4 class="feature-title">Statistiken & Auswertung</h4>
|
||||
<p class="feature-description">
|
||||
Erhalte Trainings‑ und Teilnahmeübersichten, erkenne Entwicklung und plane gezielt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h4 class="feature-title">Sicherheit & DSGVO</h4>
|
||||
<p class="feature-description">
|
||||
Datenschutzfreundliche Architektur, Freigaben durch Mitglieder und transparente Zugriffe.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">⚙️</div>
|
||||
<h4 class="feature-title">Vordefinierte Aktivitäten</h4>
|
||||
<p class="feature-description">
|
||||
Nutze Vorlagen für wiederkehrende Übungen und beschleunige deine Dokumentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="how-it-works">
|
||||
<h3 class="section-title">So funktioniert es</h3>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<h4 class="step-title">Registrieren</h4>
|
||||
<p>Lege kostenlos einen Account an und aktiviere ihn per E‑Mail.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<h4 class="step-title">Verein anlegen</h4>
|
||||
<p>Erstelle deinen Verein, lade Mitglieder ein und richte Gruppen ein.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<h4 class="step-title">Planen & dokumentieren</h4>
|
||||
<p>Plane Termine, dokumentiere Trainings und verfolge Fortschritte.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="seo-copy">
|
||||
<h3 class="section-title">Für wen ist das TrainingsTagebuch?</h3>
|
||||
<p class="long-text">
|
||||
Das TrainingsTagebuch ist die zentrale Plattform für Vereine, Abteilungen und Trainerteams.
|
||||
Es vereint Mitgliederverwaltung, Trainingsplanung, Spiel‑ und Turnierorganisation sowie aussagekräftige
|
||||
Statistiken in einer modernen Web‑Anwendung. Durch klare Rollen und Freigaben behalten Verantwortliche die
|
||||
Kontrolle, während Mitglieder selbstbestimmt mitwirken können. Ideal für Mannschafts‑, Racket‑ und
|
||||
Individualsportarten – vom Nachwuchs bis zum Leistungsbereich.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="faq">
|
||||
<h3 class="section-title">Häufige Fragen</h3>
|
||||
<details>
|
||||
<summary>Ist die Nutzung kostenlos?</summary>
|
||||
<p>Ja, du kannst kostenlos starten. Erweiterungen können später folgen.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Wie steht es um den Datenschutz?</summary>
|
||||
<p>Wir setzen auf Datensparsamkeit, transparente Freigaben und rollenbasierte Zugriffe.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Benötige ich eine Installation?</summary>
|
||||
<p>Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<div class="cta-bottom">
|
||||
<router-link to="/register" class="btn-primary">
|
||||
<span class="btn-icon">✅</span>
|
||||
Jetzt kostenlos registrieren
|
||||
</router-link>
|
||||
<router-link to="/login" class="btn-secondary">
|
||||
<span class="btn-icon">🔐</span>
|
||||
Ich habe schon einen Account
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="user-welcome">
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-icon">👋</span>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
Herzlich Willkommen zurück! Du bist erfolgreich eingeloggt.
|
||||
</p>
|
||||
<div class="user-actions">
|
||||
<button @click="logout" class="btn-secondary">
|
||||
<span class="btn-icon">🚪</span>
|
||||
Ausloggen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isAuthenticated" class="features-section">
|
||||
<h3 class="section-title">Was kannst du hier machen?</h3>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Mitglieder verwalten</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte deine Vereinsmitglieder, erstelle Gruppen und behalte den Überblick über alle Teilnehmer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📝</div>
|
||||
<h4 class="feature-title">Tagebuch führen</h4>
|
||||
<p class="feature-description">
|
||||
Dokumentiere deine Trainingsaktivitäten, Notizen und wichtige Ereignisse im Verein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📅</div>
|
||||
<h4 class="feature-title">Spielpläne organisieren</h4>
|
||||
<p class="feature-description">
|
||||
Plane und organisiere Spiele, Turniere und andere Veranstaltungen für deinen Verein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🏆</div>
|
||||
<h4 class="feature-title">Turniere verwalten</h4>
|
||||
<p class="feature-description">
|
||||
Erstelle und verwalte Turniere, Gruppen und Ergebnisse für deine Vereinsaktivitäten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +205,7 @@
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated']),
|
||||
},
|
||||
@@ -18,3 +214,289 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.05), rgba(160, 112, 64, 0.05));
|
||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.auth-message, .user-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.marketing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.875rem;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
margin: 0.25rem 0 0.25rem 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 780px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-bullets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.5rem 1rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.75rem 0 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-text, .welcome-text {
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
max-width: 600px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-actions, .user-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 0.375rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Features Section */
|
||||
.features-section {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
text-align: center;
|
||||
padding: 1.5rem 1.25rem;
|
||||
transition: var(--transition);
|
||||
border: 1px solid var(--border-color);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-heavy);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* How it works */
|
||||
.how-it-works .steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.seo-copy .long-text {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.faq details {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background: white;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cta-bottom {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.home-container {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
margin: 0 0.375rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
|
||||
.auth-actions, .user-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card-title {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
65
frontend/src/views/Impressum.vue
Normal file
65
frontend/src/views/Impressum.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Impressum</h1>
|
||||
<p class="back-home"><router-link to="/">Zur Startseite</router-link></p>
|
||||
<section>
|
||||
<h2>Diensteanbieter</h2>
|
||||
<p>
|
||||
Torsten Schulz<br/>
|
||||
Friedrich-Stampfer-Str. 21<br/>
|
||||
60437 Frankfurt<br/>
|
||||
Deutschland
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Kontakt</h2>
|
||||
<p>
|
||||
E-Mail: <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Vertretungsberechtigte Person</h2>
|
||||
<p>
|
||||
Torsten Schulz
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Umsatzsteuer-ID</h2>
|
||||
<p>
|
||||
Keine USt-IdNr. vorhanden (privat)
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Inhaltlich Verantwortlicher</h2>
|
||||
<p>
|
||||
Torsten Schulz (Anschrift wie oben)
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Impressum',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
section + section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.back-home {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<div class="register-link">
|
||||
<p>Noch kein Konto? <router-link to="/register">Registrieren</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,8 +33,7 @@ export default {
|
||||
await this.login({ token: response.data.token, username: this.email });
|
||||
this.$router.push('/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Login failed.');
|
||||
alert('Login fehlgeschlagen');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,9 +20,17 @@
|
||||
<label><span>Geburtsdatum:</span> <input type="date" v-model="newBirthdate"></label>
|
||||
<label><span>Telefon-Nr.:</span> <input type="text" v-model="newPhone"></label>
|
||||
<label><span>Email-Adresse:</span> <input type="email" v-model="newEmail"></label>
|
||||
<label><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
|
||||
<label><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership" </label>
|
||||
<label><span>Geschlecht:</span>
|
||||
<select v-model="newGender">
|
||||
<option value="unknown">Unbekannt</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="checkbox-item"><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
|
||||
<label class="checkbox-item"><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
<label><span>Bild:</span> <input type="file" @change="onFileSelected"></label>
|
||||
<div v-if="memberImagePreview">
|
||||
<img :src="memberImagePreview" alt="Vorschau des Mitgliedsbildes"
|
||||
@@ -35,8 +43,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="showInactiveMembers"> Inaktive Mitglieder anzeigen
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="showInactiveMembers">
|
||||
<span>Inaktive Mitglieder anzeigen</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
@@ -54,7 +65,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="member in members" :key="member.id">
|
||||
<tr v-if="member.active || showInactiveMembers" class="member-row" @click="editMember(member)">
|
||||
<tr v-if="member.active || showInactiveMembers" class="member-row" :class="{ 'row-inactive': !member.active }" @click="editMember(member)">
|
||||
<td>
|
||||
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
|
||||
style="max-width: 50px; max-height: 50px;"
|
||||
@@ -62,7 +73,13 @@
|
||||
<span>{{ member.picsInInternetAllowed ? '✓' : '' }}</span>
|
||||
</td>
|
||||
<td>{{ member.testMembership ? '*' : '' }}</td>
|
||||
<td>{{ member.lastName }}, {{ member.firstName }}</td>
|
||||
<td>
|
||||
<span class="gender-symbol" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]" :title="labelGender(member.gender)">{{ genderSymbol(member.gender) }}</span>
|
||||
<span class="gender-name" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]">
|
||||
{{ member.lastName }}, {{ member.firstName }}
|
||||
<span v-if="!member.active && showInactiveMembers" class="inactive-badge">inaktiv</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ member.street }}, {{ member.city }}</td>
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ member.phone }}</td>
|
||||
@@ -93,7 +110,7 @@
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="note in notes" :key="note.id">
|
||||
<button @click="deleteNote(note.id)" class="cancel-action">Löschen</button>
|
||||
<button @click="deleteNote(note.id)" class="trash-btn">🗑️</button>
|
||||
{{ note.content }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -120,9 +137,10 @@ export default {
|
||||
newLastname: '',
|
||||
newStreet: '',
|
||||
newCity: '',
|
||||
newBirthdate: '01.01.2010',
|
||||
newBirthdate: '',
|
||||
newPhone: '',
|
||||
newEmail: '',
|
||||
newGender: 'unknown',
|
||||
newActive: true,
|
||||
memberToEdit: null,
|
||||
memberImage: null,
|
||||
@@ -171,12 +189,13 @@ export default {
|
||||
this.newLastname = '';
|
||||
this.newStreet = '';
|
||||
this.newCity = '';
|
||||
this.newBirthdate = '01.01.2010';
|
||||
this.newBirthdate = '';
|
||||
this.newPhone = '';
|
||||
this.newEmail = '';
|
||||
this.newActive = true;
|
||||
this.newPicsInInternetAllowed = false;
|
||||
this.testMembership = true;
|
||||
this.newGender = 'unknown';
|
||||
this.memberImage = null;
|
||||
this.memberImagePreview = null;
|
||||
},
|
||||
@@ -200,6 +219,7 @@ export default {
|
||||
birthdate: this.newBirthdate,
|
||||
phone: this.newPhone,
|
||||
email: this.newEmail,
|
||||
gender: this.newGender,
|
||||
active: this.newActive,
|
||||
id: this.memberToEdit ? this.memberToEdit.id : null,
|
||||
testMembership: this.testMembership,
|
||||
@@ -211,7 +231,7 @@ export default {
|
||||
response = await apiClient.post(`/clubmembers/set/${this.currentClub}`, memberData);
|
||||
this.loadMembers();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Speichern des Mitglieds:", error);
|
||||
alert('Fehler beim Speichern des Mitglieds');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +246,7 @@ export default {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Hochladen des Bildes:", error);
|
||||
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +254,7 @@ export default {
|
||||
this.memberFormIsOpen = false;
|
||||
},
|
||||
async editMember(member) {
|
||||
console.log(member.birthDate);
|
||||
const birthDate = member.birthDate;
|
||||
this.memberToEdit = member;
|
||||
this.memberFormIsOpen = true;
|
||||
this.newFirstname = member.firstName;
|
||||
@@ -243,9 +263,9 @@ export default {
|
||||
this.newCity = member.city;
|
||||
this.newPhone = member.phone;
|
||||
this.newEmail = member.email;
|
||||
this.newGender = member.gender || 'unknown';
|
||||
this.newActive = member.active;
|
||||
const date = new Date(member.birthDate);
|
||||
this.newBirthdate = date.toISOString().split('T')[0];
|
||||
this.newBirthdate = this.formatDateForInput(birthDate);
|
||||
this.testMembership = member.testMembership;
|
||||
this.newPicsInInternetAllowed = member.picsInInternetAllowed;
|
||||
try {
|
||||
@@ -254,10 +274,26 @@ export default {
|
||||
});
|
||||
this.memberImagePreview = URL.createObjectURL(response.data);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden des Bildes:", error);
|
||||
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
|
||||
this.memberImagePreview = null;
|
||||
}
|
||||
},
|
||||
formatDateForInput(value) {
|
||||
if (!value || typeof value !== 'string') return '';
|
||||
const v = value.trim();
|
||||
// dd.mm.yyyy
|
||||
const m1 = v.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (m1) {
|
||||
const yyyy = m1[3];
|
||||
const mm = String(Number(m1[2])).padStart(2, '0');
|
||||
const dd = String(Number(m1[1])).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
// ISO/yyy-mm-dd
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) return d.toISOString().split('T')[0];
|
||||
return '';
|
||||
},
|
||||
resetToNewMember() {
|
||||
this.memberToEdit = null;
|
||||
this.resetNewMember();
|
||||
@@ -308,7 +344,7 @@ export default {
|
||||
const imageUrl = URL.createObjectURL(response.data);
|
||||
member.imageUrl = imageUrl;
|
||||
} catch (error) {
|
||||
console.error("Failed to load member image:", error);
|
||||
// Kein Alert - es ist normal, dass nicht alle Mitglieder Bilder haben
|
||||
member.imageUrl = null;
|
||||
}
|
||||
},
|
||||
@@ -322,6 +358,20 @@ export default {
|
||||
const date = new Date(birthDate);
|
||||
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
|
||||
},
|
||||
labelGender(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return 'Männlich';
|
||||
if (v === 'female') return 'Weiblich';
|
||||
if (v === 'diverse') return 'Divers';
|
||||
return 'Unbekannt';
|
||||
},
|
||||
genderSymbol(g) {
|
||||
const v = (g || 'unknown');
|
||||
if (v === 'male') return '♂';
|
||||
if (v === 'female') return '♀';
|
||||
if (v === 'diverse') return '⚧';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -416,4 +466,27 @@ table td {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid #ddd;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.gender-symbol, .gender-name { background: transparent; border: none; }
|
||||
|
||||
.gender-name.gender-male { color: #1a73e8; }
|
||||
.gender-name.gender-female { color: #d81b60; }
|
||||
.gender-name.gender-diverse { color: #6a1b9a; }
|
||||
.gender-name.gender-unknown { color: #444; }
|
||||
.gender-symbol.gender-male { color: #1a73e8; }
|
||||
.gender-symbol.gender-female { color: #d81b60; }
|
||||
.gender-symbol.gender-diverse { color: #6a1b9a; }
|
||||
.gender-symbol.gender-unknown { color: #444; }
|
||||
.gender-symbol { margin-right: .35rem; opacity: .9; font-size: 1.05em; display: inline-block; width: 1.1em; text-align: center; }
|
||||
.row-inactive { opacity: .6; }
|
||||
.is-inactive { text-decoration: line-through; }
|
||||
.inactive-badge { margin-left: .5rem; font-size: .85em; color: #666; text-transform: lowercase; }
|
||||
</style>
|
||||
|
||||
1335
frontend/src/views/OfficialTournaments.vue
Normal file
1335
frontend/src/views/OfficialTournaments.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ export default {
|
||||
const response = await apiClient.get(`/clubs/pending/${this.currentClub}`);
|
||||
this.pendingUsers = response.data.map(entry => entry.user);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der ausstehenden Anfragen:', error);
|
||||
alert('Fehler beim Laden der ausstehenden Anfragen');
|
||||
}
|
||||
},
|
||||
async approveUser(userId) {
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
});
|
||||
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Genehmigen des Benutzers:', error);
|
||||
alert('Fehler beim Genehmigen des Benutzers');
|
||||
}
|
||||
},
|
||||
async rejectUser(userId) {
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
});
|
||||
this.pendingUsers = this.pendingUsers.filter(user => user.id !== userId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Ablehnen des Benutzers:', error);
|
||||
alert('Fehler beim Ablehnen des Benutzers');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
358
frontend/src/views/PredefinedActivities.vue
Normal file
358
frontend/src/views/PredefinedActivities.vue
Normal file
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="predef-activities">
|
||||
<h2>Vordefinierte Aktivitäten</h2>
|
||||
<div class="grid">
|
||||
<div class="list">
|
||||
<div class="toolbar">
|
||||
<button @click="startCreate" class="btn-primary">Neu</button>
|
||||
<button @click="reload" class="btn-secondary">Neu laden</button>
|
||||
<div>
|
||||
<div>
|
||||
<button @click="deduplicate" class="btn-secondary">Doppelungen zusammenführen</button>
|
||||
</div
|
||||
<div class="merge-tools">
|
||||
<select v-model="mergeSourceId">
|
||||
<option disabled value="">Quelle wählen…</option>
|
||||
<option v-for="a in sortedActivities" :key="'s'+a.id" :value="a.id">{{ formatItem(a) }}</option>
|
||||
</select>
|
||||
<span>→</span>
|
||||
<select v-model="mergeTargetId">
|
||||
<option disabled value="">Ziel wählen…</option>
|
||||
<option v-for="a in sortedActivities" :key="'t'+a.id" :value="a.id">{{ formatItem(a) }}</option>
|
||||
</select>
|
||||
<button class="btn-secondary" :disabled="!canMerge" @click="mergeSelected">Zusammenführen</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="items">
|
||||
<li v-for="a in sortedActivities" :key="a.id" :class="{ active: selectedActivity && selectedActivity.id === a.id }" @click="select(a)">
|
||||
<div class="title">
|
||||
<strong>{{ a.code ? '[' + a.code + '] ' : '' }}{{ a.name }}</strong>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span v-if="a.duration">{{ a.duration }} min</span>
|
||||
<span v-if="a.durationText"> ({{ a.durationText }})</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="detail" v-if="editModel">
|
||||
<h3>{{ editModel.id ? 'Aktivität bearbeiten' : 'Neue Aktivität' }}</h3>
|
||||
<form @submit.prevent="save">
|
||||
<label>Name
|
||||
<input type="text" v-model="editModel.name" required />
|
||||
</label>
|
||||
<label>Kürzel
|
||||
<input type="text" v-model="editModel.code" />
|
||||
</label>
|
||||
<label>Dauer (Minuten)
|
||||
<input type="number" v-model.number="editModel.duration" min="0" />
|
||||
</label>
|
||||
<label>Dauer (Text)
|
||||
<input type="text" v-model="editModel.durationText" placeholder="z.B. 2x7" />
|
||||
</label>
|
||||
<label>Beschreibung
|
||||
<textarea v-model="editModel.description" rows="4" />
|
||||
</label>
|
||||
<div class="image-section">
|
||||
<h4>Bild hinzufügen</h4>
|
||||
<p class="image-help">Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:</p>
|
||||
|
||||
<label>Bild-Link (optional)
|
||||
<input type="text" v-model="editModel.imageLink" placeholder="z.B. https://example.com/bild.jpg oder /api/predefined-activities/:id/image/:imageId" />
|
||||
</label>
|
||||
|
||||
<div class="upload-section">
|
||||
<label>Oder Bild hochladen:
|
||||
<input type="file" accept="image/*" @change="onFileChange" />
|
||||
</label>
|
||||
<button class="btn-secondary" :disabled="!selectedFile" @click="uploadImage">
|
||||
{{ editModel.id ? 'Hochladen' : 'Nach Speichern hochladen' }}
|
||||
</button>
|
||||
<p v-if="!editModel.id" class="upload-note">
|
||||
Hinweis: Das Bild wird erst nach dem Speichern der Aktivität hochgeladen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image-list" v-if="images && images.length">
|
||||
<h5>Hochgeladene Bilder:</h5>
|
||||
<div class="image-grid">
|
||||
<div v-for="img in images" :key="img.id" class="image-item">
|
||||
<img :src="imageUrl(img)" alt="Predefined Activity Image" />
|
||||
<button class="btn-small btn-danger" @click="deleteImage(img.id)">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<button type="button" class="btn-secondary" @click="cancel">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient.js';
|
||||
|
||||
export default {
|
||||
name: 'PredefinedActivities',
|
||||
data() {
|
||||
return {
|
||||
activities: [],
|
||||
selectedActivity: null,
|
||||
editModel: null,
|
||||
images: [],
|
||||
selectedFile: null,
|
||||
mergeSourceId: '',
|
||||
mergeTargetId: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedActivities() {
|
||||
return [...(this.activities || [])].sort((a, b) => {
|
||||
const ac = (a.code || '').toLocaleLowerCase('de-DE');
|
||||
const bc = (b.code || '').toLocaleLowerCase('de-DE');
|
||||
const aEmpty = ac === '';
|
||||
const bEmpty = bc === '';
|
||||
if (aEmpty !== bEmpty) return aEmpty ? 1 : -1; // leere Codes nach hinten
|
||||
if (ac < bc) return -1; if (ac > bc) return 1;
|
||||
const an = (a.name || '').toLocaleLowerCase('de-DE');
|
||||
const bn = (b.name || '').toLocaleLowerCase('de-DE');
|
||||
if (an < bn) return -1; if (an > bn) return 1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
canMerge() {
|
||||
return this.mergeSourceId && this.mergeTargetId && String(this.mergeSourceId) !== String(this.mergeTargetId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async reload() {
|
||||
const r = await apiClient.get('/predefined-activities');
|
||||
this.activities = r.data || [];
|
||||
},
|
||||
async select(a) {
|
||||
this.selectedActivity = a;
|
||||
const r = await apiClient.get(`/predefined-activities/${a.id}`);
|
||||
const { images, ...activity } = r.data;
|
||||
this.images = images || [];
|
||||
this.editModel = { ...activity };
|
||||
},
|
||||
formatItem(a) {
|
||||
return `${a.code ? '[' + a.code + '] ' : ''}${a.name}`;
|
||||
},
|
||||
async mergeSelected() {
|
||||
if (!this.canMerge) return;
|
||||
const src = this.mergeSourceId; const tgt = this.mergeTargetId;
|
||||
if (!confirm(`Eintrag #${src} in #${tgt} zusammenführen?\nAlle Verknüpfungen werden auf das Ziel umgebogen, die Quelle wird gelöscht.`)) return;
|
||||
await apiClient.post('/predefined-activities/merge', { sourceId: src, targetId: tgt });
|
||||
this.mergeSourceId = '';
|
||||
this.mergeTargetId = '';
|
||||
await this.reload();
|
||||
},
|
||||
startCreate() {
|
||||
this.selectedActivity = null;
|
||||
this.images = [];
|
||||
this.editModel = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
duration: null,
|
||||
durationText: '',
|
||||
imageLink: '',
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
this.editModel = null;
|
||||
this.selectedActivity = null;
|
||||
this.images = [];
|
||||
},
|
||||
async save() {
|
||||
if (!this.editModel) return;
|
||||
if (this.editModel.id) {
|
||||
const { id, ...payload } = this.editModel;
|
||||
const r = await apiClient.put(`/predefined-activities/${id}`, payload);
|
||||
this.editModel = r.data;
|
||||
} else {
|
||||
const r = await apiClient.post('/predefined-activities', this.editModel);
|
||||
this.editModel = r.data;
|
||||
// Nach dem Erstellen einer neuen Aktivität, falls ein Bild ausgewählt wurde, hochladen
|
||||
if (this.selectedFile) {
|
||||
await this.uploadImage();
|
||||
}
|
||||
}
|
||||
await this.reload();
|
||||
},
|
||||
onFileChange(e) {
|
||||
this.selectedFile = e.target.files && e.target.files[0] ? e.target.files[0] : null;
|
||||
},
|
||||
imageUrl(img) {
|
||||
return `/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
|
||||
},
|
||||
async uploadImage() {
|
||||
if (!this.editModel || !this.editModel.id || !this.selectedFile) return;
|
||||
const fd = new FormData();
|
||||
fd.append('image', this.selectedFile);
|
||||
await apiClient.post(`/predefined-activities/${this.editModel.id}/image`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
// Nach Upload Details neu laden
|
||||
await this.select(this.editModel);
|
||||
this.selectedFile = null;
|
||||
},
|
||||
async deleteImage(imageId) {
|
||||
if (!this.editModel || !this.editModel.id) return;
|
||||
if (!confirm('Bild wirklich löschen?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/predefined-activities/${this.editModel.id}/image/${imageId}`);
|
||||
// Nach Löschen Details neu laden
|
||||
await this.select(this.editModel);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Bildes:', error);
|
||||
alert('Fehler beim Löschen des Bildes');
|
||||
}
|
||||
},
|
||||
async deduplicate() {
|
||||
if (!confirm('Alle Aktivitäten mit identischem Namen werden zusammengeführt. Fortfahren?')) return;
|
||||
await apiClient.post('/predefined-activities/deduplicate', {});
|
||||
await this.reload();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.predef-activities {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
.list {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.merge-tools { display: inline-flex; align-items: center; gap: .35rem; margin-left: auto; }
|
||||
select { max-width: 220px; }
|
||||
.items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.items li {
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--border-radius-small);
|
||||
cursor: pointer;
|
||||
}
|
||||
.items li:hover { background: var(--primary-light);
|
||||
}
|
||||
.items li.active { background: var(--primary-light); color: var(--primary-color); }
|
||||
.detail {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
max-height: calc(100vh - 170px);
|
||||
overflow: auto;
|
||||
}
|
||||
label { display: block; margin-bottom: 0.5rem; }
|
||||
input[type="text"], input[type="number"], textarea { width: 100%; }
|
||||
.actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
|
||||
.image-section {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.image-help {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.upload-note {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius-small);
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-item button {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
<div class="login-link">
|
||||
<p>Bereits ein Konto? <router-link to="/login">Zum Login</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,11 +26,10 @@
|
||||
methods: {
|
||||
async register() {
|
||||
try {
|
||||
await axios.post('/api/auth/register', { email: this.email, password: this.password });
|
||||
await axios.post(`${import.meta.env.VITE_BACKEND}/api/auth/register`, { email: this.email, password: this.password });
|
||||
alert('Registration successful! Please check your email to activate your account.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Registration failed.');
|
||||
alert('Registrierung fehlgeschlagen');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
<div>
|
||||
<h2>Spielpläne</h2>
|
||||
<button @click="openImportModal">Spielplanimport</button>
|
||||
<div v-if="hoveredMatch" class="hover-info">
|
||||
<p><strong>{{ hoveredMatch.location.name }}</strong></p>
|
||||
<p>{{ hoveredMatch.location.address }}</p>
|
||||
<p>{{ hoveredMatch.location.zip }} {{ hoveredMatch.location.city }}</p>
|
||||
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
|
||||
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
|
||||
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
|
||||
<p>{{ hoveredMatch.location.zip || '' }} {{ hoveredMatch.location.city || 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="output">
|
||||
<ul>
|
||||
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
|
||||
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
|
||||
<li class="divider"></li>
|
||||
<li v-for="league in leagues" :key="league" @click="loadMatchesForLeague(league.id, league.name)">{{
|
||||
league.name }}</li>
|
||||
</ul>
|
||||
@@ -16,22 +19,24 @@
|
||||
<button @click="generatePDF">Download PDF</button>
|
||||
<div v-if="matches.length > 0">
|
||||
<h3>Spiele für {{ selectedLeague }}</h3>
|
||||
<table>
|
||||
<table id="schedule-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Uhrzeit</th>
|
||||
<th>Heimmannschaft</th>
|
||||
<th>Gastmannschaft</th>
|
||||
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="match in matches" :key="match.id" @mouseover="hoveredMatch = match"
|
||||
@mouseleave="hoveredMatch = null">
|
||||
@mouseleave="hoveredMatch = null" :class="getRowClass(match.date)">
|
||||
<td>{{ formatDate(match.date) }}</td>
|
||||
<td>{{ match.time.toString().slice(0, 5) }} Uhr</td>
|
||||
<td v-html="highlightClubName(match.homeTeam.name)"></td>
|
||||
<td v-html="highlightClubName(match.guestTeam.name)"></td>
|
||||
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}</td>
|
||||
<td v-html="highlightClubName(match.homeTeam?.name || 'N/A')"></td>
|
||||
<td v-html="highlightClubName(match.guestTeam?.name || 'N/A')"></td>
|
||||
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -59,8 +64,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import jsPDF from 'jspdf';
|
||||
import html2canvas from 'html2canvas';
|
||||
import PDFGenerator from '../components/PDFGenerator.js';
|
||||
|
||||
export default {
|
||||
@@ -97,7 +100,7 @@ export default {
|
||||
formData.append('clubId', this.currentClub);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/matches/import', formData, {
|
||||
await apiClient.post('/matches/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
@@ -106,17 +109,75 @@ export default {
|
||||
this.closeImportModal();
|
||||
this.loadLeagues();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Importieren der CSV:', error);
|
||||
alert('Fehler beim Importieren der CSV.');
|
||||
alert('Fehler beim Importieren der CSV-Datei');
|
||||
}
|
||||
},
|
||||
// Sortierfunktion für Ligen
|
||||
sortLeagues(leagues) {
|
||||
// Ligen-Priorität
|
||||
const leagueOrder = [
|
||||
'1. Bundesliga',
|
||||
'2. Bundesliga',
|
||||
'3. Bundesliga',
|
||||
'Regionalliga',
|
||||
'Oberliga',
|
||||
'Verbandsliga (Hessen)',
|
||||
'Bezirksoberliga',
|
||||
'Bezirksliga',
|
||||
'Bezirksklasse',
|
||||
'Kreisliga',
|
||||
'1. Kreisklasse',
|
||||
'2. Kreisklasse',
|
||||
'3. Kreisklasse',
|
||||
];
|
||||
|
||||
// Hilfsfunktionen
|
||||
function getLeagueIndex(name) {
|
||||
for (let i = 0; i < leagueOrder.length; i++) {
|
||||
if (name.includes(leagueOrder[i])) return i;
|
||||
}
|
||||
return leagueOrder.length;
|
||||
}
|
||||
function parseYouth(name) {
|
||||
// Gibt {type: 'J'|'M'|'Jugend'|null, age: Zahl|null} zurück
|
||||
const m = name.match(/([JM])(\d{1,2})/i);
|
||||
if (m) return { type: m[1].toUpperCase(), age: parseInt(m[2]) };
|
||||
if (/jugend/i.test(name)) return { type: 'Jugend', age: null };
|
||||
return { type: null, age: null };
|
||||
}
|
||||
|
||||
// Sortierlogik
|
||||
return leagues.slice().sort((a, b) => {
|
||||
const ya = parseYouth(a.name);
|
||||
const yb = parseYouth(b.name);
|
||||
// Erwachsene zuerst
|
||||
if (!ya.type && yb.type) return -1;
|
||||
if (ya.type && !yb.type) return 1;
|
||||
if (!ya.type && !yb.type) {
|
||||
// Beide Erwachsene: nach Liga
|
||||
return getLeagueIndex(a.name) - getLeagueIndex(b.name);
|
||||
}
|
||||
// Beide Jugend: erst nach Alter aufsteigend (älteste unten), dann J vor M vor "Jugend", dann Liga
|
||||
// "Jugend" ohne Zahl ist die jüngste Jugendklasse
|
||||
if (ya.age !== yb.age) {
|
||||
if (ya.age === null) return -1;
|
||||
if (yb.age === null) return 1;
|
||||
return ya.age - yb.age;
|
||||
}
|
||||
// Reihenfolge: J < M < Jugend
|
||||
const typeOrder = { 'J': 0, 'M': 1, 'Jugend': 2 };
|
||||
if (ya.type !== yb.type) return (typeOrder[ya.type] || 2) - (typeOrder[yb.type] || 2);
|
||||
return getLeagueIndex(a.name) - getLeagueIndex(b.name);
|
||||
});
|
||||
},
|
||||
|
||||
async loadLeagues() {
|
||||
try {
|
||||
const clubId = this.currentClub;
|
||||
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
|
||||
this.leagues = response.data;
|
||||
this.leagues = this.sortLeagues(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leagues:', error);
|
||||
alert('Fehler beim Laden der Ligen');
|
||||
}
|
||||
},
|
||||
async loadMatchesForLeague(leagueId, leagueName) {
|
||||
@@ -125,15 +186,49 @@ export default {
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${leagueId}`);
|
||||
this.matches = response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to load matches:', error);
|
||||
alert('Fehler beim Laden der Matches');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
async loadAllMatches() {
|
||||
this.selectedLeague = 'Gesamtspielplan';
|
||||
try {
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
|
||||
this.matches = response.data;
|
||||
} catch (error) {
|
||||
alert('Fehler beim Laden des Gesamtspielplans');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
async loadAdultMatches() {
|
||||
this.selectedLeague = 'Spielplan Erwachsene';
|
||||
try {
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
|
||||
// Filtere nur Erwachsenenligen (keine Jugendligen)
|
||||
const allMatches = response.data;
|
||||
this.matches = allMatches.filter(match => {
|
||||
const leagueName = match.leagueDetails?.name || '';
|
||||
// Prüfe, ob es eine Jugendliga ist (J, M, Jugend im Namen)
|
||||
const isYouth = /[JM]\d|jugend/i.test(leagueName);
|
||||
return !isYouth;
|
||||
});
|
||||
} catch (error) {
|
||||
alert('Fehler beim Laden des Erwachsenenspielplans');
|
||||
this.matches = [];
|
||||
}
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return 'N/A';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return 'N/A';
|
||||
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
const wd = weekdays[d.getDay()];
|
||||
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
||||
return new Date(date).toLocaleDateString('de-DE', options);
|
||||
const day = d.toLocaleDateString('de-DE', options);
|
||||
return `${wd} ${day}`;
|
||||
},
|
||||
highlightClubName(teamName) {
|
||||
if (!teamName) return 'N/A';
|
||||
const clubName = this.currentClubName;
|
||||
if (clubName && teamName.includes(clubName)) {
|
||||
return `<strong>${teamName}</strong>`;
|
||||
@@ -141,35 +236,40 @@ export default {
|
||||
return teamName;
|
||||
},
|
||||
getCurrentClubName() {
|
||||
const club = this.clubs.find(club => club.id === this.currentClub);
|
||||
const clubIdNum = Number(this.currentClub);
|
||||
const club = this.clubs.find(c => c.id === clubIdNum);
|
||||
return club ? club.name : '';
|
||||
},
|
||||
async generatePDF() {
|
||||
const element = this.$el.querySelector('.flex-item > div');
|
||||
const highlightName = this.getCurrentClubName();
|
||||
if (element) {
|
||||
const pdfGen = new PDFGenerator();
|
||||
await pdfGen.addSchedule(element);
|
||||
pdfGen.addTitle(`Spiele für ${highlightName} in ${this.selectedLeague}`);
|
||||
pdfGen.addTable('schedule-table', highlightName);
|
||||
pdfGen.addNewPage();
|
||||
pdfGen.addHeader('Hallen-Adressen');
|
||||
const uniqueLocations = this.getUniqueLocations();
|
||||
uniqueLocations.forEach((addressLines, clubName) => {
|
||||
pdfGen.addAddress(clubName, addressLines);
|
||||
if (!clubName.includes(highlightName)) {
|
||||
pdfGen.addAddress(clubName, addressLines);
|
||||
}
|
||||
});
|
||||
|
||||
pdfGen.save('Spielpläne.pdf');
|
||||
} else {
|
||||
console.error('No matches found to generate PDF.');
|
||||
alert('Keine Matches gefunden, um PDF zu generieren.');
|
||||
}
|
||||
},
|
||||
getUniqueLocations() {
|
||||
const uniqueLocations = new Map();
|
||||
this.matches.forEach(match => {
|
||||
if (!match.location || !match.homeTeam) return;
|
||||
const location = match.location;
|
||||
const clubName = match.homeTeam.name;
|
||||
const addressLines = [
|
||||
location.name,
|
||||
location.address,
|
||||
`${location.zip} ${location.city}`
|
||||
location.name || 'N/A',
|
||||
location.address || 'N/A',
|
||||
`${location.zip || ''} ${location.city || ''}`.trim()
|
||||
];
|
||||
const addressKey = addressLines.join('; ');
|
||||
if (!uniqueLocations.has(addressKey)) {
|
||||
@@ -178,7 +278,29 @@ export default {
|
||||
});
|
||||
|
||||
return uniqueLocations;
|
||||
}
|
||||
},
|
||||
getRowClass(matchDate) {
|
||||
if (!matchDate) return '';
|
||||
|
||||
const today = new Date();
|
||||
const match = new Date(matchDate);
|
||||
|
||||
// Setze die Zeit auf Mitternacht für genaue Datumsvergleiche
|
||||
today.setHours(0, 0, 0, 0);
|
||||
match.setHours(0, 0, 0, 0);
|
||||
|
||||
// Berechne die Differenz in Tagen
|
||||
const diffTime = match.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'match-today'; // Heute - gelb
|
||||
} else if (diffDays > 0 && diffDays <= 7) {
|
||||
return 'match-next-week'; // Nächste Woche - hellblau
|
||||
}
|
||||
|
||||
return ''; // Keine besondere Farbe
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.loadLeagues();
|
||||
@@ -291,4 +413,42 @@ li {
|
||||
color: #45a049;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.special-link {
|
||||
font-weight: bold;
|
||||
color: #2c5aa0 !important;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.special-link:hover {
|
||||
background-color: #f0f8ff;
|
||||
padding-left: 5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
margin: 10px 0;
|
||||
cursor: default !important;
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
.match-today {
|
||||
background-color: #fff3cd !important; /* Gelb für heute */
|
||||
}
|
||||
|
||||
.match-next-week {
|
||||
background-color: #d1ecf1 !important; /* Hellblau für nächste Woche */
|
||||
}
|
||||
|
||||
.match-today:hover {
|
||||
background-color: #ffeaa7 !important; /* Dunkleres Gelb beim Hover */
|
||||
}
|
||||
|
||||
.match-next-week:hover {
|
||||
background-color: #b8daff !important; /* Dunkleres Blau beim Hover */
|
||||
}
|
||||
</style>
|
||||
|
||||
1500
frontend/src/views/TournamentsView.vue
Normal file
1500
frontend/src/views/TournamentsView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user