// calendar_tab.dart // This file contains the CalendarTab widget, which displays a calendar view of trainings and allows users to view, add, and manage training events. import 'package:flutter/material.dart'; 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'; /// Mapping of training categories to colors for calendar display. const Map categoryColors = { 'Aufwärmen & Mobilisation': Colors.deepOrange, 'Wurf- & Torabschluss': Colors.orange, 'Torwarttraining': Colors.green, 'Athletik': Colors.blue, 'Pass': Colors.purple, 'Koordination': Colors.teal, }; /// The CalendarTab displays a calendar with all training events for the user. class CalendarTab extends StatefulWidget { final DateTime? initialDate; const CalendarTab({super.key, this.initialDate}); @override State createState() => _CalendarTabState(); } /// State for the CalendarTab, manages event loading, calendar state, and user interactions. class _CalendarTabState extends State { CalendarFormat _calendarFormat = CalendarFormat.week; late DateTime _focusedDay; DateTime? _selectedDay; Map>> _events = {}; bool _isLoading = false; String? _currentUserId; String? _userRole; final _exerciseController = TextEditingController(); final _durationController = TextEditingController(); @override void dispose() { _exerciseController.dispose(); _durationController.dispose(); super.dispose(); } @override void initState() { super.initState(); _focusedDay = widget.initialDate ?? DateTime.now(); _selectedDay = _focusedDay; _currentUserId = FirebaseAuth.instance.currentUser?.uid; _initializeData(); } @override void didUpdateWidget(covariant CalendarTab oldWidget) { super.didUpdateWidget(oldWidget); if (widget.initialDate != null && widget.initialDate != oldWidget.initialDate) { setState(() { _focusedDay = widget.initialDate!; _selectedDay = widget.initialDate!; }); } } /// Initializes user data and loads events for the calendar. Future _initializeData() async { if (_currentUserId != null) { final userDoc = await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .get(); if (userDoc.exists) { setState(() { _userRole = userDoc.data()?['role'] as String?; }); await _loadEvents(); } } } /// Loads all training events for the current user (trainer or player). Future _loadEvents() async { if (_userRole == null) return; setState(() => _isLoading = true); try { QuerySnapshot trainersSnapshot; if (_userRole == 'trainer') { // Trainer: only their own trainings trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where(FieldPath.documentId, isEqualTo: _currentUserId) .get(); } else { // Player: trainings from trainers in their club final userDoc = await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .get(); if (!userDoc.exists) { setState(() => _isLoading = false); return; } final userData = userDoc.data() as Map; final userClub = userData['club'] as String?; if (userClub == null || userClub.isEmpty) { setState(() { _events = {}; _isLoading = false; }); return; } trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where('club', isEqualTo: userClub) .get(); } final events = >>{}; 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? ?? {}; // Load all specific trainings for each date 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; }); // Build the event map for this training 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]; } } }); // Add recurring weekly trainings for the year 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, }; // Create a set of cancelled dates for fast lookup final cancelledDates = cancelledTrainings .where((cancelled) => cancelled is Map) .map((cancelled) => DateTime.parse((cancelled as Map)['date'] as String)) .toSet(); trainingTimes.forEach((day, timeStr) { if (timeStr == null) return; 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; // Find the first desired weekday date from the start of the year DateTime firstDate = yearStart; while (firstDate.weekday != targetWeekday) { firstDate = firstDate.add(const Duration(days: 1)); } // Generate weekly trainings for the whole year while (firstDate.isBefore(yearEnd)) { final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day); final dateString = normalizedDate.toIso8601String(); // Check if the training for this date is cancelled final isCancelled = cancelledDates.any((cancelledDate) => isSameDay(cancelledDate, normalizedDate) ); // Check if a specific training already exists for this date final hasSpecificTraining = trainings.containsKey(dateString); if (!isCancelled && !hasSpecificTraining) { final event = { 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'time': timeStr, 'duration': duration, 'trainerId': trainerDoc.id, 'isCurrentUser': trainerDoc.id == _currentUserId, 'day': day, 'date': dateString, 'exercises': [], 'remainingTime': duration, 'club': trainerData['club'] ?? 'Kein Verein', 'id': 'weekly_${day}_${dateString}', }; if (events.containsKey(normalizedDate)) { events[normalizedDate]!.add(event); } else { events[normalizedDate] = [event]; } } firstDate = firstDate.add(const Duration(days: 7)); } }); } setState(() { _events = events; _isLoading = false; }); } catch (e) { setState(() => _isLoading = false); } } Future _addExercise(Map event) async { if (_userRole != 'trainer' || !event['isCurrentUser']) return; // Navigiere zum Suchbildschirm und warte auf das Ergebnis final result = await Navigator.pushNamed( context, '/search', arguments: { 'selectMode': true, 'remainingTime': event['remainingTime'], }, ); // Wenn eine Übung ausgewählt wurde if (result != null && result is Map) { try { bool shouldAddExercise = true; // Prüfe die Bewertungen der Übung in der Training-Collection final exerciseDoc = await FirebaseFirestore.instance .collection('Training') .doc(result['id']) .get(); // NEU: Prüfe, ob die Übung in den letzten 4 Wochen schon einmal durchgeführt wurde final userDocForCheck = await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .get(); if (userDocForCheck.exists) { final userData = userDocForCheck.data() as Map; final trainings = Map.from(userData['trainings'] ?? {}); final trainingDate = DateTime.tryParse(event['date'] as String) ?? DateTime.now(); final fourWeeksAgo = trainingDate.subtract(const Duration(days: 28)); bool foundInLast4Weeks = false; trainings.forEach((dateString, trainingsList) { final date = DateTime.tryParse(dateString); // Prüfe: im Zeitraum [fourWeeksAgo, trainingDate) (also vor dem aktuellen Training) if (date != null && date.isAfter(fourWeeksAgo) && date.isBefore(trainingDate)) { final list = List>.from(trainingsList); for (final training in list) { final exercises = List>.from(training['exercises'] ?? []); for (final exercise in exercises) { if (exercise['id'] == result['id']) { foundInLast4Weeks = true; } } } } }); if (foundInLast4Weeks) { shouldAddExercise = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('Übung kürzlich verwendet'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Die Übung "${result['title']}" wurde in den 4 Wochen vor diesem Training bereits durchgeführt.'), const SizedBox(height: 16), const Text('Möchten Sie diese Übung trotzdem zu Ihrem Training hinzufügen?'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Abbrechen'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('Trotzdem hinzufügen'), ), ], ), ) ?? false; if (!shouldAddExercise) { return; } } } if (exerciseDoc.exists) { final exerciseData = exerciseDoc.data() as Map; // Versuche die Bewertungen aus verschiedenen möglichen Feldern zu lesen List> ratings = []; if (exerciseData.containsKey('ratings')) { ratings = List>.from(exerciseData['ratings'] ?? []); } else if (exerciseData.containsKey('rating')) { ratings = List>.from(exerciseData['rating'] ?? []); } if (ratings.isNotEmpty) { // Berechne den Durchschnitt der Bewertungen double totalRating = 0; for (var rating in ratings) { if (rating.containsKey('rating')) { totalRating += (rating['rating'] as num).toDouble(); } } final averageRating = totalRating / ratings.length; // Wenn die durchschnittliche Bewertung unter 3 liegt, zeige eine Warnung if (averageRating < 3) { if (mounted) { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('Hinweis zur Übungsbewertung'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Die Übung "${result['title']}" wurde von anderen Trainern mit ${averageRating.toStringAsFixed(1)} von 5 Sternen bewertet.'), const SizedBox(height: 16), const Text('Möchten Sie diese Übung trotzdem zu Ihrem Training hinzufügen?'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Abbrechen'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('Trotzdem hinzufügen'), ), ], ), ); //?? false; } } } } if (!shouldAddExercise) { return; } // Füge die Übung zum Training hinzu 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 newExerciseData = { 'id': result['id'], 'name': result['title'], 'description': result['description'], 'duration': result['duration'], }; final trainingsForDay = List>.from(trainings[dateString] ?? []); final trainingIndex = trainingsForDay.indexWhere((t) => t['id'] == trainingId); String finalTrainingId = trainingId; if (trainingIndex != -1) { // Fall 1: Training ist bereits spezifisch, füge die Übung hinzu final exercises = List>.from(trainingsForDay[trainingIndex]['exercises'] ?? []); exercises.add(newExerciseData); trainingsForDay[trainingIndex]['exercises'] = exercises; } else { // Fall 2: Training ist wiederkehrend, erstelle eine neue spezifische Instanz final newId = const Uuid().v4(); finalTrainingId = newId; trainingsForDay.add({ 'id': newId, 'time': event['time'], 'duration': event['duration'], 'exercises': [newExerciseData], }); } trainings[dateString] = trainingsForDay; final updates = {'trainings': trainings}; // Wenn das Original-Event wiederkehrend war, unterdrücke es für diesen Tag if (trainingId.startsWith('weekly_')) { final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); if (!cancelledTrainings.any((c) => c['date'] == dateString)) { cancelledTrainings.add({'date': dateString}); updates['cancelledTrainings'] = cancelledTrainings; } } // Aktualisiere die Datenbank await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .update(updates); // Aktualisiere die UI sofort setState(() { final normalizedDate = DateTime.parse(dateString); if (_events.containsKey(normalizedDate)) { final eventIndex = _events[normalizedDate]!.indexWhere((e) => e['id'] == trainingId); if (eventIndex != -1) { final oldEvent = _events[normalizedDate]![eventIndex]; final exercises = List>.from(oldEvent['exercises'] ?? []); exercises.add(newExerciseData); oldEvent['exercises'] = exercises; oldEvent['remainingTime'] = (oldEvent['duration'] as int) - exercises.fold(0, (sum, exercise) => sum + (exercise['duration'] as int)); if (trainingId.startsWith('weekly_')) { oldEvent['id'] = finalTrainingId; } } } }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Übung wurde hinzugefügt')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fehler beim Hinzufügen der Übung')), ); } } } } Future _deleteTraining(Map event) async { if (_userRole != 'trainer' || !event['isCurrentUser']) return; print('=== DEBUG: Starting _deleteTraining ==='); print('DEBUG: Event to delete: $event'); print('DEBUG: Training ID: ${event['id']}'); print('DEBUG: Date: ${event['date']}'); print('DEBUG: Is weekly training: ${event['id']?.toString().startsWith('weekly_')}'); try { final userDoc = await FirebaseFirestore.instance.collection('User').doc(_currentUserId).get(); if (!userDoc.exists) { print('DEBUG: User document does not exist'); 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'] ?? []); print('DEBUG: Current trainings in DB: $trainings'); print('DEBUG: Current cancelledTrainings in DB: $cancelledTrainings'); // Wenn es sich um ein regelmäßiges Training handelt (ID beginnt mit 'weekly_') if (trainingId != null && trainingId.startsWith('weekly_')) { print('DEBUG: Processing weekly training deletion'); // Für regelmäßige Trainings: Lösche alle Trainings dieser Serie 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; print('DEBUG: Weekday to delete: $weekday'); // Lösche alle Trainings an diesem Wochentag trainings.forEach((dateStr, trainingsList) { final trainingDate = DateTime.tryParse(dateStr); if (trainingDate != null && trainingDate.weekday == weekday) { print('DEBUG: Checking date $dateStr (weekday: ${trainingDate.weekday})'); final list = List>.from(trainingsList); print('DEBUG: Trainings on this date before deletion: $list'); final beforeCount = list.length; list.removeWhere((t) => t['id'] == trainingId); final afterCount = list.length; print('DEBUG: Removed ${beforeCount - afterCount} trainings with ID $trainingId'); if (list.isEmpty) { trainings.remove(dateStr); print('DEBUG: Removed empty date $dateStr from trainings'); } else { trainings[dateStr] = list; print('DEBUG: Updated trainings for date $dateStr: $list'); } } }); // Entferne alle cancelledTrainings für diesen Wochentag final beforeCancelledCount = cancelledTrainings.length; cancelledTrainings.removeWhere((cancelled) => cancelled.containsKey('date') && DateTime.parse(cancelled['date'] as String).weekday == weekday ); final afterCancelledCount = cancelledTrainings.length; print('DEBUG: Removed ${beforeCancelledCount - afterCancelledCount} cancelled trainings for weekday $weekday'); } else { print('DEBUG: Processing specific training deletion'); // Für spezifische Trainings: Entferne nur das Training an diesem Tag if (trainings.containsKey(dateString)) { final trainingsList = List>.from(trainings[dateString]); print('DEBUG: Trainings on date $dateString before deletion: $trainingsList'); final beforeCount = trainingsList.length; trainingsList.removeWhere((t) => t['id'] == trainingId); final afterCount = trainingsList.length; print('DEBUG: Removed ${beforeCount - afterCount} trainings with ID $trainingId'); if (trainingsList.isEmpty) { trainings.remove(dateString); print('DEBUG: Removed empty date $dateString from trainings'); } else { trainings[dateString] = trainingsList; print('DEBUG: Updated trainings for date $dateString: $trainingsList'); } } // 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? ?? {}; print('DEBUG: Weekday name: $weekdayName'); print('DEBUG: Training times: $trainingTimes'); print('DEBUG: Has regular training on this weekday: ${trainingTimes.containsKey(weekdayName)}'); // Wenn an diesem Tag kein regelmäßiges Training stattfindet, entferne den Eintrag aus cancelledTrainings if (!trainingTimes.containsKey(weekdayName)) { final beforeCancelledCount = cancelledTrainings.length; cancelledTrainings.removeWhere((cancelled) => cancelled.containsKey('date') && cancelled['date'] == dateString ); final afterCancelledCount = cancelledTrainings.length; print('DEBUG: Removed ${beforeCancelledCount - afterCancelledCount} cancelled trainings for date $dateString'); } } print('DEBUG: Final trainings after deletion: $trainings'); print('DEBUG: Final cancelledTrainings after deletion: $cancelledTrainings'); // Aktualisiere die Datenbank final updates = {}; // Aktualisiere trainings nur, wenn es nicht leer ist if (trainings.isNotEmpty) { updates['trainings'] = trainings; print('DEBUG: Will update trainings in DB'); } else { updates['trainings'] = null; print('DEBUG: Will set trainings to null in DB'); } // Aktualisiere cancelledTrainings nur, wenn es nicht leer ist if (cancelledTrainings.isNotEmpty) { updates['cancelledTrainings'] = cancelledTrainings; print('DEBUG: Will update cancelledTrainings in DB'); } else { updates['cancelledTrainings'] = null; print('DEBUG: Will set cancelledTrainings to null in DB'); } print('DEBUG: Final updates to DB: $updates'); // Führe die Aktualisierung durch await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update(updates); print('DEBUG: Database update completed successfully'); // Aktualisiere die UI sofort setState(() { print('DEBUG: Updating UI...'); // Für regelmäßige Trainings: Entferne alle Events an diesem Wochentag if (trainingId != null && trainingId.startsWith('weekly_')) { final date = DateTime.parse(dateString); final weekday = date.weekday; print('DEBUG: Removing events from UI for weekday $weekday'); _events.forEach((eventDate, eventList) { if (eventDate.weekday == weekday) { print('DEBUG: Checking event date $eventDate (weekday: ${eventDate.weekday})'); print('DEBUG: Events before removal: $eventList'); final beforeCount = eventList.length; eventList.removeWhere((e) => e['id'] == trainingId); final afterCount = eventList.length; print('DEBUG: Removed ${beforeCount - afterCount} events with ID $trainingId'); print('DEBUG: Events after removal: $eventList'); } }); // Entferne leere Event-Listen final beforeEmptyCount = _events.length; _events.removeWhere((date, events) => events.isEmpty); final afterEmptyCount = _events.length; print('DEBUG: Removed ${beforeEmptyCount - afterEmptyCount} empty event lists'); } else { // Für spezifische Trainings: Entferne nur das Event an diesem Tag final normalizedDate = DateTime.parse(dateString); if (_events.containsKey(normalizedDate)) { print('DEBUG: Removing specific event from UI for date $normalizedDate'); print('DEBUG: Events before removal: ${_events[normalizedDate]}'); final beforeCount = _events[normalizedDate]!.length; _events[normalizedDate]!.removeWhere((e) => e['id'] == trainingId); final afterCount = _events[normalizedDate]!.length; print('DEBUG: Removed ${beforeCount - afterCount} events with ID $trainingId'); print('DEBUG: Events after removal: ${_events[normalizedDate]}'); if (_events[normalizedDate]!.isEmpty) { _events.remove(normalizedDate); print('DEBUG: Removed empty date $normalizedDate from events'); } } } print('DEBUG: UI update completed'); }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Training wurde gelöscht')), ); } print('=== DEBUG: _deleteTraining completed successfully ==='); } catch (e) { print('DEBUG: Error in _deleteTraining: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fehler beim Löschen des Trainings')), ); } } } Future _removeExercise(Map event, Map exercise) async { if (_userRole != 'trainer' || !event['isCurrentUser']) return; try { 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; // Finde das Training und entferne die Übung if (trainings.containsKey(dateString)) { final trainingsList = List>.from(trainings[dateString]); final trainingIndex = trainingsList.indexWhere((t) => t['id'] == trainingId); if (trainingIndex != -1) { final exercises = List>.from(trainingsList[trainingIndex]['exercises'] ?? []); exercises.removeWhere((e) => e['id'] == exercise['id']); trainingsList[trainingIndex]['exercises'] = exercises; trainings[dateString] = trainingsList; } } // Aktualisiere die Datenbank await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .update({ 'trainings': trainings, }); // Aktualisiere die UI sofort setState(() { final normalizedDate = DateTime.parse(dateString); if (_events.containsKey(normalizedDate)) { final eventIndex = _events[normalizedDate]!.indexWhere((e) => e['id'] == trainingId); if (eventIndex != -1) { final exercises = List>.from(_events[normalizedDate]![eventIndex]['exercises'] ?? []); exercises.removeWhere((e) => e['id'] == exercise['id']); _events[normalizedDate]![eventIndex]['exercises'] = exercises; _events[normalizedDate]![eventIndex]['remainingTime'] = (_events[normalizedDate]![eventIndex]['duration'] as int) - exercises.fold(0, (sum, exercise) => sum + (exercise['duration'] as int)); } } }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Übung wurde entfernt')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fehler beim Entfernen der Übung')), ); } } } List> _getEventsForDay(DateTime day) { final normalizedDate = DateTime(day.year, day.month, day.day); return _events[normalizedDate] ?? []; } @override Widget build(BuildContext context) { final isTrainer = _userRole == 'trainer'; return Scaffold( appBar: AppBar( title: const Text('Kalender'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _isLoading ? null : _loadEvents, ), ], ), body: SafeArea( child: Column( children: [ Container( padding: const EdgeInsets.all(8.0), child: TableCalendar( firstDay: DateTime.utc(2024, 1, 1), lastDay: DateTime.utc(2025, 12, 31), focusedDay: _focusedDay, calendarFormat: _calendarFormat, locale: 'de_DE', selectedDayPredicate: (day) { return isSameDay(_selectedDay, day); }, onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); }, onFormatChanged: (format) { setState(() { _calendarFormat = format; }); }, onPageChanged: (focusedDay) { setState(() { _focusedDay = focusedDay; }); }, eventLoader: _getEventsForDay, calendarStyle: const CalendarStyle( markersMaxCount: 1, markerDecoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), headerStyle: const HeaderStyle( formatButtonVisible: true, titleCentered: true, ), calendarBuilders: CalendarBuilders( markerBuilder: (context, date, events) { if (events.isEmpty) return null; return Positioned( bottom: 1, child: Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), ); }, ), startingDayOfWeek: StartingDayOfWeek.monday, daysOfWeekStyle: const DaysOfWeekStyle( weekdayStyle: TextStyle(fontWeight: FontWeight.bold), weekendStyle: TextStyle(color: Colors.red), ), availableCalendarFormats: const { CalendarFormat.week: 'Woche', CalendarFormat.month: 'Monat', }, ), ), 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')) : (() { 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, ), ), 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'], trainingId: event['id'], ), ); if (result != null) { await _addOrEditTraining( _selectedDay!, result['time'], result['duration'], isException: true, trainingId: result['trainingId'], ); await _loadEvents(); } }, ), 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: 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, size: 20), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Übung entfernen'), content: Text('Möchten Sie die Übung "${exercise['name']}" 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)), ), ], ), ); }, ), ], ), ); } return const SizedBox.shrink(); }), ], ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Schließen'), ), ], ), ); }, ), 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'), ), TextButton( onPressed: () { Navigator.pop(context); _deleteTraining(event); }, child: const Text('Löschen', style: TextStyle(color: Colors.red)), ), ], ), ); }, ), ], ), ), 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, size: 20), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Übung entfernen'), content: Text('Möchten Sie die Übung "${exercise['name']}" 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)), ), ], ), ); }, ), ], ), ); } return const SizedBox.shrink(); }), ], ), ), ], ), ); }, ); })() ), ], ), ), ); } 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 trainings = Map.from(data['trainings'] ?? {}); final uuid = const Uuid(); final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); final trainingTimes = data['trainingTimes'] as Map? ?? {}; // 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': [], }); } 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 ); } // 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]; } }); } } class _TrainingEditDialog extends StatefulWidget { final DateTime date; final String? initialTime; final int? initialDuration; final String? trainingId; const _TrainingEditDialog({ required this.date, this.initialTime, this.initialDuration, this.trainingId, }); @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: _duration < 300 ? () => setState(() => _duration += 15) : null, ), ], ), ], ), 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, 'trainingId': widget.trainingId, }); } : null, child: const Text('Speichern'), ), ], ); } }