From c69618cbd96a90ed1edf90d68e591bcf4be0a813 Mon Sep 17 00:00:00 2001 From: Dayron Date: Tue, 21 Oct 2025 00:42:36 +0200 Subject: [PATCH] feat: Implement account management features - Added ExpenseDetailDialog for displaying expense details and actions. - Created ExpensesTab to list expenses for a group. - Developed GroupExpensesPage to manage group expenses with tabs for expenses, balances, and settlements. - Introduced SettlementsTab to show optimized repayment plans. - Refactored create_trip_content.dart to remove CountBloc and related logic. - Added Account model to manage user accounts and group members. - Replaced CountRepository with AccountRepository for account-related operations. - Removed CountService and CountRepository as part of the refactor. - Updated main.dart and home.dart to integrate new account management components. --- lib/blocs/account/account_bloc.dart | 0 lib/blocs/account/account_event.dart | 0 lib/blocs/account/account_state.dart | 0 lib/blocs/count/count_bloc.dart | 196 --------------- lib/blocs/count/count_event.dart | 91 ------- lib/blocs/count/count_state.dart | 42 ---- .../account_content.dart} | 0 .../add_expense_dialog.dart | 10 +- .../{count => account}/balances_tab.dart | 0 .../expense_detail_dialog.dart | 6 +- .../{count => account}/expenses_tab.dart | 0 .../group_expenses_page.dart | 6 +- .../{count => account}/settlements_tab.dart | 0 lib/components/home/create_trip_content.dart | 8 +- lib/data/models/account.dart | 48 ++++ lib/main.dart | 12 +- lib/pages/home.dart | 2 +- lib/repositories/account_repository.dart | 117 +++++++++ lib/repositories/count_repository.dart | 165 ------------- lib/services/account_service.dart | 0 lib/services/count_service.dart | 226 ------------------ 21 files changed, 182 insertions(+), 747 deletions(-) create mode 100644 lib/blocs/account/account_bloc.dart create mode 100644 lib/blocs/account/account_event.dart create mode 100644 lib/blocs/account/account_state.dart delete mode 100644 lib/blocs/count/count_bloc.dart delete mode 100644 lib/blocs/count/count_event.dart delete mode 100644 lib/blocs/count/count_state.dart rename lib/components/{count/count_content.dart => account/account_content.dart} (100%) rename lib/components/{count => account}/add_expense_dialog.dart (98%) rename lib/components/{count => account}/balances_tab.dart (100%) rename lib/components/{count => account}/expense_detail_dialog.dart (98%) rename lib/components/{count => account}/expenses_tab.dart (100%) rename lib/components/{count => account}/group_expenses_page.dart (95%) rename lib/components/{count => account}/settlements_tab.dart (100%) create mode 100644 lib/data/models/account.dart create mode 100644 lib/repositories/account_repository.dart delete mode 100644 lib/repositories/count_repository.dart create mode 100644 lib/services/account_service.dart delete mode 100644 lib/services/count_service.dart diff --git a/lib/blocs/account/account_bloc.dart b/lib/blocs/account/account_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/blocs/account/account_event.dart b/lib/blocs/account/account_event.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/blocs/account/account_state.dart b/lib/blocs/account/account_state.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/blocs/count/count_bloc.dart b/lib/blocs/count/count_bloc.dart deleted file mode 100644 index d5ffaa0..0000000 --- a/lib/blocs/count/count_bloc.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:async'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../data/models/expense.dart'; -import '../../services/count_service.dart'; -import '../../repositories/count_repository.dart'; -import 'count_event.dart'; -import 'count_state.dart'; - -class CountBloc extends Bloc { - final CountService _countService; - StreamSubscription>? _expensesSubscription; - Map _exchangeRates = {}; - - CountBloc({CountService? countService}) - : _countService = countService ?? CountService( - countRepository: CountRepository(), - ), - super(CountInitial()) { - on(_onLoadExpenses); - on(_onCreateExpense); - on(_onUpdateExpense); - on(_onDeleteExpense); - on(_onArchiveExpense); - on(_onMarkSplitAsPaid); - on(_onLoadExchangeRates); - on<_ExpensesUpdated>(_onExpensesUpdated); - } - - Future _onLoadExpenses( - LoadExpenses event, - Emitter emit, - ) async { - emit(CountLoading()); - - // Charger les taux de change - if (_exchangeRates.isEmpty) { - _exchangeRates = await _countService.getExchangeRates(); - } - - await _expensesSubscription?.cancel(); - - _expensesSubscription = _countService - .getExpensesStream(event.groupId, includeArchived: event.includeArchived) - .listen( - (expenses) { - add(_ExpensesUpdated( - groupId: event.groupId, - expenses: expenses, - )); - }, - onError: (error) { - add(_ExpensesError('Erreur lors du chargement des dépenses: $error')); - }, - ); - } - - void _onExpensesUpdated( - _ExpensesUpdated event, - Emitter emit, - ) { - // Récupérer les membres du groupe et calculer les balances - final memberIds = {}; - final memberNames = {}; - - for (final expense in event.expenses) { - memberIds.add(expense.paidById); - memberNames[expense.paidById] = expense.paidByName; - - for (final split in expense.splits) { - memberIds.add(split.userId); - memberNames[split.userId] = split.userName; - } - } - - final balances = _countService.calculateBalances( - event.expenses, - memberIds.toList(), - memberNames, - ); - - final settlements = _countService.calculateOptimizedSettlements(balances); - - emit(ExpensesLoaded( - groupId: event.groupId, - expenses: event.expenses, - balances: balances, - settlements: settlements, - exchangeRates: _exchangeRates, - )); - } - - Future _onCreateExpense( - CreateExpense event, - Emitter emit, - ) async { - try { - await _countService.createExpense( - event.expense, - receiptImage: event.receiptImage, - ); - } catch (e) { - emit(CountError('Erreur lors de la création de la dépense: $e')); - } - } - - Future _onUpdateExpense( - UpdateExpense event, - Emitter emit, - ) async { - try { - await _countService.updateExpense( - event.expense, - newReceiptImage: event.newReceiptImage, - ); - } catch (e) { - emit(CountError('Erreur lors de la modification de la dépense: $e')); - } - } - - Future _onDeleteExpense( - DeleteExpense event, - Emitter emit, - ) async { - try { - await _countService.deleteExpense(event.groupId, event.expenseId); - } catch (e) { - emit(CountError('Erreur lors de la suppression de la dépense: $e')); - } - } - - Future _onArchiveExpense( - ArchiveExpense event, - Emitter emit, - ) async { - try { - await _countService.archiveExpense(event.groupId, event.expenseId); - } catch (e) { - emit(CountError('Erreur lors de l\'archivage de la dépense: $e')); - } - } - - Future _onMarkSplitAsPaid( - MarkSplitAsPaid event, - Emitter emit, - ) async { - try { - await _countService.markSplitAsPaid( - event.groupId, - event.expenseId, - event.userId, - ); - } catch (e) { - emit(CountError('Erreur lors du marquage du paiement: $e')); - } - } - - Future _onLoadExchangeRates( - LoadExchangeRates event, - Emitter emit, - ) async { - try { - _exchangeRates = await _countService.getExchangeRates(); - } catch (e) { - emit(CountError('Erreur lors du chargement des taux de change: $e')); - } - } - - @override - Future close() { - _expensesSubscription?.cancel(); - return super.close(); - } -} - -// Events internes -class _ExpensesUpdated extends CountEvent { - final String groupId; - final List expenses; - - const _ExpensesUpdated({ - required this.groupId, - required this.expenses, - }); - - @override - List get props => [groupId, expenses]; -} - -class _ExpensesError extends CountEvent { - final String error; - - const _ExpensesError(this.error); - - @override - List get props => [error]; -} diff --git a/lib/blocs/count/count_event.dart b/lib/blocs/count/count_event.dart deleted file mode 100644 index ffab27e..0000000 --- a/lib/blocs/count/count_event.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:io'; -import 'package:equatable/equatable.dart'; -import '../../data/models/expense.dart'; - -abstract class CountEvent extends Equatable { - const CountEvent(); - - @override - List get props => []; -} - -class LoadExpenses extends CountEvent { - final String groupId; - final bool includeArchived; - - const LoadExpenses(this.groupId, {this.includeArchived = false}); - - @override - List get props => [groupId, includeArchived]; -} - -class CreateExpense extends CountEvent { - final Expense expense; - final File? receiptImage; - - const CreateExpense({ - required this.expense, - this.receiptImage, - }); - - @override - List get props => [expense, receiptImage]; -} - -class UpdateExpense extends CountEvent { - final Expense expense; - final File? newReceiptImage; - - const UpdateExpense({ - required this.expense, - this.newReceiptImage, - }); - - @override - List get props => [expense, newReceiptImage]; -} - -class DeleteExpense extends CountEvent { - final String groupId; - final String expenseId; - - const DeleteExpense({ - required this.groupId, - required this.expenseId, - }); - - @override - List get props => [groupId, expenseId]; -} - -class ArchiveExpense extends CountEvent { - final String groupId; - final String expenseId; - - const ArchiveExpense({ - required this.groupId, - required this.expenseId, - }); - - @override - List get props => [groupId, expenseId]; -} - -class MarkSplitAsPaid extends CountEvent { - final String groupId; - final String expenseId; - final String userId; - - const MarkSplitAsPaid({ - required this.groupId, - required this.expenseId, - required this.userId, - }); - - @override - List get props => [groupId, expenseId, userId]; -} - -class LoadExchangeRates extends CountEvent { - const LoadExchangeRates(); -} diff --git a/lib/blocs/count/count_state.dart b/lib/blocs/count/count_state.dart deleted file mode 100644 index fc49cb9..0000000 --- a/lib/blocs/count/count_state.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:equatable/equatable.dart'; -import '../../data/models/expense.dart'; -import '../../data/models/balance.dart'; - -abstract class CountState extends Equatable { - const CountState(); - - @override - List get props => []; -} - -class CountInitial extends CountState {} - -class CountLoading extends CountState {} - -class ExpensesLoaded extends CountState { - final String groupId; - final List expenses; - final List balances; - final List settlements; - final Map exchangeRates; - - const ExpensesLoaded({ - required this.groupId, - required this.expenses, - required this.balances, - required this.settlements, - required this.exchangeRates, - }); - - @override - List get props => [groupId, expenses, balances, settlements, exchangeRates]; -} - -class CountError extends CountState { - final String message; - - const CountError(this.message); - - @override - List get props => [message]; -} diff --git a/lib/components/count/count_content.dart b/lib/components/account/account_content.dart similarity index 100% rename from lib/components/count/count_content.dart rename to lib/components/account/account_content.dart diff --git a/lib/components/count/add_expense_dialog.dart b/lib/components/account/add_expense_dialog.dart similarity index 98% rename from lib/components/count/add_expense_dialog.dart rename to lib/components/account/add_expense_dialog.dart index 535d627..4f00106 100644 --- a/lib/components/count/add_expense_dialog.dart +++ b/lib/components/account/add_expense_dialog.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; -import '../../blocs/count/count_bloc.dart'; -import '../../blocs/count/count_event.dart'; -import '../../blocs/count/count_state.dart'; +import '../../blocs/account/account_bloc.dart'; +import '../../blocs/account/account_event.dart'; +import '../../blocs/account/account_state.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../data/models/group.dart'; import '../../data/models/expense.dart'; @@ -148,7 +148,7 @@ class _AddExpenseDialogState extends State { try { // Convertir en EUR - final amountInEur = await context.read().state is ExpensesLoaded + final amountInEur = context.read().state is ExpensesLoaded ? (context.read().state as ExpensesLoaded) .exchangeRates[_selectedCurrency]! * amount : amount; @@ -435,7 +435,7 @@ class _AddExpenseDialogState extends State { }); }, ); - }).toList(), + }), ], ), ), diff --git a/lib/components/count/balances_tab.dart b/lib/components/account/balances_tab.dart similarity index 100% rename from lib/components/count/balances_tab.dart rename to lib/components/account/balances_tab.dart diff --git a/lib/components/count/expense_detail_dialog.dart b/lib/components/account/expense_detail_dialog.dart similarity index 98% rename from lib/components/count/expense_detail_dialog.dart rename to lib/components/account/expense_detail_dialog.dart index bfabbb6..d1a30b2 100644 --- a/lib/components/count/expense_detail_dialog.dart +++ b/lib/components/account/expense_detail_dialog.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; -import '../../blocs/count/count_bloc.dart'; -import '../../blocs/count/count_event.dart'; +import '../../blocs/account/account_bloc.dart'; +import '../../blocs/account/account_event.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../data/models/expense.dart'; @@ -67,7 +67,7 @@ class ExpenseDetailDialog extends StatelessWidget { width: 80, height: 80, decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), + color: Colors.blue.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( diff --git a/lib/components/count/expenses_tab.dart b/lib/components/account/expenses_tab.dart similarity index 100% rename from lib/components/count/expenses_tab.dart rename to lib/components/account/expenses_tab.dart diff --git a/lib/components/count/group_expenses_page.dart b/lib/components/account/group_expenses_page.dart similarity index 95% rename from lib/components/count/group_expenses_page.dart rename to lib/components/account/group_expenses_page.dart index f54a96e..8b99a02 100644 --- a/lib/components/count/group_expenses_page.dart +++ b/lib/components/account/group_expenses_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../blocs/count/count_bloc.dart'; -import '../../blocs/count/count_event.dart'; -import '../../blocs/count/count_state.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'; diff --git a/lib/components/count/settlements_tab.dart b/lib/components/account/settlements_tab.dart similarity index 100% rename from lib/components/count/settlements_tab.dart rename to lib/components/account/settlements_tab.dart diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 9dd9105..15db114 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -9,8 +9,6 @@ import '../../blocs/trip/trip_event.dart'; import '../../blocs/trip/trip_state.dart'; import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_event.dart'; -import '../../blocs/count/count_bloc.dart'; -import '../../blocs/count/count_event.dart'; import '../../data/models/group.dart'; import '../../data/models/group_member.dart'; import '../../services/user_service.dart'; @@ -572,7 +570,7 @@ class _CreateTripContentState extends State { final group = Group( id: '', // Sera généré par Firestore name: _titleController.text.trim(), - tripId: tripId, + tripId: tripId, // ✅ ID du voyage récupéré createdBy: currentUser.id, ); @@ -591,15 +589,11 @@ class _CreateTripContentState extends State { )), ]; - // Créer le groupe context.read().add(CreateGroupWithMembers( group: group, members: groupMembers, )); - // ✅ AJOUT : Attendre un court instant pour que le groupe soit créé - await Future.delayed(const Duration(milliseconds: 500)); - if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/data/models/account.dart b/lib/data/models/account.dart new file mode 100644 index 0000000..178a280 --- /dev/null +++ b/lib/data/models/account.dart @@ -0,0 +1,48 @@ +import 'group_member.dart'; + +class Account { + final String id; + final String tripId; + final String groupId; + final List members; + + Account({ + required this.id, + required this.tripId, + required this.groupId, + List? members, + }) : members = members ?? []; + + + factory Account.fromMap(Map map) { + return Account( + id: map['id'] as String, + tripId: map['tripId'] as String, + groupId: map['groupId'] as String, + members: [], + ); + } + + Map toMap() { + return { + 'id': id, + 'tripId': tripId, + 'groupId': groupId, + 'members': members.map((member) => member.toMap()).toList(), + }; + } + + Account copyWith({ + String? id, + String? tripId, + String? groupId, + List? members, + }) { + return Account( + id: id ?? this.id, + tripId: tripId ?? this.tripId, + groupId: groupId ?? this.groupId, + members: members ?? this.members, + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 45efd8d..13a5030 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,13 +12,13 @@ import 'blocs/theme/theme_state.dart'; import 'blocs/group/group_bloc.dart'; import 'blocs/user/user_bloc.dart'; import 'blocs/trip/trip_bloc.dart'; -import 'blocs/count/count_bloc.dart'; +import 'blocs/account/account_bloc.dart'; import 'repositories/auth_repository.dart'; import 'repositories/trip_repository.dart'; import 'repositories/user_repository.dart'; import 'repositories/group_repository.dart'; import 'repositories/message_repository.dart'; -import 'repositories/count_repository.dart'; +import 'repositories/account_repository.dart'; import 'pages/login.dart'; import 'pages/home.dart'; import 'pages/signup.dart'; @@ -54,9 +54,7 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => MessageRepository(), ), - RepositoryProvider( - create: (context) => CountRepository(), - ), + ], child: MultiBlocProvider( providers: [ @@ -79,9 +77,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => MessageBloc(), ), - BlocProvider( - create: (context) => CountBloc(), - ), + ], child: BlocBuilder( builder: (context, themeState) { diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ea995a0..99d9746 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -6,7 +6,7 @@ import '../components/home/home_content.dart'; import '../components/settings/settings_content.dart'; import '../components/map/map_content.dart'; import '../components/group/group_content.dart'; -import '../components/count/count_content.dart'; +import '../components/account/account_content.dart'; import '../blocs/user/user_bloc.dart'; import '../blocs/user/user_event.dart'; import '../blocs/auth/auth_bloc.dart'; diff --git a/lib/repositories/account_repository.dart b/lib/repositories/account_repository.dart new file mode 100644 index 0000000..42f77e2 --- /dev/null +++ b/lib/repositories/account_repository.dart @@ -0,0 +1,117 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:travel_mate/services/error_service.dart'; +import '../data/models/group_member.dart'; +import '../data/models/account.dart'; + +class AccountRepository { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final _errorService = ErrorService(); + + CollectionReference get _accountCollection => _firestore.collection('accounts'); + + CollectionReference _membersCollection(String accountId) { + return _accountCollection.doc(accountId).collection('members'); + } + + Future createAccountWithMembers({ + required Account account, + required List members, + }) async { + try { + return await _firestore.runTransaction((transaction) async { + final accountRef = _accountCollection.doc(); + + final accountData = account.toMap(); + + transaction.set(accountRef, accountData); + + for (var member in members) { + final memberRef = accountRef.collection('members').doc(member.userId); + transaction.set(memberRef, member.toMap()); + } + + return accountRef.id; + }); + } catch (e) { + throw Exception('Erreur lors de la création du compte: $e'); + } + } + + Stream> getAccountByUserId(String userId) { + return _accountCollection + .snapshots() + .asyncMap((snapshot) async { + + List userAccounts = []; + + for (var accountDoc in snapshot.docs) { + try { + final accountId = accountDoc.id; + + final memberDoc = await accountDoc.reference + .collection('members') + .doc(userId) + .get(); + if (memberDoc.exists) { + final accountData = accountDoc.data() as Map; + final account = Account.fromMap(accountData); + final members = await getAccountMembers(accountId); + + userAccounts.add(account.copyWith(members: members)); + } else { + _errorService.logInfo('account_repository.dart', 'Utilisateur NON membre de $accountId'); + } + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + } + } + return userAccounts; + }) + .distinct((prev, next) { + if (prev.length != next.length) return false; + final prevIds = prev.map((a) => a.id).toSet(); + final nextIds = next.map((a) => a.id).toSet(); + + final identical = prevIds.difference(nextIds).isEmpty && + nextIds.difference(prevIds).isEmpty; + + return identical; + }) + .handleError((error, stackTrace) { + _errorService.logError(error, stackTrace); + return []; + }); + } + + Future> getAccountMembers(String accountId) async { + try { + final snapshot = await _membersCollection(accountId).get(); + return snapshot.docs + .map((doc) { + return GroupMember.fromMap( + doc.data() as Map, + doc.id, + ); + }) + .toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des membres: $e'); + } + } + + Future getAccountById(String accountId) async { + return await _firestore.collection('accounts').doc(accountId).get(); + } + + Future createAccount(Map accountData) async { + await _firestore.collection('accounts').add(accountData); + } + + Future updateAccount(String accountId, Map accountData) async { + await _firestore.collection('accounts').doc(accountId).update(accountData); + } + + Future deleteAccount(String accountId) async { + await _firestore.collection('accounts').doc(accountId).delete(); + } +} \ No newline at end of file diff --git a/lib/repositories/count_repository.dart b/lib/repositories/count_repository.dart deleted file mode 100644 index f38dc87..0000000 --- a/lib/repositories/count_repository.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'dart:io'; -import '../data/models/expense.dart'; - -class CountRepository { - final FirebaseFirestore _firestore; - final FirebaseStorage _storage; - - CountRepository({ - FirebaseFirestore? firestore, - FirebaseStorage? storage, - }) : _firestore = firestore ?? FirebaseFirestore.instance, - _storage = storage ?? FirebaseStorage.instance; - - // Créer une dépense - Future createExpense(Expense expense) async { - final docRef = await _firestore - .collection('groups') - .doc(expense.groupId) - .collection('expenses') - .add(expense.toMap()); - - return docRef.id; - } - - // Mettre à jour une dépense - Future updateExpense(String groupId, Expense expense) async { - await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expense.id) - .update(expense.toMap()); - } - - // Supprimer une dépense - Future deleteExpense(String groupId, String expenseId) async { - final expense = await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expenseId) - .get(); - - final data = expense.data(); - if (data != null && data['receiptUrl'] != null) { - await deleteReceipt(data['receiptUrl']); - } - - await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expenseId) - .delete(); - } - - // Archiver une dépense - Future archiveExpense(String groupId, String expenseId) async { - await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expenseId) - .update({'isArchived': true}); - } - - // Marquer une split comme payée - Future markSplitAsPaid({ - required String groupId, - required String expenseId, - required String userId, - }) async { - final doc = await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expenseId) - .get(); - - final expense = Expense.fromFirestore(doc); - final updatedSplits = expense.splits.map((split) { - if (split.userId == userId) { - return ExpenseSplit( - userId: split.userId, - userName: split.userName, - amount: split.amount, - isPaid: true, - ); - } - return split; - }).toList(); - - await _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .doc(expenseId) - .update({ - 'splits': updatedSplits.map((s) => s.toMap()).toList(), - }); - } - - // Stream des dépenses d'un groupe - Stream> getExpensesStream(String groupId, {bool includeArchived = false}) { - Query query = _firestore - .collection('groups') - .doc(groupId) - .collection('expenses') - .orderBy('date', descending: true); - - if (!includeArchived) { - query = query.where('isArchived', isEqualTo: false); - } - - return query.snapshots().map((snapshot) { - return snapshot.docs.map((doc) => Expense.fromFirestore(doc)).toList(); - }); - } - - // Uploader un reçu - Future uploadReceipt(String groupId, String expenseId, File imageFile) async { - final fileName = 'receipts/$groupId/$expenseId/${DateTime.now().millisecondsSinceEpoch}.jpg'; - final ref = _storage.ref().child(fileName); - - final uploadTask = await ref.putFile(imageFile); - return await uploadTask.ref.getDownloadURL(); - } - - // Supprimer un reçu - Future deleteReceipt(String receiptUrl) async { - try { - final ref = _storage.refFromURL(receiptUrl); - await ref.delete(); - } catch (e) { - // Le fichier n'existe peut-être plus - print('Erreur lors de la suppression du reçu: $e'); - } - } - - // Obtenir les taux de change (API externe ou valeurs fixes) - Future> getExchangeRates() async { - // TODO: Intégrer une API de taux de change réels - // Pour l'instant, valeurs approximatives - return { - ExpenseCurrency.eur: 1.0, - ExpenseCurrency.usd: 0.92, - ExpenseCurrency.gbp: 1.17, - ExpenseCurrency.jpy: 0.0062, - ExpenseCurrency.chf: 1.05, - ExpenseCurrency.cad: 0.68, - ExpenseCurrency.aud: 0.61, - }; - } - - // Convertir un montant en EUR - Future convertToEur(double amount, ExpenseCurrency currency) async { - if (currency == ExpenseCurrency.eur) return amount; - - final rates = await getExchangeRates(); - final rate = rates[currency] ?? 1.0; - return amount * rate; - } -} diff --git a/lib/services/account_service.dart b/lib/services/account_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/services/count_service.dart b/lib/services/count_service.dart deleted file mode 100644 index 47fc04a..0000000 --- a/lib/services/count_service.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'dart:io'; -import '../data/models/expense.dart'; -import '../data/models/balance.dart'; -import '../repositories/count_repository.dart'; -import 'error_service.dart'; - -class CountService { - final CountRepository _countRepository; - final ErrorService _errorService; - - CountService({ - CountRepository? countRepository, - ErrorService? errorService, - }) : _countRepository = countRepository ?? CountRepository(), - _errorService = errorService ?? ErrorService(); - - // Créer une dépense - Future createExpense(Expense expense, {File? receiptImage}) async { - try { - final expenseId = await _countRepository.createExpense(expense); - - if (receiptImage != null) { - final receiptUrl = await _countRepository.uploadReceipt( - expense.groupId, - expenseId, - receiptImage, - ); - - final updatedExpense = expense.copyWith( - id: expenseId, - receiptUrl: receiptUrl, - ); - - await _countRepository.updateExpense(expense.groupId, updatedExpense); - } - - return expenseId; - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la création de la dépense: $e'); - rethrow; - } - } - - // Mettre à jour une dépense - Future updateExpense(Expense expense, {File? newReceiptImage}) async { - try { - if (newReceiptImage != null) { - // Supprimer l'ancien reçu si existe - if (expense.receiptUrl != null) { - await _countRepository.deleteReceipt(expense.receiptUrl!); - } - - // Uploader le nouveau - final receiptUrl = await _countRepository.uploadReceipt( - expense.groupId, - expense.id, - newReceiptImage, - ); - - expense = expense.copyWith(receiptUrl: receiptUrl); - } - - await _countRepository.updateExpense(expense.groupId, expense); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la mise à jour de la dépense: $e'); - rethrow; - } - } - - // Supprimer une dépense - Future deleteExpense(String groupId, String expenseId) async { - try { - await _countRepository.deleteExpense(groupId, expenseId); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la suppression de la dépense: $e'); - rethrow; - } - } - - // Archiver une dépense - Future archiveExpense(String groupId, String expenseId) async { - try { - await _countRepository.archiveExpense(groupId, expenseId); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de l\'archivage de la dépense: $e'); - rethrow; - } - } - - // Marquer une split comme payée - Future markSplitAsPaid(String groupId, String expenseId, String userId) async { - try { - await _countRepository.markSplitAsPaid( - groupId: groupId, - expenseId: expenseId, - userId: userId, - ); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors du marquage du paiement: $e'); - rethrow; - } - } - - // Stream des dépenses - Stream> getExpensesStream(String groupId, {bool includeArchived = false}) { - try { - return _countRepository.getExpensesStream(groupId, includeArchived: includeArchived); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la récupération des dépenses: $e'); - rethrow; - } - } - - // Calculer les balances - List calculateBalances(List expenses, List memberIds, Map memberNames) { - final balances = {}; - - // Initialiser les balances - for (final memberId in memberIds) { - balances[memberId] = Balance( - userId: memberId, - userName: memberNames[memberId] ?? 'Unknown', - totalPaid: 0, - totalOwed: 0, - ); - } - - // Calculer pour chaque dépense - for (final expense in expenses) { - if (expense.isArchived) continue; - - // Ajouter au total payé - final payer = balances[expense.paidById]; - if (payer != null) { - balances[expense.paidById] = Balance( - userId: payer.userId, - userName: payer.userName, - totalPaid: payer.totalPaid + expense.amountInEur, - totalOwed: payer.totalOwed, - ); - } - - // Ajouter au total dû pour chaque split - for (final split in expense.splits) { - if (!split.isPaid) { - final debtor = balances[split.userId]; - if (debtor != null) { - balances[split.userId] = Balance( - userId: debtor.userId, - userName: debtor.userName, - totalPaid: debtor.totalPaid, - totalOwed: debtor.totalOwed + split.amount, - ); - } - } - } - } - - return balances.values.toList(); - } - - // Calculer les remboursements optimisés - List calculateOptimizedSettlements(List balances) { - final settlements = []; - - // Créer des copies mutables - final creditors = balances.where((b) => b.shouldReceive).map((b) => - {'userId': b.userId, 'userName': b.userName, 'amount': b.balance} - ).toList(); - - final debtors = balances.where((b) => b.shouldPay).map((b) => - {'userId': b.userId, 'userName': b.userName, 'amount': b.absoluteBalance} - ).toList(); - - // Trier par montant décroissant - creditors.sort((a, b) => (b['amount'] as double).compareTo(a['amount'] as double)); - debtors.sort((a, b) => (b['amount'] as double).compareTo(a['amount'] as double)); - - int i = 0, j = 0; - while (i < creditors.length && j < debtors.length) { - final creditor = creditors[i]; - final debtor = debtors[j]; - - final creditorAmount = creditor['amount'] as double; - final debtorAmount = debtor['amount'] as double; - - final settleAmount = creditorAmount < debtorAmount ? creditorAmount : debtorAmount; - - settlements.add(Settlement( - fromUserId: debtor['userId'] as String, - fromUserName: debtor['userName'] as String, - toUserId: creditor['userId'] as String, - toUserName: creditor['userName'] as String, - amount: settleAmount, - )); - - creditor['amount'] = creditorAmount - settleAmount; - debtor['amount'] = debtorAmount - settleAmount; - - if (creditor['amount'] == 0) i++; - if (debtor['amount'] == 0) j++; - } - - return settlements; - } - - // Convertir un montant en EUR - Future convertToEur(double amount, ExpenseCurrency currency) async { - try { - return await _countRepository.convertToEur(amount, currency); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la conversion: $e'); - rethrow; - } - } - - // Obtenir les taux de change - Future> getExchangeRates() async { - try { - return await _countRepository.getExchangeRates(); - } catch (e) { - _errorService.logError('count_service.dart', 'Erreur lors de la récupération des taux: $e'); - rethrow; - } - } -}