import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../blocs/activity/activity_bloc.dart'; import '../../blocs/activity/activity_event.dart'; import '../../blocs/activity/activity_state.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../models/activity.dart'; import '../../models/trip.dart'; import '../../services/error_service.dart'; import 'activity_card.dart'; import 'add_activity_bottom_sheet.dart'; /// Page principale des activités pour un voyage class ActivitiesPage extends StatefulWidget { final Trip trip; const ActivitiesPage({ Key? key, required this.trip, }) : super(key: key); @override State createState() => _ActivitiesPageState(); } class _ActivitiesPageState extends State with TickerProviderStateMixin { late TabController _tabController; final ErrorService _errorService = ErrorService(); String? _selectedCategory; final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); // Charger les activités if (widget.trip.id != null) { context.read().add(LoadActivities(widget.trip.id!)); } } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: BlocListener( listener: (context, state) { if (state is ActivityOperationSuccess) { _errorService.showSnackbar(message: state.message, isError: false); } else if (state is ActivityError) { _errorService.showSnackbar(message: state.message, isError: true); } }, child: CustomScrollView( slivers: [ // AppBar personnalisée SliverAppBar( expandedHeight: 120, floating: false, pinned: true, backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), onPressed: () => Navigator.pop(context), ), actions: [ IconButton( icon: Icon(Icons.person_add, color: theme.colorScheme.onSurface), onPressed: () { // TODO: Ajouter participants }, ), ], flexibleSpace: FlexibleSpaceBar( titlePadding: const EdgeInsets.only(left: 16, bottom: 16), title: Text( 'Voyage à ${widget.trip.location}', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), ), ), // Barre de recherche SliverToBoxAdapter( child: Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.black.withOpacity(0.1), ), ), child: Row( children: [ Icon( Icons.search, color: theme.colorScheme.onSurface.withOpacity(0.5), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher restaurants, musées...', border: InputBorder.none, hintStyle: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.5), ), ), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, ), onSubmitted: (query) { if (query.isNotEmpty) { context.read().add( SearchActivitiesByText( tripId: widget.trip.id!, destination: widget.trip.location, query: query, ), ); } }, ), ), ], ), ), ), // Filtres par catégorie SliverToBoxAdapter( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Bouton suggestions Google const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _searchGoogleActivities, icon: const Icon(Icons.place), label: const Text('Découvrir des activités avec Google'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(height: 16), // Catégories populaires Text( 'Catégories populaires', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildCategoryButton(ActivityCategory.attraction, 'Attractions'), const SizedBox(width: 8), _buildCategoryButton(ActivityCategory.restaurant, 'Restaurants'), const SizedBox(width: 8), _buildCategoryButton(ActivityCategory.museum, 'Musées'), const SizedBox(width: 8), _buildCategoryButton(ActivityCategory.nature, 'Nature'), const SizedBox(width: 8), _buildCategoryButton(ActivityCategory.culture, 'Culture'), ], ), ), const SizedBox(height: 16), // Filtres Row( children: [ _buildFilterChip( label: 'Catégorie', icon: Icons.filter_list, isActive: _selectedCategory != null, onTap: _showCategoryFilter, ), const SizedBox(width: 8), _buildFilterChip( label: 'Prix', icon: Icons.euro, isActive: false, onTap: _showPriceFilter, ), const SizedBox(width: 8), _buildFilterChip( label: 'Heure', icon: Icons.access_time, isActive: false, onTap: () { // TODO: Filtre par heure }, ), ], ), ], ), ), ), // Onglets SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, indicator: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(8), ), labelColor: Colors.white, unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.7), dividerColor: Colors.transparent, tabs: const [ Tab(text: 'Suggestions'), Tab(text: 'Activités votées'), ], ), ), ), // Contenu des onglets SliverFillRemaining( child: TabBarView( controller: _tabController, children: [ _buildSuggestionsTab(), _buildVotedActivitiesTab(), ], ), ), ], ), ), // Bouton flottant pour ajouter une activité floatingActionButton: FloatingActionButton( onPressed: () => _showAddActivitySheet(), backgroundColor: Colors.blue, child: const Icon(Icons.add, color: Colors.white), ), ); } Widget _buildFilterChip({ required String label, required IconData icon, required bool isActive, required VoidCallback onTap, }) { final theme = Theme.of(context); return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: isActive ? Colors.blue : theme.cardColor, borderRadius: BorderRadius.circular(20), border: Border.all( color: isActive ? Colors.blue : theme.colorScheme.onSurface.withOpacity(0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 16, color: isActive ? Colors.white : theme.colorScheme.onSurface.withOpacity(0.7), ), const SizedBox(width: 4), Text( label, style: theme.textTheme.bodySmall?.copyWith( color: isActive ? Colors.white : theme.colorScheme.onSurface.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _buildSuggestionsTab() { return BlocBuilder( builder: (context, state) { if (state is ActivityLoading || state is ActivitySearching) { return const Center(child: CircularProgressIndicator()); } if (state is ActivitySearchResults) { return _buildActivityList(state.searchResults, isSearchResults: true); } if (state is ActivityLoaded) { return _buildActivityList(state.filteredActivities); } // État initial - montrer les suggestions par défaut return _buildInitialSuggestions(); }, ); } Widget _buildVotedActivitiesTab() { return BlocBuilder( builder: (context, state) { if (state is ActivityLoading) { return const Center(child: CircularProgressIndicator()); } if (state is ActivityLoaded) { // Filtrer les activités avec des votes final votedActivities = state.activities .where((activity) => activity.votes.isNotEmpty) .toList(); // Trier par score de vote votedActivities.sort((a, b) => b.totalVotes.compareTo(a.totalVotes)); return _buildActivityList(votedActivities); } return const Center( child: Text('Aucune activité votée pour le moment'), ); }, ); } Widget _buildActivityList(List activities, {bool isSearchResults = false}) { if (activities.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.explore_off, size: 64, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), ), const SizedBox(height: 16), Text( isSearchResults ? 'Aucun résultat trouvé' : 'Aucune activité pour le moment', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), ), ), const SizedBox(height: 8), Text( isSearchResults ? 'Essayez une autre recherche ou explorez les catégories' : 'Utilisez le bouton "Découvrir des activités avec Google" pour commencer', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), ), textAlign: TextAlign.center, ), if (isSearchResults) ...[ const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { context.read().add(LoadActivities(widget.trip.id!)); }, icon: const Icon(Icons.arrow_back), label: const Text('Retour aux suggestions'), ), ] else ...[ const SizedBox(height: 16), ElevatedButton.icon( onPressed: _searchGoogleActivities, icon: const Icon(Icons.place), label: const Text('Découvrir des activités'), ), ], ], ), ); } return BlocBuilder( builder: (context, userState) { final currentUserId = userState is user_state.UserLoaded ? userState.user.id : ''; return ListView.builder( padding: const EdgeInsets.all(16), itemCount: activities.length, itemBuilder: (context, index) { final activity = activities[index]; return Padding( padding: const EdgeInsets.only(bottom: 16), child: ActivityCard( activity: activity, currentUserId: currentUserId, onVote: (vote) => _handleVote(activity.id, currentUserId, vote), onAddToTrip: isSearchResults ? () => _addActivityToTrip(activity) : null, ), ); }, ); }, ); } Widget _buildInitialSuggestions() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.explore, size: 64, color: Theme.of(context).colorScheme.primary, ), const SizedBox(height: 16), Text( 'Découvrez ${widget.trip.location}', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( 'Utilisez le bouton "Découvrir des activités avec Google" ci-dessus\npour explorer les meilleures attractions de la région', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lightbulb_outline, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( 'Conseil : Explorez par catégorie ci-dessous', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ], ), ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: ActivityCategory.values.take(6).map((category) { return ElevatedButton.icon( onPressed: () => _searchByCategory(category), icon: Icon(_getCategoryIcon(category), size: 18), label: Text(category.displayName), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).cardColor, foregroundColor: Theme.of(context).colorScheme.onSurface, elevation: 2, ), ); }).toList(), ), ], ), ); } void _searchByCategory(ActivityCategory category) { context.read().add( SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: category, ), ); } void _handleVote(String activityId, String userId, int vote) { if (userId.isEmpty) { _errorService.showSnackbar( message: 'Vous devez être connecté pour voter', isError: true, ); return; } context.read().add( VoteForActivity( activityId: activityId, userId: userId, vote: vote, ), ); } void _addActivityToTrip(Activity activity) { context.read().add(AddActivity(activity)); } void _searchGoogleActivities() { // Rechercher toutes les catégories d'activités context.read().add( SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, // Null pour rechercher toutes les catégories ), ); } Widget _buildCategoryButton(ActivityCategory category, String label) { return ElevatedButton.icon( onPressed: () => _searchByCategory(category), icon: Icon(_getCategoryIcon(category), size: 18), label: Text(label), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), ); } void _showAddActivitySheet() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => AddActivityBottomSheet( trip: widget.trip, ), ); } void _showCategoryFilter() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => _buildCategoryFilterSheet(), ); } void _showPriceFilter() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => _buildPriceFilterSheet(), ); } Widget _buildCategoryFilterSheet() { final theme = Theme.of(context); return Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.onSurface.withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.all(16), child: Text( 'Filtrer par catégorie', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), ...ActivityCategory.values.map((category) { final isSelected = _selectedCategory == category.displayName; return ListTile( leading: Icon(_getCategoryIcon(category)), title: Text(category.displayName), trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null, onTap: () { setState(() { _selectedCategory = isSelected ? null : category.displayName; }); context.read().add( FilterActivities(category: _selectedCategory), ); Navigator.pop(context); }, ); }).toList(), const SizedBox(height: 16), ], ), ), ); } Widget _buildPriceFilterSheet() { final theme = Theme.of(context); return Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.onSurface.withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.all(16), child: Text( 'Filtrer par prix', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), ...PriceLevel.values.map((priceLevel) { return ListTile( leading: Icon(Icons.euro), title: Text(priceLevel.displayName), onTap: () { // TODO: Implémenter le filtre par prix Navigator.pop(context); }, ); }).toList(), const SizedBox(height: 16), ], ), ), ); } IconData _getCategoryIcon(ActivityCategory category) { switch (category) { case ActivityCategory.museum: return Icons.museum; case ActivityCategory.restaurant: return Icons.restaurant; case ActivityCategory.attraction: return Icons.place; case ActivityCategory.entertainment: return Icons.sports_esports; case ActivityCategory.shopping: return Icons.shopping_bag; case ActivityCategory.nature: return Icons.nature; case ActivityCategory.culture: return Icons.palette; case ActivityCategory.nightlife: return Icons.nightlife; case ActivityCategory.sports: return Icons.sports; case ActivityCategory.relaxation: return Icons.spa; } } }