feat: Add logger service and improve expense dialog with enhanced receipt management and calculation logic.

This commit is contained in:
Van Leemput Dayron
2025-11-28 12:54:54 +01:00
parent cad9d42128
commit fd710b8cb8
35 changed files with 2148 additions and 1296 deletions

View File

@@ -46,7 +46,7 @@ class Activity {
/// Calcule le score total des votes
int get totalVotes {
return votes.values.fold(0, (sum, vote) => sum + vote);
return votes.values.fold(0, (total, vote) => total + vote);
}
/// Calcule le nombre de votes positifs

View File

@@ -4,107 +4,114 @@ import 'package:flutter/material.dart';
import 'expense_split.dart';
/// Enumeration of supported currencies for expenses.
///
///
/// Each currency includes both a display symbol and standard currency code.
enum ExpenseCurrency {
/// Euro currency
eur('', 'EUR'),
/// US Dollar currency
/// US Dollar currency
usd('\$', 'USD'),
/// British Pound currency
gbp('£', 'GBP');
const ExpenseCurrency(this.symbol, this.code);
/// Currency symbol for display (e.g., €, $, £)
final String symbol;
/// Standard currency code (e.g., EUR, USD, GBP)
final String code;
}
/// Enumeration of expense categories with display names and icons.
///
///
/// Provides predefined categories for organizing travel expenses.
enum ExpenseCategory {
/// Restaurant and food expenses
restaurant('Restaurant', Icons.restaurant),
/// Transportation expenses
transport('Transport', Icons.directions_car),
/// Accommodation and lodging expenses
accommodation('Accommodation', Icons.hotel),
/// Entertainment and activity expenses
entertainment('Entertainment', Icons.local_activity),
/// Shopping expenses
shopping('Shopping', Icons.shopping_bag),
/// Other miscellaneous expenses
other('Other', Icons.category);
const ExpenseCategory(this.displayName, this.icon);
/// Human-readable display name for the category
final String displayName;
/// Icon representing the category
final IconData icon;
}
/// Model representing a travel expense.
///
///
/// This class encapsulates all information about an expense including
/// amount, currency, category, who paid, how it's split among participants,
/// and receipt information. It extends [Equatable] for value comparison.
class Expense extends Equatable {
/// Unique identifier for the expense
final String id;
/// ID of the group this expense belongs to
final String groupId;
/// Description of the expense
final String description;
/// Amount of the expense in the original currency
final double amount;
/// Currency of the expense
final ExpenseCurrency currency;
/// Amount converted to EUR for standardized calculations
final double amountInEur;
/// Category of the expense
final ExpenseCategory category;
/// ID of the user who paid for this expense
final String paidById;
/// Name of the user who paid for this expense
final String paidByName;
/// Date when the expense occurred
final DateTime date;
/// Timestamp when the expense was created
final DateTime createdAt;
/// Timestamp when the expense was last edited (null if never edited)
final DateTime? editedAt;
/// Whether this expense has been edited after creation
final bool isEdited;
/// Whether this expense has been archived
final bool isArchived;
/// URL to the receipt image (optional)
final String? receiptUrl;
/// List of expense splits showing how the cost is divided
final List<ExpenseSplit> splits;
/// Creates a new [Expense] instance.
///
///
/// All parameters except [editedAt] and [receiptUrl] are required.
const Expense({
required this.id,
@@ -144,13 +151,17 @@ class Expense extends Equatable {
paidByName: map['paidByName'] ?? '',
date: _parseDateTime(map['date']),
createdAt: _parseDateTime(map['createdAt']),
editedAt: map['editedAt'] != null ? _parseDateTime(map['editedAt']) : null,
editedAt: map['editedAt'] != null
? _parseDateTime(map['editedAt'])
: null,
isEdited: map['isEdited'] ?? false,
isArchived: map['isArchived'] ?? false,
receiptUrl: map['receiptUrl'],
splits: (map['splits'] as List?)
?.map((s) => ExpenseSplit.fromMap(s))
.toList() ?? [],
splits:
(map['splits'] as List?)
?.map((s) => ExpenseSplit.fromMap(s))
.toList() ??
[],
);
}
@@ -243,25 +254,36 @@ class Expense extends Equatable {
// Marquer comme archivé
Expense copyWithArchived() {
return copyWith(
isArchived: true,
);
return copyWith(isArchived: true);
}
// Ajouter/mettre à jour l'URL du reçu
Expense copyWithReceipt(String receiptUrl) {
return copyWith(
receiptUrl: receiptUrl,
);
return copyWith(receiptUrl: receiptUrl);
}
// Mettre à jour les splits
Expense copyWithSplits(List<ExpenseSplit> newSplits) {
return copyWith(
splits: newSplits,
);
return copyWith(splits: newSplits);
}
@override
List<Object?> get props => [id];
}
List<Object?> get props => [
id,
groupId,
description,
amount,
currency,
amountInEur,
category,
paidById,
paidByName,
date,
createdAt,
editedAt,
isEdited,
isArchived,
receiptUrl,
splits,
];
}

View File

@@ -22,12 +22,22 @@ class GroupBalance extends Equatable {
factory GroupBalance.fromMap(Map<String, dynamic> map) {
return GroupBalance(
groupId: map['groupId'] ?? '',
userBalances: (map['userBalances'] as List?)
?.map((userBalance) => UserBalance.fromMap(userBalance as Map<String, dynamic>))
.toList() ?? [],
settlements: (map['settlements'] as List?)
?.map((settlement) => Settlement.fromMap(settlement as Map<String, dynamic>))
.toList() ?? [],
userBalances:
(map['userBalances'] as List?)
?.map(
(userBalance) =>
UserBalance.fromMap(userBalance as Map<String, dynamic>),
)
.toList() ??
[],
settlements:
(map['settlements'] as List?)
?.map(
(settlement) =>
Settlement.fromMap(settlement as Map<String, dynamic>),
)
.toList() ??
[],
totalExpenses: (map['totalExpenses'] as num?)?.toDouble() ?? 0.0,
calculatedAt: _parseDateTime(map['calculatedAt']),
);
@@ -37,8 +47,12 @@ class GroupBalance extends Equatable {
Map<String, dynamic> toMap() {
return {
'groupId': groupId,
'userBalances': userBalances.map((userBalance) => userBalance.toMap()).toList(),
'settlements': settlements.map((settlement) => settlement.toMap()).toList(),
'userBalances': userBalances
.map((userBalance) => userBalance.toMap())
.toList(),
'settlements': settlements
.map((settlement) => settlement.toMap())
.toList(),
'totalExpenses': totalExpenses,
'calculatedAt': Timestamp.fromDate(calculatedAt),
};
@@ -71,16 +85,20 @@ class GroupBalance extends Equatable {
}
// Méthodes utilitaires pour la logique métier
bool get hasUnbalancedUsers => userBalances.any((balance) => !balance.isBalanced);
bool get hasUnbalancedUsers =>
userBalances.any((balance) => !balance.isBalanced);
bool get hasSettlements => settlements.isNotEmpty;
double get totalSettlementAmount => settlements.fold(0.0, (sum, settlement) => sum + settlement.amount);
List<UserBalance> get creditors => userBalances.where((b) => b.shouldReceive).toList();
List<UserBalance> get debtors => userBalances.where((b) => b.shouldPay).toList();
double get totalSettlementAmount =>
settlements.fold(0.0, (total, settlement) => total + settlement.amount);
List<UserBalance> get creditors =>
userBalances.where((b) => b.shouldReceive).toList();
List<UserBalance> get debtors =>
userBalances.where((b) => b.shouldPay).toList();
int get participantCount => userBalances.length;
@override
@@ -90,4 +108,4 @@ class GroupBalance extends Equatable {
String toString() {
return 'GroupBalance(groupId: $groupId, totalExpenses: $totalExpenses, participantCount: $participantCount, calculatedAt: $calculatedAt)';
}
}
}