From ae6e2bd0ed7a9538963f6eab5c44f23788e44457 Mon Sep 17 00:00:00 2001 From: joschy2002 Date: Fri, 30 May 2025 22:05:18 +0200 Subject: [PATCH] Bugs in Trainingsplanung behoben --- trainerbox/lib/screens/calendar_tab.dart | 464 +++++++++++++++-------- trainerbox/pubspec.lock | 32 ++ trainerbox/pubspec.yaml | 1 + 3 files changed, 348 insertions(+), 149 deletions(-) diff --git a/trainerbox/lib/screens/calendar_tab.dart b/trainerbox/lib/screens/calendar_tab.dart index df5133e..606b7a8 100644 --- a/trainerbox/lib/screens/calendar_tab.dart +++ b/trainerbox/lib/screens/calendar_tab.dart @@ -3,6 +3,7 @@ import 'package:table_calendar/table_calendar.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'training_detail_screen.dart'; +import 'package:uuid/uuid.dart'; const Map categoryColors = { 'Aufwärmen & Mobilisation': Colors.deepOrange, @@ -121,49 +122,107 @@ class _CalendarTabState extends State { for (var trainerDoc in trainersSnapshot.docs) { final trainerData = trainerDoc.data() as Map; + final trainings = trainerData['trainings'] as Map? ?? {}; + final cancelledTrainings = trainerData['cancelledTrainings'] as List? ?? []; final trainingTimes = trainerData['trainingTimes'] as Map? ?? {}; final trainingDurations = trainerData['trainingDurations'] as Map? ?? {}; - final cancelledTrainings = trainerData['cancelledTrainings'] as List? ?? []; - final trainingExercises = trainerData['trainingExercises'] as Map? ?? {}; - // 1. Wochentagsbasierte Trainings (wie gehabt) + print('Lade Events:'); + print('trainings: $trainings'); + print('cancelledTrainings: $cancelledTrainings'); + print('trainingTimes: $trainingTimes'); + print('trainingDurations: $trainingDurations'); + + // Lade alle Trainings für das Datum + trainings.forEach((dateString, trainingsList) { + final date = DateTime.tryParse(dateString); + if (date == null) return; + final normalizedDate = DateTime(date.year, date.month, date.day); + final list = List>.from(trainingsList); + for (final training in list) { + final timeStr = training['time'] as String? ?? '00:00'; + final duration = training['duration'] as int? ?? 60; + final exercises = training['exercises'] as List? ?? []; + final totalExerciseDuration = exercises.fold(0, (sum, exercise) { + if (exercise is Map) { + return sum + (exercise['duration'] as int? ?? 0); + } + return sum; + }); + + final event = { + 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', + 'time': timeStr, + 'duration': duration, + 'trainerId': trainerDoc.id, + 'isCurrentUser': trainerDoc.id == _currentUserId, + 'day': dateString, + 'date': dateString, + 'exercises': exercises, + 'remainingTime': duration - totalExerciseDuration, + 'club': trainerData['club'] ?? 'Kein Verein', + 'id': training['id'], + }; + if (events.containsKey(normalizedDate)) { + events[normalizedDate]!.add(event); + } else { + events[normalizedDate] = [event]; + } + } + }); + + // Füge regelmäßige Trainings hinzu + final now = DateTime.now(); + final yearStart = DateTime(now.year, 1, 1); + final yearEnd = DateTime(now.year + 1, 1, 1); + final weekdays = { + 'Montag': 1, + 'Dienstag': 2, + 'Mittwoch': 3, + 'Donnerstag': 4, + 'Freitag': 5, + 'Samstag': 6, + 'Sonntag': 7, + }; + + // Erstelle eine Set von gecancelten Daten für schnelleren Zugriff + final cancelledDates = cancelledTrainings + .where((cancelled) => cancelled is Map) + .map((cancelled) => DateTime.parse((cancelled as Map)['date'] as String)) + .toSet(); + + print('Gecancelte Daten: $cancelledDates'); + trainingTimes.forEach((day, timeStr) { - // Prüfe, ob der Key KEIN Datum ist (also ein Wochentag) - if (DateTime.tryParse(day) != null) return; if (timeStr == null) return; - final duration = trainingDurations[day] as int? ?? 60; final timeParts = (timeStr as String).split(':'); if (timeParts.length != 2) return; + final hour = int.tryParse(timeParts[0]) ?? 0; final minute = int.tryParse(timeParts[1]) ?? 0; + final duration = trainingDurations[day] ?? 60; + final targetWeekday = weekdays[day] ?? 1; - final now = DateTime.now(); - final daysUntilNext = _getDaysUntilNext(day, now.weekday); - final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute); + // Finde das erste gewünschte Wochentags-Datum ab Jahresanfang + DateTime firstDate = yearStart; + while (firstDate.weekday != targetWeekday) { + firstDate = firstDate.add(const Duration(days: 1)); + } - for (var i = 0; i < 52; i++) { - final date = eventDate.add(Duration(days: i * 7)); - final normalizedDate = DateTime(date.year, date.month, date.day); + // Generiere Trainings für das gesamte Jahr + while (firstDate.isBefore(yearEnd)) { + final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day); final dateString = normalizedDate.toIso8601String(); - final isCancelled = cancelledTrainings.any((cancelled) { - if (cancelled is Map) { - final cancelledDate = DateTime.parse(cancelled['date'] as String); - return isSameDay(cancelledDate, normalizedDate); - } - return false; - }); - // NEU: Prüfe, ob es ein datumsspezifisches Training für diesen Tag gibt - final hasDateSpecific = trainingTimes.containsKey(dateString); - if (!isCancelled || hasDateSpecific) { - // Wenn es ein datumsspezifisches Training gibt, blende das wochentagsbasierte Training aus - if (isCancelled && hasDateSpecific) continue; - final exercises = trainingExercises[dateString] as List? ?? []; - final totalExerciseDuration = exercises.fold(0, (sum, exercise) { - if (exercise is Map) { - return sum + (exercise['duration'] as int? ?? 0); - } - return sum; - }); + + // Prüfe, ob das Training für dieses Datum gecancelt ist + final isCancelled = cancelledDates.any((cancelledDate) => + isSameDay(cancelledDate, normalizedDate) + ); + + // Prüfe, ob bereits ein spezifisches Training für dieses Datum existiert + final hasSpecificTraining = trainings.containsKey(dateString); + + if (!isCancelled && !hasSpecificTraining) { final event = { 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'time': timeStr, @@ -172,9 +231,10 @@ class _CalendarTabState extends State { 'isCurrentUser': trainerDoc.id == _currentUserId, 'day': day, 'date': dateString, - 'exercises': exercises, - 'remainingTime': duration - totalExerciseDuration, + 'exercises': [], + 'remainingTime': duration, 'club': trainerData['club'] ?? 'Kein Verein', + 'id': 'weekly_${day}_${dateString}', }; if (events.containsKey(normalizedDate)) { events[normalizedDate]!.add(event); @@ -182,47 +242,7 @@ class _CalendarTabState extends State { events[normalizedDate] = [event]; } } - } - }); - - // 2. Datumsspezifische Trainings (NEU) - trainingTimes.forEach((key, timeStr) { - // Prüfe, ob der Key ein Datum ist - final date = DateTime.tryParse(key); - if (date == null) return; - if (timeStr == null) return; - final duration = trainingDurations[key] as int? ?? 60; - final timeParts = (timeStr as String).split(':'); - if (timeParts.length != 2) return; - final hour = int.tryParse(timeParts[0]) ?? 0; - final minute = int.tryParse(timeParts[1]) ?? 0; - final normalizedDate = DateTime(date.year, date.month, date.day); - final dateString = normalizedDate.toIso8601String(); - // Für datumsspezifische Trainings: cancelledTrainings ignorieren! - // Event immer erzeugen - final exercises = trainingExercises[dateString] as List? ?? []; - final totalExerciseDuration = exercises.fold(0, (sum, exercise) { - if (exercise is Map) { - return sum + (exercise['duration'] as int? ?? 0); - } - return sum; - }); - final event = { - 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', - 'time': timeStr, - 'duration': duration, - 'trainerId': trainerDoc.id, - 'isCurrentUser': trainerDoc.id == _currentUserId, - 'day': key, - 'date': dateString, - 'exercises': exercises, - 'remainingTime': duration - totalExerciseDuration, - 'club': trainerData['club'] ?? 'Kein Verein', - }; - if (events.containsKey(normalizedDate)) { - events[normalizedDate]!.add(event); - } else { - events[normalizedDate] = [event]; + firstDate = firstDate.add(const Duration(days: 7)); } }); } @@ -300,49 +320,106 @@ class _CalendarTabState extends State { Future _deleteTraining(Map event) async { if (_userRole != 'trainer' || !event['isCurrentUser']) return; - try { - final userDoc = await FirebaseFirestore.instance - .collection('User') - .doc(_currentUserId) - .get(); - + final userDoc = await FirebaseFirestore.instance.collection('User').doc(_currentUserId).get(); if (!userDoc.exists) return; - final data = userDoc.data() as Map; + final trainings = Map.from(data['trainings'] ?? {}); + final dateString = event['date'] as String; + final trainingId = event['id'] as String?; final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); - final trainingTimes = Map.from(data['trainingTimes'] ?? {}); - final trainingDurations = Map.from(data['trainingDurations'] ?? {}); - final trainingExercises = Map.from(data['trainingExercises'] ?? {}); - // Stelle sicher, dass das Datum im richtigen Format gespeichert wird - final date = DateTime.parse(event['date']); - final normalizedDateString = DateTime(date.year, date.month, date.day).toIso8601String(); + print('Lösche Training:'); + print('dateString: $dateString'); + print('trainingId: $trainingId'); + print('Vorher - trainings: $trainings'); + print('Vorher - cancelledTrainings: $cancelledTrainings'); - // Entferne ALLE Einträge für das Datum - trainingTimes.remove(normalizedDateString); - trainingDurations.remove(normalizedDateString); - trainingExercises.remove(normalizedDateString); + // Wenn es sich um ein regelmäßiges Training handelt (ID beginnt mit 'weekly_') + if (trainingId != null && trainingId.startsWith('weekly_')) { + // Füge das Datum zu cancelledTrainings hinzu, wenn es noch nicht existiert + if (!cancelledTrainings.any((cancelled) => + cancelled is Map && + cancelled['date'] == dateString + )) { + cancelledTrainings.add({ + 'date': dateString, + }); + } + } else { + // Für spezifische Trainings: Entferne das Training aus der Liste + if (trainings.containsKey(dateString)) { + final trainingsList = List>.from(trainings[dateString]); + trainingsList.removeWhere((t) => t['id'] == trainingId); + + if (trainingsList.isEmpty) { + trainings.remove(dateString); + } else { + trainings[dateString] = trainingsList; + } + } - cancelledTrainings.add({ - 'date': normalizedDateString, - 'day': event['day'], - 'time': event['time'], - 'duration': event['duration'], + // Entferne den Eintrag aus cancelledTrainings nur, wenn es sich um ein spezifisches Training handelt + // und kein regelmäßiges Training an diesem Tag stattfindet + final weekdays = { + 'Montag': 1, + 'Dienstag': 2, + 'Mittwoch': 3, + 'Donnerstag': 4, + 'Freitag': 5, + 'Samstag': 6, + 'Sonntag': 7, + }; + + final date = DateTime.parse(dateString); + final weekday = date.weekday; + final weekdayName = weekdays.entries.firstWhere((entry) => entry.value == weekday).key; + final trainingTimes = data['trainingTimes'] as Map? ?? {}; + + // Wenn an diesem Tag kein regelmäßiges Training stattfindet, entferne den Eintrag aus cancelledTrainings + if (!trainingTimes.containsKey(weekdayName)) { + cancelledTrainings.removeWhere((cancelled) => + cancelled is Map && + cancelled.containsKey('date') && + cancelled['date'] == dateString + ); + } + } + + print('Nachher - trainings: $trainings'); + print('Nachher - cancelledTrainings: $cancelledTrainings'); + + // Aktualisiere die Datenbank + final updates = {}; + + // Aktualisiere trainings nur, wenn es nicht leer ist + if (trainings.isNotEmpty) { + updates['trainings'] = trainings; + } else { + updates['trainings'] = null; + } + + // Aktualisiere cancelledTrainings nur, wenn es nicht leer ist + if (cancelledTrainings.isNotEmpty) { + updates['cancelledTrainings'] = cancelledTrainings; + } else { + updates['cancelledTrainings'] = null; + } + + // Führe die Aktualisierung durch + await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update(updates); + + // Aktualisiere die UI sofort + setState(() { + final normalizedDate = DateTime.parse(dateString); + if (_events.containsKey(normalizedDate)) { + _events[normalizedDate]!.removeWhere((e) => e['id'] == trainingId); + if (_events[normalizedDate]!.isEmpty) { + _events.remove(normalizedDate); + } + } }); - await FirebaseFirestore.instance - .collection('User') - .doc(_currentUserId) - .update({ - 'cancelledTrainings': cancelledTrainings, - 'trainingTimes': trainingTimes, - 'trainingDurations': trainingDurations, - 'trainingExercises': trainingExercises, - }); - - await _loadEvents(); - if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Training wurde gelöscht')), @@ -590,10 +667,17 @@ class _CalendarTabState extends State { date: _selectedDay!, initialTime: event['time'], initialDuration: event['duration'], + trainingId: event['id'], ), ); if (result != null) { - await _addOrEditTraining(_selectedDay!, result['time'], result['duration'], isException: true); + await _addOrEditTraining( + _selectedDay!, + result['time'], + result['duration'], + isException: true, + trainingId: result['trainingId'], + ); await _loadEvents(); } }, @@ -736,52 +820,124 @@ class _CalendarTabState extends State { ); } - Future _addOrEditTraining(DateTime date, String time, int duration, {bool isException = false}) async { - // Trainingsdatum als String + Future _addOrEditTraining(DateTime date, String time, int duration, {bool isException = false, String? trainingId}) async { final dateString = DateTime(date.year, date.month, date.day).toIso8601String(); final userDoc = await FirebaseFirestore.instance.collection('User').doc(_currentUserId).get(); final data = userDoc.data() ?? {}; - final trainingExercises = Map.from(data['trainingExercises'] ?? {}); - final trainingTimes = Map.from(data['trainingTimes'] ?? {}); - final trainingDurations = Map.from(data['trainingDurations'] ?? {}); - - // Entferne nur das Training für das konkrete Datum - trainingExercises.remove(dateString); - trainingTimes.remove(dateString); - trainingDurations.remove(dateString); - - // Neues Training speichern (nur mit Datum als Key) - trainingExercises[dateString] = []; - trainingTimes[dateString] = time; - trainingDurations[dateString] = duration; - - // cancelledTrainings bereinigen und ggf. Eintrag hinzufügen, wenn an diesem Tag ein wochentagsbasiertes Training existiert + final trainings = Map.from(data['trainings'] ?? {}); + final uuid = const Uuid(); final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); - cancelledTrainings.removeWhere((cancelled) => - cancelled is Map && - cancelled.containsKey('date') && - cancelled['date'] == dateString - ); + final trainingTimes = data['trainingTimes'] as Map? ?? {}; - // Prüfe, ob für diesen Tag ein wochentagsbasiertes Training existiert und ob es eine Ausnahme ist - final weekdayNames = [ - 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' - ]; - final weekdayKey = weekdayNames[date.weekday - 1]; - if (isException && trainingTimes.containsKey(weekdayKey)) { - cancelledTrainings.add({ - 'date': dateString, - 'day': weekdayKey, - 'time': trainingTimes[weekdayKey], - 'duration': trainingDurations[weekdayKey] ?? 60, + print('Füge/Bearbeite Training:'); + print('dateString: $dateString'); + print('trainingId: $trainingId'); + print('Vorher - trainings: $trainings'); + print('Vorher - cancelledTrainings: $cancelledTrainings'); + + // Trainingsliste für das Datum holen oder neu anlegen + final trainingsList = List>.from(trainings[dateString] ?? []); + + if (trainingId != null) { + // Bearbeiten: Training mit ID ersetzen + final idx = trainingsList.indexWhere((t) => t['id'] == trainingId); + if (idx != -1) { + trainingsList[idx] = { + 'id': trainingId, + 'time': time, + 'duration': duration, + 'exercises': trainingsList[idx]['exercises'] ?? [], + }; + } + } else { + // Hinzufügen: neues Training-Objekt anfügen + trainingsList.add({ + 'id': uuid.v4(), + 'time': time, + 'duration': duration, + 'exercises': [], }); } - await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update({ - 'trainingExercises': trainingExercises, - 'trainingTimes': trainingTimes, - 'trainingDurations': trainingDurations, - 'cancelledTrainings': cancelledTrainings, + trainings[dateString] = trainingsList; + + // Prüfe, ob an diesem Tag ein regelmäßiges Training stattfindet + final weekdays = { + 'Montag': 1, + 'Dienstag': 2, + 'Mittwoch': 3, + 'Donnerstag': 4, + 'Freitag': 5, + 'Samstag': 6, + 'Sonntag': 7, + }; + + final weekday = date.weekday; + final weekdayName = weekdays.entries.firstWhere((entry) => entry.value == weekday).key; + + // Wenn an diesem Tag ein regelmäßiges Training stattfindet, füge es zu cancelledTrainings hinzu + if (trainingTimes.containsKey(weekdayName)) { + if (!cancelledTrainings.any((cancelled) => + cancelled is Map && + cancelled['date'] == dateString + )) { + cancelledTrainings.add({ + 'date': dateString, + }); + } + } else { + // Wenn kein regelmäßiges Training stattfindet, entferne den Eintrag aus cancelledTrainings + cancelledTrainings.removeWhere((cancelled) => + cancelled is Map && + cancelled.containsKey('date') && + cancelled['date'] == dateString + ); + } + + print('Nachher - trainings: $trainings'); + print('Nachher - cancelledTrainings: $cancelledTrainings'); + + // Aktualisiere die Datenbank + final updates = {}; + + // Aktualisiere trainings nur, wenn es nicht leer ist + if (trainings.isNotEmpty) { + updates['trainings'] = trainings; + } else { + updates['trainings'] = null; + } + + // Aktualisiere cancelledTrainings nur, wenn es nicht leer ist + if (cancelledTrainings.isNotEmpty) { + updates['cancelledTrainings'] = cancelledTrainings; + } else { + updates['cancelledTrainings'] = null; + } + + // Führe die Aktualisierung durch + await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update(updates); + + // Aktualisiere die UI sofort + setState(() { + final normalizedDate = DateTime(date.year, date.month, date.day); + final event = { + 'trainerName': data['name'] ?? 'Unbekannter Trainer', + 'time': time, + 'duration': duration, + 'trainerId': _currentUserId, + 'isCurrentUser': true, + 'day': dateString, + 'date': dateString, + 'exercises': [], + 'remainingTime': duration, + 'club': data['club'] ?? 'Kein Verein', + 'id': trainingId ?? uuid.v4(), + }; + if (_events.containsKey(normalizedDate)) { + _events[normalizedDate]!.add(event); + } else { + _events[normalizedDate] = [event]; + } }); } @@ -791,7 +947,13 @@ class _TrainingEditDialog extends StatefulWidget { final DateTime date; final String? initialTime; final int? initialDuration; - const _TrainingEditDialog({required this.date, this.initialTime, this.initialDuration}); + final String? trainingId; + const _TrainingEditDialog({ + required this.date, + this.initialTime, + this.initialDuration, + this.trainingId, + }); @override State<_TrainingEditDialog> createState() => _TrainingEditDialogState(); } @@ -863,7 +1025,11 @@ class _TrainingEditDialogState extends State<_TrainingEditDialog> { onPressed: _selectedTime != null ? () { final timeString = _selectedTime!.hour.toString().padLeft(2, '0') + ':' + _selectedTime!.minute.toString().padLeft(2, '0'); - Navigator.pop(context, {'time': timeString, 'duration': _duration}); + Navigator.pop(context, { + 'time': timeString, + 'duration': _duration, + 'trainingId': widget.trainingId, + }); } : null, child: const Text('Speichern'), diff --git a/trainerbox/pubspec.lock b/trainerbox/pubspec.lock index 301671d..a5de009 100644 --- a/trainerbox/pubspec.lock +++ b/trainerbox/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -201,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.7" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -437,6 +453,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -493,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: diff --git a/trainerbox/pubspec.yaml b/trainerbox/pubspec.yaml index a1e2acf..3bc135f 100644 --- a/trainerbox/pubspec.yaml +++ b/trainerbox/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: firebase_storage: ^11.5.6 image_picker: ^1.0.7 provider: ^6.1.1 + uuid: ^4.2.1 dev_dependencies: flutter_test: