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 '../../models/trip.dart'; import '../../models/activity.dart'; import '../activities/add_activity_bottom_sheet.dart'; class ActivitiesPage extends StatefulWidget { final Trip trip; const ActivitiesPage({ super.key, required this.trip, }); @override State createState() => _ActivitiesPageState(); } class _ActivitiesPageState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { late TabController _tabController; final TextEditingController _searchController = TextEditingController(); String _selectedCategory = 'Toutes les catégories'; String _selectedPrice = 'Prix'; String _selectedRating = 'Note'; // Cache pour éviter de recharger les données bool _activitiesLoaded = false; bool _googleSearchPerformed = false; // Variables pour stocker les activités localement List _tripActivities = []; List _approvedActivities = []; bool _isLoadingTripActivities = false; int _totalGoogleActivitiesRequested = 0; // Compteur pour les recherches progressives bool _autoReloadInProgress = false; // Protection contre les rechargements en boucle int _lastAutoReloadTriggerCount = 0; // Éviter de redéclencher pour le même nombre @override bool get wantKeepAlive => true; // Maintient l'état de la page @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // Charger les activités au démarrage WidgetsBinding.instance.addPostFrameCallback((_) { _loadActivitiesIfNeeded(); }); // Écouter les changements d'onglets _tabController.addListener(() { if (_tabController.indexIsChanging) { _handleTabChange(); } }); } @override void didChangeDependencies() { super.didChangeDependencies(); // Recharger si nécessaire quand on revient sur la page if (!_activitiesLoaded) { _loadActivitiesIfNeeded(); } } void _loadActivitiesIfNeeded() { final state = context.read().state; if (state is! ActivityLoaded || !_activitiesLoaded) { _loadActivities(); } } void _handleTabChange() { // Si on va sur l'onglet suggestions Google et qu'aucune recherche n'a été faite if (_tabController.index == 2 && !_googleSearchPerformed) { _searchGoogleActivities(); } } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } void _loadActivities() { setState(() { _isLoadingTripActivities = true; }); context.read().add(LoadActivities(widget.trip.id!)); _activitiesLoaded = true; } @override Widget build(BuildContext context) { super.build(context); // Nécessaire pour AutomaticKeepAliveClientMixin final theme = Theme.of(context); return BlocListener( listener: (context, state) { if (state is ActivityError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, action: SnackBarAction( label: 'Réessayer', textColor: Colors.white, onPressed: () { if (_tabController.index == 2) { _searchGoogleActivities(); } else { _loadActivities(); } }, ), ), ); } if (state is ActivityLoaded) { print('✅ Activités chargées: ${state.activities.length}'); // Stocker les activités localement WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _tripActivities = state.activities; _approvedActivities = state.activities.where((a) => a.totalVotes > 0).toList(); _isLoadingTripActivities = false; }); print('🔄 [ActivityLoaded] Activités du voyage mises à jour: ${_tripActivities.length}'); // Vérifier si on a besoin de charger plus d'activités dans les suggestions Future.delayed(const Duration(milliseconds: 500), () { print('🚀 [ActivityLoaded] Déclenchement de la vérification auto-reload'); _checkAndLoadMoreActivitiesIfNeeded(); }); }); } if (state is ActivitySearchResults) { print('🔍 Résultats Google: ${state.searchResults.length}'); // Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats) // et pas pour les rechargements automatiques if (state.searchResults.length <= 6 && !_autoReloadInProgress) { WidgetsBinding.instance.addPostFrameCallback((_) { print('🎯 [ActivitySearchResults] Première recherche avec peu de résultats, vérification auto-reload'); Future.delayed(const Duration(milliseconds: 500), () { _checkAndLoadMoreActivitiesIfNeeded(); }); }); } } if (state is ActivityVoteRecorded) { print('�️ Vote enregistré pour activité: ${state.activityId}'); // Recharger les activités du voyage pour mettre à jour les votes _loadActivities(); } if (state is ActivityAdded) { print('✅ Activité ajoutée avec succès: ${state.activity.name}'); // Recharger automatiquement les activités du voyage _loadActivities(); } }, child: Scaffold( backgroundColor: theme.colorScheme.surface, appBar: AppBar( title: Text( 'Voyage à ${widget.trip.location}', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), backgroundColor: theme.colorScheme.surface, elevation: 0, foregroundColor: theme.colorScheme.onSurface, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: _showAddActivityBottomSheet, ), ], ), body: Column( children: [ // Barre de recherche _buildSearchBar(theme), // Filtres _buildFilters(theme), // Onglets de catégories _buildCategoryTabs(theme), // Contenu des onglets Expanded( child: TabBarView( controller: _tabController, children: [ _buildTripActivitiesTab(), _buildApprovedActivitiesTab(), _buildGoogleSuggestionsTab(), ], ), ), ], ), ), ); } Widget _buildSearchBar(ThemeData theme) { return Container( padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher restaurants, musées...', hintStyle: TextStyle( color: theme.colorScheme.onSurface.withOpacity(0.6), ), prefixIcon: Icon( Icons.search, color: theme.colorScheme.onSurface.withOpacity(0.6), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), onChanged: (value) { // TODO: Implémenter la recherche }, ), ), ); } Widget _buildFilters(ThemeData theme) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: _buildFilterButton( theme, _selectedCategory, Icons.category, () => _showCategoryFilter(), ), ), const SizedBox(width: 12), _buildFilterButton( theme, _selectedPrice, Icons.euro, () => _showPriceFilter(), ), const SizedBox(width: 12), _buildFilterButton( theme, _selectedRating, Icons.star, () => _showRatingFilter(), ), ], ), ); } Widget _buildFilterButton( ThemeData theme, String text, IconData icon, VoidCallback onPressed, ) { return GestureDetector( onTap: onPressed, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( border: Border.all( color: theme.colorScheme.outline.withOpacity(0.5), ), borderRadius: BorderRadius.circular(20), color: theme.colorScheme.surface, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 16, color: theme.colorScheme.onSurface.withOpacity(0.7), ), const SizedBox(width: 6), Text( text, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _buildCategoryTabs(ThemeData theme) { return Container( padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, labelColor: Colors.white, unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.7), indicator: BoxDecoration( color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(6), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, tabs: const [ Tab(text: 'Activités du voyage'), Tab(text: 'Activités approuvées'), Tab(text: 'Suggestions Google'), ], ), ), ); } void _showCategoryFilter() { showModalBottomSheet( context: context, builder: (context) => _buildCategoryFilterSheet(), ); } void _showPriceFilter() { showModalBottomSheet( context: context, builder: (context) => _buildPriceFilterSheet(), ); } void _showRatingFilter() { showModalBottomSheet( context: context, builder: (context) => _buildRatingFilterSheet(), ); } Widget _buildCategoryFilterSheet() { final theme = Theme.of(context); final categories = [ 'Toutes les catégories', ...ActivityCategory.values.map((e) => e.displayName), ]; return Container( padding: const EdgeInsets.all(16), constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, // Limite à 70% de l'écran ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Catégories', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Flexible( child: SingleChildScrollView( child: Column( children: categories.map((category) => ListTile( title: Text(category), onTap: () { setState(() { _selectedCategory = category; }); Navigator.pop(context); _applyFilters(); }, trailing: _selectedCategory == category ? Icon(Icons.check, color: theme.colorScheme.primary) : null, )).toList(), ), ), ), ], ), ); } Widget _buildPriceFilterSheet() { final theme = Theme.of(context); final prices = ['Prix', 'Gratuit', 'Bon marché', 'Modéré', 'Cher', 'Très cher']; return Container( padding: const EdgeInsets.all(16), constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Niveau de prix', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Flexible( child: SingleChildScrollView( child: Column( children: prices.map((price) => ListTile( title: Text(price), onTap: () { setState(() { _selectedPrice = price; }); Navigator.pop(context); _applyFilters(); }, trailing: _selectedPrice == price ? Icon(Icons.check, color: theme.colorScheme.primary) : null, )).toList(), ), ), ), ], ), ); } Widget _buildRatingFilterSheet() { final theme = Theme.of(context); final ratings = ['Note', '4+ étoiles', '3+ étoiles', '2+ étoiles', '1+ étoiles']; return Container( padding: const EdgeInsets.all(16), constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Note minimale', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Flexible( child: SingleChildScrollView( child: Column( children: ratings.map((rating) => ListTile( title: Text(rating), onTap: () { setState(() { _selectedRating = rating; }); Navigator.pop(context); _applyFilters(); }, trailing: _selectedRating == rating ? Icon(Icons.check, color: theme.colorScheme.primary) : null, )).toList(), ), ), ), ], ), ); } void _applyFilters() { String? category = _selectedCategory == 'Toutes les catégories' ? null : _selectedCategory; double? minRating = _getMinRatingFromString(_selectedRating); context.read().add(FilterActivities( category: category, minRating: minRating, )); } double? _getMinRatingFromString(String rating) { switch (rating) { case '4+ étoiles': return 4.0; case '3+ étoiles': return 3.0; case '2+ étoiles': return 2.0; case '1+ étoiles': return 1.0; default: return null; } } void _showAddActivityBottomSheet() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: AddActivityBottomSheet(trip: widget.trip), ), ); } Widget _buildTripActivitiesTab() { // Utiliser les données locales au lieu du BLoC if (_isLoadingTripActivities) { return const Center(child: CircularProgressIndicator()); } if (_tripActivities.isEmpty) { return _buildEmptyState( 'Aucune activité du voyage', 'Ajoutez vos premières activités pour ce voyage', Icons.add_location, ); } return _buildActivityList(_tripActivities); } Widget _buildApprovedActivitiesTab() { // Utiliser les données locales au lieu du BLoC if (_isLoadingTripActivities) { return const Center(child: CircularProgressIndicator()); } if (_approvedActivities.isEmpty) { return _buildEmptyState( 'Aucune activité approuvée', 'Les activités avec des votes positifs apparaîtront ici', Icons.thumb_up_outlined, ); } return _buildActivityList(_approvedActivities); } Widget _buildGoogleSuggestionsTab() { return BlocBuilder( builder: (context, state) { if (state is ActivitySearching) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( 'Recherche d\'activités en cours...', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ); } if (state is ActivitySearchResults) { final googleActivities = state.searchResults; // Filtrer les activités déjà présentes dans le voyage final filteredActivities = googleActivities.where((googleActivity) { return !_tripActivities.any((tripActivity) => tripActivity.name.toLowerCase().trim() == googleActivity.name.toLowerCase().trim()); }).toList(); print('🔍 [Google Search] ${googleActivities.length} résultats trouvés, ${filteredActivities.length} après filtrage'); if (filteredActivities.isEmpty && googleActivities.isNotEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Toutes les activités sont déjà dans votre voyage', 'Recherchez plus d\'activités pour découvrir de nouvelles suggestions', Icons.check_circle, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _loadMoreGoogleActivities(), icon: const Icon(Icons.add_circle_outline), label: const Text('Rechercher plus d\'activités'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ); } if (filteredActivities.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Aucun résultat trouvé', 'Aucune activité trouvée pour cette destination', Icons.search_off, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _searchGoogleActivities(), icon: const Icon(Icons.refresh), label: const Text('Rechercher à nouveau'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ); } return Column( children: [ // Liste des activités Expanded( child: RefreshIndicator( onRefresh: () async { _resetAndSearchGoogleActivities(); }, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: filteredActivities.length, itemBuilder: (context, index) { final activity = filteredActivities[index]; return _buildActivityCard(activity, isGoogleSuggestion: true); }, ), ), ), // Bouton "Rechercher plus d'activités" if (state.isLoading) Container( padding: const EdgeInsets.all(16), child: const Center( child: CircularProgressIndicator(), ), ) else Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), child: ElevatedButton.icon( onPressed: () => _loadMoreGoogleActivities(), icon: const Icon(Icons.add_circle_outline), label: const Text('Rechercher plus d\'activités'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), // Bouton de debug temporaire Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), child: OutlinedButton.icon( onPressed: () { print('🧪 [DEBUG] Force auto-reload check - État actuel:'); print('🧪 [DEBUG] _tripActivities: ${_tripActivities.length}'); print('🧪 [DEBUG] _autoReloadInProgress: $_autoReloadInProgress'); print('🧪 [DEBUG] _lastAutoReloadTriggerCount: $_lastAutoReloadTriggerCount'); _checkAndLoadMoreActivitiesIfNeeded(); }, icon: const Icon(Icons.bug_report, size: 16), label: const Text('🧪 Test Auto-Reload'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 8), ), ), ), ], ), ], ); } // État initial - aucune recherche effectuée return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Découvrir des activités', 'Trouvez des restaurants, musées et attractions près de ${widget.trip.location}', Icons.explore, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _searchGoogleActivities(), icon: const Icon(Icons.search), label: const Text('Rechercher des activités (6 résultats)'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), ), const SizedBox(height: 16), Text( 'Recherche powered by Google Places', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), ], ); }, ); } Widget _buildEmptyState(String title, String subtitle, IconData icon) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 64, color: theme.colorScheme.outline, ), const SizedBox(height: 16), Text( title, style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( subtitle, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ), ), ); } Widget _buildActivityList(List activities) { return RefreshIndicator( onRefresh: () async { // Rafraîchir selon l'onglet actuel if (_tabController.index == 2) { // Onglet Google - relancer la recherche _searchGoogleActivities(); } else { // Onglets activités du voyage - recharger depuis la base de données _loadActivities(); } }, child: activities.isEmpty ? ListView( children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: _buildEmptyState( 'Aucune activité', 'Tirez vers le bas pour actualiser', Icons.refresh, ), ), ], ) : ListView.builder( padding: const EdgeInsets.all(16), itemCount: activities.length, itemBuilder: (context, index) { final activity = activities[index]; return _buildActivityCard(activity); }, ), ); } Widget _buildActivityCard(Activity activity, {bool isGoogleSuggestion = false}) { final theme = Theme.of(context); // Vérifier si l'activité existe déjà dans le voyage (pour les suggestions Google) final bool activityAlreadyExists = isGoogleSuggestion && _tripActivities.any((tripActivity) => tripActivity.name.toLowerCase().trim() == activity.name.toLowerCase().trim()); return Card( margin: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image de l'activité if (activity.imageUrl != null && activity.imageUrl!.isNotEmpty) ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Container( height: 200, width: double.infinity, child: Image.network( activity.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( height: 200, color: theme.colorScheme.surfaceContainerHighest, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.image_not_supported, size: 48, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(height: 8), Text( 'Image non disponible', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ); }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( height: 200, color: theme.colorScheme.surfaceContainerHighest, child: Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ), ); }, ), ), ), // Contenu de la carte Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Icône de catégorie Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( _getCategoryIcon(activity.category), color: theme.colorScheme.primary, size: 20, ), ), const SizedBox(width: 12), // Nom et catégorie Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( activity.name, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), Text( activity.category, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, ), ), ], ), ), // Note if (activity.rating != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.amber.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.star, color: Colors.amber, size: 16), const SizedBox(width: 4), Text( activity.rating!.toStringAsFixed(1), style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ), ], ), if (activity.description.isNotEmpty) ...[ const SizedBox(height: 12), Text( activity.description, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.8), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], if (activity.address != null) ...[ const SizedBox(height: 8), Row( children: [ Icon( Icons.location_on, size: 16, color: theme.colorScheme.onSurface.withOpacity(0.6), ), const SizedBox(width: 4), Expanded( child: Text( activity.address!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), ), ], ), ], const SizedBox(height: 12), // Boutons d'action et votes (différents selon le contexte) if (isGoogleSuggestion) ...[ // Pour les suggestions Google : bouton d'ajout ou indication si déjà ajoutée Row( children: [ if (activityAlreadyExists) ...[ // Activité déjà dans le voyage Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, size: 16, color: Colors.orange.shade700, ), const SizedBox(width: 6), Text( 'Déjà dans le voyage', style: theme.textTheme.bodySmall?.copyWith( color: Colors.orange.shade700, fontWeight: FontWeight.w600, ), ), ], ), ), ] else ...[ // Bouton pour ajouter l'activité Expanded( child: ElevatedButton.icon( onPressed: () => _addGoogleActivityToTrip(activity), icon: const Icon(Icons.add, size: 18), label: const Text('Ajouter au voyage'), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ], ), ] else ...[ // Pour les activités du voyage : système de votes Row( children: [ // Votes positifs (pouces verts) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${activity.positiveVotes}', style: theme.textTheme.bodySmall?.copyWith( color: Colors.green.shade700, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), Icon( Icons.thumb_up, size: 16, color: Colors.green.shade700, ), ], ), ), const SizedBox(width: 8), // Votes négatifs (pouces rouges) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${activity.negativeVotes}', style: theme.textTheme.bodySmall?.copyWith( color: Colors.red.shade700, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), Icon( Icons.thumb_down, size: 16, color: Colors.red.shade700, ), ], ), ), const Spacer(), // Bouton J'aime/J'aime pas IconButton( onPressed: () => _voteForActivity(activity.id, 1), icon: const Icon(Icons.thumb_up), iconSize: 20, ), IconButton( onPressed: () => _voteForActivity(activity.id, -1), icon: const Icon(Icons.thumb_down), iconSize: 20, ), ], ), ], ], ), ), ], ), ); } IconData _getCategoryIcon(String category) { switch (category.toLowerCase()) { case 'museum': case 'musée': return Icons.museum; case 'restaurant': return Icons.restaurant; case 'attraction': return Icons.place; case 'divertissement': case 'entertainment': return Icons.sports_esports; case 'shopping': return Icons.shopping_bag; case 'nature': return Icons.park; case 'culture': return Icons.account_balance; case 'vie nocturne': case 'nightlife': return Icons.nightlife; case 'sports': return Icons.sports; case 'détente': case 'relaxation': return Icons.spa; default: return Icons.place; } } void _voteForActivity(String activityId, int vote) { print('🗳️ Vote pour activité $activityId: $vote'); // TODO: Récupérer l'ID utilisateur actuel // Pour l'instant, on utilise un ID temporaire final userId = 'current_user_id'; context.read().add(VoteForActivity( activityId: activityId, userId: userId, vote: vote, )); // Afficher un feedback à l'utilisateur final message = vote == 1 ? 'Vote positif ajouté !' : 'Vote négatif ajouté !'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), duration: const Duration(seconds: 1), backgroundColor: vote == 1 ? Colors.green : Colors.orange, ), ); } void _addGoogleActivityToTrip(Activity activity) { print('➕ [Add Activity] Adding ${activity.name} to trip'); // Créer une nouvelle activité avec l'ID du voyage final newActivity = activity.copyWith( tripId: widget.trip.id, // Générer un nouvel ID unique pour cette activité dans le voyage id: DateTime.now().millisecondsSinceEpoch.toString(), ); context.read().add(AddActivity(newActivity)); // Afficher un feedback à l'utilisateur ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${activity.name} ajoutée au voyage !'), duration: const Duration(seconds: 2), backgroundColor: Colors.green, action: SnackBarAction( label: 'Voir', textColor: Colors.white, onPressed: () { // Revenir à l'onglet des activités du voyage _tabController.animateTo(0); }, ), ), ); } void _checkAndLoadMoreActivitiesIfNeeded() { // Protection contre les rechargements en boucle if (_autoReloadInProgress) { print('⏸️ [Auto-reload] Auto-reload déjà en cours, skip'); return; } final currentState = context.read().state; if (currentState is ActivitySearchResults) { final googleActivities = currentState.searchResults; print('🔍 [Auto-reload] Activités du voyage en mémoire: ${_tripActivities.length}'); print('🔍 [Auto-reload] Activités Google total: ${googleActivities.length}'); // Filtrer les activités déjà présentes dans le voyage final filteredActivities = googleActivities.where((googleActivity) { final isDuplicate = _tripActivities.any((tripActivity) => tripActivity.name.toLowerCase().trim() == googleActivity.name.toLowerCase().trim()); if (isDuplicate) { print('🔍 [Auto-reload] Activité filtrée: ${googleActivity.name}'); } return !isDuplicate; }).toList(); print('🔍 [Auto-reload] ${filteredActivities.length} activités visibles après filtrage sur ${googleActivities.length} total'); // Protection: ne pas redéclencher pour le même nombre d'activités Google if (googleActivities.length == _lastAutoReloadTriggerCount) { print('🔒 [Auto-reload] Même nombre qu\'avant (${googleActivities.length}), skip pour éviter la boucle'); return; } // Si on a moins de 4 activités visibles ET qu'on n'a pas déjà beaucoup d'activités Google if (filteredActivities.length < 4 && googleActivities.length < 20) { _autoReloadInProgress = true; _lastAutoReloadTriggerCount = googleActivities.length; // Calculer combien d'activités on doit demander final activitiesNeeded = 6 - filteredActivities.length; // Manque pour arriver à 6 final newTotalToRequest = googleActivities.length + activitiesNeeded + 6; // Activités actuelles + ce qui manque + buffer de 6 print('🔄 [Auto-reload] DÉCLENCHEMENT: Besoin de ${activitiesNeeded} activités supplémentaires'); print('📊 [Auto-reload] Demande totale: ${newTotalToRequest} activités (actuellement: ${googleActivities.length})'); // Mettre à jour le compteur et recharger avec le nouveau total _totalGoogleActivitiesRequested = newTotalToRequest; _loadMoreGoogleActivitiesWithTotal(newTotalToRequest); // Libérer le verrou après un délai Future.delayed(const Duration(seconds: 3), () { _autoReloadInProgress = false; print('🔓 [Auto-reload] Verrou libéré'); }); } else if (filteredActivities.length >= 4) { print('✅ [Auto-reload] Suffisamment d\'activités visibles (${filteredActivities.length} >= 4)'); } else { print('🚫 [Auto-reload] Trop d\'activités Google déjà chargées (${googleActivities.length} >= 20), arrêt auto-reload'); } } else { print('⚠️ [Auto-reload] État pas prêt pour auto-chargement: ${currentState.runtimeType}'); } } void _searchGoogleActivities() { print('🔍 [Google Search] Initializing first search with 6 results'); _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; context.read().add(SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, // Rechercher dans toutes les catégories maxResults: 6, // Charger 6 résultats à la fois reset: true, // Nouveau flag pour reset )); _googleSearchPerformed = true; } void _resetAndSearchGoogleActivities() { print('🔄 [Google Search] Resetting and starting fresh search'); _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; context.read().add(SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: 6, reset: true, )); _googleSearchPerformed = true; } void _loadMoreGoogleActivities() { print('📄 [Google Search] Loading more activities (next 6 results)'); final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final newTotal = currentCount + 6; print('📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal'); _totalGoogleActivitiesRequested = newTotal; context.read().add(SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: newTotal, // Demander le total cumulé reset: true, // Reset pour avoir tous les résultats d'un coup )); } } void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) { print('📈 [Google Search] Loading activities with specific total: $totalToRequest'); // Au lieu de reset, on utilise l'offset et append pour forcer plus de résultats final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final additionalNeeded = totalToRequest - currentCount; print('📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded'); if (additionalNeeded > 0) { context.read().add(SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: additionalNeeded, offset: currentCount, appendToExisting: true, // Ajouter aux résultats existants )); } else { print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)'); } } else { // Si pas de résultats existants, faire une recherche complète context.read().add(SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: totalToRequest, reset: true, )); } } }