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'; const Map categoryColors = { 'Aufwärmen & Mobilisation': Colors.deepOrange, 'Wurf- & Torabschluss': Colors.orange, 'Torwarttraining': Colors.green, 'Athletik': Colors.blue, 'Pass': Colors.purple, 'Koordination': Colors.teal, }; class CalendarTab extends StatefulWidget { final DateTime? initialDate; const CalendarTab({super.key, this.initialDate}); @override State createState() => _CalendarTabState(); } 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!; }); } } 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(); } } } Future _loadEvents() async { if (_userRole == null) return; setState(() => _isLoading = true); try { QuerySnapshot trainersSnapshot; if (_userRole == 'trainer') { trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where(FieldPath.documentId, isEqualTo: _currentUserId) .get(); } else { 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 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) 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 now = DateTime.now(); final daysUntilNext = _getDaysUntilNext(day, now.weekday); final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute); for (var i = 0; i < 52; i++) { 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); 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; }); final event = { 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'time': timeStr, 'duration': duration, 'trainerId': trainerDoc.id, 'isCurrentUser': trainerDoc.id == _currentUserId, 'day': day, 'date': dateString, 'exercises': exercises, 'remainingTime': duration - totalExerciseDuration, 'club': trainerData['club'] ?? 'Kein Verein', }; if (events.containsKey(normalizedDate)) { events[normalizedDate]!.add(event); } else { 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]; } }); } setState(() { _events = events; _isLoading = false; }); } catch (e) { print('Error loading events: $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 { final userDoc = await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .get(); if (!userDoc.exists) return; final data = userDoc.data() as Map; final trainingExercises = Map.from(data['trainingExercises'] ?? {}); final exercises = List>.from(trainingExercises[event['date']] ?? []); exercises.add({ 'id': result['id'], 'name': result['title'], 'description': result['description'], 'duration': result['duration'], }); trainingExercises[event['date']] = exercises; await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .update({ 'trainingExercises': trainingExercises, }); await _loadEvents(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Übung wurde hinzugefügt')), ); } } catch (e) { print('Error adding exercise: $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; try { final userDoc = await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .get(); if (!userDoc.exists) return; 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': normalizedDateString, 'day': event['day'], 'time': event['time'], 'duration': event['duration'], }); 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')), ); } } catch (e) { print('Error deleting training: $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 trainingExercises = Map.from(data['trainingExercises'] ?? {}); final exercises = List>.from(trainingExercises[event['date']] ?? []); // Entferne die Übung aus der Liste exercises.removeWhere((e) => e['id'] == exercise['id']); trainingExercises[event['date']] = exercises; await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) .update({ 'trainingExercises': trainingExercises, }); await _loadEvents(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Übung wurde entfernt')), ); } } catch (e) { print('Error removing exercise: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fehler beim Entfernen der Übung')), ); } } } int _getDaysUntilNext(String day, int currentWeekday) { final weekdays = { 'Montag': 1, 'Dienstag': 2, 'Mittwoch': 3, 'Donnerstag': 4, 'Freitag': 5, 'Samstag': 6, 'Sonntag': 7, }; final targetWeekday = weekdays[day] ?? 1; var daysUntilNext = targetWeekday - currentWeekday; if (daysUntilNext <= 0) { daysUntilNext += 7; } return daysUntilNext; } 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, 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'], ), ); 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.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'), ), ], ), ); }, ), 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, ), ), ), ), ], ), ); } 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'), ), ], ); } }