- 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
227 lines
6.9 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|