feat: Add User and UserBalance models with serialization methods
feat: Implement BalanceRepository for group balance calculations feat: Create ExpenseRepository for managing expenses feat: Add services for handling expenses and storage operations fix: Update import paths for models in repositories and services refactor: Rename CountContent to AccountContent in HomePage chore: Add StorageService for image upload and management
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import '../models/group_member.dart';
|
||||
import '../models/account.dart';
|
||||
|
||||
class AccountRepository {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../data/models/user.dart';
|
||||
import '../models/user.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class AuthRepository {
|
||||
|
||||
146
lib/repositories/balance_repository.dart
Normal file
146
lib/repositories/balance_repository.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import '../models/group_balance.dart';
|
||||
import '../models/expense.dart';
|
||||
import '../services/error_service.dart';
|
||||
import 'expense_repository.dart';
|
||||
import '../models/user_balance.dart';
|
||||
import '../models/settlement.dart';
|
||||
|
||||
class BalanceRepository {
|
||||
final ExpenseRepository _expenseRepository;
|
||||
final ErrorService _errorService;
|
||||
|
||||
BalanceRepository({
|
||||
required ExpenseRepository expenseRepository,
|
||||
ErrorService? errorService,
|
||||
}) : _expenseRepository = expenseRepository,
|
||||
_errorService = errorService ?? ErrorService();
|
||||
|
||||
// Calculer la balance d'un groupe
|
||||
Future<GroupBalance> calculateGroupBalance(String groupId) async {
|
||||
try {
|
||||
// Pour l'instant, on utilise une liste statique des dépenses
|
||||
// En production, vous récupéreriez depuis le stream
|
||||
final expenses = await _expenseRepository
|
||||
.getExpensesStream(groupId)
|
||||
.first;
|
||||
|
||||
final userBalances = _calculateUserBalances(expenses);
|
||||
final settlements = _calculateOptimalSettlements(userBalances);
|
||||
final totalExpenses = expenses
|
||||
.where((e) => !e.isArchived)
|
||||
.fold(0.0, (sum, e) => sum + e.amountInEur);
|
||||
|
||||
return GroupBalance(
|
||||
groupId: groupId,
|
||||
userBalances: userBalances,
|
||||
settlements: settlements,
|
||||
totalExpenses: totalExpenses,
|
||||
calculatedAt: DateTime.now(),
|
||||
);
|
||||
} catch (e) {
|
||||
_errorService.logError('BalanceRepository', 'Erreur calcul balance: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer les balances individuelles
|
||||
List<UserBalance> _calculateUserBalances(List<Expense> expenses) {
|
||||
final Map<String, Map<String, dynamic>> userBalanceMap = {};
|
||||
|
||||
// Initialiser les utilisateurs
|
||||
for (final expense in expenses) {
|
||||
if (expense.isArchived) continue;
|
||||
|
||||
// Ajouter le payeur
|
||||
userBalanceMap.putIfAbsent(expense.paidById, () => {
|
||||
'name': expense.paidByName,
|
||||
'paid': 0.0,
|
||||
'owed': 0.0,
|
||||
});
|
||||
|
||||
// Ajouter les participants
|
||||
for (final split in expense.splits) {
|
||||
userBalanceMap.putIfAbsent(split.userId, () => {
|
||||
'name': split.userName,
|
||||
'paid': 0.0,
|
||||
'owed': 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer les montants
|
||||
for (final expense in expenses) {
|
||||
if (expense.isArchived) continue;
|
||||
|
||||
// Ajouter au montant payé
|
||||
userBalanceMap[expense.paidById]!['paid'] += expense.amountInEur;
|
||||
|
||||
// Ajouter aux montants dus
|
||||
for (final split in expense.splits) {
|
||||
userBalanceMap[split.userId]!['owed'] += split.amount;
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir en liste de UserBalance
|
||||
return userBalanceMap.entries.map((entry) {
|
||||
final userId = entry.key;
|
||||
final data = entry.value;
|
||||
final paid = data['paid'] as double;
|
||||
final owed = data['owed'] as double;
|
||||
|
||||
return UserBalance(
|
||||
userId: userId,
|
||||
userName: data['name'] as String,
|
||||
totalPaid: paid,
|
||||
totalOwed: owed,
|
||||
balance: paid - owed,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Algorithme d'optimisation des règlements
|
||||
List<Settlement> _calculateOptimalSettlements(List<UserBalance> balances) {
|
||||
final settlements = <Settlement>[];
|
||||
|
||||
// Séparer les créditeurs et débiteurs
|
||||
final creditors = balances.where((b) => b.shouldReceive).toList();
|
||||
final debtors = balances.where((b) => b.shouldPay).toList();
|
||||
|
||||
// Trier par montant (plus gros montants en premier)
|
||||
creditors.sort((a, b) => b.balance.compareTo(a.balance));
|
||||
debtors.sort((a, b) => a.balance.compareTo(b.balance));
|
||||
|
||||
// Créer des copies mutables des montants
|
||||
final creditorsRemaining = Map.fromEntries(
|
||||
creditors.map((c) => MapEntry(c.userId, c.balance))
|
||||
);
|
||||
final debtorsRemaining = Map.fromEntries(
|
||||
debtors.map((d) => MapEntry(d.userId, -d.balance))
|
||||
);
|
||||
|
||||
// Algorithme glouton pour minimiser le nombre de transactions
|
||||
for (final creditor in creditors) {
|
||||
for (final debtor in debtors) {
|
||||
final creditAmount = creditorsRemaining[creditor.userId] ?? 0;
|
||||
final debtAmount = debtorsRemaining[debtor.userId] ?? 0;
|
||||
|
||||
if (creditAmount <= 0.01 || debtAmount <= 0.01) continue;
|
||||
|
||||
final settlementAmount = [creditAmount, debtAmount].reduce((a, b) => a < b ? a : b);
|
||||
|
||||
settlements.add(Settlement(
|
||||
fromUserId: debtor.userId,
|
||||
fromUserName: debtor.userName,
|
||||
toUserId: creditor.userId,
|
||||
toUserName: creditor.userName,
|
||||
amount: settlementAmount,
|
||||
));
|
||||
|
||||
creditorsRemaining[creditor.userId] = creditAmount - settlementAmount;
|
||||
debtorsRemaining[debtor.userId] = debtAmount - settlementAmount;
|
||||
}
|
||||
}
|
||||
|
||||
return settlements;
|
||||
}
|
||||
}
|
||||
113
lib/repositories/expense_repository.dart
Normal file
113
lib/repositories/expense_repository.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../models/expense.dart';
|
||||
import '../services/error_service.dart';
|
||||
|
||||
class ExpenseRepository {
|
||||
final FirebaseFirestore _firestore;
|
||||
final ErrorService _errorService;
|
||||
|
||||
ExpenseRepository({
|
||||
FirebaseFirestore? firestore,
|
||||
ErrorService? errorService,
|
||||
}) : _firestore = firestore ?? FirebaseFirestore.instance,
|
||||
_errorService = errorService ?? ErrorService();
|
||||
|
||||
CollectionReference get _expensesCollection => _firestore.collection('expenses');
|
||||
|
||||
// Stream des dépenses d'un groupe
|
||||
Stream<List<Expense>> getExpensesStream(String groupId) {
|
||||
return _expensesCollection
|
||||
.where('groupId', isEqualTo: groupId)
|
||||
.where('isArchived', isEqualTo: false)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.snapshots()
|
||||
.map((snapshot) {
|
||||
return snapshot.docs
|
||||
.map((doc) => Expense.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
||||
.toList();
|
||||
}).handleError((error) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur stream expenses: $error');
|
||||
return <Expense>[];
|
||||
});
|
||||
}
|
||||
|
||||
// Créer une dépense
|
||||
Future<String> createExpense(Expense expense) async {
|
||||
try {
|
||||
final docRef = await _expensesCollection.add(expense.toMap());
|
||||
return docRef.id;
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur création expense: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour une dépense
|
||||
Future<void> updateExpense(Expense expense) async {
|
||||
try {
|
||||
final updateData = expense.toMap();
|
||||
updateData['editedAt'] = FieldValue.serverTimestamp();
|
||||
updateData['isEdited'] = true;
|
||||
|
||||
await _expensesCollection.doc(expense.id).update(updateData);
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur update expense: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer une dépense
|
||||
Future<void> deleteExpense(String expenseId) async {
|
||||
try {
|
||||
await _expensesCollection.doc(expenseId).delete();
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur delete expense: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Marquer un split comme payé
|
||||
Future<void> markSplitAsPaid(String expenseId, String userId) async {
|
||||
try {
|
||||
await _firestore.runTransaction((transaction) async {
|
||||
final expenseDoc = await transaction.get(_expensesCollection.doc(expenseId));
|
||||
|
||||
if (!expenseDoc.exists) {
|
||||
throw Exception('Dépense non trouvée');
|
||||
}
|
||||
|
||||
final expense = Expense.fromMap(
|
||||
expenseDoc.data() as Map<String, dynamic>,
|
||||
expenseDoc.id
|
||||
);
|
||||
|
||||
final updatedSplits = expense.splits.map((split) {
|
||||
if (split.userId == userId) {
|
||||
return split.copyWith(isPaid: true, paidAt: DateTime.now());
|
||||
}
|
||||
return split;
|
||||
}).toList();
|
||||
|
||||
transaction.update(expenseDoc.reference, {
|
||||
'splits': updatedSplits.map((s) => s.toMap()).toList(),
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur mark split paid: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Archiver une dépense
|
||||
Future<void> archiveExpense(String expenseId) async {
|
||||
try {
|
||||
await _expensesCollection.doc(expenseId).update({
|
||||
'isArchived': true,
|
||||
'archivedAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseRepository', 'Erreur archive expense: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:travel_mate/services/error_service.dart';
|
||||
import '../data/models/group.dart';
|
||||
import '../data/models/group_member.dart';
|
||||
import '../models/group.dart';
|
||||
import '../models/group_member.dart';
|
||||
|
||||
class GroupRepository {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../data/models/message.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
class MessageRepository {
|
||||
final FirebaseFirestore _firestore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../data/models/trip.dart';
|
||||
import '../models/trip.dart';
|
||||
|
||||
class TripRepository {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../data/models/user.dart';
|
||||
import '../models/user.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class UserRepository {
|
||||
|
||||
Reference in New Issue
Block a user