From e6293adbc700d7b2e94cf32be6f452947a4db83d Mon Sep 17 00:00:00 2001 From: joschy2002 Date: Fri, 20 Jun 2025 23:55:33 +0200 Subject: [PATCH] =?UTF-8?q?Vorschl=C3=A4ge=20added?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trainerbox/lib/screens/favorites_tab.dart | 219 ++++++++++++---------- trainerbox/lib/screens/home_screen.dart | 215 ++++++++++++++++----- 2 files changed, 287 insertions(+), 147 deletions(-) diff --git a/trainerbox/lib/screens/favorites_tab.dart b/trainerbox/lib/screens/favorites_tab.dart index 1a07078..684aa03 100644 --- a/trainerbox/lib/screens/favorites_tab.dart +++ b/trainerbox/lib/screens/favorites_tab.dart @@ -68,121 +68,138 @@ class _FavoritesTabState extends State { ), Expanded( child: StreamBuilder( - stream: FirebaseFirestore.instance.collection('User').doc(user.uid).snapshots(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - if (!snapshot.hasData || !snapshot.data!.exists) { - return const Center(child: Text('Keine Favoriten gefunden')); - } - final data = snapshot.data!.data() as Map; - final favorites = List.from(data['favorites'] ?? []); + stream: FirebaseFirestore.instance.collection('User').doc(user.uid).snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (!snapshot.hasData || !snapshot.data!.exists) { + return const Center(child: Text('Keine Favoriten gefunden')); + } + final data = snapshot.data!.data() as Map; + final allFavorites = List.from(data['favorites'] ?? []); - if (favorites.isEmpty) { - return const Center(child: Text('Keine Favoriten gefunden')); - } + if (allFavorites.isEmpty) { + return const Center(child: Text('Keine Favoriten gefunden')); + } - return GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - childAspectRatio: 0.75, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemCount: favorites.length, - itemBuilder: (context, index) { - return FutureBuilder( - future: FirebaseFirestore.instance.collection('Training').doc(favorites[index]).get(), - builder: (context, trainingSnapshot) { - if (!trainingSnapshot.hasData || !trainingSnapshot.data!.exists) { - return const SizedBox.shrink(); - } - final trainingData = trainingSnapshot.data!.data() as Map; - // Filter nach Kategorie, falls gesetzt - if (_selectedCategory != null && trainingData['category'] != _selectedCategory) { - return const SizedBox.shrink(); - } - return Card( - child: Stack( - children: [ - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TrainingDetailScreen(trainingId: favorites[index]), - ), - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + // Lade alle Favoriten-Dokumente auf einmal + return FutureBuilder>( + future: Future.wait(allFavorites.map((id) => + FirebaseFirestore.instance.collection('Training').doc(id).get() + )), + builder: (context, multiSnapshot) { + if (multiSnapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (!multiSnapshot.hasData) { + return const Center(child: Text('Favoriten konnten nicht geladen werden')); + } + + // Filtere die Favoriten basierend auf der Kategorie + final filteredFavorites = multiSnapshot.data!.where((doc) { + if (!doc.exists) return false; + if (_selectedCategory == null) return true; + final trainingData = doc.data() as Map; + return trainingData['category'] == _selectedCategory; + }).toList(); + + if (filteredFavorites.isEmpty) { + return const Center(child: Text('Keine Favoriten in dieser Kategorie gefunden')); + } + + return GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 300, + childAspectRatio: 0.75, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: filteredFavorites.length, + itemBuilder: (context, index) { + final doc = filteredFavorites[index]; + final trainingData = doc.data() as Map; + + return Card( + child: Stack( children: [ - Expanded( - child: Container( - color: Colors.grey[200], - child: const Center( - child: Icon(Icons.fitness_center, size: 50), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TrainingDetailScreen(trainingId: doc.id), + ), + ); + }, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - trainingData['title'] ?? 'Unbekannt', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Row( - children: [ - const Icon(Icons.star, color: Colors.amber, size: 16), - const SizedBox(width: 4), - Text( - (trainingData['rating overall'] ?? 0.0).toStringAsFixed(1), - style: const TextStyle(fontSize: 12), + Expanded( + child: Container( + color: Colors.grey[200], + child: const Center( + child: Icon(Icons.fitness_center, size: 50), ), - ], + ), ), - const SizedBox(height: 4), - Text( + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + trainingData['title'] ?? 'Unbekannt', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon(Icons.star, color: Colors.amber, size: 16), + const SizedBox(width: 4), + Text( + (trainingData['rating overall'] ?? 0.0).toStringAsFixed(1), + style: const TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 4), + Text( 'Dauer: \t${trainingData['duration'] ?? '-'} Minuten', - style: const TextStyle(fontSize: 12, color: Colors.grey), + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), ), ], ), ), + Positioned( + top: 4, + right: 4, + child: IconButton( + icon: const Icon(Icons.favorite, color: Colors.red), + onPressed: () async { + await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ + 'favorites': FieldValue.arrayRemove([doc.id]), + }); + }, + ), + ), ], ), - ), - Positioned( - top: 4, - right: 4, - child: IconButton( - icon: const Icon(Icons.favorite, color: Colors.red), - onPressed: () async { - await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ - 'favorites': FieldValue.arrayRemove([favorites[index]]), - }); - }, - ), - ), - ], - ), - ); - }, - ); - }, - ); - }, + ); + }, + ); + }, + ); + }, ), ), ], diff --git a/trainerbox/lib/screens/home_screen.dart b/trainerbox/lib/screens/home_screen.dart index 37f4bb5..7cbdf2d 100644 --- a/trainerbox/lib/screens/home_screen.dart +++ b/trainerbox/lib/screens/home_screen.dart @@ -8,6 +8,7 @@ import 'search_tab.dart'; import 'favorites_tab.dart'; import 'calendar_tab.dart'; import 'profile_tab.dart'; +import 'training_detail_screen.dart'; class HomeScreen extends StatefulWidget { final VoidCallback? onLogoutSuccess; @@ -23,11 +24,76 @@ class _HomeScreenState extends State { bool _isLoading = true; DateTime? _calendarInitialDate; String? _favoriteCategoryFilter; + List> _suggestions = []; + bool _isLoadingSuggestions = true; + String? _suggestionsError; @override void initState() { super.initState(); _loadNextTraining(); + _loadSuggestions(); + } + + Future _loadSuggestions() async { + setState(() { + _isLoadingSuggestions = true; + _suggestionsError = null; + }); + + try { + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + setState(() => _isLoadingSuggestions = false); + return; + } + + // 1. Hole alle vom User genutzten Übungs-IDs + 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. Hole alle Übungen + final exercisesSnapshot = await FirebaseFirestore.instance.collection('Training').get(); + + final allExercises = exercisesSnapshot.docs.map((doc) { + return {'id': doc.id, ...doc.data() as Map}; + }).toList(); + + // 3. Filtere nach "nicht genutzt" und "gut bewertet" + 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 = 'Fehler beim Laden.'; + }); + } } Future _loadNextTraining() async { @@ -158,9 +224,15 @@ class _HomeScreenState extends State { } void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - }); + if (_selectedIndex != index) { + setState(() { + _selectedIndex = index; + }); + // Wenn zum Home-Tab gewechselt wird, lade die Vorschläge neu + if (index == 0) { + _loadSuggestions(); + } + } } List get _screens => [ @@ -326,21 +398,38 @@ class _HomeScreenState extends State { ), IconButton( icon: const Icon(Icons.chevron_right), - onPressed: () {}, + onPressed: () { + setState(() => _selectedIndex = 1); + }, ), ], ), - SizedBox( - height: 170, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - _buildSuggestionCard('Wurf- & Torabschluss', 'Sprungwurf', Icons.sports_handball, categoryColors['Wurf- & Torabschluss']!), - _buildSuggestionCard('Pass', 'Doppelpass', Icons.compare_arrows, categoryColors['Pass']!), - _buildSuggestionCard('Koordination', 'Leiterlauf', Icons.directions_walk, categoryColors['Koordination']!), - ], + 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]); + }, + ), ), - ), ], ), ), @@ -381,48 +470,82 @@ class _HomeScreenState extends State { ); } - Widget _buildSuggestionCard(String category, String title, IconData icon, Color color) { - return Container( - width: 130, - margin: const EdgeInsets.only(right: 16), - child: Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - elevation: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 80, - width: double.infinity, - decoration: BoxDecoration( + 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), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), + child: Center( + child: Icon(icon, color: color, size: 40), ), ), - 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: [ + Text( + category, + style: const TextStyle(fontSize: 12, color: Colors.grey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(category, style: const TextStyle(fontSize: 12, color: Colors.grey)), - const SizedBox(height: 4), - Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), - ], - ), - ), - ], + ], + ), ), ), ); } } +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, +}; + const Map categoryColors = { 'Aufwärmen & Mobilisation': Colors.deepOrange, 'Wurf- & Torabschluss': Colors.orange,