import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:travel_mate/blocs/trip/trip_bloc.dart'; import 'package:travel_mate/blocs/trip/trip_event.dart'; import 'package:travel_mate/blocs/activity/activity_bloc.dart'; import 'package:travel_mate/blocs/activity/activity_event.dart'; import 'package:travel_mate/components/home/create_trip_content.dart'; import 'package:travel_mate/models/trip.dart'; import 'package:travel_mate/components/map/map_content.dart'; import 'package:travel_mate/services/error_service.dart'; import 'package:travel_mate/repositories/group_repository.dart'; import 'package:travel_mate/repositories/user_repository.dart'; import 'package:travel_mate/repositories/account_repository.dart'; import 'package:travel_mate/models/group_member.dart'; import 'package:travel_mate/components/activities/activities_page.dart'; import 'package:travel_mate/components/home/calendar/calendar_page.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:intl/intl.dart'; import 'package:travel_mate/models/activity.dart'; import 'package:travel_mate/blocs/activity/activity_state.dart'; import 'package:travel_mate/blocs/balance/balance_bloc.dart'; import 'package:travel_mate/blocs/balance/balance_event.dart'; import 'package:travel_mate/blocs/balance/balance_state.dart'; import 'package:travel_mate/models/settlement.dart'; import 'package:travel_mate/blocs/user/user_bloc.dart'; import 'package:travel_mate/blocs/user/user_state.dart' as user_state; import 'package:travel_mate/components/account/group_expenses_page.dart'; import 'package:travel_mate/models/group.dart'; import 'package:travel_mate/models/account.dart'; import 'package:travel_mate/models/user_balance.dart'; class ShowTripDetailsContent extends StatefulWidget { final Trip trip; const ShowTripDetailsContent({super.key, required this.trip}); @override State createState() => _ShowTripDetailsContentState(); } class _ShowTripDetailsContentState extends State { final ErrorService _errorService = ErrorService(); final GroupRepository _groupRepository = GroupRepository(); final UserRepository _userRepository = UserRepository(); final AccountRepository _accountRepository = AccountRepository(); Group? _group; Account? _account; @override void initState() { super.initState(); // Charger les activités du voyage depuis la DB if (widget.trip.id != null) { context.read().add(LoadActivities(widget.trip.id!)); _loadGroupAndAccount(); } } Future _loadGroupAndAccount() async { if (widget.trip.id == null) return; try { final group = await _groupRepository.getGroupByTripId(widget.trip.id!); final account = await _accountRepository.getAccountByTripId( widget.trip.id!, ); if (mounted) { setState(() { _group = group; _account = account; }); if (group != null) { context.read().add(LoadGroupBalances(group.id)); } } } catch (e) { _errorService.logError( 'ShowTripDetailsContent', 'Error loading group/account: $e', ); } } // Calculer les jours restants avant le voyage int get daysUntilTrip { final now = DateTime.now(); final tripStart = widget.trip.startDate; final difference = tripStart.difference(now).inDays; return difference > 0 ? difference : 0; } // Méthode pour ouvrir la carte interne void _openInternalMap() { Navigator.push( context, MaterialPageRoute( builder: (context) => MapContent(initialSearchQuery: widget.trip.location), ), ); } // Méthode pour afficher le dialogue de sélection de carte void _showMapOptions() { final theme = Theme.of(context); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface, title: Text( 'Ouvrir la carte', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Choisissez comment vous souhaitez ouvrir la carte :', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 20), // Options centrées verticalement Column( children: [ // Carte de l'application SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); _openInternalMap(); }, icon: const Icon(Icons.map), label: const Text('Carte de l\'app'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), const SizedBox(height: 12), // Google Maps SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); _openGoogleMaps(); }, icon: const Icon(Icons.directions), label: const Text('Google Maps'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), const SizedBox(height: 12), // Waze SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); _openWaze(); }, icon: const Icon(Icons.navigation), label: const Text('Waze'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), ], ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Annuler', style: TextStyle(color: theme.colorScheme.primary), ), ), ], ); }, ); } // Méthode pour ouvrir Google Maps Future _openGoogleMaps() async { final location = Uri.encodeComponent(widget.trip.location); try { // Essayer d'abord l'URL scheme pour l'app mobile final appUrl = 'comgooglemaps://?q=$location'; final appUri = Uri.parse(appUrl); if (await canLaunchUrl(appUri)) { await launchUrl(appUri); return; } // Fallback vers l'URL web final webUrl = 'https://www.google.com/maps/search/?api=1&query=$location'; final webUri = Uri.parse(webUrl); if (await canLaunchUrl(webUri)) { await launchUrl(webUri, mode: LaunchMode.externalApplication); return; } _errorService.showError( message: 'Impossible d\'ouvrir Google Maps. Vérifiez que l\'application est installée.', ); } catch (e) { _errorService.showError( message: 'Erreur lors de l\'ouverture de Google Maps', ); } } // Méthode pour ouvrir Waze Future _openWaze() async { final location = Uri.encodeComponent(widget.trip.location); try { // Essayer d'abord l'URL scheme pour l'app mobile final appUrl = 'waze://?q=$location'; final appUri = Uri.parse(appUrl); if (await canLaunchUrl(appUri)) { await launchUrl(appUri); return; } // Fallback vers l'URL web final webUrl = 'https://waze.com/ul?q=$location'; final webUri = Uri.parse(webUrl); if (await canLaunchUrl(webUri)) { await launchUrl(webUri, mode: LaunchMode.externalApplication); return; } _errorService.showError( message: 'Impossible d\'ouvrir Waze. Vérifiez que l\'application est installée.', ); } catch (e) { _errorService.showError(message: 'Erreur lors de l\'ouverture de Waze'); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return Scaffold( backgroundColor: isDarkMode ? theme.scaffoldBackgroundColor : Colors.grey[50], appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), onPressed: () => Navigator.pop(context), ), title: Text( widget.trip.title, style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), actions: [ IconButton( icon: Icon(Icons.more_vert, color: theme.colorScheme.onSurface), onPressed: () => _showOptionsMenu(), ), ], ), body: SingleChildScrollView( child: Column( children: [ // Image du voyage Container( height: 250, width: double.infinity, margin: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: widget.trip.imageUrl != null && widget.trip.imageUrl!.isNotEmpty ? Image.network( widget.trip.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildPlaceholderImage(), ) : _buildPlaceholderImage(), ), ), // Contenu principal Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section "Départ dans X jours" Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all( color: isDarkMode ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.1), width: 1, ), boxShadow: [ BoxShadow( color: isDarkMode ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1), blurRadius: isDarkMode ? 8 : 5, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.teal.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.flight_takeoff, color: Colors.teal, size: 20, ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Départ dans', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), ), Text( daysUntilTrip > 0 ? '$daysUntilTrip Jours' : 'Voyage terminé', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), Text( widget.trip.formattedDates, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), ), ], ), ], ), ), const SizedBox(height: 24), // Section Participants Text( 'Participants', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), // Afficher les participants avec leurs images _buildParticipantsSection(), const SizedBox(height: 32), // Grille d'actions GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, childAspectRatio: 1.5, crossAxisSpacing: 16, mainAxisSpacing: 16, children: [ _buildActionButton( icon: Icons.calendar_today, title: 'Calendrier', color: Colors.blue, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => CalendarPage(trip: widget.trip), ), ), ), _buildActionButton( icon: Icons.local_activity, title: 'Activités', color: Colors.green, onTap: () => _navigateToActivities(), ), _buildActionButton( icon: Icons.account_balance_wallet, title: 'Dépenses', color: Colors.orange, onTap: () { if (_group != null && _account != null) { Navigator.push( context, MaterialPageRoute( builder: (context) => GroupExpensesPage( group: _group!, account: _account!, ), ), ); } }, ), _buildActionButton( icon: Icons.map, title: 'Ouvrir la carte', color: Colors.purple, onTap: _showMapOptions, ), ], ), const SizedBox(height: 32), _buildNextActivitiesSection(), _buildExpensesCard(), ], ), ), ], ), ), ); } Widget _buildPlaceholderImage() { return Container( color: Colors.grey[200], child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.location_city, size: 48, color: Colors.grey), SizedBox(height: 8), Text( 'Aucune image', style: TextStyle(color: Colors.grey, fontSize: 14), ), ], ), ), ); } Widget _buildActionButton({ required IconData icon, required String title, required Color color, required VoidCallback onTap, }) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all( color: isDarkMode ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.1), width: 1, ), boxShadow: [ BoxShadow( color: isDarkMode ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1), blurRadius: isDarkMode ? 8 : 5, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 24), ), const SizedBox(height: 8), Text( title, style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), textAlign: TextAlign.center, ), ], ), ), ); } void _showOptionsMenu() { final theme = Theme.of(context); showModalBottomSheet( context: context, backgroundColor: theme.bottomSheetTheme.backgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) => Container( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.edit, color: theme.colorScheme.primary), title: Text( 'Modifier le voyage', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), onTap: () { Navigator.pop(context); Navigator.push( context, MaterialPageRoute( builder: (context) => CreateTripContent(tripToEdit: widget.trip), ), ); }, ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( 'Supprimer le voyage', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), onTap: () { Navigator.pop(context); _showDeleteConfirmation(); }, ), ], ), ), ); } void _showDeleteConfirmation() { final theme = Theme.of(context); showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface, title: Text( 'Confirmer la suppression', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), content: Text( 'Êtes-vous sûr de vouloir supprimer ce voyage ? Cette action est irréversible.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Annuler', style: TextStyle(color: theme.colorScheme.primary), ), ), TextButton( onPressed: () { context.read().add( TripDeleteRequested(tripId: widget.trip.id!), ); Navigator.pop(context); Navigator.pop(context, true); }, child: const Text('Supprimer', style: TextStyle(color: Colors.red)), ), ], ), ); } /// Construire la section des participants avec leurs images de profil Widget _buildParticipantsSection() { // Vérifier que le trip a un ID if (widget.trip.id == null || widget.trip.id!.isEmpty) { return const Center(child: Text('Aucun participant')); } return FutureBuilder( future: _groupRepository.getGroupByTripId(widget.trip.id!), builder: (context, groupSnapshot) { if (groupSnapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (groupSnapshot.hasError || !groupSnapshot.hasData || groupSnapshot.data == null) { return const Center(child: Text('Aucun participant')); } final groupId = groupSnapshot.data!.id; return StreamBuilder>( stream: _groupRepository.watchGroupMembers(groupId), builder: (context, snapshot) { // En attente if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } // Erreur if (snapshot.hasError) { return Center( child: Text( 'Erreur: ${snapshot.error}', style: TextStyle(color: Colors.red), ), ); } final members = snapshot.data ?? []; if (members.isEmpty) { return const Center(child: Text('Aucun participant')); } return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ ...List.generate(members.length, (index) { final member = members[index]; return Padding( padding: const EdgeInsets.only(right: 12), child: _buildParticipantAvatar(member), ); }), // Bouton "+" pour ajouter un participant Padding( padding: const EdgeInsets.only(right: 12), child: _buildAddParticipantButton(), ), ], ), ); }, ); }, ); } /// Construire un avatar pour un participant Widget _buildParticipantAvatar(dynamic member) { final theme = Theme.of(context); final initials = member.pseudo.isNotEmpty ? member.pseudo[0].toUpperCase() : (member.firstName.isNotEmpty ? member.firstName[0].toUpperCase() : '?'); final name = member.pseudo.isNotEmpty ? member.pseudo : member.firstName; return Tooltip( message: name, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.3), width: 2, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: CircleAvatar( radius: 28, backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2), backgroundImage: (member.profilePictureUrl != null && member.profilePictureUrl!.isNotEmpty) ? NetworkImage(member.profilePictureUrl!) : null, child: (member.profilePictureUrl == null || member.profilePictureUrl!.isEmpty) ? Text( initials, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ) : null, ), ), ); } /// Construire le bouton pour ajouter un participant Widget _buildAddParticipantButton() { final theme = Theme.of(context); return Tooltip( message: 'Ajouter un participant', child: Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.3), width: 2, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: GestureDetector( onTap: _showAddParticipantDialog, child: CircleAvatar( radius: 28, backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), child: Icon(Icons.add, color: theme.colorScheme.primary, size: 28), ), ), ), ); } /// Afficher le dialogue pour ajouter un participant void _showAddParticipantDialog() { final theme = Theme.of(context); final TextEditingController emailController = TextEditingController(); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface, title: Text( 'Ajouter un participant', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Entrez l\'email du participant à ajouter :', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 16), TextField( controller: emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: 'participant@example.com', hintStyle: TextStyle( color: theme.colorScheme.onSurface.withValues(alpha: 0.5), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), style: TextStyle(color: theme.colorScheme.onSurface), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Annuler', style: TextStyle(color: theme.colorScheme.primary), ), ), TextButton( onPressed: () { if (emailController.text.isNotEmpty) { _addParticipantByEmail(emailController.text); Navigator.pop(context); } else { _errorService.showError( message: 'Veuillez entrer un email valide', ); } }, child: Text( 'Ajouter', style: TextStyle(color: theme.colorScheme.primary), ), ), ], ); }, ); } /// Ajouter un participant par email Future _addParticipantByEmail(String email) async { try { // Chercher l'utilisateur par email final user = await _userRepository.getUserByEmail(email); if (user == null) { _errorService.showError( message: 'Utilisateur non trouvé avec cet email', ); return; } if (user.id == null) { _errorService.showError(message: 'ID utilisateur invalide'); return; } // Ajouter l'utilisateur au groupe if (widget.trip.id != null) { final group = await _groupRepository.getGroupByTripId(widget.trip.id!); if (group != null) { // Créer un GroupMember à partir du User final newMember = GroupMember( userId: user.id!, firstName: user.prenom, lastName: user.nom, pseudo: user.prenom, profilePictureUrl: user.profilePictureUrl, ); // Ajouter le membre au groupe await _groupRepository.addMember(group.id, newMember); // Ajouter le membre au compte final account = await _accountRepository.getAccountByTripId( widget.trip.id!, ); if (account != null) { await _accountRepository.addMemberToAccount(account.id, newMember); } // Mettre à jour la liste des participants du voyage final newParticipants = [...widget.trip.participants, user.id!]; final updatedTrip = widget.trip.copyWith( participants: newParticipants, ); if (mounted) { context.read().add( TripUpdateRequested(trip: updatedTrip), ); _errorService.showSnackbar( message: '${user.prenom} a été ajouté au voyage', isError: false, ); // Rafraîchir la page setState(() {}); } } } } catch (e) { _errorService.showError( message: 'Erreur lors de l\'ajout du participant: $e', ); } } void _navigateToActivities() { Navigator.push( context, MaterialPageRoute( builder: (context) => ActivitiesPage(trip: widget.trip), ), ); } Widget _buildNextActivitiesSection() { final theme = Theme.of(context); return BlocBuilder( builder: (context, state) { List activities = []; if (state is ActivityLoaded) { activities = state.activities; } // Filter scheduled activities and sort by date final scheduledActivities = activities .where((a) => a.date != null && a.date!.isAfter(DateTime.now())) .toList(); scheduledActivities.sort((a, b) => a.date!.compareTo(b.date!)); // Take next 3 activities final nextActivities = scheduledActivities.take(3).toList(); if (nextActivities.isEmpty) { return const SizedBox.shrink(); } return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Prochaines activités', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (context) => CalendarPage(trip: widget.trip), ), ), child: Text( 'Voir calendrier', style: TextStyle( color: Colors.teal, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 8), ...nextActivities.map((activity) { if (activity.date == null) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildActivityCard( title: activity.name, date: DateFormat( 'd MMM, HH:mm', 'fr_FR', ).format(activity.date!), icon: _getCategoryIcon(activity.category), ), ); }), ], ); }, ); } IconData _getCategoryIcon(String category) { if (category.toLowerCase().contains('musée')) return Icons.museum; if (category.toLowerCase().contains('restaurant')) return Icons.restaurant; if (category.toLowerCase().contains('nature')) return Icons.nature; if (category.toLowerCase().contains('photo')) return Icons.camera_alt; if (category.toLowerCase().contains('détente')) return Icons.icecream; return Icons.place; } Widget _buildActivityCard({ required String title, required String date, required IconData icon, }) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), border: Border.all( color: isDarkMode ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.teal.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: Colors.teal, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( date, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ), Icon( Icons.chevron_right, color: theme.colorScheme.onSurface.withValues(alpha: 0.4), ), ], ), ); } Widget _buildExpensesCard() { final theme = Theme.of(context); return BlocBuilder( builder: (context, state) { String balanceText = 'Chargement...'; bool isLoading = state is BalanceLoading; bool isPositive = true; if (state is GroupBalancesLoaded) { final userState = context.read().state; if (userState is user_state.UserLoaded) { final currentUserId = userState.user.id; // Filter settlements involving the current user final mySettlements = state.settlements .where( (s) => !s.isCompleted && (s.fromUserId == currentUserId || s.toUserId == currentUserId), ) .toList(); if (mySettlements.isEmpty) { // Check if user has a balance of 0 final myBalanceObj = state.balances.firstWhere( (b) => b.userId == currentUserId, orElse: () => const UserBalance( userId: '', userName: '', totalPaid: 0, totalOwed: 0, balance: 0, ), ); if (myBalanceObj.balance.abs() < 0.01) { balanceText = 'Vous êtes à jour'; } else { // Fallback to total balance if no settlements found but balance exists isPositive = myBalanceObj.balance >= 0; final amountStr = '${myBalanceObj.balance.abs().toStringAsFixed(2)} €'; balanceText = isPositive ? 'On vous doit $amountStr' : 'Vous devez $amountStr'; } } else { // Construct detailed string final debtsToPay = mySettlements .where((s) => s.fromUserId == currentUserId) .toList(); final debtsToReceive = mySettlements .where((s) => s.toUserId == currentUserId) .toList(); if (debtsToPay.isNotEmpty) { isPositive = false; final details = debtsToPay .map( (s) => '${s.amount.toStringAsFixed(2)}€ à ${s.toUserName}', ) .join(' et '); balanceText = 'Vous devez $details'; } else if (debtsToReceive.isNotEmpty) { isPositive = true; final details = debtsToReceive .map( (s) => '${s.amount.toStringAsFixed(2)}€ de ${s.fromUserName}', ) .join(' et '); balanceText = 'On vous doit $details'; // Or "X owes you..." but "On vous doit" is generic enough or we can be specific // Let's be specific as requested: "X doit vous payer..." or similar? // The user asked: "vous devez 21 euros à John..." (active voice for user paying). // For receiving, "John vous doit 21 euros..." would be symmetric. // Let's try to match the requested format for paying first. if (debtsToReceive.length == 1) { balanceText = '${debtsToReceive.first.fromUserName} vous doit ${debtsToReceive.first.amount.toStringAsFixed(2)}€'; } else { balanceText = debtsToReceive .map( (s) => '${s.fromUserName} (${s.amount.toStringAsFixed(2)}€)', ) .join(' et ') + ' vous doivent de l\'argent'; } } } } } return GestureDetector( onTap: () { if (_group != null && _account != null) { Navigator.push( context, MaterialPageRoute( builder: (context) => GroupExpensesPage(group: _group!, account: _account!), ), ); } }, child: Container( margin: const EdgeInsets.only(top: 24), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFDF4E3), // Light beige background borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: const BoxDecoration( color: Colors.orange, shape: BoxShape.circle, ), child: const Icon( Icons.warning_amber_rounded, color: Colors.white, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Dépenses', style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: const Color(0xFF5D4037), // Brown text ), ), const SizedBox(height: 4), if (isLoading) const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) else Text( balanceText, style: theme.textTheme.bodyMedium?.copyWith( color: const Color(0xFF8D6E63), // Lighter brown ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), TextButton( onPressed: () { if (_group != null && _account != null) { Navigator.push( context, MaterialPageRoute( builder: (context) => GroupExpensesPage( group: _group!, account: _account!, ), ), ); } }, child: Text( 'Régler', style: TextStyle( color: const Color(0xFF5D4037), fontWeight: FontWeight.bold, ), ), ), ], ), ), ); }, ); } }