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:
0
lib/blocs/account/account_bloc.dart
Normal file
0
lib/blocs/account/account_bloc.dart
Normal file
0
lib/blocs/account/account_event.dart
Normal file
0
lib/blocs/account/account_event.dart
Normal file
0
lib/blocs/account/account_state.dart
Normal file
0
lib/blocs/account/account_state.dart
Normal 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];
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../blocs/count/count_bloc.dart';
|
import '../../blocs/account/account_bloc.dart';
|
||||||
import '../../blocs/count/count_event.dart';
|
import '../../blocs/account/account_event.dart';
|
||||||
import '../../blocs/count/count_state.dart';
|
import '../../blocs/account/account_state.dart';
|
||||||
import '../../blocs/user/user_state.dart' as user_state;
|
import '../../blocs/user/user_state.dart' as user_state;
|
||||||
import '../../data/models/group.dart';
|
import '../../data/models/group.dart';
|
||||||
import '../../data/models/expense.dart';
|
import '../../data/models/expense.dart';
|
||||||
@@ -148,7 +148,7 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Convertir en EUR
|
// 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)
|
? (context.read<CountBloc>().state as ExpensesLoaded)
|
||||||
.exchangeRates[_selectedCurrency]! * amount
|
.exchangeRates[_selectedCurrency]! * amount
|
||||||
: amount;
|
: amount;
|
||||||
@@ -435,7 +435,7 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../blocs/count/count_bloc.dart';
|
import '../../blocs/account/account_bloc.dart';
|
||||||
import '../../blocs/count/count_event.dart';
|
import '../../blocs/account/account_event.dart';
|
||||||
import '../../blocs/user/user_bloc.dart';
|
import '../../blocs/user/user_bloc.dart';
|
||||||
import '../../blocs/user/user_state.dart' as user_state;
|
import '../../blocs/user/user_state.dart' as user_state;
|
||||||
import '../../data/models/expense.dart';
|
import '../../data/models/expense.dart';
|
||||||
@@ -67,7 +67,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withOpacity(0.1),
|
color: Colors.blue.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../blocs/count/count_bloc.dart';
|
import '../../blocs/account/account_bloc.dart';
|
||||||
import '../../blocs/count/count_event.dart';
|
import '../../blocs/account/account_event.dart';
|
||||||
import '../../blocs/count/count_state.dart';
|
import '../../blocs/account/account_state.dart';
|
||||||
import '../../blocs/user/user_bloc.dart';
|
import '../../blocs/user/user_bloc.dart';
|
||||||
import '../../blocs/user/user_state.dart' as user_state;
|
import '../../blocs/user/user_state.dart' as user_state;
|
||||||
import '../../data/models/group.dart';
|
import '../../data/models/group.dart';
|
||||||
@@ -9,8 +9,6 @@ import '../../blocs/trip/trip_event.dart';
|
|||||||
import '../../blocs/trip/trip_state.dart';
|
import '../../blocs/trip/trip_state.dart';
|
||||||
import '../../blocs/group/group_bloc.dart';
|
import '../../blocs/group/group_bloc.dart';
|
||||||
import '../../blocs/group/group_event.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.dart';
|
||||||
import '../../data/models/group_member.dart';
|
import '../../data/models/group_member.dart';
|
||||||
import '../../services/user_service.dart';
|
import '../../services/user_service.dart';
|
||||||
@@ -572,7 +570,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
final group = Group(
|
final group = Group(
|
||||||
id: '', // Sera généré par Firestore
|
id: '', // Sera généré par Firestore
|
||||||
name: _titleController.text.trim(),
|
name: _titleController.text.trim(),
|
||||||
tripId: tripId,
|
tripId: tripId, // ✅ ID du voyage récupéré
|
||||||
createdBy: currentUser.id,
|
createdBy: currentUser.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -591,15 +589,11 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Créer le groupe
|
|
||||||
context.read<GroupBloc>().add(CreateGroupWithMembers(
|
context.read<GroupBloc>().add(CreateGroupWithMembers(
|
||||||
group: group,
|
group: group,
|
||||||
members: groupMembers,
|
members: groupMembers,
|
||||||
));
|
));
|
||||||
|
|
||||||
// ✅ AJOUT : Attendre un court instant pour que le groupe soit créé
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|||||||
48
lib/data/models/account.dart
Normal file
48
lib/data/models/account.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,13 @@ import 'blocs/theme/theme_state.dart';
|
|||||||
import 'blocs/group/group_bloc.dart';
|
import 'blocs/group/group_bloc.dart';
|
||||||
import 'blocs/user/user_bloc.dart';
|
import 'blocs/user/user_bloc.dart';
|
||||||
import 'blocs/trip/trip_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/auth_repository.dart';
|
||||||
import 'repositories/trip_repository.dart';
|
import 'repositories/trip_repository.dart';
|
||||||
import 'repositories/user_repository.dart';
|
import 'repositories/user_repository.dart';
|
||||||
import 'repositories/group_repository.dart';
|
import 'repositories/group_repository.dart';
|
||||||
import 'repositories/message_repository.dart';
|
import 'repositories/message_repository.dart';
|
||||||
import 'repositories/count_repository.dart';
|
import 'repositories/account_repository.dart';
|
||||||
import 'pages/login.dart';
|
import 'pages/login.dart';
|
||||||
import 'pages/home.dart';
|
import 'pages/home.dart';
|
||||||
import 'pages/signup.dart';
|
import 'pages/signup.dart';
|
||||||
@@ -54,9 +54,7 @@ class MyApp extends StatelessWidget {
|
|||||||
RepositoryProvider<MessageRepository>(
|
RepositoryProvider<MessageRepository>(
|
||||||
create: (context) => MessageRepository(),
|
create: (context) => MessageRepository(),
|
||||||
),
|
),
|
||||||
RepositoryProvider<CountRepository>(
|
|
||||||
create: (context) => CountRepository(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: MultiBlocProvider(
|
child: MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -79,9 +77,7 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => MessageBloc(),
|
create: (context) => MessageBloc(),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
|
||||||
create: (context) => CountBloc(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: BlocBuilder<ThemeBloc, ThemeState>(
|
child: BlocBuilder<ThemeBloc, ThemeState>(
|
||||||
builder: (context, themeState) {
|
builder: (context, themeState) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import '../components/home/home_content.dart';
|
|||||||
import '../components/settings/settings_content.dart';
|
import '../components/settings/settings_content.dart';
|
||||||
import '../components/map/map_content.dart';
|
import '../components/map/map_content.dart';
|
||||||
import '../components/group/group_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_bloc.dart';
|
||||||
import '../blocs/user/user_event.dart';
|
import '../blocs/user/user_event.dart';
|
||||||
import '../blocs/auth/auth_bloc.dart';
|
import '../blocs/auth/auth_bloc.dart';
|
||||||
|
|||||||
117
lib/repositories/account_repository.dart
Normal file
117
lib/repositories/account_repository.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
lib/services/account_service.dart
Normal file
0
lib/services/account_service.dart
Normal 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user