diff --git a/trainerbox/lib/main.dart b/trainerbox/lib/main.dart index d92ba98..bbe3cf7 100644 --- a/trainerbox/lib/main.dart +++ b/trainerbox/lib/main.dart @@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:trainerbox/firebase_options.dart'; import 'screens/home_screen.dart'; import 'screens/login_screen.dart'; +import 'screens/search_tab.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -48,6 +49,12 @@ class _MyAppState extends State { home: _loggedIn ? HomeScreen(onLogoutSuccess: _handleLogoutSuccess) : LoginScreen(onLoginSuccess: _handleLoginSuccess), + routes: { + '/search': (context) => SearchTab( + selectMode: (ModalRoute.of(context)?.settings.arguments as Map?)?['selectMode'] ?? false, + remainingTime: (ModalRoute.of(context)?.settings.arguments as Map?)?['remainingTime'] as int?, + ), + }, ); } } diff --git a/trainerbox/lib/screens/calendar_tab.dart b/trainerbox/lib/screens/calendar_tab.dart index 7c8566b..968b0bc 100644 --- a/trainerbox/lib/screens/calendar_tab.dart +++ b/trainerbox/lib/screens/calendar_tab.dart @@ -2,6 +2,7 @@ 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'; class CalendarTab extends StatefulWidget { const CalendarTab({super.key}); @@ -18,6 +19,15 @@ class _CalendarTabState extends State { 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() { @@ -52,17 +62,37 @@ class _CalendarTabState extends State { QuerySnapshot trainersSnapshot; if (_userRole == 'trainer') { - // Trainer sieht nur seine eigenen Trainings trainersSnapshot = await FirebaseFirestore.instance .collection('User') .where('role', isEqualTo: 'trainer') .where(FieldPath.documentId, isEqualTo: _currentUserId) .get(); } else { - // Spieler sehen alle Trainings + 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(); } @@ -73,6 +103,7 @@ class _CalendarTabState extends State { final trainingTimes = trainerData['trainingTimes'] as Map? ?? {}; final trainingDurations = trainerData['trainingDurations'] as Map? ?? {}; final cancelledTrainings = trainerData['cancelledTrainings'] as List? ?? []; + final trainingExercises = trainerData['trainingExercises'] as Map? ?? {}; trainingTimes.forEach((day, timeStr) { if (timeStr == null) return; @@ -88,12 +119,11 @@ class _CalendarTabState extends State { final daysUntilNext = _getDaysUntilNext(day, now.weekday); final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute); - // Erstelle Trainings für ein ganzes Jahr 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(); - // Prüfe, ob das Training an diesem Tag abgesagt wurde final isCancelled = cancelledTrainings.any((cancelled) { if (cancelled is Map) { final cancelledDate = DateTime.parse(cancelled['date'] as String); @@ -103,6 +133,14 @@ class _CalendarTabState extends State { }); if (!isCancelled) { + 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, @@ -110,7 +148,10 @@ class _CalendarTabState extends State { 'trainerId': trainerDoc.id, 'isCurrentUser': trainerDoc.id == _currentUserId, 'day': day, - 'date': normalizedDate.toIso8601String(), + 'date': dateString, + 'exercises': exercises, + 'remainingTime': duration - totalExerciseDuration, + 'club': trainerData['club'] ?? 'Kein Verein', }; if (events.containsKey(normalizedDate)) { @@ -133,6 +174,67 @@ class _CalendarTabState extends State { } } + 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; @@ -147,7 +249,6 @@ class _CalendarTabState extends State { final data = userDoc.data() as Map; final cancelledTrainings = List>.from(data['cancelledTrainings'] ?? []); - // Füge das Training zur Liste der abgesagten Trainings hinzu cancelledTrainings.add({ 'date': event['date'], 'day': event['day'], @@ -155,7 +256,6 @@ class _CalendarTabState extends State { 'duration': event['duration'], }); - // Aktualisiere die Daten in Firestore await FirebaseFirestore.instance .collection('User') .doc(_currentUserId) @@ -163,7 +263,6 @@ class _CalendarTabState extends State { 'cancelledTrainings': cancelledTrainings, }); - // Lade die Events neu await _loadEvents(); if (mounted) { @@ -181,6 +280,50 @@ class _CalendarTabState extends State { } } + 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, @@ -297,80 +440,183 @@ class _CalendarTabState extends State { 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; + return Card( margin: const EdgeInsets.only(bottom: 8), color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null, - child: ListTile( - leading: Icon( - Icons.sports, - color: isCurrentUser ? Colors.blue : null, - ), - title: Text( - isCurrentUser ? 'Training' : event['trainerName'], - style: TextStyle( - fontWeight: isCurrentUser ? FontWeight.bold : null, - ), - ), - subtitle: Text( - '${event['time']} - ${event['duration']} Minuten', - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Schließen'), - ), - ], - ), - ); - }, + child: Column( + children: [ + ListTile( + leading: Icon( + Icons.sports, + color: isCurrentUser ? Colors.blue : null, ), - 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)), - ), - ], - ), - ); - }, + 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.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, + ), + ), + ), + ), + 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: 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)), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }), + ], + ), + ), + ], ), ); }, diff --git a/trainerbox/lib/screens/profile_tab.dart b/trainerbox/lib/screens/profile_tab.dart index a1b0234..2271652 100644 --- a/trainerbox/lib/screens/profile_tab.dart +++ b/trainerbox/lib/screens/profile_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; class ProfileTab extends StatefulWidget { final VoidCallback? onLogoutSuccess; @@ -14,8 +15,13 @@ class _ProfileTabState extends State { final _formKey = GlobalKey(); bool _isTrainer = false; bool _isLoading = false; - Map _trainingTimes = {}; + Map _trainingTimes = {}; Map _trainingDurations = {}; + String _name = ''; + String _email = ''; + String _club = ''; + String? _userRole; + DateTime? _joinDate; @override void initState() { @@ -27,30 +33,49 @@ class _ProfileTabState extends State { final user = FirebaseAuth.instance.currentUser; if (user == null) return; + setState(() => _isLoading = true); try { final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); if (doc.exists) { final data = doc.data()!; - setState(() { - _isTrainer = data['role'] == 'trainer'; - if (_isTrainer) { - _trainingTimes = Map.from( - (data['trainingTimes'] ?? {}).map( - (key, value) => MapEntry( - key, - TimeOfDay( - hour: int.parse(value.split(':')[0]), - minute: int.parse(value.split(':')[1]), - ), - ), - ), - ); - _trainingDurations = Map.from(data['trainingDurations'] ?? {}); + final trainingTimes = data['trainingTimes'] as Map? ?? {}; + final trainingDurations = data['trainingDurations'] as Map? ?? {}; + + final convertedTrainingTimes = {}; + trainingTimes.forEach((key, value) { + if (value != null) { + final timeStr = value.toString(); + final timeParts = timeStr.split(':'); + if (timeParts.length == 2) { + final hour = int.tryParse(timeParts[0]) ?? 0; + final minute = int.tryParse(timeParts[1]) ?? 0; + convertedTrainingTimes[key] = TimeOfDay(hour: hour, minute: minute); + } } }); + + setState(() { + _name = data['name'] ?? ''; + _email = data['email'] ?? ''; + _club = data['club'] ?? ''; + _isTrainer = data['role'] == 'trainer'; + _userRole = data['role'] as String?; + _trainingTimes = convertedTrainingTimes; + _trainingDurations = Map.from(trainingDurations); + _joinDate = (data['createdAt'] as Timestamp?)?.toDate(); + }); } } catch (e) { print('Error loading user data: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Fehler beim Laden der Daten: $e')), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -74,15 +99,21 @@ class _ProfileTabState extends State { _trainingDurations[day] = duration; }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Trainingszeit gespeichert')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Trainingszeit gespeichert')), + ); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Fehler beim Speichern: $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Fehler beim Speichern: $e')), + ); + } } finally { - setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -102,15 +133,21 @@ class _ProfileTabState extends State { _trainingDurations.remove(day); }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Trainingszeit entfernt')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Trainingszeit entfernt')), + ); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Fehler beim Entfernen: $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Fehler beim Entfernen: $e')), + ); + } } finally { - setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -132,6 +169,47 @@ class _ProfileTabState extends State { } } + Future _saveUserData() async { + if (FirebaseAuth.instance.currentUser == null) return; + + setState(() => _isLoading = true); + try { + final trainingTimesMap = {}; + _trainingTimes.forEach((key, value) { + if (value != null) { + trainingTimesMap[key] = '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}'; + } + }); + + await FirebaseFirestore.instance + .collection('User') + .doc(FirebaseAuth.instance.currentUser!.uid) + .update({ + 'name': _name, + 'club': _club, + 'trainingTimes': trainingTimesMap, + 'trainingDurations': _trainingDurations, + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profil wurde aktualisiert')), + ); + } + } catch (e) { + print('Error saving user data: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Fehler beim Speichern des Profils')), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + @override Widget build(BuildContext context) { final user = FirebaseAuth.instance.currentUser; @@ -152,92 +230,167 @@ class _ProfileTabState extends State { } }, ), + IconButton( + icon: const Icon(Icons.save), + onPressed: _isLoading ? null : _saveUserData, + ), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Persönliche Informationen', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - FutureBuilder( - future: FirebaseFirestore.instance.collection('User').doc(user.uid).get(), - 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 Daten gefunden')); - } - final data = snapshot.data!.data() as Map; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Name: ${data['name'] ?? '-'}'), - const SizedBox(height: 8), - Text('E-Mail: ${user.email ?? '-'}'), - const SizedBox(height: 8), - Text('Rolle: ${data['role'] ?? '-'}'), - if (_isTrainer) ...[ - const SizedBox(height: 24), - const Text( - 'Trainingszeiten', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Persönliche Informationen', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + TextField( + controller: TextEditingController(text: _name), + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + onChanged: (value) => _name = value, + ), + const SizedBox(height: 16), + TextField( + controller: TextEditingController(text: _email), + decoration: const InputDecoration( + labelText: 'E-Mail', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + enabled: false, + ), + const SizedBox(height: 16), + TextField( + controller: TextEditingController(text: _club), + decoration: const InputDecoration( + labelText: 'Verein', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.sports), + hintText: 'Geben Sie Ihren Verein ein', + ), + onChanged: (value) => _club = value, + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const Icon(Icons.work), + const SizedBox(width: 12), + Text( + 'Rolle: ${_userRole == 'trainer' ? 'Trainer' : 'Spieler'}', + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + if (_joinDate != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const Icon(Icons.calendar_today), + const SizedBox(width: 12), + Text( + 'Beigetreten am: ${DateFormat('dd.MM.yyyy').format(_joinDate!)}', + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ], + ], + ), + ), + ), + if (_userRole == 'trainer') ...[ + const SizedBox(height: 24), + Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Trainingszeiten', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'] + .map((day) => Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: const Icon(Icons.access_time), + title: Text(day), + subtitle: _trainingTimes[day] != null + ? Text( + '${_trainingTimes[day]!.format(context)} - ${_trainingDurations[day]} Minuten', + ) + : const Text('Keine Trainingszeit'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + _trainingTimes[day] != null + ? Icons.edit + : Icons.add, + ), + onPressed: _isLoading + ? null + : () => _selectTime(context, day), + ), + if (_trainingTimes[day] != null) + IconButton( + icon: const Icon(Icons.delete), + onPressed: _isLoading + ? null + : () => _removeTrainingTime(day), + ), + ], + ), + ), + )) + .toList(), + ], ), ), - const SizedBox(height: 16), - ...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'] - .map((day) => Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - title: Text(day), - subtitle: _trainingTimes[day] != null - ? Text( - '${_trainingTimes[day]!.format(context)} - ${_trainingDurations[day]} Minuten', - ) - : const Text('Keine Trainingszeit'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - _trainingTimes[day] != null - ? Icons.edit - : Icons.add, - ), - onPressed: _isLoading - ? null - : () => _selectTime(context, day), - ), - if (_trainingTimes[day] != null) - IconButton( - icon: const Icon(Icons.delete), - onPressed: _isLoading - ? null - : () => _removeTrainingTime(day), - ), - ], - ), - ), - )) - .toList(), - ], + ), ], - ); - }, + ], + ), ), - ], - ), - ), ); } } diff --git a/trainerbox/lib/screens/search_tab.dart b/trainerbox/lib/screens/search_tab.dart index 7feac7b..d019a03 100644 --- a/trainerbox/lib/screens/search_tab.dart +++ b/trainerbox/lib/screens/search_tab.dart @@ -7,7 +7,14 @@ import 'dart:io'; import 'training_detail_screen.dart'; class SearchTab extends StatefulWidget { - const SearchTab({super.key}); + final bool selectMode; + final int? remainingTime; + + const SearchTab({ + super.key, + this.selectMode = false, + this.remainingTime, + }); @override State createState() => _SearchTabState(); @@ -63,7 +70,7 @@ class _SearchTabState extends State { } } - void _showCreateTrainingDialog() { + void _showCreateTrainingDialog(BuildContext context) { showDialog( context: context, builder: (context) => _CreateTrainingDialog(categories: _categories), @@ -88,191 +95,202 @@ class _SearchTabState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: TextField( + appBar: AppBar( + title: const Text('Übungen'), + actions: [ + if (widget.selectMode) + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + if (_isTrainer && !widget.selectMode) + IconButton( + icon: const Icon(Icons.add), + onPressed: () => _showCreateTrainingDialog(context), + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Suche nach Training...', - border: InputBorder.none, + hintText: 'Suche nach Übungen...', prefixIcon: const Icon(Icons.search), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => _searchController.clear(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), ), ), - actions: [ - if (_trainerChecked && _isTrainer) - IconButton( - icon: const Icon(Icons.add), - tooltip: 'Neues Training erstellen', - onPressed: _showCreateTrainingDialog, - ), - ], ), - SliverPadding( + if (widget.selectMode && widget.remainingTime != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'Verbleibende Zeit: ${widget.remainingTime} Minuten', + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( padding: const EdgeInsets.all(16.0), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Kategorien', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: _categories.map((category) { - return FilterChip( - label: Text(category), - selected: _selectedCategory == category, - onSelected: (bool selected) { - setState(() { - _selectedCategory = selected ? category : null; - }); - }, - ); - }).toList(), - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Kategorien', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: _categories.map((category) { + return FilterChip( + label: Text(category), + selected: _selectedCategory == category, + onSelected: (bool selected) { + setState(() { + _selectedCategory = selected ? category : null; + }); + }, + ); + }).toList(), + ), + ], ), ), - if (_searchTerm.isNotEmpty || _selectedCategory != null) - SliverPadding( - padding: const EdgeInsets.all(16.0), - sliver: FutureBuilder( - future: FirebaseFirestore.instance.collection('Training').get(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SliverToBoxAdapter( - child: Center(child: CircularProgressIndicator())); - } - if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { - return const SliverToBoxAdapter( - child: Center(child: Text('Keine Trainings gefunden.'))); - } - final docs = snapshot.data!.docs.where((doc) { + Expanded( + child: FutureBuilder( + future: FirebaseFirestore.instance.collection('Training').get(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('Fehler: ${snapshot.error}')); + } + + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return const Center(child: Text('Keine Übungen gefunden')); + } + + var exercises = snapshot.data!.docs.where((doc) { + final data = doc.data() as Map; + final title = data['title']?.toString().toLowerCase() ?? ''; + final description = data['description']?.toString().toLowerCase() ?? ''; + final category = data['category']?.toString() ?? ''; + final searchTerm = _searchTerm.toLowerCase(); + + final matchesSearch = title.contains(searchTerm) || description.contains(searchTerm); + final matchesCategory = _selectedCategory == null || category == _selectedCategory; + + return matchesSearch && matchesCategory; + }).toList(); + + if (exercises.isEmpty) { + return const Center(child: Text('Keine Übungen gefunden')); + } + + return GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: exercises.length, + itemBuilder: (context, index) { + final doc = exercises[index]; final data = doc.data() as Map; - final title = (data['title'] ?? '').toString().toLowerCase(); - final description = (data['description'] ?? '').toString().toLowerCase(); - final category = (data['category'] ?? '').toString(); - final matchesSearch = _searchTerm.isEmpty || - title.contains(_searchTerm.toLowerCase()) || - description.contains(_searchTerm.toLowerCase()); - final matchesCategory = _selectedCategory == null || category == _selectedCategory; - return matchesSearch && matchesCategory; - }).toList(); - if (docs.isEmpty) { - return const SliverToBoxAdapter( - child: Center(child: Text('Keine Trainings gefunden.'))); - } - return SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 0.75, - ), - delegate: SliverChildBuilderDelegate((context, index) { - final data = docs[index].data() as Map; - final isFavorite = _favorites.contains(docs[index].id); - return Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TrainingDetailScreen(trainingId: docs[index].id), - ), - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: (data['picture'] is String && data['picture'] != '') - ? Image.network( - data['picture'], - width: double.infinity, - fit: BoxFit.cover, - ) - : Container( - color: Colors.grey[300], - child: const Center( - child: Icon(Icons.fitness_center, size: 40), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data['title'] ?? '-', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + final duration = (data['duration'] as num?)?.toInt() ?? 0; + final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0); + + return Card( + child: InkWell( + onTap: isDisabled + ? null + : () { + if (widget.selectMode) { + Navigator.pop(context, { + 'id': doc.id, + 'title': data['title']?.toString() ?? 'Unbekannte Übung', + 'description': data['description']?.toString() ?? '', + 'duration': duration, + }); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TrainingDetailScreen(trainingId: doc.id), ), - const SizedBox(height: 4), - Text( - data['description'] ?? '-', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.grey[600], - fontSize: 13, - ), - ), - const SizedBox(height: 4), - Text( - '${data['duration'] ?? '-'} Minuten', - style: TextStyle( - color: Colors.grey[600], - fontSize: 13, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - const Icon(Icons.star, size: 16, color: Colors.amber), - const SizedBox(width: 4), - Text('${data['rating overall'] ?? '-'}'), - ], - ), - const SizedBox(height: 4), - Text('Level: ${data['year'] ?? '-'}'), - const SizedBox(height: 4), - Align( - alignment: Alignment.bottomRight, - child: IconButton( - icon: Icon( - isFavorite ? Icons.favorite : Icons.favorite_border, - color: isFavorite ? Colors.red : null, - ), - onPressed: () => _toggleFavorite(docs[index].id, isFavorite), - ), - ), - ], + ); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + 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), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data['title']?.toString() ?? 'Unbekannte Übung', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + '${data['description']?.toString() ?? ''}\nDauer: $duration Minuten', + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + if (isDisabled) + const Padding( + padding: EdgeInsets.only(top: 4), + child: Text( + 'Passt nicht in die verbleibende Zeit', + style: TextStyle( + color: Colors.orange, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], ), - ); - }, childCount: docs.length), - ); - }, - ), + ), + ); + }, + ); + }, ), + ), ], ), );