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); 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 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 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 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? 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? 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 newSplits) { return copyWith( splits: newSplits, ); } @override List get props => [id]; }