Files
TravelMate/lib/services/count_service.dart
Dayron ce754c1e6c feat: Add expense management features with tabs for expenses, balances, and settlements
- Implemented ExpensesTab to display a list of expenses with details.
- Created GroupExpensesPage to manage group expenses with a tabbed interface.
- Added SettlementsTab to show optimized settlements between users.
- Developed data models for Expense and Balance, including necessary methods for serialization.
- Introduced CountRepository for Firestore interactions related to expenses.
- Added CountService to handle business logic for expenses and settlements.
- Integrated image picker for receipt uploads.
- Updated main.dart to include CountBloc and CountRepository.
- Enhanced pubspec.yaml with new dependencies for image picking and Firebase storage.

Not Tested yet
2025-10-20 19:22:57 +02:00

227 lines
6.9 KiB
Dart

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;
}
}
}