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.
This commit is contained in:
Dayron
2025-10-21 00:42:36 +02:00
parent a3ced0e812
commit c69618cbd9
21 changed files with 182 additions and 747 deletions

View File

View File

View File

View File

@@ -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<CountEvent, CountState> {
final CountService _countService;
StreamSubscription<List<Expense>>? _expensesSubscription;
Map<ExpenseCurrency, double> _exchangeRates = {};
CountBloc({CountService? countService})
: _countService = countService ?? CountService(
countRepository: CountRepository(),
),
super(CountInitial()) {
on<LoadExpenses>(_onLoadExpenses);
on<CreateExpense>(_onCreateExpense);
on<UpdateExpense>(_onUpdateExpense);
on<DeleteExpense>(_onDeleteExpense);
on<ArchiveExpense>(_onArchiveExpense);
on<MarkSplitAsPaid>(_onMarkSplitAsPaid);
on<LoadExchangeRates>(_onLoadExchangeRates);
on<_ExpensesUpdated>(_onExpensesUpdated);
}
Future<void> _onLoadExpenses(
LoadExpenses event,
Emitter<CountState> 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<CountState> emit,
) {
// Récupérer les membres du groupe et calculer les balances
final memberIds = <String>{};
final memberNames = <String, String>{};
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<void> _onCreateExpense(
CreateExpense event,
Emitter<CountState> 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<void> _onUpdateExpense(
UpdateExpense event,
Emitter<CountState> 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<void> _onDeleteExpense(
DeleteExpense event,
Emitter<CountState> 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<void> _onArchiveExpense(
ArchiveExpense event,
Emitter<CountState> 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<void> _onMarkSplitAsPaid(
MarkSplitAsPaid event,
Emitter<CountState> emit,
) async {
try {
await _countService.markSplitAsPaid(
event.groupId,
event.expenseId,
event.userId,
);
} catch (e) {
emit(CountError('Erreur lors du marquage du paiement: $e'));
}
}
Future<void> _onLoadExchangeRates(
LoadExchangeRates event,
Emitter<CountState> emit,
) async {
try {
_exchangeRates = await _countService.getExchangeRates();
} catch (e) {
emit(CountError('Erreur lors du chargement des taux de change: $e'));
}
}
@override
Future<void> close() {
_expensesSubscription?.cancel();
return super.close();
}
}
// Events internes
class _ExpensesUpdated extends CountEvent {
final String groupId;
final List<Expense> expenses;
const _ExpensesUpdated({
required this.groupId,
required this.expenses,
});
@override
List<Object?> get props => [groupId, expenses];
}
class _ExpensesError extends CountEvent {
final String error;
const _ExpensesError(this.error);
@override
List<Object?> get props => [error];
}

View File

@@ -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<Object?> get props => [];
}
class LoadExpenses extends CountEvent {
final String groupId;
final bool includeArchived;
const LoadExpenses(this.groupId, {this.includeArchived = false});
@override
List<Object?> get props => [groupId, includeArchived];
}
class CreateExpense extends CountEvent {
final Expense expense;
final File? receiptImage;
const CreateExpense({
required this.expense,
this.receiptImage,
});
@override
List<Object?> get props => [expense, receiptImage];
}
class UpdateExpense extends CountEvent {
final Expense expense;
final File? newReceiptImage;
const UpdateExpense({
required this.expense,
this.newReceiptImage,
});
@override
List<Object?> get props => [expense, newReceiptImage];
}
class DeleteExpense extends CountEvent {
final String groupId;
final String expenseId;
const DeleteExpense({
required this.groupId,
required this.expenseId,
});
@override
List<Object?> get props => [groupId, expenseId];
}
class ArchiveExpense extends CountEvent {
final String groupId;
final String expenseId;
const ArchiveExpense({
required this.groupId,
required this.expenseId,
});
@override
List<Object?> 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<Object?> get props => [groupId, expenseId, userId];
}
class LoadExchangeRates extends CountEvent {
const LoadExchangeRates();
}

View File

@@ -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<Object?> get props => [];
}
class CountInitial extends CountState {}
class CountLoading extends CountState {}
class ExpensesLoaded extends CountState {
final String groupId;
final List<Expense> expenses;
final List<Balance> balances;
final List<Settlement> settlements;
final Map<ExpenseCurrency, double> exchangeRates;
const ExpensesLoaded({
required this.groupId,
required this.expenses,
required this.balances,
required this.settlements,
required this.exchangeRates,
});
@override
List<Object?> get props => [groupId, expenses, balances, settlements, exchangeRates];
}
class CountError extends CountState {
final String message;
const CountError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -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<AddExpenseDialog> {
try {
// Convertir en EUR
final amountInEur = await context.read<CountBloc>().state is ExpensesLoaded
final amountInEur = context.read<CountBloc>().state is ExpensesLoaded
? (context.read<CountBloc>().state as ExpensesLoaded)
.exchangeRates[_selectedCurrency]! * amount
: amount;
@@ -435,7 +435,7 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
});
},
);
}).toList(),
}),
],
),
),

