diff --git a/lib/blocs/balance/balance_bloc.dart b/lib/blocs/balance/balance_bloc.dart index 168ae00..9d7672c 100644 --- a/lib/blocs/balance/balance_bloc.dart +++ b/lib/blocs/balance/balance_bloc.dart @@ -5,6 +5,7 @@ import '../../services/balance_service.dart'; import '../../services/error_service.dart'; import 'balance_event.dart'; import 'balance_state.dart'; + class BalanceBloc extends Bloc { final BalanceRepository _balanceRepository; final BalanceService _balanceService; @@ -19,20 +20,28 @@ class BalanceBloc extends Bloc { _balanceService = balanceService ?? BalanceService(balanceRepository: balanceRepository, expenseRepository: expenseRepository), _errorService = errorService ?? ErrorService(), super(BalanceInitial()) { - on(_onLoadGroupBalance); + on(_onLoadGroupBalance); on(_onRefreshBalance); on(_onMarkSettlementAsCompleted); } Future _onLoadGroupBalance( - LoadGroupBalance event, + LoadGroupBalances event, Emitter emit, ) async { try { emit(BalanceLoading()); - final groupBalance = await _balanceRepository.calculateGroupBalance(event.groupId); - emit(BalanceLoaded(groupBalance)); + // Calculer les balances du groupe + final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId); + + // Calculer les règlements optimisés + final settlements = await _balanceService.calculateOptimalSettlements(event.groupId); + + emit(GroupBalancesLoaded( + balances: userBalances, + settlements: settlements, + )); } catch (e) { _errorService.logError('BalanceBloc', 'Erreur chargement balance: $e'); emit(BalanceError(e.toString())); @@ -45,12 +54,20 @@ class BalanceBloc extends Bloc { ) async { try { // Garde l'état actuel pendant le refresh si possible - if (state is! BalanceLoaded) { + if (state is! GroupBalancesLoaded) { emit(BalanceLoading()); } - final groupBalance = await _balanceRepository.calculateGroupBalance(event.groupId); - emit(BalanceLoaded(groupBalance)); + // Calculer les balances du groupe + final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId); + + // Calculer les règlements optimisés + final settlements = await _balanceService.calculateOptimalSettlements(event.groupId); + + emit(GroupBalancesLoaded( + balances: userBalances, + settlements: settlements, + )); } catch (e) { _errorService.logError('BalanceBloc', 'Erreur refresh balance: $e'); emit(BalanceError(e.toString())); diff --git a/lib/blocs/balance/balance_event.dart b/lib/blocs/balance/balance_event.dart index d7c2c4d..2c71710 100644 --- a/lib/blocs/balance/balance_event.dart +++ b/lib/blocs/balance/balance_event.dart @@ -7,13 +7,13 @@ abstract class BalanceEvent extends Equatable { List get props => []; } -class LoadGroupBalance extends BalanceEvent { +class LoadGroupBalances extends BalanceEvent { final String groupId; - - const LoadGroupBalance(this.groupId); - + + const LoadGroupBalances(this.groupId); + @override - List get props => [groupId]; + List get props => [groupId]; } class RefreshBalance extends BalanceEvent { diff --git a/lib/blocs/balance/balance_state.dart b/lib/blocs/balance/balance_state.dart index f79a956..b8ae1ce 100644 --- a/lib/blocs/balance/balance_state.dart +++ b/lib/blocs/balance/balance_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; -import '../../models/group_balance.dart'; +import '../../models/settlement.dart'; +import '../../models/user_balance.dart'; abstract class BalanceState extends Equatable { const BalanceState(); @@ -12,13 +13,17 @@ class BalanceInitial extends BalanceState {} class BalanceLoading extends BalanceState {} -class BalanceLoaded extends BalanceState { - final GroupBalance groupBalance; - - const BalanceLoaded(this.groupBalance); - +class GroupBalancesLoaded extends BalanceState { + final List balances; + final List settlements; + + const GroupBalancesLoaded({ + required this.balances, + required this.settlements, + }); + @override - List get props => [groupBalance]; + List get props => [balances, settlements]; } class BalanceOperationSuccess extends BalanceState { diff --git a/lib/components/account/account_content.dart b/lib/components/account/account_content.dart index 8a530df..fefbfd2 100644 --- a/lib/components/account/account_content.dart +++ b/lib/components/account/account_content.dart @@ -7,7 +7,8 @@ import '../../blocs/account/account_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:travel_mate/components/error/error_content.dart'; import '../../blocs/user/user_state.dart' as user_state; - +import '../../repositories/group_repository.dart'; // Ajouter cet import +import 'group_expenses_page.dart'; // Ajouter cet import class AccountContent extends StatefulWidget { const AccountContent({super.key}); @@ -17,6 +18,8 @@ class AccountContent extends StatefulWidget { } class _AccountContentState extends State { + final _groupRepository = GroupRepository(); // Ajouter cette ligne + @override void initState() { super.initState(); @@ -45,6 +48,44 @@ class _AccountContentState extends State { } } + // Nouvelle méthode pour naviguer vers la page des dépenses de groupe + Future _navigateToGroupExpenses(Account account) async { + try { + // Récupérer le groupe associé au compte + final group = await _groupRepository.getGroupByTripId(account.tripId); + + if (group != null && mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupExpensesPage( + account: account, + group: group, + ), + ), + ); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Groupe non trouvé pour ce compte'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors du chargement du groupe: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -221,7 +262,7 @@ class _AccountContentState extends State { ), subtitle: Text(memberInfo), trailing: const Icon(Icons.chevron_right), - onTap: () {}, + onTap: () => _navigateToGroupExpenses(account), // Modifier cette ligne ), ); } catch (e) { @@ -234,6 +275,4 @@ class _AccountContentState extends State { ); } } - - -} +} \ No newline at end of file diff --git a/lib/components/account/group_expenses_page.dart b/lib/components/account/group_expenses_page.dart index e69de29..71f8e42 100644 --- a/lib/components/account/group_expenses_page.dart +++ b/lib/components/account/group_expenses_page.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../blocs/expense/expense_bloc.dart'; +import '../../blocs/expense/expense_event.dart'; +import '../../blocs/expense/expense_state.dart'; +import '../../blocs/balance/balance_bloc.dart'; +import '../../blocs/balance/balance_event.dart'; +import '../../blocs/balance/balance_state.dart'; +import '../../blocs/user/user_bloc.dart'; +import '../../blocs/user/user_state.dart' as user_state; +import '../../models/account.dart'; +import '../../models/group.dart'; +import 'add_expense_dialog.dart'; +import 'balances_tab.dart'; +import 'expenses_tab.dart'; +import 'settlements_tab.dart'; + +class GroupExpensesPage extends StatefulWidget { + final Account account; + final Group group; + + const GroupExpensesPage({ + super.key, + required this.account, + required this.group, + }); + + @override + State createState() => _GroupExpensesPageState(); +} + +class _GroupExpensesPageState extends State + with SingleTickerProviderStateMixin { + + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void _loadData() { + // Charger les dépenses du groupe + context.read().add(LoadExpensesByGroup(widget.group.id)); + + // Charger les balances du groupe + context.read().add(LoadGroupBalances(widget.group.id)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.account.name), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + elevation: 0, + bottom: TabBar( + controller: _tabController, + indicatorColor: Colors.white, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + tabs: const [ + Tab( + icon: Icon(Icons.balance), + text: 'Balances', + ), + Tab( + icon: Icon(Icons.receipt_long), + text: 'Dépenses', + ), + Tab( + icon: Icon(Icons.payment), + text: 'Règlements', + ), + ], + ), + ), + body: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state is ExpenseOperationSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + ), + ); + _loadData(); // Recharger les données après une opération + } else if (state is ExpenseError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), + ); + } + }, + ), + ], + child: TabBarView( + controller: _tabController, + children: [ + // Onglet Balances + BlocBuilder( + builder: (context, state) { + if (state is BalanceLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is GroupBalancesLoaded) { + return BalancesTab(balances: state.balances); + } else if (state is BalanceError) { + return _buildErrorState('Erreur lors du chargement des balances: ${state.message}'); + } + return _buildEmptyState('Aucune balance disponible'); + }, + ), + + // Onglet Dépenses + BlocBuilder( + builder: (context, state) { + if (state is ExpenseLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is ExpensesLoaded) { + return ExpensesTab( + expenses: state.expenses, + group: widget.group, + ); + } else if (state is ExpenseError) { + return _buildErrorState('Erreur lors du chargement des dépenses: ${state.message}'); + } + return _buildEmptyState('Aucune dépense trouvée'); + }, + ), + + // Onglet Règlements + BlocBuilder( + builder: (context, state) { + if (state is BalanceLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is GroupBalancesLoaded) { + return SettlementsTab(settlements: state.settlements); + } else if (state is BalanceError) { + return _buildErrorState('Erreur lors du chargement des règlements: ${state.message}'); + } + return _buildEmptyState('Aucun règlement nécessaire'); + }, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _showAddExpenseDialog, + heroTag: "add_expense_fab", + tooltip: 'Ajouter une dépense', + child: const Icon(Icons.add), + ), + ); + } + + Widget _buildErrorState(String message) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 80, + color: Colors.red[300], + ), + const SizedBox(height: 16), + Text( + 'Erreur', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red[600], + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + message, + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: _loadData, + icon: const Icon(Icons.refresh), + label: const Text('Réessayer'), + ), + ], + ), + ); + } + + Widget _buildEmptyState(String message) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'Aucune donnée', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + message, + style: TextStyle( + fontSize: 16, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + void _showAddExpenseDialog() { + final userState = context.read().state; + + if (userState is user_state.UserLoaded) { + showDialog( + context: context, + builder: (context) => AddExpenseDialog( + group: widget.group, + currentUser: userState.user, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Erreur: utilisateur non connecté'), + backgroundColor: Colors.red, + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 62ef4e7..7dc298e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,11 +66,23 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => ExpenseRepository(), ), + // Provide service instances so BLoCs can read them with context.read() + RepositoryProvider( + create: (context) => ExpenseService( + expenseRepository: context.read(), + ), + ), RepositoryProvider( create: (context) => BalanceRepository( expenseRepository: context.read(), ), ), + RepositoryProvider( + create: (context) => BalanceService( + balanceRepository: context.read(), + expenseRepository: context.read(), + ), + ), ], child: MultiBlocProvider( diff --git a/lib/models/trip.dart b/lib/models/trip.dart index 117a11a..9851958 100644 --- a/lib/models/trip.dart +++ b/lib/models/trip.dart @@ -76,8 +76,6 @@ class Trip { status: map['status'] as String? ?? 'draft', ); } catch (e) { - print('❌ Erreur parsing Trip: $e'); - print('Map reçue: $map'); rethrow; } } diff --git a/lib/repositories/balance_repository.dart b/lib/repositories/balance_repository.dart index db8e941..54fc036 100644 --- a/lib/repositories/balance_repository.dart +++ b/lib/repositories/balance_repository.dart @@ -43,6 +43,19 @@ class BalanceRepository { } } + Future> calculateGroupUserBalances(String groupId) async { + try { + final expenses = await _expenseRepository + .getExpensesStream(groupId) + .first; + + return _calculateUserBalances(expenses); + } catch (e) { + _errorService.logError('BalanceRepository', 'Erreur calcul user balances: $e'); + rethrow; + } + } + // Calculer les balances individuelles List _calculateUserBalances(List expenses) { final Map> userBalanceMap = {}; diff --git a/lib/repositories/trip_repository.dart b/lib/repositories/trip_repository.dart index 38a4767..bf46ad6 100644 --- a/lib/repositories/trip_repository.dart +++ b/lib/repositories/trip_repository.dart @@ -7,35 +7,26 @@ class TripRepository { CollectionReference get _tripsCollection => _firestore.collection('trips'); // Récupérer tous les voyages d'un utilisateur - Stream> getTripsByUserId(String userId) { - print('🔍 Chargement des trips pour userId: $userId'); - + Stream> getTripsByUserId(String userId) { try { return _tripsCollection .where('participants', arrayContains: userId) .snapshots() - .map((snapshot) { - print('📦 Snapshot reçu: ${snapshot.docs.length} documents'); - + .map((snapshot) { final trips = snapshot.docs .map((doc) { try { final data = doc.data() as Map; - print('📄 Document ${doc.id}: ${data.keys.toList()}'); return Trip.fromMap(data, doc.id); } catch (e) { - print('❌ Erreur parsing trip ${doc.id}: $e'); return null; } }) .whereType() .toList(); - - print('✅ ${trips.length} trips parsés avec succès'); return trips; }); } catch (e) { - print('❌ Erreur getTripsByUserId: $e'); throw Exception('Erreur lors de la récupération des voyages: $e'); } } @@ -43,16 +34,11 @@ class TripRepository { // Créer un voyage et retourner son ID Future createTrip(Trip trip) async { try { - print('📝 Création du voyage: ${trip.title}'); - final tripData = trip.toMap(); // Ne pas modifier les timestamps ici, ils sont déjà au bon format final docRef = await _tripsCollection.add(tripData); - - print('✅ Voyage créé avec ID: ${docRef.id}'); return docRef.id; } catch (e) { - print('❌ Erreur création voyage: $e'); throw Exception('Erreur lors de la création du voyage: $e'); } } @@ -63,13 +49,11 @@ class TripRepository { final doc = await _tripsCollection.doc(tripId).get(); if (!doc.exists) { - print('⚠️ Voyage $tripId non trouvé'); return null; } return Trip.fromMap(doc.data() as Map, doc.id); } catch (e) { - print('❌ Erreur getTripById: $e'); throw Exception('Erreur lors de la récupération du voyage: $e'); } } @@ -77,17 +61,12 @@ class TripRepository { // Mettre à jour un voyage Future updateTrip(String tripId, Trip trip) async { try { - print('📝 Mise à jour du voyage: $tripId'); - final tripData = trip.toMap(); // Mettre à jour le timestamp de modification tripData['updatedAt'] = Timestamp.now(); await _tripsCollection.doc(tripId).update(tripData); - - print('✅ Voyage $tripId mis à jour'); } catch (e) { - print('❌ Erreur mise à jour voyage: $e'); throw Exception('Erreur lors de la mise à jour du voyage: $e'); } } @@ -95,13 +74,8 @@ class TripRepository { // Supprimer un voyage Future deleteTrip(String tripId) async { try { - print('🗑️ Suppression du voyage: $tripId'); - await _tripsCollection.doc(tripId).delete(); - - print('✅ Voyage $tripId supprimé'); } catch (e) { - print('❌ Erreur suppression voyage: $e'); throw Exception('Erreur lors de la suppression du voyage: $e'); } } diff --git a/lib/services/balance_service.dart b/lib/services/balance_service.dart index 712a024..eb65978 100644 --- a/lib/services/balance_service.dart +++ b/lib/services/balance_service.dart @@ -30,6 +30,20 @@ class BalanceService { } } + Future> calculateOptimalSettlements(String groupId) async { + try { + final expenses = await _expenseRepository + .getExpensesStream(groupId) + .first; + + final userBalances = calculateUserBalances(expenses); + return optimizeSettlements(userBalances); + } catch (e) { + _errorService.logError('BalanceService', 'Erreur calcul settlements: $e'); + rethrow; + } + } + /// Stream de la balance en temps réel Stream getGroupBalanceStream(String groupId) { return _expenseRepository.getExpensesStream(groupId).asyncMap((expenses) async {