import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../repositories/expense_repository.dart'; import '../../services/expense_service.dart'; import '../../services/error_service.dart'; import 'expense_event.dart'; import 'expense_state.dart'; /// BLoC for managing expense operations and state. /// /// This BLoC handles expense-related operations including loading expenses, /// creating new expenses, updating existing ones, deleting expenses, and /// managing expense splits. It coordinates with the expense repository and /// service to provide business logic and data persistence. class ExpenseBloc extends Bloc { /// Repository for expense data operations. final ExpenseRepository _expenseRepository; /// Service for expense business logic and validation. final ExpenseService _expenseService; /// Service for error handling and logging. final ErrorService _errorService; /// Subscription to the expenses stream for real-time updates. StreamSubscription? _expensesSubscription; /// Creates a new [ExpenseBloc] with required dependencies. /// /// [expenseRepository] is required for data operations. /// [expenseService] and [errorService] have default implementations if not provided. ExpenseBloc({ required ExpenseRepository expenseRepository, ExpenseService? expenseService, ErrorService? errorService, }) : _expenseRepository = expenseRepository, _expenseService = expenseService ?? ExpenseService(expenseRepository: expenseRepository), _errorService = errorService ?? ErrorService(), super(ExpenseInitial()) { on(_onLoadExpensesByGroup); on(_onExpensesUpdated); on(_onCreateExpense); on(_onUpdateExpense); on(_onDeleteExpense); on(_onMarkSplitAsPaid); on(_onArchiveExpense); } /// Handles [LoadExpensesByGroup] events. /// /// Sets up a stream subscription to receive real-time updates for expenses /// in the specified group. Cancels any existing subscription before creating a new one. Future _onLoadExpensesByGroup( LoadExpensesByGroup event, Emitter emit, ) async { try { // Emit empty state initially to avoid infinite spinner // The stream will update with actual data when available emit(const ExpensesLoaded(expenses: [])); await _expensesSubscription?.cancel(); _expensesSubscription = _expenseRepository .getExpensesStream(event.groupId) .listen( (expenses) => add(ExpensesUpdated(expenses)), onError: (error) => add(ExpensesUpdated([], error: error.toString())), ); } catch (e) { _errorService.logError('ExpenseBloc', 'Error loading expenses: $e'); emit(const ExpenseError('Impossible de charger les dépenses')); } } /// Handles [ExpensesUpdated] events. /// /// Processes real-time updates from the expense stream, either emitting /// the updated expense list or an error state if the stream encountered an error. /// /// Args: /// [event]: The ExpensesUpdated event containing expenses or error information /// [emit]: State emitter function Future _onExpensesUpdated( ExpensesUpdated event, Emitter emit, ) async { if (event.error != null) { emit(ExpenseError(event.error!)); } else { emit(ExpensesLoaded(expenses: event.expenses)); } } /// Handles [CreateExpense] events. /// /// Creates a new expense with validation and optional receipt image upload. /// Uses the expense service to handle business logic and validation, /// including currency conversion and split calculations. /// /// Args: /// [event]: The CreateExpense event containing expense data and optional receipt /// [emit]: State emitter function Future _onCreateExpense( CreateExpense event, Emitter emit, ) async { try { await _expenseService.createExpenseWithValidation( event.expense, event.receiptImage, ); emit(const ExpenseOperationSuccess('Expense created successfully')); } catch (e) { _errorService.logError('ExpenseBloc', 'Error creating expense: $e'); emit(const ExpenseError('Impossible de créer la dépense')); } } /// Handles [UpdateExpense] events. /// /// Updates an existing expense with validation and optional new receipt image. /// Uses the expense service to handle business logic, validation, and /// recalculation of splits if expense details change. /// /// Args: /// [event]: The UpdateExpense event containing updated expense data and optional new receipt /// [emit]: State emitter function Future _onUpdateExpense( UpdateExpense event, Emitter emit, ) async { try { await _expenseService.updateExpenseWithValidation( event.expense, event.newReceiptImage, ); emit(const ExpenseOperationSuccess('Expense updated successfully')); } catch (e) { _errorService.logError('ExpenseBloc', 'Error updating expense: $e'); emit(const ExpenseError('Impossible de mettre à jour la dépense')); } } /// Handles [DeleteExpense] events. /// /// Permanently deletes an expense from the database. This action /// cannot be undone and will affect group balance calculations. /// /// Args: /// [event]: The DeleteExpense event containing the expense ID to delete /// [emit]: State emitter function Future _onDeleteExpense( DeleteExpense event, Emitter emit, ) async { try { await _expenseRepository.deleteExpense(event.expenseId); emit(const ExpenseOperationSuccess('Expense deleted successfully')); } catch (e) { _errorService.logError('ExpenseBloc', 'Error deleting expense: $e'); emit(const ExpenseError('Impossible de supprimer la dépense')); } } /// Handles [MarkSplitAsPaid] events. /// /// Marks a user's portion of an expense split as paid, updating the /// expense's split information and affecting balance calculations. /// This helps track who has settled their portion of shared expenses. /// /// Args: /// [event]: The MarkSplitAsPaid event containing expense ID and user ID /// [emit]: State emitter function Future _onMarkSplitAsPaid( MarkSplitAsPaid event, Emitter emit, ) async { try { await _expenseRepository.markSplitAsPaid(event.expenseId, event.userId); emit(const ExpenseOperationSuccess('Payment marked as completed')); } catch (e) { _errorService.logError('ExpenseBloc', 'Error marking split as paid: $e'); emit(const ExpenseError('Impossible de marquer comme payé')); } } /// Handles [ArchiveExpense] events. /// /// Archives an expense, moving it out of the active expense list /// while preserving it for historical records and audit purposes. /// Archived expenses are not included in current balance calculations. /// /// Args: /// [event]: The ArchiveExpense event containing the expense ID to archive /// [emit]: State emitter function Future _onArchiveExpense( ArchiveExpense event, Emitter emit, ) async { try { await _expenseRepository.archiveExpense(event.expenseId); emit(const ExpenseOperationSuccess('Expense archived successfully')); } catch (e) { _errorService.logError('ExpenseBloc', 'Error archiving expense: $e'); emit(const ExpenseError('Impossible d\'archiver la dépense')); } } /// Cleans up resources when the bloc is closed. /// /// Cancels the expense stream subscription to prevent memory leaks /// and ensure proper disposal of resources. @override Future close() { _expensesSubscription?.cancel(); return super.close(); } }