import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'training_detail_screen.dart'; import '../models/categories.dart'; /// The SearchTab displays a searchable and filterable list of training exercises. class SearchTab extends StatefulWidget { /// If true, enables selection mode for choosing exercises. final bool selectMode; /// Remaining time for selection mode (optional). final int? remainingTime; const SearchTab({ super.key, this.selectMode = false, this.remainingTime, }); @override State createState() => _SearchTabState(); } class _SearchTabState extends State { // Controller for the search input field. final TextEditingController _searchController = TextEditingController(); // List of available exercise categories. List get _categories => kTrainingCategories.map((c) => c.name).toList(); // Currently selected category for filtering. String? _selectedCategory; // Current search term entered by the user. String _searchTerm = ''; // Indicates if the user is a trainer. bool _isTrainer = false; // Indicates if the trainer check has completed. bool _trainerChecked = false; // Set of favorite exercise IDs. Set _favorites = {}; // Filter state variables double _minDuration = 0; double _maxDuration = 120; String? _selectedYear; double _minRating = 0.0; // Temporary filter state for modal double _tempMinDuration = 0; double _tempMaxDuration = 120; String? _tempSelectedYear; double _tempMinRating = 0.0; // Level categories for filter final List _levelCategories = [ 'Bambini', 'F-Jugend', 'E-Jugend', 'D-Jugend', 'C-Jugend', 'B-Jugend', 'A-Jugend', 'Herren', ]; @override void initState() { super.initState(); // Listen for changes in the search field and update the search term. _searchController.addListener(() { setState(() { _searchTerm = _searchController.text.trim(); }); }); _checkIfTrainer(); _loadFavorites(); } /// Checks if the current user is a trainer and updates state. Future _checkIfTrainer() async { final user = FirebaseAuth.instance.currentUser; if (user == null) return; final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); setState(() { _isTrainer = doc.data()?['role'] == 'trainer'; _trainerChecked = true; }); } /// Loads the user's favorite exercises from Firestore. Future _loadFavorites() async { final user = FirebaseAuth.instance.currentUser; if (user == null) return; final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); final data = doc.data(); if (data != null && data['favorites'] != null) { setState(() { _favorites = Set.from(data['favorites']); }); } } /// Shows a dialog for creating a new training exercise (trainer only). void _showCreateTrainingDialog(BuildContext context) { showDialog( context: context, builder: (context) => _CreateTrainingDialog(categories: _categories), ).then((_) => setState(() {})); // Refresh after adding } /// Toggles the favorite status of an exercise for the current user. Future _toggleFavorite(String trainingId, bool isFavorite) async { final user = FirebaseAuth.instance.currentUser; if (user == null) return; if (isFavorite) { await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ 'favorites': FieldValue.arrayRemove([trainingId]), }); } else { await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ 'favorites': FieldValue.arrayUnion([trainingId]), }); } await _loadFavorites(); // Update favorites after toggling } void _openFilterModal() async { // Set temp values to current filter state _tempMinDuration = _minDuration; _tempMaxDuration = _maxDuration; _tempSelectedYear = _selectedYear; _tempMinRating = _minRating; await showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, left: 16, right: 16, top: 24, ), child: StatefulBuilder( builder: (context, setModalState) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Filter', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), // Duration filter Row( children: [ const Text('Dauer:'), Expanded( child: RangeSlider( values: RangeValues(_tempMinDuration, _tempMaxDuration), min: 0, max: 300, divisions: 30, labels: RangeLabels('${_tempMinDuration.toInt()} min', '${_tempMaxDuration.toInt()} min'), onChanged: (values) { setModalState(() { _tempMinDuration = values.start; _tempMaxDuration = values.end; }); }, ), ), ], ), // Year/Level filter Row( children: [ const Text('Level:'), const SizedBox(width: 8), Expanded( child: DropdownButton( value: _tempSelectedYear, hint: const Text('Alle'), items: [ const DropdownMenuItem(value: null, child: Text('Alle')), ..._levelCategories.map((y) => DropdownMenuItem(value: y, child: Text(y))), ], onChanged: (value) { setModalState(() { _tempSelectedYear = value; }); }, ), ), ], ), // Minimum rating filter Row( children: [ const Text('Mindestbewertung:'), Expanded( child: Slider( value: _tempMinRating, min: 0, max: 5, divisions: 10, label: _tempMinRating.toStringAsFixed(1), onChanged: (value) { setModalState(() { _tempMinRating = value; }); }, ), ), Text(_tempMinRating.toStringAsFixed(1)), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('Abbrechen'), ), ElevatedButton( onPressed: () { setState(() { _minDuration = _tempMinDuration; _maxDuration = _tempMaxDuration; _selectedYear = _tempSelectedYear; _minRating = _tempMinRating; }); Navigator.pop(context); }, child: const Text('Anwenden'), ), ], ), const SizedBox(height: 8), ], ); }, ), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Suche'), actions: [ if (_isTrainer && _trainerChecked) IconButton( icon: const Icon(Icons.add), onPressed: () => _showCreateTrainingDialog(context), ), ], ), body: Column( children: [ // Search bar Padding( padding: const EdgeInsets.all(16.0), child: TextField( controller: _searchController, decoration: const InputDecoration( labelText: 'Suche nach Übungen...', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), ), ), // Category filter chips Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Kategorien', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), // Filter button opens the modal with advanced filters ElevatedButton.icon( onPressed: _openFilterModal, icon: const Icon(Icons.filter_list), label: const Text('Filter'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), textStyle: const TextStyle(fontSize: 14), ), ), ], ), const SizedBox(height: 12), // Horizontal scrollable category chips SizedBox( height: 40, child: ListView( scrollDirection: Axis.horizontal, children: [ // "Alle" chip Padding( padding: const EdgeInsets.only(right: 8.0), child: FilterChip( label: const Text('Alle'), selected: _selectedCategory == null, onSelected: (selected) { setState(() { _selectedCategory = null; }); }, ), ), // Category chips ..._categories.map((category) => Padding( padding: const EdgeInsets.only(right: 8.0), child: FilterChip( label: Text(category), selected: _selectedCategory == category, onSelected: (selected) { setState(() { _selectedCategory = selected ? category : null; }); }, ), )).toList(), ], ), ), ], ), ), const SizedBox(height: 16), // Exercise list 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')); } // Filter exercises by search term and category. 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 duration = (data['duration'] as num?)?.toInt() ?? 0; final year = data['year']?.toString() ?? ''; final rating = (data['rating overall'] as num?)?.toDouble() ?? 0.0; final searchTerm = _searchTerm.toLowerCase(); final matchesSearch = title.contains(searchTerm) || description.contains(searchTerm); final matchesCategory = _selectedCategory == null || category == _selectedCategory; final matchesDuration = duration >= _minDuration && duration <= _maxDuration; final matchesYear = _selectedYear == null || _selectedYear == '' || year == _selectedYear; final matchesRating = rating >= _minRating; return matchesSearch && matchesCategory && matchesDuration && matchesYear && matchesRating; }).toList(); if (exercises.isEmpty) { return const Center(child: Text('Keine Übungen gefunden')); } return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 300, childAspectRatio: 0.75, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: exercises.length, itemBuilder: (context, index) { final doc = exercises[index]; final data = doc.data() as Map; final duration = (data['duration'] as num?)?.toInt() ?? 0; // Disable selection if duration exceeds remaining time in select mode. final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0); return Card( child: Stack( children: [ InkWell( onTap: isDisabled ? null : () { if (widget.selectMode) { // Return selected exercise data in select mode. Navigator.pop(context, { 'id': doc.id, 'title': data['title']?.toString() ?? 'Unbekannte Übung', 'description': data['description']?.toString() ?? '', 'duration': duration, }); } else { // Navigate to exercise detail screen. Navigator.push( context, MaterialPageRoute( builder: (context) => TrainingDetailScreen(trainingId: doc.id), ), ); } }, 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), Row( children: [ const Icon(Icons.star, color: Colors.amber, size: 16), const SizedBox(width: 4), Text( (data['rating overall'] ?? 0.0).toStringAsFixed(1), style: const TextStyle(fontSize: 12), ), ], ), const SizedBox(height: 4), Text( 'Dauer: ${duration} Minuten', style: const TextStyle(fontSize: 12, color: Colors.grey), ), if (isDisabled) const Padding( padding: EdgeInsets.only(top: 4), child: Text( 'Passt nicht in die verbleibende Zeit', style: TextStyle( color: Colors.orange, fontSize: 12, ), ), ), ], ), ), ], ), ), // Favorite icon button (toggle favorite status). Positioned( top: 4, right: 4, child: IconButton( icon: Icon( _favorites.contains(doc.id) ? Icons.favorite : Icons.favorite_border, color: _favorites.contains(doc.id) ? Colors.red : Colors.grey, ), onPressed: () => _toggleFavorite(doc.id, _favorites.contains(doc.id)), ), ), ], ), ); }, ); }, ), ), ], ), ); } @override void dispose() { _searchController.dispose(); super.dispose(); } } /// Dialog for creating a new training exercise. class _CreateTrainingDialog extends StatefulWidget { final List categories; const _CreateTrainingDialog({required this.categories}); @override State<_CreateTrainingDialog> createState() => _CreateTrainingDialogState(); } class _CreateTrainingDialogState extends State<_CreateTrainingDialog> { // Form key for validating the create training form. final _formKey = GlobalKey(); // Selected category for the new training. String? _category; // Title of the new training. String? _title; // Description of the new training. String? _description; // Duration of the new training. int? _duration; // Difficulty level or year. String? _year; // Indicates if the dialog is loading (creating training). bool _loading = false; // Selected image file for the training. File? _imageFile; // Image picker instance. final _picker = ImagePicker(); // Level categories for dropdown final List _levelCategories = [ 'Bambini', 'F-Jugend', 'E-Jugend', 'D-Jugend', 'C-Jugend', 'B-Jugend', 'A-Jugend', 'Herren', ]; /// Opens the image picker to select an image from the gallery. Future _pickImage() async { final pickedFile = await _picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { setState(() { _imageFile = File(pickedFile.path); }); } } /// Uploads the selected image to Firebase Storage and returns the download URL. Future _uploadImage() async { if (_imageFile == null) return null; final storageRef = FirebaseStorage.instance .ref() .child('training_images') .child('${DateTime.now().millisecondsSinceEpoch}.jpg'); try { final uploadTask = await storageRef.putFile(_imageFile!); return await uploadTask.ref.getDownloadURL(); } catch (e) { print('Error uploading image: $e'); return null; } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Neues Training erstellen'), content: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_imageFile != null) Container( height: 200, width: double.infinity, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( image: DecorationImage( image: FileImage(_imageFile!), fit: BoxFit.cover, ), ), ), ElevatedButton.icon( onPressed: _pickImage, icon: const Icon(Icons.image), label: Text(_imageFile == null ? 'Bild auswählen' : 'Bild ändern'), ), const SizedBox(height: 16), DropdownButtonFormField( value: _category, items: widget.categories .map((cat) => DropdownMenuItem(value: cat, child: Text(cat))) .toList(), onChanged: (v) => setState(() => _category = v), decoration: const InputDecoration(labelText: 'Kategorie'), validator: (v) => v == null ? 'Kategorie wählen' : null, ), TextFormField( decoration: const InputDecoration(labelText: 'Titel'), onChanged: (v) => _title = v, validator: (v) => v == null || v.isEmpty ? 'Titel angeben' : null, ), TextFormField( decoration: const InputDecoration(labelText: 'Beschreibung'), onChanged: (v) => _description = v, validator: (v) => v == null || v.isEmpty ? 'Beschreibung angeben' : null, maxLines: 2, ), TextFormField( decoration: const InputDecoration(labelText: 'Dauer (Minuten)'), keyboardType: TextInputType.number, onChanged: (v) => _duration = int.tryParse(v), validator: (v) => v == null || int.tryParse(v) == null ? 'Zahl angeben' : null, ), DropdownButtonFormField( value: _year, items: _levelCategories .map((level) => DropdownMenuItem(value: level, child: Text(level))) .toList(), onChanged: (v) => setState(() => _year = v), decoration: const InputDecoration(labelText: 'Level'), validator: (v) => v == null ? 'Level angeben' : null, ), ], ), ), ), actions: [ TextButton( onPressed: _loading ? null : () => Navigator.pop(context), child: const Text('Abbrechen'), ), ElevatedButton( onPressed: _loading ? null : () async { if (_formKey.currentState!.validate()) { setState(() => _loading = true); try { final imageUrl = await _uploadImage(); await FirebaseFirestore.instance.collection('Training').add({ 'category': _category, 'title': _title, 'description': _description, 'duration': _duration, 'picture': imageUrl, 'rating overall': 0.0, 'year': _year, 'ratings': [], // Array für einzelne Bewertungen }); Navigator.pop(context); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Fehler beim Erstellen: $e')), ); } finally { setState(() => _loading = false); } } }, child: _loading ? const CircularProgressIndicator() : const Text('Erstellen'), ), ], ); } }