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
This commit is contained in:
226
lib/services/count_service.dart
Normal file
226
lib/services/count_service.dart
Normal file
@@ -0,0 +1,226 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user