// screens/home_screen.dart // This file contains the main HomeScreen widget, which manages the bottom navigation bar and navigation between the main screens of the app. import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:intl/intl.dart'; import 'search_tab.dart'; import 'favorites_tab.dart'; import 'calendar_tab.dart'; import 'profile_tab.dart'; import 'training_detail_screen.dart'; import '../models/categories.dart'; /// The main home screen of the app, containing the bottom navigation bar and all main tabs. class HomeScreen extends StatefulWidget { final VoidCallback? onLogoutSuccess; const HomeScreen({super.key, this.onLogoutSuccess}); @override State createState() => _HomeScreenState(); } /// State for the HomeScreen, manages navigation and data loading for the home tab. class _HomeScreenState extends State { int _selectedIndex = 0; Map? _nextTraining; bool _isLoading = true; DateTime? _calendarInitialDate; String? _favoriteCategoryFilter; List> _suggestions = []; bool _isLoadingSuggestions = true; String? _suggestionsError; @override void initState() { super.initState(); _loadNextTraining(); _loadSuggestions(); } /// Loads exercise suggestions for the user based on unused and highly rated exercises. Future _loadSuggestions() async { setState(() { _isLoadingSuggestions = true; _suggestionsError = null; }); try { final user = FirebaseAuth.instance.currentUser; if (user == null) { setState(() => _isLoadingSuggestions = false); return; } // 1. Get all exercise IDs used by the user final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); final Set usedExerciseIds = {}; if (userDoc.exists) { final userData = userDoc.data() as Map; final trainings = Map.from(userData['trainings'] ?? {}); trainings.forEach((date, trainingsList) { final list = List>.from(trainingsList); for (final training in list) { final exercises = List>.from(training['exercises'] ?? []); for (final exercise in exercises) { if (exercise.containsKey('id')) { usedExerciseIds.add(exercise['id']); } } } }); } // 2. Get all exercises final exercisesSnapshot = await FirebaseFirestore.instance.collection('Training').get(); final allExercises = exercisesSnapshot.docs.map((doc) { return {'id': doc.id, ...doc.data()}; }).toList(); // 3. Filter for unused and highly rated exercises final suggestions = allExercises.where((exercise) { final isUsed = usedExerciseIds.contains(exercise['id']); final rating = (exercise['rating overall'] as num?)?.toDouble() ?? 0.0; return !isUsed && rating >= 4.0; }).toList(); suggestions.shuffle(); setState(() { _suggestions = suggestions.take(5).toList(); _isLoadingSuggestions = false; }); } catch (e) { setState(() { _isLoadingSuggestions = false; _suggestionsError = 'Error loading suggestions.'; }); } } /// Loads the next training event for the user, depending on their role and club. Future _loadNextTraining() async { setState(() => _isLoading = true); try { final user = FirebaseAuth.instance.currentUser; if (user == null) return; final userDoc = await FirebaseFirestore.instance .collection('User') .doc(user.uid) .get(); if (!userDoc.exists) return; final userData = userDoc.data() as Map; final userRole = userData['role'] as String?; final userClub = userData['club'] as String?; QuerySnapshot trainersSnapshot; if (userRole == 'trainer') { // Trainer sees only their own trainings trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where(FieldPath.documentId, isEqualTo: user.uid) .get(); } else { // Player sees only trainings from trainers in their club if (userClub == null || userClub.isEmpty) { setState(() { _nextTraining = null; _isLoading = false; }); return; } trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where('club', isEqualTo: userClub) .get(); } final now = DateTime.now(); DateTime? nextTrainingDate; Map? nextTraining; 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? ?? []; 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 daysUntilNext = _getDaysUntilNext(day, now.weekday); final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute); // Check the next 4 weeks for (var i = 0; i < 4; 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; }); if (!isCancelled && (nextTrainingDate == null || date.isBefore(nextTrainingDate!))) { nextTrainingDate = date; nextTraining = { 'date': dateString, 'time': timeStr, 'duration': trainingDurations[day] ?? 60, 'trainerName': trainerData['name'] ?? 'Unknown Trainer', 'day': day, }; } } }); } setState(() { _nextTraining = nextTraining; _isLoading = false; }); } catch (e) { print('Error loading next training: $e'); setState(() { _isLoading = false; }); } } /// Returns the number of days until the next occurrence of a given weekday. 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; } /// Checks if two DateTime objects represent the same day. bool isSameDay(DateTime a, DateTime b) { return a.year == b.year && a.month == b.month && a.day == b.day; } /// Handles navigation bar item taps and reloads suggestions if needed. void _onItemTapped(int index) { if (_selectedIndex != index) { setState(() { _selectedIndex = index; }); // Reload suggestions when switching to the Home tab if (index == 0) { _loadSuggestions(); } } } /// List of main screens for the bottom navigation bar. List get _screens => [ _buildHomeTab(), const SearchTab(), FavoritesTab(categoryFilter: _favoriteCategoryFilter), CalendarTab(initialDate: _calendarInitialDate), ProfileTab(onLogoutSuccess: widget.onLogoutSuccess), ]; @override Widget build(BuildContext context) { return Scaffold( body: _screens[_selectedIndex], bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: _selectedIndex, onTap: _onItemTapped, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suche'), BottomNavigationBarItem( icon: Icon(Icons.favorite_border), label: 'Favoriten', ), BottomNavigationBarItem( icon: Icon(Icons.calendar_today), label: 'Kalender', ), BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profil', ), ], ), ); } /// Builds the Home tab content, including next training, favorites, and suggestions. Widget _buildHomeTab() { return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), const Center( child: Text( 'Hallo Trainer!', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFFE57399), ), ), ), const SizedBox(height: 24), GestureDetector( onTap: () { if (_nextTraining != null) { setState(() { _calendarInitialDate = DateTime.parse(_nextTraining!['date']); _selectedIndex = 3; }); } else { setState(() => _selectedIndex = 3); } }, child: Stack( children: [ Container( height: 140, width: double.infinity, decoration: BoxDecoration( color: Colors.pink[100], borderRadius: BorderRadius.circular(20), ), child: const Center( child: Icon(Icons.sports_handball, size: 80, color: Colors.white70), ), ), Positioned( left: 20, bottom: 20, child: FittedBox( fit: BoxFit.scaleDown, child: Text( 'Dein nächstes Training', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, shadows: [ Shadow( blurRadius: 8, color: Colors.black.withOpacity(0.5), offset: Offset(2, 2), ), ], ), ), ), ), if (_isLoading) const Positioned.fill( child: Center(child: CircularProgressIndicator()), ) else if (_nextTraining != null) Positioned( right: 20, bottom: 20, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(12), ), child: Text( DateFormat('EEEE, dd.MM.yyyy', 'de_DE').format( DateTime.parse(_nextTraining!['date']), ), style: const TextStyle(color: Colors.white, fontSize: 14), ), ), ), ], ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Favoriten', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: () {}, ), ], ), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: kTrainingCategories.map((cat) => _buildFavoriteCircle( cat.name, cat.icon, categoryColors[cat.name] ?? Colors.grey, )).toList(), ), ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Vorschläge', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: () { setState(() => _selectedIndex = 1); }, ), ], ), if (_isLoadingSuggestions) const SizedBox( height: 170, child: Center(child: CircularProgressIndicator()), ) else if (_suggestionsError != null) SizedBox( height: 170, child: Center(child: Text(_suggestionsError!)), ) else if (_suggestions.isEmpty) const SizedBox( height: 170, child: Center(child: Text('Keine neuen Vorschläge gefunden.')), ) else SizedBox( height: 170, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _suggestions.length, itemBuilder: (context, index) { return _buildSuggestionCard(_suggestions[index]); }, ), ), ], ), ), ); } /// Builds a favorite category circle with icon and label. Widget _buildFavoriteCircle(String label, IconData icon, Color color) { return Padding( padding: const EdgeInsets.only(right: 16.0), child: GestureDetector( onTap: () { setState(() { _favoriteCategoryFilter = label; _selectedIndex = 2; }); }, child: Column( children: [ CircleAvatar( radius: 24, backgroundColor: color.withOpacity(0.2), child: Icon(icon, color: color, size: 26), ), const SizedBox(height: 4), SizedBox( width: 60, child: Text( label, style: const TextStyle(fontSize: 12), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } /// Builds a suggestion card for an exercise. Widget _buildSuggestionCard(Map exercise) { final category = exercise['category'] as String? ?? 'Sonstiges'; final title = exercise['title'] as String? ?? 'Unbekannte Übung'; final id = exercise['id'] as String; final color = categoryColors[category] ?? Colors.grey; final icon = categoryIcons[category] ?? Icons.help_outline; return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => TrainingDetailScreen(trainingId: id), ), ); }, child: Container( width: 130, margin: const EdgeInsets.only(right: 16), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 3, clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 80, width: double.infinity, color: color.withOpacity(0.15), child: Center( child: Icon(icon, color: color, size: 40), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( category, style: const TextStyle(fontSize: 12, color: Colors.grey), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), const SizedBox(height: 4), Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), ], ), ), ), ], ), ), ), ); } } /// Mapping of category names to icons. const Map categoryIcons = { 'Aufwärmen & Mobilisation': Icons.directions_run, 'Wurf- & Torabschluss': Icons.sports_handball, 'Torwarttraining': Icons.sports, 'Athletik': Icons.fitness_center, 'Pass': Icons.compare_arrows, 'Koordination': Icons.directions_walk, }; /// Mapping of category names to colors. const Map categoryColors = { 'Aufwärmen & Mobilisation': Colors.deepOrange, 'Wurf- & Torabschluss': Colors.orange, 'Torwarttraining': Colors.green, 'Athletik': Colors.blue, 'Pass': Colors.purple, 'Koordination': Colors.teal, 'Spielformen': Colors.indigo, 'kleine Spiele': Colors.brown, };