293 lines
7.8 KiB
Dart
293 lines
7.8 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
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
|
|
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),
|
|
|
|
/// Reimbursement for settling debts
|
|
reimbursement('Remboursement', Icons.monetization_on);
|
|
|
|
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,
|
|
required this.groupId,
|
|
required this.description,
|
|
required this.amount,
|
|
required this.currency,
|
|
required this.amountInEur,
|
|
required this.category,
|
|
required this.paidById,
|
|
required this.paidByName,
|
|
required this.date,
|
|
required this.createdAt,
|
|
this.editedAt,
|
|
this.isEdited = false,
|
|
this.isArchived = false,
|
|
this.receiptUrl,
|
|
required this.splits,
|
|
});
|
|
|
|
factory Expense.fromMap(Map<String, dynamic> map, String id) {
|
|
return Expense(
|
|
id: id,
|
|
groupId: map['groupId'] ?? '',
|
|
description: map['description'] ?? '',
|
|
amount: (map['amount'] as num?)?.toDouble() ?? 0.0,
|
|
currency: ExpenseCurrency.values.firstWhere(
|
|
(c) => c.code == map['currency'],
|
|
orElse: () => ExpenseCurrency.eur,
|
|
),
|
|
amountInEur: (map['amountInEur'] as num?)?.toDouble() ?? 0.0,
|
|
category: ExpenseCategory.values.firstWhere(
|
|
(c) => c.name == map['category'],
|
|
orElse: () => ExpenseCategory.other,
|
|
),
|
|
paidById: map['paidById'] ?? '',
|
|
paidByName: map['paidByName'] ?? '',
|
|
date: _parseDateTime(map['date']),
|
|
createdAt: _parseDateTime(map['createdAt']),
|
|
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() ??
|
|
[],
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toMap() {
|
|
return {
|
|
'groupId': groupId,
|
|
'description': description,
|
|
'amount': amount,
|
|
'currency': currency.code,
|
|
'amountInEur': amountInEur,
|
|
'category': category.name,
|
|
'paidById': paidById,
|
|
'paidByName': paidByName,
|
|
'date': Timestamp.fromDate(date),
|
|
'createdAt': Timestamp.fromDate(createdAt),
|
|
'editedAt': editedAt != null ? Timestamp.fromDate(editedAt!) : null,
|
|
'isEdited': isEdited,
|
|
'isArchived': isArchived,
|
|
'receiptUrl': receiptUrl,
|
|
'splits': splits.map((s) => s.toMap()).toList(),
|
|
};
|
|
}
|
|
|
|
static DateTime _parseDateTime(dynamic value) {
|
|
if (value is Timestamp) return value.toDate();
|
|
if (value is String) return DateTime.parse(value);
|
|
if (value is DateTime) return value;
|
|
return DateTime.now();
|
|
}
|
|
|
|
Expense copyWith({
|
|
String? id,
|
|
String? groupId,
|
|
String? description,
|
|
double? amount,
|
|
ExpenseCurrency? currency,
|
|
double? amountInEur,
|
|
ExpenseCategory? category,
|
|
String? paidById,
|
|
String? paidByName,
|
|
DateTime? date,
|
|
DateTime? createdAt,
|
|
DateTime? editedAt,
|
|
bool? isEdited,
|
|
bool? isArchived,
|
|
String? receiptUrl,
|
|
List<ExpenseSplit>? splits,
|
|
}) {
|
|
return Expense(
|
|
id: id ?? this.id,
|
|
groupId: groupId ?? this.groupId,
|
|
description: description ?? this.description,
|
|
amount: amount ?? this.amount,
|
|
currency: currency ?? this.currency,
|
|
amountInEur: amountInEur ?? this.amountInEur,
|
|
category: category ?? this.category,
|
|
paidById: paidById ?? this.paidById,
|
|
paidByName: paidByName ?? this.paidByName,
|
|
date: date ?? this.date,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
editedAt: editedAt ?? this.editedAt,
|
|
isEdited: isEdited ?? this.isEdited,
|
|
isArchived: isArchived ?? this.isArchived,
|
|
receiptUrl: receiptUrl ?? this.receiptUrl,
|
|
splits: splits ?? this.splits,
|
|
);
|
|
}
|
|
|
|
Expense copyWithEdit({
|
|
String? description,
|
|
double? amount,
|
|
ExpenseCurrency? currency,
|
|
double? amountInEur,
|
|
ExpenseCategory? category,
|
|
List<ExpenseSplit>? splits,
|
|
String? receiptUrl,
|
|
}) {
|
|
return copyWith(
|
|
description: description,
|
|
amount: amount,
|
|
currency: currency,
|
|
amountInEur: amountInEur,
|
|
category: category,
|
|
splits: splits,
|
|
receiptUrl: receiptUrl,
|
|
editedAt: DateTime.now(),
|
|
isEdited: true,
|
|
);
|
|
}
|
|
|
|
// Marquer comme archivé
|
|
Expense copyWithArchived() {
|
|
return copyWith(isArchived: true);
|
|
}
|
|
|
|
// Ajouter/mettre à jour l'URL du reçu
|
|
Expense copyWithReceipt(String receiptUrl) {
|
|
return copyWith(receiptUrl: receiptUrl);
|
|
}
|
|
|
|
// Mettre à jour les splits
|
|
Expense copyWithSplits(List<ExpenseSplit> newSplits) {
|
|
return copyWith(splits: newSplits);
|
|
}
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
id,
|
|
groupId,
|
|
description,
|
|
amount,
|
|
currency,
|
|
amountInEur,
|
|
category,
|
|
paidById,
|
|
paidByName,
|
|
date,
|
|
createdAt,
|
|
editedAt,
|
|
isEdited,
|
|
isArchived,
|
|
receiptUrl,
|
|
splits,
|
|
];
|
|
}
|