diff --git a/lib/blocs/account/account_bloc.dart b/lib/blocs/account/account_bloc.dart index e69de29..982e0e1 100644 --- a/lib/blocs/account/account_bloc.dart +++ b/lib/blocs/account/account_bloc.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:travel_mate/services/error_service.dart'; +import 'account_event.dart'; +import 'account_state.dart'; +import '../../repositories/account_repository.dart'; +import '../../data/models/account.dart'; + +class AccountBloc extends Bloc { + final AccountRepository _repository; + StreamSubscription? _accountsSubscription; + final _errorService = ErrorService(); + + AccountBloc(this._repository) : super(AccountInitial()) { + on(_onLoadAccountsByUserId); + on<_AccountsUpdated>(_onAccountsUpdated); + on(_onCreateAccount); + on(_onCreateAccountWithMembers); + } + + Future _onLoadAccountsByUserId( + LoadAccountsByUserId event, + Emitter emit, + ) async { + try { + emit(AccountLoading()); + await _accountsSubscription?.cancel(); + _accountsSubscription = _repository.getAccountByUserId(event.userId).listen( + (accounts) { + add(_AccountsUpdated(accounts)); + }, + onError: (error) { + add(_AccountsUpdated([], error: error.toString())); + }, + ); + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + emit(AccountError(e.toString())); + } + } + + Future _onAccountsUpdated( + _AccountsUpdated event, + Emitter emit, + ) async { + if (event.error != null) { + _errorService.logError(event.error!, StackTrace.current); + emit(AccountError(event.error!)); + } else { + emit(AccountsLoaded(event.accounts)); + } + } + + Future _onCreateAccount( + CreateAccount event, + Emitter emit, + ) async { + try { + emit(AccountLoading()); + final accountId = await _repository.createAccountWithMembers( + account: event.account, + members: [], + ); + emit(AccountOperationSuccess('Compte créé avec succès. ID: $accountId')); + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + emit(AccountError('Erreur lors de la création du compte: ${e.toString()}')); + } + } + + Future _onCreateAccountWithMembers( + CreateAccountWithMembers event, + Emitter emit, + ) async { + try{ + emit(AccountLoading()); + final accountId = await _repository.createAccountWithMembers( + account: event.account, + members: event.members, + ); + emit(AccountOperationSuccess('Compte créé avec succès. ID: $accountId')); + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + emit(AccountError('Erreur lors de la création du compte: ${e.toString()}')); + } + } + + @override + Future close() { + _accountsSubscription?.cancel(); + return super.close(); + } +} + +class _AccountsUpdated extends AccountEvent { + final List accounts; + final String? error; + + const _AccountsUpdated(this.accounts, {this.error}); + + @override + List get props => [accounts, error]; +} \ No newline at end of file diff --git a/lib/blocs/account/account_event.dart b/lib/blocs/account/account_event.dart index e69de29..3b1b33d 100644 --- a/lib/blocs/account/account_event.dart +++ b/lib/blocs/account/account_event.dart @@ -0,0 +1,63 @@ +import 'package:equatable/equatable.dart'; +import '../../data/models/account.dart'; +import '../../data/models/group_member.dart'; + +abstract class AccountEvent extends Equatable { + const AccountEvent(); + + @override + List get props => []; +} + +class LoadAccountsByUserId extends AccountEvent { + final String userId; + + const LoadAccountsByUserId(this.userId); + + @override + List get props => [userId]; +} + +class LoadAccountsByTrip extends AccountEvent { + final String tripId; + + const LoadAccountsByTrip(this.tripId); + + @override + List get props => [tripId]; +} + +class CreateAccount extends AccountEvent { + final Account account; + + const CreateAccount(this.account); + + @override + List get props => [account]; +} + +class UpdateAccount extends AccountEvent { + final String accountId; + final Account account; + + const UpdateAccount({ + required this.accountId, + required this.account, + }); + + @override + List get props => [accountId, account]; +} + +class CreateAccountWithMembers extends AccountEvent { + final Account account; + final List members; + + const CreateAccountWithMembers({ + required this.account, + required this.members, + }); + + @override + List get props => [account, members]; +} \ No newline at end of file diff --git a/lib/blocs/account/account_state.dart b/lib/blocs/account/account_state.dart index e69de29..21db115 100644 --- a/lib/blocs/account/account_state.dart +++ b/lib/blocs/account/account_state.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import '../../data/models/account.dart'; + +abstract class AccountState extends Equatable { + const AccountState(); + + @override + List get props => []; +} + +class AccountInitial extends AccountState {} + +class AccountLoading extends AccountState {} + +class AccountsLoaded extends AccountState { + final List accounts; + + const AccountsLoaded(this.accounts); + + @override + List get props => [accounts]; +} + +class AccountOperationSuccess extends AccountState { + final String message; + + const AccountOperationSuccess(this.message); + + @override + List get props => [message]; +} + +class AccountError extends AccountState { + final String message; + + const AccountError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/components/account/account_content.dart b/lib/components/account/account_content.dart index 711a4b9..9b170d5 100644 --- a/lib/components/account/account_content.dart +++ b/lib/components/account/account_content.dart @@ -1,143 +1,239 @@ import 'package:flutter/material.dart'; +import 'package:travel_mate/blocs/user/user_bloc.dart'; +import '../../data/models/account.dart'; +import '../../blocs/account/account_bloc.dart'; +import '../../blocs/account/account_event.dart'; +import '../../blocs/account/account_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../blocs/group/group_bloc.dart'; -import '../../blocs/group/group_state.dart'; -import '../../data/models/group.dart'; -import 'group_expenses_page.dart'; +import 'package:travel_mate/components/error/error_content.dart'; +import '../../blocs/user/user_state.dart' as user_state; -class CountContent extends StatelessWidget { - const CountContent({super.key}); + +class AccountContent extends StatefulWidget { + const AccountContent({super.key}); + + @override + State createState() => _AccountContentState(); +} + +class _AccountContentState extends State { + @override + void initState() { + super.initState(); + + // Charger immédiatement sans attendre le prochain frame + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadInitialData(); + }); + } + + void _loadInitialData() { + try { + final userState = context.read().state; + + if (userState is user_state.UserLoaded) { + final userId = userState.user.id; + context.read().add(LoadAccountsByUserId(userId)); + } else { + throw Exception('Utilisateur non connecté'); + } + } catch (e) { + ErrorContent( + message: 'Erreur lors du chargement des comptes: $e', + onRetry: () {}, + ); + } + } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is GroupLoading) { - return const Center(child: CircularProgressIndicator()); - } - - if (state is GroupLoaded) { - if (state.groups.isEmpty) { - return _buildEmptyState(); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: state.groups.length, - itemBuilder: (context, index) { - final group = state.groups[index]; - return _buildGroupCard(context, group); - }, + return BlocBuilder( + builder: (context, userState) { + if (userState is user_state.UserLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), ); } - if (state is GroupError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error, size: 64, color: Colors.red), - const SizedBox(height: 16), - Text(state.message), - ], - ), - ); + if (userState is user_state.UserError) { + return ErrorContent( + message: 'Erreur utilisateur: ${userState.message}', + onRetry: () {}, + ); } - return _buildEmptyState(); - }, + if (userState is! user_state.UserLoaded) { + return const Scaffold( + body: Center(child: Text('Utilisateur non connecté')), + ); + } + final user = userState.user; + + return BlocConsumer( + listener: (context, accountState) { + if (accountState is AccountError) { + ErrorContent( + message: 'Erreur de chargement des comptes: ${accountState.message}', + onRetry: () { + context.read().add(LoadAccountsByUserId(user.id)); + }, + ); + } + }, + builder: (context, accountState) { + return Scaffold( + body: SafeArea(child: _buildContent(accountState, user.id)) + ); + }, + ); + }, ); } - Widget _buildEmptyState() { - return const Center( + Widget _buildContent(AccountState accountState, String userId) { + if (accountState is AccountLoading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des comptes...'), + ], + ), + ); + } + + if (accountState is AccountError) { + return ErrorContent( + message: 'Erreur de chargement des comptes...', + onRetry: () { + context.read().add(LoadAccountsByUserId(userId)); + }, + ); + } + + if (accountState is AccountsLoaded) { + if (accountState.accounts.isEmpty) { + return _buildEmptyState(); + } + + return _buildAccountsList(accountState.accounts, userId); + } + return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.account_balance_wallet, size: 80, color: Colors.grey), - SizedBox(height: 16), - Text( - 'Aucun groupe', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - 'Créez un groupe pour commencer à gérer vos dépenses', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: Colors.grey), + const Text('État inconnu'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add(LoadAccountsByUserId(userId)); + }, + child: const Text('Charger les comptes'), ), ], ), - ); + ); } - Widget _buildGroupCard(BuildContext context, Group group) { - final isDark = Theme.of(context).brightness == Brightness.dark; - - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupExpensesPage(group: group), + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.account_balance_wallet, size: 80, color: Colors.grey), + const SizedBox(height: 16), + const Text( + 'Aucun compte trouvé', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - ); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: isDark ? Colors.blue[900] : Colors.blue[100], - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.group, - color: Colors.blue, - size: 32, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - group.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - '${group.members.length} membre${group.members.length > 1 ? 's' : ''}', - style: TextStyle( - fontSize: 14, - color: isDark ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - ), - Icon( - Icons.chevron_right, - color: isDark ? Colors.grey[400] : Colors.grey[600], - ), - ], - ), + const SizedBox(height: 8), + const Text( + 'Les comptes sont créés automatiquement lorsque vous créez un voyage', + style: TextStyle(fontSize: 14, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], ), ), ); } + + Widget _buildAccountsList(List accounts, String userId) { + return RefreshIndicator( + onRefresh: () async { + context.read().add(LoadAccountsByUserId(userId)); + await Future.delayed(const Duration(milliseconds: 500)); + }, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + const Text( + 'Mes comptes', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Gérez vos comptes de voyage', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 24), + + ...accounts.map((account) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildSimpleAccountCard(account), + ); + }) + ], + ) + ); + } + + Widget _buildSimpleAccountCard(Account account) { + try { + final colors = [Colors.blue, Colors.purple, Colors.green, Colors.orange]; + final color = colors[account.name.hashCode.abs() % colors.length]; + + String memberInfo = '${account.members.length} membre${account.members.length > 1 ? 's' : ''}'; + + if(account.members.isNotEmpty){ + final names = account.members + .take(2) + .map((m) => m.pseudo.isNotEmpty ? m.pseudo : m.firstName) + .join(', '); + memberInfo += '\n$names'; + } + + return Card( + elevation: 2, + child: ListTile( + leading: CircleAvatar( + backgroundColor: color, + child: const Icon(Icons.account_balance_wallet, color: Colors.white), + ), + title: Text( + account.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(memberInfo), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ); + } catch (e) { + return Card( + color: Colors.red, + child: const ListTile( + leading: Icon(Icons.error, color: Colors.red), + title: Text('Erreur d\'affichage'), + ) + ); + } + } + + } diff --git a/lib/components/account/group_expenses_page.dart b/lib/components/account/group_expenses_page.dart index 8b99a02..e69de29 100644 --- a/lib/components/account/group_expenses_page.dart +++ b/lib/components/account/group_expenses_page.dart @@ -1,116 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../blocs/account/account_bloc.dart'; -import '../../blocs/account/account_event.dart'; -import '../../blocs/account/account_state.dart'; -import '../../blocs/user/user_bloc.dart'; -import '../../blocs/user/user_state.dart' as user_state; -import '../../data/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 Group group; - - const GroupExpensesPage({ - super.key, - 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); - context.read().add(LoadExpenses(widget.group.id)); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.group.name), - bottom: TabBar( - controller: _tabController, - tabs: const [ - Tab(text: 'Dépenses', icon: Icon(Icons.receipt_long)), - Tab(text: 'Balances', icon: Icon(Icons.account_balance)), - Tab(text: 'Remboursements', icon: Icon(Icons.payments)), - ], - ), - ), - body: BlocConsumer( - listener: (context, state) { - if (state is CountError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.red, - ), - ); - } - }, - builder: (context, state) { - if (state is CountLoading) { - return const Center(child: CircularProgressIndicator()); - } - - if (state is ExpensesLoaded) { - return TabBarView( - controller: _tabController, - children: [ - ExpensesTab( - expenses: state.expenses, - group: widget.group, - ), - BalancesTab(balances: state.balances), - SettlementsTab(settlements: state.settlements), - ], - ); - } - - return const Center(child: Text('Aucune donnée')); - }, - ), - floatingActionButton: BlocBuilder( - builder: (context, userState) { - if (userState is! user_state.UserLoaded) return const SizedBox(); - - return FloatingActionButton.extended( - onPressed: () => _showAddExpenseDialog(context, userState.user), - icon: const Icon(Icons.add), - label: const Text('Dépense'), - ); - }, - ), - ); - } - - void _showAddExpenseDialog(BuildContext context, user_state.UserModel currentUser) { - showDialog( - context: context, - builder: (dialogContext) => BlocProvider.value( - value: context.read(), - child: AddExpenseDialog( - group: widget.group, - currentUser: currentUser, - ), - ), - ); - } -} diff --git a/lib/data/models/account.dart b/lib/data/models/account.dart index 178a280..3e74cc7 100644 --- a/lib/data/models/account.dart +++ b/lib/data/models/account.dart @@ -4,12 +4,14 @@ class Account { final String id; final String tripId; final String groupId; + final String name; final List members; Account({ required this.id, required this.tripId, required this.groupId, + required this.name, List? members, }) : members = members ?? []; @@ -19,6 +21,7 @@ class Account { id: map['id'] as String, tripId: map['tripId'] as String, groupId: map['groupId'] as String, + name: map['name'] as String, members: [], ); } @@ -28,6 +31,7 @@ class Account { 'id': id, 'tripId': tripId, 'groupId': groupId, + 'name': name, 'members': members.map((member) => member.toMap()).toList(), }; } @@ -36,12 +40,14 @@ class Account { String? id, String? tripId, String? groupId, + String? name, List? members, }) { return Account( id: id ?? this.id, tripId: tripId ?? this.tripId, groupId: groupId ?? this.groupId, + name: name ?? this.name, members: members ?? this.members, ); } diff --git a/lib/repositories/account_repository.dart b/lib/repositories/account_repository.dart index 42f77e2..ee9b29f 100644 --- a/lib/repositories/account_repository.dart +++ b/lib/repositories/account_repository.dart @@ -103,15 +103,44 @@ class AccountRepository { return await _firestore.collection('accounts').doc(accountId).get(); } - Future createAccount(Map accountData) async { - await _firestore.collection('accounts').add(accountData); + Future updateAccount(String accountId, Account account) async { + try { + await _firestore.collection('accounts').doc(accountId).update(account.toMap()); + } catch (e) { + _errorService.logError('account_repository.dart', 'Erreur lors de la mise à jour du compte: $e'); + } } - Future updateAccount(String accountId, Map accountData) async { - await _firestore.collection('accounts').doc(accountId).update(accountData); + Future deleteAccount(String tripId) async { + try { + final querySnapshot = await _firestore + .collection('accounts') + .where('tripId', isEqualTo: tripId) + .get(); + if (querySnapshot.docs.isEmpty) { + throw Exception('Aucun compte trouvé pour ce voyage'); + } + + final docId = querySnapshot.docs.first.id; + + final membersSnapshot = await _membersCollection(docId).get(); + for (var memberDoc in membersSnapshot.docs) { + await _membersCollection(docId).doc(memberDoc.id).delete(); + } + await _accountCollection.doc(docId).delete(); + } catch (e) { + _errorService.logError('account_repository.dart', 'Erreur lors de la suppression du compte: $e'); + } } - Future deleteAccount(String accountId) async { - await _firestore.collection('accounts').doc(accountId).delete(); + Stream> watchGroupMembers(String accountId) { + return _membersCollection(accountId).snapshots().map( + (snapshot) => snapshot.docs + .map((doc) => GroupMember.fromMap( + doc.data() as Map, + doc.id, + )) + .toList(), + ); } } \ No newline at end of file diff --git a/lib/services/account_service.dart b/lib/services/account_service.dart index e69de29..e42265a 100644 --- a/lib/services/account_service.dart +++ b/lib/services/account_service.dart @@ -0,0 +1,62 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:travel_mate/services/error_service.dart'; +import '../data/models/account.dart'; + +class AccountService { + final _errorService = ErrorService(); + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + Stream> getAccountsStream() { + return _firestore.collection('accounts').snapshots().map((snapshot) { + return snapshot.docs.map((doc) { + return Account.fromMap(doc.data()); + }).toList(); + }); + } + + Future createAccount(Account account) async { + try { + await _firestore.collection('accounts').add(account.toMap()); + return true; + } catch (e) { + _errorService.logError('Erreur lors de la création du compte: $e', StackTrace.current); + return false; + } + } + + Future updateAccount(Account account) async { + try { + await _firestore.collection('accounts').doc(account.id).update(account.toMap()); + return true; + } catch (e) { + _errorService.logError('Erreur lors de la mise à jour du compte: $e', StackTrace.current); + return false; + } + } + + Future deleteAccount(String accountId) async { + try { + await _firestore.collection('accounts').doc(accountId).delete(); + return true; + } catch (e) { + _errorService.logError('Erreur lors de la suppression du compte: $e', StackTrace.current); + return false; + } + } + + + Stream> getAccountsStreamByUser(String userId) { + return _firestore + .collection('accounts') + .where('members', arrayContains: userId) + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + final account = Account.fromMap(doc.data()); + _errorService.logError('Compte: ${account.name}, Membres: ${account.members.length}', StackTrace.current); + return account; + }).toList(); + }); + } + +} \ No newline at end of file diff --git a/lib/services/group_service.dart b/lib/services/group_service.dart index f202316..8190cab 100644 --- a/lib/services/group_service.dart +++ b/lib/services/group_service.dart @@ -58,12 +58,4 @@ class GroupService { }).toList(); }); } - - Future removeMemberFromGroup(String groupId, String memberId) async { - // TODO: Implémenter la suppression d'un membre d'un groupe - } - - Future addMemberToGroup(String groupId, String memberId) async { - // TODO: Implémenter l'ajout d'un membre à un groupe - } } \ No newline at end of file