View File

@@ -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(

View File

@@ -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';

View File

@@ -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<CreateTripContent> {
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<CreateTripContent> {
)),
];
// Créer le groupe
context.read<GroupBloc>().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(

View File

@@ -0,0 +1,48 @@
import 'group_member.dart';
class Account {
final String id;
final String tripId;
final String groupId;
final List<GroupMember> members;
Account({
required this.id,
required this.tripId,
required this.groupId,
List<GroupMember>? members,
}) : members = members ?? [];
factory Account.fromMap(Map<String, dynamic> map) {
return Account(
id: map['id'] as String,
tripId: map['tripId'] as String,
groupId: map['groupId'] as String,
members: [],
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'tripId': tripId,
'groupId': groupId,
'members': members.map((member) => member.toMap()).toList(),
};
}
Account copyWith({
String? id,
String? tripId,
String? groupId,
List<GroupMember>? members,
}) {
return Account(
id: id ?? this.id,
tripId: tripId ?? this.tripId,
groupId: groupId ?? this.groupId,
members: members ?? this.members,
);
}
}

View File

@@ -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<MessageRepository>(
create: (context) => MessageRepository(),
),
RepositoryProvider<CountRepository>(
create: (context) => CountRepository(),
),
],
child: MultiBlocProvider(
providers: [
@@ -79,9 +77,7 @@ class MyApp extends StatelessWidget {
BlocProvider(
create: (context) => MessageBloc(),
),
BlocProvider(
create: (context) => CountBloc(),
),
],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, themeState) {

View File

@@ -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';

View File

@@ -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<String> createAccountWithMembers({
required Account account,
required List<GroupMember> members,
}) async {
try {
return await _firestore.runTransaction<String>((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<List<Account>> getAccountByUserId(String userId) {
return _accountCollection
.snapshots()
.asyncMap((snapshot) async {
List<Account> 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<String, dynamic>;
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 <Account>[];
});
}
Future<List<GroupMember>> getAccountMembers(String accountId) async {
try {
final snapshot = await _membersCollection(accountId).get();
return snapshot.docs
.map((doc) {
return GroupMember.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
})
.toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des membres: $e');
}
}
Future<DocumentSnapshot> getAccountById(String accountId) async {
return await _firestore.collection('accounts').doc(accountId).get();
}
Future<void> createAccount(Map<String, dynamic> accountData) async {
await _firestore.collection('accounts').add(accountData);
}
Future<void> updateAccount(String accountId, Map<String, dynamic> accountData) async {
await _firestore.collection('accounts').doc(accountId).update(accountData);
}
Future<void> deleteAccount(String accountId) async {
await _firestore.collection('accounts').doc(accountId).delete();
}
}

View File

@@ -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<String> 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<void> 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<void> 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<void> 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<void> 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<List<Expense>> 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<String> 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<void> 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<Map<ExpenseCurrency, double>> 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<double> 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;
}
}

View File

View File

@@ -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<String> 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<void> 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<void> 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<void> 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<void> 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<List<Expense>> 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<Balance> calculateBalances(List<Expense> expenses, List<String> memberIds, Map<String, String> memberNames) {
final balances = <String, Balance>{};
// 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<Settlement> calculateOptimizedSettlements(List<Balance> balances) {
final settlements = <Settlement>[];
// 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<double> 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<Map<ExpenseCurrency, double>> 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;
}
}
}