diff --git a/trainerbox/lib/screens/calendar_tab.dart b/trainerbox/lib/screens/calendar_tab.dart index b307e69..df5133e 100644 --- a/trainerbox/lib/screens/calendar_tab.dart +++ b/trainerbox/lib/screens/calendar_tab.dart @@ -126,13 +126,14 @@ class _CalendarTabState extends State { final cancelledTrainings = trainerData['cancelledTrainings'] as List? ?? []; final trainingExercises = trainerData['trainingExercises'] as Map? ?? {}; + // 1. Wochentagsbasierte Trainings (wie gehabt) 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; @@ -144,7 +145,6 @@ class _CalendarTabState extends State { final date = eventDate.add(Duration(days: i * 7)); final normalizedDate = DateTime(date.year, date.month, date.day); final dateString = normalizedDate.toIso8601String(); - final isCancelled = cancelledTrainings.any((cancelled) { if (cancelled is Map) { final cancelledDate = DateTime.parse(cancelled['date'] as String); @@ -152,8 +152,11 @@ class _CalendarTabState extends State { } return false; }); - - if (!isCancelled) { + // 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) { @@ -161,7 +164,6 @@ class _CalendarTabState extends State { } return sum; }); - final event = { 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'time': timeStr, @@ -174,7 +176,6 @@ class _CalendarTabState extends State { 'remainingTime': duration - totalExerciseDuration, 'club': trainerData['club'] ?? 'Kein Verein', }; - if (events.containsKey(normalizedDate)) { events[normalizedDate]!.add(event); } else { @@ -183,6 +184,47 @@ class _CalendarTabState extends State { } } }); + + // 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]; + } + }); } setState(() { @@ -269,9 +311,21 @@ class _CalendarTabState extends State { final data = userDoc.data() as Map; 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(); + + // Entferne ALLE Einträge für das Datum + trainingTimes.remove(normalizedDateString); + trainingDurations.remove(normalizedDateString); + trainingExercises.remove(normalizedDateString); cancelledTrainings.add({ - 'date': event['date'], + 'date': normalizedDateString, 'day': event['day'], 'time': event['time'], 'duration': event['duration'], @@ -282,6 +336,9 @@ class _CalendarTabState extends State { .doc(_currentUserId) .update({ 'cancelledTrainings': cancelledTrainings, + 'trainingTimes': trainingTimes, + 'trainingDurations': trainingDurations, + 'trainingExercises': trainingExercises, }); await _loadEvents(); @@ -371,6 +428,7 @@ class _CalendarTabState extends State { @override Widget build(BuildContext context) { + final isTrainer = _userRole == 'trainer'; return Scaffold( appBar: AppBar( title: const Text('Kalender'), @@ -449,203 +507,368 @@ class _CalendarTabState extends State { }, ), ), + if (isTrainer && _selectedDay != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.add), + label: const Text('Training an diesem Tag hinzufügen'), + onPressed: () async { + final result = await showDialog>( + context: context, + builder: (context) => _TrainingEditDialog( + date: _selectedDay!, + ), + ); + if (result != null) { + await _addOrEditTraining(_selectedDay!, result['time'], result['duration'], isException: false); + await _loadEvents(); + } + }, + ), + ), + ), + ], const Divider(), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _selectedDay == null ? const Center(child: Text('Bitte wähle einen Tag aus')) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _getEventsForDay(_selectedDay!).length, - itemBuilder: (context, index) { - final event = _getEventsForDay(_selectedDay!)[index]; - final isCurrentUser = event['isCurrentUser'] as bool; - final exercises = event['exercises'] as List; - final remainingTime = event['remainingTime'] as int; + : (() { + final events = _getEventsForDay(_selectedDay!); + // Sortiere nach Uhrzeit (Format: 'HH:mm') + events.sort((a, b) { + final aTime = a['time'] as String? ?? '00:00'; + final bTime = b['time'] as String? ?? '00:00'; + final aParts = aTime.split(':').map(int.parse).toList(); + final bParts = bTime.split(':').map(int.parse).toList(); + final aMinutes = aParts[0] * 60 + aParts[1]; + final bMinutes = bParts[0] * 60 + bParts[1]; + return aMinutes.compareTo(bMinutes); + }); + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: events.length, + itemBuilder: (context, index) { + final event = events[index]; + final isCurrentUser = event['isCurrentUser'] as bool; + final exercises = event['exercises'] as List; + final remainingTime = event['remainingTime'] as int; - return Card( - margin: const EdgeInsets.only(bottom: 8), - color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null, - child: Column( - children: [ - ListTile( - leading: Icon( - Icons.sports, - color: categoryColors[event['day']] ?? (isCurrentUser ? Colors.blue : Colors.grey), - ), - title: Text( - isCurrentUser ? 'Training' : event['trainerName'], - style: TextStyle( - fontWeight: isCurrentUser ? FontWeight.bold : null, + return Card( + margin: const EdgeInsets.only(bottom: 8), + color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null, + child: Column( + children: [ + ListTile( + leading: Icon( + Icons.sports, + color: categoryColors[event['day']] ?? (isCurrentUser ? Colors.blue : Colors.grey), ), - ), - subtitle: Text( - '${event['time']} - ${event['duration']} Minuten\nVerbleibende Zeit: $remainingTime Minuten', - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isCurrentUser) - IconButton( - icon: const Icon(Icons.add), - onPressed: () => _addExercise(event), - ), - IconButton( - icon: const Icon(Icons.info), - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(isCurrentUser ? 'Training' : event['trainerName']), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Zeit: ${event['time']}'), - const SizedBox(height: 8), - Text('Dauer: ${event['duration']} Minuten'), - const SizedBox(height: 8), - Text('Verbleibende Zeit: $remainingTime Minuten'), - if (exercises.isNotEmpty) ...[ - const SizedBox(height: 16), - const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - ...exercises.map((exercise) { - if (exercise is Map) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - '${exercise['name']} - ${exercise['duration']} Minuten', - ), - ); - } - return const SizedBox.shrink(); - }), - ], - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Schließen'), - ), - ], - ), - ); - }, + title: Text( + isCurrentUser ? 'Training' : event['trainerName'], + style: TextStyle( + fontWeight: isCurrentUser ? FontWeight.bold : null, ), - if (isCurrentUser) + ), + subtitle: Text( + '${event['time']} - ${event['duration']} Minuten\nVerbleibende Zeit: $remainingTime Minuten', + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isCurrentUser) + IconButton( + icon: const Icon(Icons.edit), + onPressed: () async { + final result = await showDialog>( + context: context, + builder: (context) => _TrainingEditDialog( + date: _selectedDay!, + initialTime: event['time'], + initialDuration: event['duration'], + ), + ); + if (result != null) { + await _addOrEditTraining(_selectedDay!, result['time'], result['duration'], isException: true); + await _loadEvents(); + } + }, + ), + if (isCurrentUser) + IconButton( + icon: const Icon(Icons.add), + onPressed: () => _addExercise(event), + ), IconButton( - icon: const Icon(Icons.delete, color: Colors.red), + icon: const Icon(Icons.info), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Training löschen'), - content: const Text('Möchten Sie dieses Training wirklich löschen?'), + title: Text(isCurrentUser ? 'Training' : event['trainerName']), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Zeit: ${event['time']}'), + const SizedBox(height: 8), + Text('Dauer: ${event['duration']} Minuten'), + const SizedBox(height: 8), + Text('Verbleibende Zeit: $remainingTime Minuten'), + if (exercises.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...exercises.map((exercise) { + if (exercise is Map) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + '${exercise['name']} - ${exercise['duration']} Minuten', + ), + ); + } + return const SizedBox.shrink(); + }), + ], + ], + ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Abbrechen'), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - _deleteTraining(event); - }, - child: const Text('Löschen', style: TextStyle(color: Colors.red)), + child: const Text('Schließen'), ), ], ), ); }, ), - ], - ), - ), - if (exercises.isNotEmpty) - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(), - const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - ...exercises.map((exercise) { - if (exercise is Map) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - const Icon(Icons.fitness_center, size: 16), - const SizedBox(width: 8), - Expanded( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TrainingDetailScreen( - trainingId: exercise['id'], - ), - ), - ); - }, - child: Text( - '${exercise['name']} - ${exercise['duration']} Minuten', - style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blue, - ), - ), + if (isCurrentUser) + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Training löschen'), + content: const Text('Möchten Sie dieses Training wirklich löschen?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Abbrechen'), ), - ), - if (isCurrentUser) - IconButton( - icon: const Icon(Icons.delete, color: Colors.red, size: 20), + TextButton( onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Übung entfernen'), - content: const Text('Möchten Sie diese Übung wirklich entfernen?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Abbrechen'), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - _removeExercise(event, exercise); - }, - child: const Text('Entfernen', style: TextStyle(color: Colors.red)), - ), - ], - ), - ); + Navigator.pop(context); + _deleteTraining(event); }, + child: const Text('Löschen', style: TextStyle(color: Colors.red)), ), - ], - ), - ); - } - return const SizedBox.shrink(); - }), + ], + ), + ); + }, + ), ], ), ), - ], - ), - ); - }, - ), + if (exercises.isNotEmpty) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...exercises.map((exercise) { + if (exercise is Map) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + const Icon(Icons.fitness_center, size: 16), + const SizedBox(width: 8), + Expanded( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TrainingDetailScreen( + trainingId: exercise['id'], + ), + ), + ); + }, + child: Text( + '${exercise['name']} - ${exercise['duration']} Minuten', + style: const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), + ), + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }), + ], + ), + ), + ], + ), + ); + }, + ); + })() ), ], ), ), ); } + + Future _addOrEditTraining(DateTime date, String time, int duration, {bool isException = false}) async { + // Trainingsdatum als String + 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 cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); + cancelledTrainings.removeWhere((cancelled) => + cancelled is Map && + cancelled.containsKey('date') && + cancelled['date'] == dateString + ); + + // 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, + }); + } + + await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update({ + 'trainingExercises': trainingExercises, + 'trainingTimes': trainingTimes, + 'trainingDurations': trainingDurations, + 'cancelledTrainings': cancelledTrainings, + }); + } + +} + +class _TrainingEditDialog extends StatefulWidget { + final DateTime date; + final String? initialTime; + final int? initialDuration; + const _TrainingEditDialog({required this.date, this.initialTime, this.initialDuration}); + @override + State<_TrainingEditDialog> createState() => _TrainingEditDialogState(); +} + +class _TrainingEditDialogState extends State<_TrainingEditDialog> { + TimeOfDay? _selectedTime; + int _duration = 60; + + @override + void initState() { + super.initState(); + if (widget.initialTime != null) { + final parts = widget.initialTime!.split(':'); + if (parts.length == 2) { + _selectedTime = TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])); + } + } + if (widget.initialDuration != null) { + _duration = widget.initialDuration!; + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Training bearbeiten (${widget.date.day}.${widget.date.month}.${widget.date.year})'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.access_time), + title: const Text('Uhrzeit wählen'), + subtitle: Text(_selectedTime != null ? _selectedTime!.format(context) : 'Keine Uhrzeit gewählt'), + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? TimeOfDay.now(), + ); + if (picked != null) { + setState(() => _selectedTime = picked); + } + }, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + if (_duration > 15) setState(() => _duration -= 15); + }, + ), + Text('$_duration Minuten', style: const TextStyle(fontSize: 18)), + IconButton( + icon: const Icon(Icons.add), + onPressed: () => setState(() => _duration += 15), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Abbrechen'), + ), + ElevatedButton( + onPressed: _selectedTime != null + ? () { + final timeString = _selectedTime!.hour.toString().padLeft(2, '0') + ':' + _selectedTime!.minute.toString().padLeft(2, '0'); + Navigator.pop(context, {'time': timeString, 'duration': _duration}); + } + : null, + child: const Text('Speichern'), + ), + ], + ); + } } diff --git a/trainerbox/lib/screens/profile_tab.dart b/trainerbox/lib/screens/profile_tab.dart index 2271652..306c570 100644 --- a/trainerbox/lib/screens/profile_tab.dart +++ b/trainerbox/lib/screens/profile_tab.dart @@ -89,11 +89,65 @@ class _ProfileTabState extends State { final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + // Update training times and durations await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ 'trainingTimes.$day': timeString, 'trainingDurations.$day': duration, }); + // Trainings ab 1. Januar des aktuellen Jahres bis 52 Wochen in die Zukunft anlegen + final now = DateTime.now(); + final yearStart = DateTime(now.year, 1, 1); + final weekdays = { + 'Montag': 1, + 'Dienstag': 2, + 'Mittwoch': 3, + 'Donnerstag': 4, + 'Freitag': 5, + 'Samstag': 6, + 'Sonntag': 7, + }; + final targetWeekday = weekdays[day] ?? 1; + + // Finde das erste gewünschte Wochentags-Datum ab Jahresanfang + DateTime firstDate = yearStart; + while (firstDate.weekday != targetWeekday) { + firstDate = firstDate.add(const Duration(days: 1)); + } + + final newTrainingDates = []; + for (var i = 0; i < 52; i++) { + final date = firstDate.add(Duration(days: i * 7)); + final normalizedDate = DateTime(date.year, date.month, date.day, time.hour, time.minute); + final dateString = DateTime(normalizedDate.year, normalizedDate.month, normalizedDate.day).toIso8601String(); + newTrainingDates.add(dateString); + } + + // Load existing trainingExercises and cancelledTrainings + final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); + final data = userDoc.data() ?? {}; + final trainingExercises = Map.from(data['trainingExercises'] ?? {}); + final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); + + // Add empty training only if not already present + for (final dateString in newTrainingDates) { + if (!trainingExercises.containsKey(dateString)) { + trainingExercises[dateString] = []; + } + } + + // Remove cancelledTrainings for these dates + cancelledTrainings.removeWhere((cancelled) => + cancelled is Map && + cancelled.containsKey('date') && + newTrainingDates.contains(cancelled['date']) + ); + + await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ + 'cancelledTrainings': cancelledTrainings, + 'trainingExercises': trainingExercises, + }); + setState(() { _trainingTimes[day] = time; _trainingDurations[day] = duration;