Enhance model and service documentation with detailed comments and descriptions

- Updated Group, Trip, User, and other model classes to include comprehensive documentation for better understanding and maintainability.
- Improved error handling and logging in services, including AuthService, ErrorService, and StorageService.
- Added validation and business logic explanations in ExpenseService and TripService.
- Refactored method comments to follow a consistent format across the codebase.
- Translated error messages and comments from French to English for consistency.
This commit is contained in:
Dayron
2025-10-30 15:56:17 +01:00
parent 1eeea6997e
commit 2faf37f145
46 changed files with 2656 additions and 220 deletions

View File

@@ -1,3 +1,26 @@
/// A BLoC (Business Logic Component) that manages account-related state and operations.
///
/// This bloc handles account operations such as loading accounts by user ID,
/// creating new accounts, and managing real-time updates from the account repository.
/// It uses stream subscriptions to listen for account changes and emits corresponding states.
///
/// The bloc supports the following operations:
/// - Loading accounts by user ID with real-time updates
/// - Creating a single account without members
/// - Creating an account with associated members
///
/// All errors are logged using [ErrorService] and emitted as [AccountError] states.
///
/// Example usage:
/// ```dart
/// final accountBloc = AccountBloc(accountRepository);
/// accountBloc.add(LoadAccountsByUserId('user123'));
/// ```
///
/// Remember to close the bloc when done to cancel active subscriptions:
/// ```dart
/// accountBloc.close();
/// ```
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/services/error_service.dart';

View File

@@ -1,14 +1,23 @@
import 'package:equatable/equatable.dart';
import '../../models/account.dart';
import '../../models/group_member.dart';
import 'package:travel_mate/models/account.dart';
import 'package:travel_mate/models/group_member.dart';
/// Abstract base class for account-related events in the BLoC pattern.
///
/// This class extends [Equatable] to enable value comparison for events.
/// All account events should inherit from this class and implement the [props] getter.
abstract class AccountEvent extends Equatable {
const AccountEvent();
@override
List<Object?> get props => [];
}
}
/// Event to load accounts associated with a specific user ID.
///
/// This event is dispatched when the application needs to fetch accounts
/// for a given user, typically for displaying user-specific account data.
class LoadAccountsByUserId extends AccountEvent {
final String userId;
@@ -18,6 +27,10 @@ class LoadAccountsByUserId extends AccountEvent {
List<Object?> get props => [userId];
}
/// Event to load accounts associated with a specific trip.
///
/// This event is dispatched when the application needs to fetch accounts
/// related to a particular trip, such as for trip expense management.
class LoadAccountsByTrip extends AccountEvent {
final String tripId;
@@ -27,6 +40,10 @@ class LoadAccountsByTrip extends AccountEvent {
List<Object?> get props => [tripId];
}
/// Event to create a new account.
///
/// This event is dispatched when a new account needs to be created,
/// passing the [Account] object containing the account details.
class CreateAccount extends AccountEvent {
final Account account;
@@ -36,6 +53,10 @@ class CreateAccount extends AccountEvent {
List<Object?> get props => [account];
}
/// Event to update an existing account.
///
/// This event is dispatched when an account needs to be modified,
/// requiring the [accountId] to identify the account and the updated [Account] object.
class UpdateAccount extends AccountEvent {
final String accountId;
final Account account;
@@ -49,6 +70,10 @@ class UpdateAccount extends AccountEvent {
List<Object?> get props => [accountId, account];
}
/// Event to create a new account along with its group members.
///
/// This event is dispatched when creating an account that includes
/// a list of [GroupMember] objects, such as for shared trip accounts.
class CreateAccountWithMembers extends AccountEvent {
final Account account;
final List<GroupMember> members;

View File

@@ -1,3 +1,38 @@
/// The abstract base class for all account-related states used by the AccountBloc.
///
/// Extends Equatable to enable value-based comparisons between state
/// instances. Subclasses should provide the relevant properties by
/// overriding `props` so that the bloc can correctly determine whether
/// the state has changed.
/// Represents the initial state of the account feature.
///
/// Used before any account-related action has started or when the bloc
/// has been freshly created.
/// Indicates that an account-related operation is currently in progress.
///
/// This state is typically emitted while fetching account data, creating,
/// updating, or deleting an account so the UI can show a loading indicator.
/// Emitted when a collection of accounts has been successfully loaded.
///
/// Contains:
/// - `accounts`: the list of Account models retrieved from the repository.
///
/// Use this state to display fetched account data in the UI.
/// Represents a successful account operation that does not necessarily
/// carry account data (e.g., after creating, updating, or deleting an account).
///
/// Contains:
/// - `message`: a human-readable success message that can be shown to the user.
/// Represents an error that occurred during an account-related operation.
///
/// Contains:
/// - `message`: a human-readable error description suitable for logging
/// or displaying to the user.
import 'package:equatable/equatable.dart';
import '../../models/account.dart';

View File

@@ -1,11 +1,38 @@
/// Business Logic Component for managing authentication state.
///
/// The [AuthBloc] handles authentication-related events and manages the
/// authentication state throughout the application. It coordinates with
/// the [AuthRepository] to perform authentication operations and emits
/// appropriate states based on the results.
///
/// Supported authentication methods:
/// - Email and password authentication
/// - Google Sign-In
/// - Apple Sign-In
/// - Password reset functionality
///
/// This bloc handles the following events:
/// - [AuthCheckRequested]: Verifies current authentication status
/// - [AuthSignInRequested]: Processes email/password sign-in
/// - [AuthSignUpRequested]: Processes user registration
/// - [AuthGoogleSignInRequested]: Processes Google authentication
/// - [AuthAppleSignInRequested]: Processes Apple authentication
/// - [AuthSignOutRequested]: Processes user sign-out
/// - [AuthPasswordResetRequested]: Processes password reset requests
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repositories/auth_repository.dart';
import 'auth_event.dart';
import 'auth_state.dart';
/// BLoC for managing authentication state and operations.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
/// Repository for authentication operations.
final AuthRepository _authRepository;
/// Creates an [AuthBloc] with the provided [authRepository].
///
/// The bloc starts in the [AuthInitial] state and registers event handlers
/// for all supported authentication events.
AuthBloc({required AuthRepository authRepository})
: _authRepository = authRepository,
super(AuthInitial()) {
@@ -18,6 +45,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<AuthPasswordResetRequested>(_onPasswordResetRequested);
}
/// Handles [AuthCheckRequested] events.
///
/// Checks if a user is currently authenticated and emits the appropriate state.
/// If a user is found, attempts to fetch user data from Firestore.
Future<void> _onAuthCheckRequested(
AuthCheckRequested event,
Emitter<AuthState> emit,
@@ -28,7 +59,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
final currentUser = _authRepository.currentUser;
if (currentUser != null) {
// Récupérer les données utilisateur depuis Firestore
// Fetch user data from Firestore
final user = await _authRepository.getUserFromFirestore(currentUser.uid);
if (user != null) {
@@ -44,6 +75,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
}
/// Handles [AuthSignInRequested] events.
///
/// Attempts to sign in a user with the provided email and password.
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
Future<void> _onSignInRequested(
AuthSignInRequested event,
Emitter<AuthState> emit,
@@ -59,13 +94,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) {
emit(AuthAuthenticated(user: user));
} else {
emit(const AuthError(message: 'Email ou mot de passe incorrect'));
emit(const AuthError(message: 'Invalid email or password'));
}
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
/// Handles [AuthSignUpRequested] events.
///
/// Attempts to create a new user account with the provided information.
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
Future<void> _onSignUpRequested(
AuthSignUpRequested event,
Emitter<AuthState> emit,
@@ -83,13 +122,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) {
emit(AuthAuthenticated(user: user));
} else {
emit(const AuthError(message: 'Erreur lors de l\'inscription'));
emit(const AuthError(message: 'Registration failed'));
}
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
/// Handles [AuthGoogleSignInRequested] events.
///
/// Attempts to sign in the user using Google authentication.
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
Future<void> _onGoogleSignInRequested(
AuthGoogleSignInRequested event,
Emitter<AuthState> emit,
@@ -102,13 +145,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) {
emit(AuthAuthenticated(user: user));
} else {
emit(const AuthError(message: 'Connexion Google annulée'));
emit(const AuthError(message: 'Google sign-in cancelled'));
}
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
/// Handles [AuthAppleSignInRequested] events.
///
/// Attempts to sign in the user using Apple authentication.
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
Future<void> _onAppleSignInRequested(
AuthAppleSignInRequested event,
Emitter<AuthState> emit,
@@ -121,13 +168,16 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) {
emit(AuthAuthenticated(user: user));
} else {
emit(const AuthError(message: 'Connexion Apple annulée'));
emit(const AuthError(message: 'Apple sign-in cancelled'));
}
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
/// Handles [AuthSignOutRequested] events.
///
/// Signs out the current user and emits [AuthUnauthenticated].
Future<void> _onSignOutRequested(
AuthSignOutRequested event,
Emitter<AuthState> emit,
@@ -136,6 +186,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(AuthUnauthenticated());
}
/// Handles [AuthPasswordResetRequested] events.
///
/// Sends a password reset email to the specified email address.
/// Emits [AuthPasswordResetSent] on success or [AuthError] on failure.
Future<void> _onPasswordResetRequested(
AuthPasswordResetRequested event,
Emitter<AuthState> emit,

View File

@@ -1,18 +1,37 @@
import 'package:equatable/equatable.dart';
/// Abstract base class for all authentication-related events.
///
/// This class extends [Equatable] to enable value equality for event comparison.
/// All authentication events in the application should inherit from this class.
abstract class AuthEvent extends Equatable {
/// Creates a new [AuthEvent].
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Event to check the current authentication status.
///
/// This event is typically dispatched when the app starts to determine
/// if a user is already authenticated.
class AuthCheckRequested extends AuthEvent {}
/// Event to request user sign-in with email and password.
///
/// This event contains the user's credentials and triggers the authentication
/// process when dispatched to the [AuthBloc].
class AuthSignInRequested extends AuthEvent {
/// The user's email address.
final String email;
/// The user's password.
final String password;
/// Creates a new [AuthSignInRequested] event.
///
/// Both [email] and [password] are required parameters.
const AuthSignInRequested({
required this.email,
required this.password,
@@ -22,12 +41,26 @@ class AuthSignInRequested extends AuthEvent {
List<Object?> get props => [email, password];
}
/// Event to request user registration with email, password, and personal information.
///
/// This event contains all necessary information to create a new user account
/// and triggers the registration process when dispatched to the [AuthBloc].
class AuthSignUpRequested extends AuthEvent {
/// The user's email address.
final String email;
/// The user's password.
final String password;
/// The user's last name.
final String nom;
/// The user's first name.
final String prenom;
/// Creates a new [AuthSignUpRequested] event.
///
/// All parameters are required for user registration.
const AuthSignUpRequested({
required this.email,
required this.password,
@@ -39,15 +72,33 @@ class AuthSignUpRequested extends AuthEvent {
List<Object?> get props => [email, password, nom, prenom];
}
/// Event to request user sign-in using Google authentication.
///
/// This event triggers the Google sign-in flow when dispatched to the [AuthBloc].
class AuthGoogleSignInRequested extends AuthEvent {}
/// Event to request user sign-in using Apple authentication.
///
/// This event triggers the Apple sign-in flow when dispatched to the [AuthBloc].
class AuthAppleSignInRequested extends AuthEvent {}
/// Event to request user sign-out.
///
/// This event triggers the sign-out process and clears the user session
/// when dispatched to the [AuthBloc].
class AuthSignOutRequested extends AuthEvent {}
/// Event to request a password reset for a user account.
///
/// This event triggers the password reset process by sending a reset email
/// to the specified email address.
class AuthPasswordResetRequested extends AuthEvent {
/// The email address to send the password reset link to.
final String email;
/// Creates a new [AuthPasswordResetRequested] event.
///
/// The [email] parameter is required.
const AuthPasswordResetRequested({required this.email});
@override

View File

@@ -1,40 +1,75 @@
import 'package:equatable/equatable.dart';
import '../../models/user.dart';
/// Abstract base class for all authentication states.
///
/// This class extends [Equatable] to enable value equality for state comparison.
/// All authentication states in the application should inherit from this class.
abstract class AuthState extends Equatable {
/// Creates a new [AuthState].
const AuthState();
@override
List<Object?> get props => [];
}
/// Initial state of the authentication bloc.
///
/// This state represents the initial state before any authentication
/// actions have been performed.
class AuthInitial extends AuthState {}
/// State indicating that an authentication operation is in progress.
///
/// This state is used to show loading indicators during authentication
/// processes like sign-in, sign-up, or sign-out.
class AuthLoading extends AuthState {}
/// State indicating that a user is successfully authenticated.
///
/// This state contains the authenticated user's information and is
/// used throughout the app to access user data.
class AuthAuthenticated extends AuthState {
/// The authenticated user.
final User user;
/// Creates an [AuthAuthenticated] state with the given [user].
const AuthAuthenticated({required this.user});
@override
List<Object?> get props => [user];
}
/// State indicating that no user is currently authenticated.
///
/// This state is used when a user is not signed in or has signed out
/// of the application.
class AuthUnauthenticated extends AuthState {}
/// State indicating that an authentication error has occurred.
///
/// This state contains an error message that can be displayed to the user
/// when authentication operations fail.
class AuthError extends AuthState {
/// The error message describing what went wrong.
final String message;
/// Creates an [AuthError] state with the given error [message].
const AuthError({required this.message});
@override
List<Object?> get props => [message];
}
/// State indicating that a password reset email has been sent.
///
/// This state is used to confirm to the user that a password reset
/// email has been successfully sent to their email address.
class AuthPasswordResetSent extends AuthState {
/// The email address to which the reset link was sent.
final String email;
/// Creates an [AuthPasswordResetSent] state with the given [email].
const AuthPasswordResetSent({required this.email});
@override

View File

@@ -44,6 +44,16 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
final BalanceService _balanceService;
final ErrorService _errorService;
/// Constructor for BalanceBloc.
///
/// Initializes the bloc with required repositories and optional services.
/// Sets up event handlers for balance-related operations.
///
/// Args:
/// [balanceRepository]: Repository for balance data operations
/// [expenseRepository]: Repository for expense data operations
/// [balanceService]: Optional service for balance calculations (auto-created if null)
/// [errorService]: Optional service for error handling (auto-created if null)
BalanceBloc({
required BalanceRepository balanceRepository,
required ExpenseRepository expenseRepository,
@@ -58,6 +68,15 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
on<MarkSettlementAsCompleted>(_onMarkSettlementAsCompleted);
}
/// Handles [LoadGroupBalances] events.
///
/// Loads and calculates user balances for a specific group along with
/// optimal settlement recommendations. This provides a complete overview
/// of who owes money to whom and the most efficient payment strategy.
///
/// Args:
/// [event]: The LoadGroupBalances event containing the group ID
/// [emit]: State emitter function
Future<void> _onLoadGroupBalance(
LoadGroupBalances event,
Emitter<BalanceState> emit,
@@ -65,10 +84,10 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
try {
emit(BalanceLoading());
// Calculer les balances du groupe
// Calculate group user balances
final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
// Calculer les règlements optimisés
// Calculate optimal settlements
final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
emit(GroupBalancesLoaded(
@@ -76,25 +95,34 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
settlements: settlements,
));
} catch (e) {
_errorService.logError('BalanceBloc', 'Erreur chargement balance: $e');
_errorService.logError('BalanceBloc', 'Error loading balance: $e');
emit(BalanceError(e.toString()));
}
}
/// Handles [RefreshBalance] events.
///
/// Refreshes the balance data for a group while trying to maintain the current
/// state when possible to provide a smoother user experience. Only shows loading
/// state if there's no existing balance data.
///
/// Args:
/// [event]: The RefreshBalance event containing the group ID
/// [emit]: State emitter function
Future<void> _onRefreshBalance(
RefreshBalance event,
Emitter<BalanceState> emit,
) async {
try {
// Garde l'état actuel pendant le refresh si possible
// Keep current state during refresh if possible
if (state is! GroupBalancesLoaded) {
emit(BalanceLoading());
}
// Calculer les balances du groupe
// Calculate group user balances
final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
// Calculer les règlements optimisés
// Calculate optimal settlements
final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
emit(GroupBalancesLoaded(
@@ -102,11 +130,20 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
settlements: settlements,
));
} catch (e) {
_errorService.logError('BalanceBloc', 'Erreur refresh balance: $e');
_errorService.logError('BalanceBloc', 'Error refreshing balance: $e');
emit(BalanceError(e.toString()));
}
}
/// Handles [MarkSettlementAsCompleted] events.
///
/// Records a settlement transaction between two users, marking that
/// a debt has been paid. This updates the balance calculations and
/// automatically refreshes the group balance data to reflect the change.
///
/// Args:
/// [event]: The MarkSettlementAsCompleted event containing settlement details
/// [emit]: State emitter function
Future<void> _onMarkSettlementAsCompleted(
MarkSettlementAsCompleted event,
Emitter<BalanceState> emit,
@@ -119,12 +156,12 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
amount: event.amount,
);
emit(const BalanceOperationSuccess('Règlement marqué comme effectué'));
emit(const BalanceOperationSuccess('Settlement marked as completed'));
// Recharger la balance après le règlement
// Reload balance after settlement
add(RefreshBalance(event.groupId));
} catch (e) {
_errorService.logError('BalanceBloc', 'Erreur mark settlement: $e');
_errorService.logError('BalanceBloc', 'Error marking settlement: $e');
emit(BalanceError(e.toString()));
}
}

View File

@@ -1,36 +1,68 @@
import 'package:equatable/equatable.dart';
/// Abstract base class for all balance-related events.
///
/// This class extends [Equatable] to enable value equality for event comparison.
/// All balance events in the application should inherit from this class.
abstract class BalanceEvent extends Equatable {
/// Creates a new [BalanceEvent].
const BalanceEvent();
@override
List<Object?> get props => [];
}
/// Event to load balance information for a specific group.
///
/// This event triggers the loading of user balances and settlements
/// for all members of the specified group. It calculates who owes
/// money to whom based on shared expenses.
class LoadGroupBalances extends BalanceEvent {
/// The ID of the group to load balances for.
final String groupId;
/// Creates a [LoadGroupBalances] event for the specified [groupId].
const LoadGroupBalances(this.groupId);
@override
List<Object> get props => [groupId];
}
/// Event to refresh balance calculations for a group.
///
/// This event recalculates all balances and settlements for a group,
/// typically called after expenses are added, modified, or deleted.
class RefreshBalance extends BalanceEvent {
/// The ID of the group to refresh balances for.
final String groupId;
/// Creates a [RefreshBalance] event for the specified [groupId].
const RefreshBalance(this.groupId);
@override
List<Object?> get props => [groupId];
}
/// Event to mark a settlement as completed between two users.
///
/// This event is dispatched when one user pays back money they owe
/// to another user, updating the balance calculations accordingly.
class MarkSettlementAsCompleted extends BalanceEvent {
/// The ID of the group where the settlement occurred.
final String groupId;
/// The ID of the user who is paying the debt.
final String fromUserId;
/// The ID of the user who is receiving the payment.
final String toUserId;
/// The amount being settled.
final double amount;
/// Creates a [MarkSettlementAsCompleted] event with the settlement details.
///
/// All parameters are required to properly record the settlement.
const MarkSettlementAsCompleted({
required this.groupId,
required this.fromUserId,

View File

@@ -2,21 +2,48 @@ import 'package:equatable/equatable.dart';
import '../../models/settlement.dart';
import '../../models/user_balance.dart';
/// Abstract base class for all balance-related states.
///
/// This class extends [Equatable] to enable value equality for state comparison.
/// All balance states in the application should inherit from this class.
abstract class BalanceState extends Equatable {
/// Creates a new [BalanceState].
const BalanceState();
@override
List<Object?> get props => [];
}
/// Initial state of the balance bloc.
///
/// This state represents the initial state before any balance
/// operations have been performed.
class BalanceInitial extends BalanceState {}
/// State indicating that a balance operation is in progress.
///
/// This state is used to show loading indicators during balance
/// calculations, data loading, or settlement operations.
class BalanceLoading extends BalanceState {}
/// State indicating that group balances have been successfully loaded.
///
/// This state contains the calculated balances for all group members
/// and the list of settlements needed to balance all debts.
class GroupBalancesLoaded extends BalanceState {
/// List of user balances showing how much each user owes or is owed.
///
/// Positive balances indicate the user is owed money,
/// negative balances indicate the user owes money.
final List<UserBalance> balances;
/// List of settlements required to balance all debts in the group.
///
/// Each settlement represents a payment that needs to be made
/// from one user to another to settle shared expenses.
final List<Settlement> settlements;
/// Creates a [GroupBalancesLoaded] state with the calculated data.
const GroupBalancesLoaded({
required this.balances,
required this.settlements,
@@ -26,18 +53,30 @@ class GroupBalancesLoaded extends BalanceState {
List<Object> get props => [balances, settlements];
}
/// State indicating that a balance operation has completed successfully.
///
/// This state is used to show success messages after operations like
/// marking settlements as completed or refreshing balance calculations.
class BalanceOperationSuccess extends BalanceState {
/// Success message to display to the user.
final String message;
/// Creates a [BalanceOperationSuccess] state with the given [message].
const BalanceOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
/// State indicating that a balance operation has failed.
///
/// This state contains an error message that can be displayed to the user
/// when balance operations fail.
class BalanceError extends BalanceState {
/// The error message describing what went wrong.
final String message;
/// Creates a [BalanceError] state with the given error [message].
const BalanceError(this.message);
@override

View File

@@ -6,13 +6,29 @@ 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<ExpenseEvent, ExpenseState> {
/// 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,
@@ -31,6 +47,10 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
on<ArchiveExpense>(_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<void> _onLoadExpensesByGroup(
LoadExpensesByGroup event,
Emitter<ExpenseState> emit,
@@ -47,11 +67,19 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
onError: (error) => add(ExpensesUpdated([], error: error.toString())),
);
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur chargement expenses: $e');
_errorService.logError('ExpenseBloc', 'Error loading expenses: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> _onExpensesUpdated(
ExpensesUpdated event,
Emitter<ExpenseState> emit,
@@ -63,71 +91,119 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
}
}
/// 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<void> _onCreateExpense(
CreateExpense event,
Emitter<ExpenseState> emit,
) async {
try {
await _expenseService.createExpenseWithValidation(event.expense, event.receiptImage);
emit(const ExpenseOperationSuccess('pense créée avec succès'));
emit(const ExpenseOperationSuccess('Expense created successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur création expense: $e');
_errorService.logError('ExpenseBloc', 'Error creating expense: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> _onUpdateExpense(
UpdateExpense event,
Emitter<ExpenseState> emit,
) async {
try {
await _expenseService.updateExpenseWithValidation(event.expense, event.newReceiptImage);
emit(const ExpenseOperationSuccess('pense modifiée avec succès'));
emit(const ExpenseOperationSuccess('Expense updated successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur mise à jour expense: $e');
_errorService.logError('ExpenseBloc', 'Error updating expense: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> _onDeleteExpense(
DeleteExpense event,
Emitter<ExpenseState> emit,
) async {
try {
await _expenseRepository.deleteExpense(event.expenseId);
emit(const ExpenseOperationSuccess('pense supprimée avec succès'));
emit(const ExpenseOperationSuccess('Expense deleted successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur suppression expense: $e');
_errorService.logError('ExpenseBloc', 'Error deleting expense: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> _onMarkSplitAsPaid(
MarkSplitAsPaid event,
Emitter<ExpenseState> emit,
) async {
try {
await _expenseRepository.markSplitAsPaid(event.expenseId, event.userId);
emit(const ExpenseOperationSuccess('Paiement marqué comme effectué'));
emit(const ExpenseOperationSuccess('Payment marked as completed'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur mark split paid: $e');
_errorService.logError('ExpenseBloc', 'Error marking split as paid: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> _onArchiveExpense(
ArchiveExpense event,
Emitter<ExpenseState> emit,
) async {
try {
await _expenseRepository.archiveExpense(event.expenseId);
emit(const ExpenseOperationSuccess('pense archivée avec succès'));
emit(const ExpenseOperationSuccess('Expense archived successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Erreur archivage expense: $e');
_errorService.logError('ExpenseBloc', 'Error archiving expense: $e');
emit(ExpenseError(e.toString()));
}
}
/// 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<void> close() {
_expensesSubscription?.cancel();

View File

@@ -2,26 +2,45 @@ import 'package:equatable/equatable.dart';
import '../../models/expense.dart';
import 'dart:io';
/// Abstract base class for all expense-related events.
///
/// This class extends [Equatable] to enable value equality for event comparison.
/// All expense events in the application should inherit from this class.
abstract class ExpenseEvent extends Equatable {
/// Creates a new [ExpenseEvent].
const ExpenseEvent();
@override
List<Object?> get props => [];
}
/// Event to load all expenses for a specific group.
///
/// This event triggers the loading of all expenses associated with
/// the specified group, setting up a stream to receive real-time updates.
class LoadExpensesByGroup extends ExpenseEvent {
/// The ID of the group to load expenses for.
final String groupId;
/// Creates a [LoadExpensesByGroup] event for the specified [groupId].
const LoadExpensesByGroup(this.groupId);
@override
List<Object?> get props => [groupId];
}
/// Event to create a new expense.
///
/// This event is dispatched when a user wants to add a new expense
/// to a group, optionally including a receipt image.
class CreateExpense extends ExpenseEvent {
/// The expense data to create.
final Expense expense;
/// Optional receipt image file to upload with the expense.
final File? receiptImage;
/// Creates a [CreateExpense] event with the expense data and optional receipt.
const CreateExpense({
required this.expense,
this.receiptImage,
@@ -31,10 +50,18 @@ class CreateExpense extends ExpenseEvent {
List<Object?> get props => [expense, receiptImage];
}
/// Event to update an existing expense.
///
/// This event is dispatched when a user modifies an existing expense,
/// optionally changing the receipt image.
class UpdateExpense extends ExpenseEvent {
/// The updated expense data.
final Expense expense;
/// Optional new receipt image file to replace the existing one.
final File? newReceiptImage;
/// Creates an [UpdateExpense] event with updated expense data.
const UpdateExpense({
required this.expense,
this.newReceiptImage,
@@ -44,19 +71,33 @@ class UpdateExpense extends ExpenseEvent {
List<Object?> get props => [expense, newReceiptImage];
}
/// Event to delete an expense.
///
/// This event is dispatched when a user wants to permanently
/// remove an expense from the group.
class DeleteExpense extends ExpenseEvent {
/// The ID of the expense to delete.
final String expenseId;
/// Creates a [DeleteExpense] event for the specified [expenseId].
const DeleteExpense(this.expenseId);
@override
List<Object?> get props => [expenseId];
}
/// Event to mark a user's split of an expense as paid.
///
/// This event is used when a user has paid their portion of
/// a shared expense to the person who originally paid for it.
class MarkSplitAsPaid extends ExpenseEvent {
/// The ID of the expense containing the split.
final String expenseId;
/// The ID of the user whose split is being marked as paid.
final String userId;
/// Creates a [MarkSplitAsPaid] event for the specified expense and user.
const MarkSplitAsPaid({
required this.expenseId,
required this.userId,
@@ -66,20 +107,33 @@ class MarkSplitAsPaid extends ExpenseEvent {
List<Object?> get props => [expenseId, userId];
}
/// Event to archive an expense.
///
/// This event moves an expense to an archived state, hiding it
/// from the main expense list while preserving it for history.
class ArchiveExpense extends ExpenseEvent {
/// The ID of the expense to archive.
final String expenseId;
/// Creates an [ArchiveExpense] event for the specified [expenseId].
const ArchiveExpense(this.expenseId);
@override
List<Object?> get props => [expenseId];
}
// Événement privé pour les mises à jour du stream
/// Internal event for handling expense stream updates.
///
/// This is a private event used internally by the bloc to handle
/// real-time updates from the Firestore stream.
class ExpensesUpdated extends ExpenseEvent {
/// The updated list of expenses from the stream.
final List<Expense> expenses;
/// Optional error message if the stream encountered an error.
final String? error;
/// Creates an [ExpensesUpdated] event with the expense list and optional error.
const ExpensesUpdated(this.expenses, {this.error});
@override

View File

@@ -1,21 +1,47 @@
import 'package:equatable/equatable.dart';
import '../../models/expense.dart';
/// Abstract base class for all expense-related states.
///
/// This class extends [Equatable] to enable value equality for state comparison.
/// All expense states in the application should inherit from this class.
abstract class ExpenseState extends Equatable {
/// Creates a new [ExpenseState].
const ExpenseState();
@override
List<Object?> get props => [];
}
/// Initial state of the expense bloc.
///
/// This state represents the initial state before any expense
/// operations have been performed.
class ExpenseInitial extends ExpenseState {}
/// State indicating that an expense operation is in progress.
///
/// This state is used to show loading indicators during expense
/// operations like loading, creating, updating, or deleting expenses.
class ExpenseLoading extends ExpenseState {}
/// State indicating that expenses have been successfully loaded.
///
/// This state contains the list of expenses for a group and
/// exchange rates for currency conversion calculations.
class ExpensesLoaded extends ExpenseState {
/// List of expenses for the current group.
final List<Expense> expenses;
/// Exchange rates for currency conversion.
///
/// Maps currency codes to their exchange rates relative to EUR.
/// Used for converting different currencies to a common base for calculations.
final Map<String, double> exchangeRates;
/// Creates an [ExpensesLoaded] state with expenses and exchange rates.
///
/// [exchangeRates] defaults to common rates if not provided.
const ExpensesLoaded({
required this.expenses,
this.exchangeRates = const {'EUR': 1.0, 'USD': 0.85, 'GBP': 1.15},
@@ -25,18 +51,30 @@ class ExpensesLoaded extends ExpenseState {
List<Object?> get props => [expenses, exchangeRates];
}
/// State indicating that an expense operation has completed successfully.
///
/// This state is used to show success messages after operations like
/// creating, updating, deleting, or archiving expenses.
class ExpenseOperationSuccess extends ExpenseState {
/// Success message to display to the user.
final String message;
/// Creates an [ExpenseOperationSuccess] state with the given [message].
const ExpenseOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
/// State indicating that an expense operation has failed.
///
/// This state contains an error message that can be displayed to the user
/// when expense operations fail.
class ExpenseError extends ExpenseState {
/// The error message describing what went wrong.
final String message;
/// Creates an [ExpenseError] state with the given error [message].
const ExpenseError(this.message);
@override

View File

@@ -1,3 +1,36 @@
/// A BLoC (Business Logic Component) that manages group-related operations.
///
/// This bloc handles all group operations including creation, updates, member management,
/// and loading groups for users or trips. It provides real-time updates through streams
/// and manages the relationship between users, groups, and trips.
///
/// The bloc processes these main events:
/// - [LoadGroupsByUserId]: Loads all groups for a specific user with real-time updates
/// - [LoadGroupsByTrip]: Loads the group associated with a specific trip
/// - [CreateGroup]: Creates a new group without members
/// - [CreateGroupWithMembers]: Creates a new group with initial members
/// - [AddMemberToGroup]: Adds a member to an existing group
/// - [RemoveMemberFromGroup]: Removes a member from a group
/// - [UpdateGroup]: Updates group information
/// - [DeleteGroup]: Deletes a group
///
/// Dependencies:
/// - [GroupRepository]: Repository for group data operations
/// - [ErrorService]: Service for error logging and handling
///
/// Example usage:
/// ```dart
/// final groupBloc = GroupBloc(groupRepository);
///
/// // Load groups for a user
/// groupBloc.add(LoadGroupsByUserId('userId123'));
///
/// // Create a new group with members
/// groupBloc.add(CreateGroupWithMembers(
/// group: newGroup,
/// members: [member1, member2],
/// ));
/// ```
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/services/error_service.dart';
@@ -6,11 +39,24 @@ import 'group_state.dart';
import '../../repositories/group_repository.dart';
import '../../models/group.dart';
/// BLoC that manages group-related operations and state.
class GroupBloc extends Bloc<GroupEvent, GroupState> {
/// Repository for group data operations
final GroupRepository _repository;
/// Subscription to group stream for real-time updates
StreamSubscription? _groupsSubscription;
/// Service for error handling and logging
final _errorService = ErrorService();
/// Constructor for GroupBloc.
///
/// Initializes the bloc with the group repository and sets up event handlers
/// for all group-related operations.
///
/// Args:
/// [_repository]: Repository for group data operations
GroupBloc(this._repository) : super(GroupInitial()) {
on<LoadGroupsByUserId>(_onLoadGroupsByUserId);
on<_GroupsUpdated>(_onGroupsUpdated);
@@ -23,6 +69,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
on<DeleteGroup>(_onDeleteGroup);
}
/// Handles [LoadGroupsByUserId] events.
///
/// Loads all groups for a specific user with real-time updates via stream subscription.
/// Cancels any existing subscription before creating a new one to prevent memory leaks.
///
/// Args:
/// [event]: The LoadGroupsByUserId event containing the user ID
/// [emit]: State emitter function
Future<void> _onLoadGroupsByUserId(
LoadGroupsByUserId event,
Emitter<GroupState> emit,
@@ -44,6 +98,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
}
}
/// Handles [_GroupsUpdated] events.
///
/// Processes real-time updates from the group stream, either emitting
/// the updated group list or an error state if the stream encountered an error.
///
/// Args:
/// [event]: The _GroupsUpdated event containing groups or error information
/// [emit]: State emitter function
Future<void> _onGroupsUpdated(
_GroupsUpdated event,
Emitter<GroupState> emit,
@@ -56,6 +118,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
}
}
/// Handles [LoadGroupsByTrip] events.
///
/// Loads the group associated with a specific trip. Since each trip typically
/// has one primary group, this returns a single group or an empty list.
///
/// Args:
/// [event]: The LoadGroupsByTrip event containing the trip ID
/// [emit]: State emitter function
Future<void> _onLoadGroupsByTrip(
LoadGroupsByTrip event,
Emitter<GroupState> emit,
@@ -73,6 +143,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
}
}
/// Handles [CreateGroup] events.
///
/// Creates a new group without any initial members. The group creator
/// can add members later using AddMemberToGroup events.
///
/// Args:
/// [event]: The CreateGroup event containing the group data
/// [emit]: State emitter function
Future<void> _onCreateGroup(
CreateGroup event,
Emitter<GroupState> emit,
@@ -84,12 +162,21 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
members: [],
);
emit(GroupCreated(groupId: groupId));
emit(const GroupOperationSuccess('Groupe créé avec succès'));
emit(const GroupOperationSuccess('Group created successfully'));
} catch (e) {
emit(GroupError('Erreur lors de la création: $e'));
emit(GroupError('Error during creation: $e'));
}
}
/// Handles [CreateGroupWithMembers] events.
///
/// Creates a new group with an initial set of members. This is useful
/// for setting up complete groups in one operation, such as when
/// planning a trip with known participants.
///
/// Args:
/// [event]: The CreateGroupWithMembers event containing group data and member list
/// [emit]: State emitter function
Future<void> _onCreateGroupWithMembers(
CreateGroupWithMembers event,
Emitter<GroupState> emit,
@@ -102,58 +189,94 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
);
emit(GroupCreated(groupId: groupId));
} catch (e) {
emit(GroupError('Erreur lors de la création: $e'));
emit(GroupError('Error during creation: $e'));
}
}
/// Handles [AddMemberToGroup] events.
///
/// Adds a new member to an existing group. The member will be able to
/// participate in group expenses and access group features.
///
/// Args:
/// [event]: The AddMemberToGroup event containing group ID and member data
/// [emit]: State emitter function
Future<void> _onAddMemberToGroup(
AddMemberToGroup event,
Emitter<GroupState> emit,
) async {
try {
await _repository.addMember(event.groupId, event.member);
emit(const GroupOperationSuccess('Membre ajouté'));
emit(const GroupOperationSuccess('Member added'));
} catch (e) {
emit(GroupError('Erreur lors de l\'ajout: $e'));
emit(GroupError('Error during addition: $e'));
}
}
/// Handles [RemoveMemberFromGroup] events.
///
/// Removes a member from a group. This will affect expense calculations
/// and the member will no longer have access to group features.
///
/// Args:
/// [event]: The RemoveMemberFromGroup event containing group ID and user ID
/// [emit]: State emitter function
Future<void> _onRemoveMemberFromGroup(
RemoveMemberFromGroup event,
Emitter<GroupState> emit,
) async {
try {
await _repository.removeMember(event.groupId, event.userId);
emit(const GroupOperationSuccess('Membre supprimé'));
emit(const GroupOperationSuccess('Member removed'));
} catch (e) {
emit(GroupError('Erreur lors de la suppression: $e'));
emit(GroupError('Error during removal: $e'));
}
}
/// Handles [UpdateGroup] events.
///
/// Updates group information such as name, description, or settings.
/// Member lists are managed through separate add/remove member events.
///
/// Args:
/// [event]: The UpdateGroup event containing group ID and updated group data
/// [emit]: State emitter function
Future<void> _onUpdateGroup(
UpdateGroup event,
Emitter<GroupState> emit,
) async {
try {
await _repository.updateGroup(event.groupId, event.group);
emit(const GroupOperationSuccess('Groupe mis à jour'));
emit(const GroupOperationSuccess('Group updated'));
} catch (e) {
emit(GroupError('Erreur lors de la mise à jour: $e'));
emit(GroupError('Error during update: $e'));
}
}
/// Handles [DeleteGroup] events.
///
/// Permanently deletes a group and all associated data. This action
/// cannot be undone and will affect all group members and expenses.
///
/// Args:
/// [event]: The DeleteGroup event containing the trip ID to delete
/// [emit]: State emitter function
Future<void> _onDeleteGroup(
DeleteGroup event,
Emitter<GroupState> emit,
) async {
try {
await _repository.deleteGroup(event.tripId);
emit(const GroupOperationSuccess('Groupe supprimé'));
emit(const GroupOperationSuccess('Group deleted'));
} catch (e) {
emit(GroupError('Erreur lors de la suppression: $e'));
emit(GroupError('Error during deletion: $e'));
}
}
/// Cleans up resources when the bloc is closed.
///
/// Cancels the group stream subscription to prevent memory leaks
/// and ensure proper disposal of resources.
@override
Future<void> close() {
_groupsSubscription?.cancel();
@@ -161,10 +284,22 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
}
}
/// Private event for handling real-time group updates from streams.
///
/// This internal event is used to process updates from the group stream
/// subscription and emit appropriate states based on the received data.
class _GroupsUpdated extends GroupEvent {
/// List of groups received from the stream
final List<Group> groups;
/// Error message if the stream encountered an error
final String? error;
/// Creates a _GroupsUpdated event.
///
/// Args:
/// [groups]: List of groups from the stream update
/// [error]: Optional error message if stream failed
const _GroupsUpdated(this.groups, {this.error});
@override

View File

@@ -1,7 +1,25 @@
/// Events for group-related operations in the GroupBloc.
///
/// This file defines all possible events that can be dispatched to the GroupBloc
/// to trigger group-related state changes and operations such as loading groups,
/// creating groups, managing members, and performing CRUD operations.
///
/// Event Categories:
/// - **Loading Events**: LoadGroupsByUserId, LoadGroupsByTrip
/// - **Creation Events**: CreateGroup, CreateGroupWithMembers
/// - **Member Management**: AddMemberToGroup, RemoveMemberFromGroup
/// - **Modification Events**: UpdateGroup, DeleteGroup
///
/// All events extend [GroupEvent] and implement [Equatable] for proper
/// equality comparison in the BLoC pattern.
import 'package:equatable/equatable.dart';
import '../../models/group.dart';
import '../../models/group_member.dart';
/// Base class for all group-related events.
///
/// All group events must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class GroupEvent extends Equatable {
const GroupEvent();
@@ -9,41 +27,96 @@ abstract class GroupEvent extends Equatable {
List<Object?> get props => [];
}
// NOUVEAU : Charger les groupes par userId
/// Event to load all groups associated with a specific user.
///
/// This is the primary method for loading groups, as it retrieves all groups
/// where the user is a member, providing a comprehensive view of the user's
/// group memberships across different trips and activities.
///
/// Args:
/// [userId]: The unique identifier of the user whose groups should be loaded
class LoadGroupsByUserId extends GroupEvent {
/// The unique identifier of the user
final String userId;
/// Creates a LoadGroupsByUserId event.
///
/// Args:
/// [userId]: The user ID to load groups for
const LoadGroupsByUserId(this.userId);
@override
List<Object?> get props => [userId];
}
// Charger les groupes d'un voyage (conservé pour compatibilité)
// Load groups for a trip (maintained for compatibility)
/// Event to load groups associated with a specific trip.
///
/// This event is maintained for backward compatibility and specific use cases
/// where you need to load groups within the context of a particular trip.
/// Most trips have one primary group, but some may have multiple sub-groups.
///
/// Args:
/// [tripId]: The unique identifier of the trip whose groups should be loaded
class LoadGroupsByTrip extends GroupEvent {
/// The unique identifier of the trip
final String tripId;
/// Creates a LoadGroupsByTrip event.
///
/// Args:
/// [tripId]: The trip ID to load groups for
const LoadGroupsByTrip(this.tripId);
@override
List<Object?> get props => [tripId];
}
// Créer un groupe simple
// Create a simple group
/// Event to create a new group without any initial members.
///
/// This creates a basic group structure that can be populated with members
/// later using AddMemberToGroup events. Useful when setting up a group
/// before knowing all participants.
///
/// Args:
/// [group]: The group object containing basic information (name, description, etc.)
class CreateGroup extends GroupEvent {
/// The group to be created
final Group group;
/// Creates a CreateGroup event.
///
/// Args:
/// [group]: The group object to create
const CreateGroup(this.group);
@override
List<Object?> get props => [group];
}
// Créer un groupe avec ses membres
// Create a group with its members
/// Event to create a new group with an initial set of members.
///
/// This is the preferred method for group creation when you know all or most
/// of the participants upfront. It creates the group and adds all specified
/// members in a single operation, ensuring data consistency.
///
/// Args:
/// [group]: The group object containing basic information
/// [members]: List of initial members to add to the group
class CreateGroupWithMembers extends GroupEvent {
/// The group to be created
final Group group;
/// Initial members to add to the group
final List<GroupMember> members;
/// Creates a CreateGroupWithMembers event.
///
/// Args:
/// [group]: The group object to create
/// [members]: List of initial group members
const CreateGroupWithMembers({
required this.group,
required this.members,
@@ -53,43 +126,107 @@ class CreateGroupWithMembers extends GroupEvent {
List<Object?> get props => [group, members];
}
// Ajouter un membre
// Add a member
/// Event to add a new member to an existing group.
///
/// This allows for dynamic group expansion by adding users to groups after
/// creation. The new member will gain access to group features like expenses,
/// messages, and shared resources.
///
/// Args:
/// [groupId]: The unique identifier of the target group
/// [member]: The group member object containing user information and role
class AddMemberToGroup extends GroupEvent {
/// The unique identifier of the group
final String groupId;
/// The member to add to the group
final GroupMember member;
/// Creates an AddMemberToGroup event.
///
/// Args:
/// [groupId]: The group ID to add the member to
/// [member]: The group member to add
const AddMemberToGroup(this.groupId, this.member);
@override
List<Object?> get props => [groupId, member];
}
// Supprimer un membre
// Remove a member
/// Event to remove a member from a group.
///
/// This removes a user from the group, revoking their access to group features.
/// The removal will affect expense calculations and the user will no longer
/// receive group-related notifications or updates.
///
/// Args:
/// [groupId]: The unique identifier of the group
/// [userId]: The unique identifier of the user to remove
class RemoveMemberFromGroup extends GroupEvent {
/// The unique identifier of the group
final String groupId;
/// The unique identifier of the user to remove
final String userId;
/// Creates a RemoveMemberFromGroup event.
///
/// Args:
/// [groupId]: The group ID to remove the member from
/// [userId]: The user ID to remove
const RemoveMemberFromGroup(this.groupId, this.userId);
@override
List<Object?> get props => [groupId, userId];
}
// Mettre à jour un groupe
// Update a group
/// Event to update an existing group's information.
///
/// This allows modification of group properties such as name, description,
/// settings, or other metadata. Member management is handled through
/// separate add/remove member events.
///
/// Args:
/// [groupId]: The unique identifier of the group to update
/// [group]: The updated group object with new information
class UpdateGroup extends GroupEvent {
/// The unique identifier of the group to update
final String groupId;
/// The updated group object
final Group group;
/// Creates an UpdateGroup event.
///
/// Args:
/// [groupId]: The group ID to update
/// [group]: The updated group object
const UpdateGroup(this.groupId, this.group);
@override
List<Object?> get props => [groupId, group];
}
// Supprimer un groupe
// Delete a group
/// Event to permanently delete a group.
///
/// This is a destructive operation that removes the group and all associated
/// data. This action cannot be undone and will affect all group members.
/// Consider archiving instead of deleting for historical records.
///
/// Args:
/// [tripId]: The unique identifier of the trip whose group should be deleted
class DeleteGroup extends GroupEvent {
/// The unique identifier of the trip (used to identify the group)
final String tripId;
/// Creates a DeleteGroup event.
///
/// Args:
/// [tripId]: The trip ID whose group should be deleted
const DeleteGroup(this.tripId);
@override

View File

@@ -1,6 +1,24 @@
/// States for group-related operations in the GroupBloc.
///
/// This file defines all possible states that the GroupBloc can emit in response
/// to group-related events. The states represent different phases of group
/// operations including loading, success, error, and data states.
///
/// State Categories:
/// - **Initial State**: GroupInitial
/// - **Loading States**: GroupLoading
/// - **Success States**: GroupsLoaded, GroupLoaded, GroupCreated, GroupOperationSuccess
/// - **Error States**: GroupError
///
/// All states extend [GroupState] and implement [Equatable] for proper
/// equality comparison and state change detection in the BLoC pattern.
import 'package:equatable/equatable.dart';
import '../../models/group.dart';
/// Base class for all group-related states.
///
/// All group states must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class GroupState extends Equatable {
const GroupState();
@@ -8,54 +26,132 @@ abstract class GroupState extends Equatable {
List<Object?> get props => [];
}
// État initial
// Initial state
/// The initial state of the GroupBloc before any operations are performed.
///
/// This is the default state when the bloc is first created and represents
/// a clean slate with no group data loaded or operations in progress.
class GroupInitial extends GroupState {}
// Chargement
// Loading
/// State indicating that a group operation is currently in progress.
///
/// This state is emitted when the bloc is performing operations like
/// loading groups, creating groups, or updating group information.
/// UI components can use this state to show loading indicators.
class GroupLoading extends GroupState {}
// Groupes chargés
// Groups loaded
/// State containing a list of successfully loaded groups.
///
/// This state is emitted when groups have been successfully retrieved
/// from the repository, either through initial loading or real-time updates.
/// The UI can use this data to display the list of available groups.
///
/// Properties:
/// [groups]: List of Group objects that were loaded
class GroupsLoaded extends GroupState {
/// The list of loaded groups
final List<Group> groups;
/// Creates a GroupsLoaded state.
///
/// Args:
/// [groups]: The list of groups to include in this state
const GroupsLoaded(this.groups);
@override
List<Object?> get props => [groups];
}
/// Alternative state for loaded groups (used in some contexts).
///
/// This state serves a similar purpose to GroupsLoaded but may be used
/// in different contexts or for backward compatibility.
///
/// Properties:
/// [groups]: List of Group objects that were loaded
class GroupLoaded extends GroupState {
/// The list of loaded groups
final List<Group> groups;
/// Creates a GroupLoaded state.
///
/// Args:
/// [groups]: The list of groups to include in this state
const GroupLoaded(this.groups);
}
/// State indicating successful group creation.
///
/// This state is emitted when a new group has been successfully created.
/// It contains the ID of the newly created group and an optional success message
/// that can be displayed to the user.
///
/// Properties:
/// [groupId]: The unique identifier of the newly created group
/// [message]: A success message to display to the user
class GroupCreated extends GroupState {
/// The unique identifier of the newly created group
final String groupId;
/// Success message for the user
final String message;
/// Creates a GroupCreated state.
///
/// Args:
/// [groupId]: The ID of the newly created group
/// [message]: Optional success message (defaults to "Group created successfully")
const GroupCreated({
required this.groupId,
this.message = 'Groupe créé avec succès',
this.message = 'Group created successfully',
});
@override
List<Object?> get props => [groupId, message];
}
// Succès d'une opération
// Operation success
/// State indicating successful completion of a group operation.
///
/// This state is emitted when operations like updating, deleting, or
/// member management have completed successfully. It contains a message
/// that can be displayed to inform the user of the successful operation.
///
/// Properties:
/// [message]: A success message describing the completed operation
class GroupOperationSuccess extends GroupState {
/// The success message to display to the user
final String message;
/// Creates a GroupOperationSuccess state.
///
/// Args:
/// [message]: The success message to display
const GroupOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
// Erreur
// Error
/// State indicating that a group operation has failed.
///
/// This state is emitted when any group operation encounters an error.
/// It contains an error message that can be displayed to the user to
/// explain what went wrong.
///
/// Properties:
/// [message]: An error message describing what went wrong
class GroupError extends GroupState {
/// The error message to display to the user
final String message;
/// Creates a GroupError state.
///
/// Args:
/// [message]: The error message to display
const GroupError(this.message);
@override

View File

@@ -1,3 +1,44 @@
/// A BLoC (Business Logic Component) that manages message-related operations for group chats.
///
/// This bloc handles all messaging functionality including sending, updating, deleting messages,
/// and managing reactions. It provides real-time updates through streams and manages the
/// relationship between users and group conversations.
///
/// The bloc processes these main events:
/// - [LoadMessages]: Loads messages for a group with real-time updates
/// - [SendMessage]: Sends a new message to a group chat
/// - [DeleteMessage]: Deletes a message from the chat
/// - [UpdateMessage]: Updates/edits an existing message
/// - [ReactToMessage]: Adds an emoji reaction to a message
/// - [RemoveReaction]: Removes a user's reaction from a message
///
/// Dependencies:
/// - [MessageService]: Service for message operations and business logic
/// - [MessageRepository]: Repository for message data operations
///
/// Example usage:
/// ```dart
/// final messageBloc = MessageBloc();
///
/// // Load messages for a group
/// messageBloc.add(LoadMessages('groupId123'));
///
/// // Send a message
/// messageBloc.add(SendMessage(
/// groupId: 'groupId123',
/// text: 'Hello everyone!',
/// senderId: 'userId123',
/// senderName: 'John Doe',
/// ));
///
/// // React to a message
/// messageBloc.add(ReactToMessage(
/// groupId: 'groupId123',
/// messageId: 'msgId456',
/// userId: 'userId123',
/// reaction: '👍',
/// ));
/// ```
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/message.dart';
@@ -6,10 +47,22 @@ import '../../repositories/message_repository.dart';
import 'message_event.dart';
import 'message_state.dart';
/// BLoC that manages message-related operations and real-time chat state.
class MessageBloc extends Bloc<MessageEvent, MessageState> {
/// Service for message operations and business logic
final MessageService _messageService;
/// Subscription to message stream for real-time updates
StreamSubscription<List<Message>>? _messagesSubscription;
/// Constructor for MessageBloc.
///
/// Initializes the bloc with an optional message service. If no service is provided,
/// creates a default MessageService with MessageRepository. Sets up event handlers
/// for all message-related operations.
///
/// Args:
/// [messageService]: Optional service for message operations (auto-created if null)
MessageBloc({MessageService? messageService})
: _messageService = messageService ?? MessageService(
messageRepository: MessageRepository(),
@@ -24,6 +77,14 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
on<_MessagesUpdated>(_onMessagesUpdated);
}
/// Handles [LoadMessages] events.
///
/// Loads messages for a specific group with real-time updates via stream subscription.
/// Cancels any existing subscription before creating a new one to prevent memory leaks.
///
/// Args:
/// [event]: The LoadMessages event containing the group ID
/// [emit]: State emitter function
Future<void> _onLoadMessages(
LoadMessages event,
Emitter<MessageState> emit,
@@ -39,11 +100,19 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
add(_MessagesUpdated(messages: messages, groupId: event.groupId));
},
onError: (error) {
add(_MessagesError('Erreur lors du chargement des messages: $error'));
add(_MessagesError('Error loading messages: $error'));
},
);
}
/// Handles [_MessagesUpdated] events.
///
/// Processes real-time updates from the message stream, emitting the
/// updated message list with the associated group ID.
///
/// Args:
/// [event]: The _MessagesUpdated event containing messages and group ID
/// [emit]: State emitter function
void _onMessagesUpdated(
_MessagesUpdated event,
Emitter<MessageState> emit,
@@ -51,12 +120,20 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
emit(MessagesLoaded(messages: event.messages, groupId: event.groupId));
}
/// Handles [SendMessage] events.
///
/// Sends a new message to a group chat. The stream subscription will
/// automatically update the UI with the new message, so no state is emitted here.
///
/// Args:
/// [event]: The SendMessage event containing message details
/// [emit]: State emitter function
Future<void> _onSendMessage(
SendMessage event,
Emitter<MessageState> emit,
) async {
try {
// Juste effectuer l'action, le stream mettra à jour
// Just perform the action, the stream will update
await _messageService.sendMessage(
groupId: event.groupId,
text: event.text,
@@ -64,50 +141,74 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
senderName: event.senderName,
);
} catch (e) {
emit(MessageError('Erreur lors de l\'envoi du message: $e'));
emit(MessageError('Error sending message: $e'));
}
}
/// Handles [DeleteMessage] events.
///
/// Deletes a message from the group chat. The Firestore stream will
/// automatically update the UI, so no state is emitted here unless there's an error.
///
/// Args:
/// [event]: The DeleteMessage event containing group ID and message ID
/// [emit]: State emitter function
Future<void> _onDeleteMessage(
DeleteMessage event,
Emitter<MessageState> emit,
) async {
try {
// Ne pas émettre d'état, juste effectuer l'action
// Le stream Firestore mettra à jour automatiquement
// Don't emit state, just perform the action
// The Firestore stream will update automatically
await _messageService.deleteMessage(
groupId: event.groupId,
messageId: event.messageId,
);
} catch (e) {
emit(MessageError('Erreur lors de la suppression du message: $e'));
emit(MessageError('Error deleting message: $e'));
}
}
/// Handles [UpdateMessage] events.
///
/// Updates/edits an existing message in the group chat. The Firestore stream will
/// automatically update the UI with the edited message, so no state is emitted here.
///
/// Args:
/// [event]: The UpdateMessage event containing message ID and new text
/// [emit]: State emitter function
Future<void> _onUpdateMessage(
UpdateMessage event,
Emitter<MessageState> emit,
) async {
try {
// Ne pas émettre d'état, juste effectuer l'action
// Le stream Firestore mettra à jour automatiquement
// Don't emit state, just perform the action
// The Firestore stream will update automatically
await _messageService.updateMessage(
groupId: event.groupId,
messageId: event.messageId,
newText: event.newText,
);
} catch (e) {
emit(MessageError('Erreur lors de la modification du message: $e'));
emit(MessageError('Error updating message: $e'));
}
}
/// Handles [ReactToMessage] events.
///
/// Adds an emoji reaction to a message. The Firestore stream will
/// automatically update the UI with the new reaction, so no state is emitted here.
///
/// Args:
/// [event]: The ReactToMessage event containing message ID, user ID, and reaction
/// [emit]: State emitter function
Future<void> _onReactToMessage(
ReactToMessage event,
Emitter<MessageState> emit,
) async {
try {
// Ne pas émettre d'état, juste effectuer l'action
// Le stream Firestore mettra à jour automatiquement
// Don't emit state, just perform the action
// The Firestore stream will update automatically
await _messageService.reactToMessage(
groupId: event.groupId,
messageId: event.messageId,
@@ -115,27 +216,39 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
reaction: event.reaction,
);
} catch (e) {
emit(MessageError('Erreur lors de l\'ajout de la réaction: $e'));
emit(MessageError('Error adding reaction: $e'));
}
}
/// Handles [RemoveReaction] events.
///
/// Removes a user's reaction from a message. The Firestore stream will
/// automatically update the UI with the removed reaction, so no state is emitted here.
///
/// Args:
/// [event]: The RemoveReaction event containing message ID and user ID
/// [emit]: State emitter function
Future<void> _onRemoveReaction(
RemoveReaction event,
Emitter<MessageState> emit,
) async {
try {
// Ne pas émettre d'état, juste effectuer l'action
// Le stream Firestore mettra à jour automatiquement
// Don't emit state, just perform the action
// The Firestore stream will update automatically
await _messageService.removeReaction(
groupId: event.groupId,
messageId: event.messageId,
userId: event.userId,
);
} catch (e) {
emit(MessageError('Erreur lors de la suppression de la réaction: $e'));
emit(MessageError('Error removing reaction: $e'));
}
}
/// Cleans up resources when the bloc is closed.
///
/// Cancels the message stream subscription to prevent memory leaks
/// and ensure proper disposal of resources.
@override
Future<void> close() {
_messagesSubscription?.cancel();
@@ -143,11 +256,22 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
}
}
// Events internes
/// Private event for handling real-time message updates from streams.
///
/// This internal event is used to process updates from the message stream
/// subscription and emit appropriate states based on the received data.
class _MessagesUpdated extends MessageEvent {
/// List of messages received from the stream
final List<Message> messages;
/// Group ID associated with the messages
final String groupId;
/// Creates a _MessagesUpdated event.
///
/// Args:
/// [messages]: List of messages from the stream update
/// [groupId]: ID of the group these messages belong to
const _MessagesUpdated({
required this.messages,
required this.groupId,
@@ -157,9 +281,18 @@ class _MessagesUpdated extends MessageEvent {
List<Object?> get props => [messages, groupId];
}
/// Private event for handling message stream errors.
///
/// This internal event is used to process errors from the message stream
/// subscription and emit appropriate error states.
class _MessagesError extends MessageEvent {
/// Error message from the stream
final String error;
/// Creates a _MessagesError event.
///
/// Args:
/// [error]: Error message from the stream failure
const _MessagesError(this.error);
@override

View File

@@ -1,5 +1,25 @@
/// Events for message-related operations in the MessageBloc.
///
/// This file defines all possible events that can be dispatched to the MessageBloc
/// to trigger message-related state changes and operations such as loading messages,
/// sending messages, managing reactions, and performing CRUD operations on chat messages.
///
/// Event Categories:
/// - **Loading Events**: LoadMessages
/// - **Message Operations**: SendMessage, DeleteMessage, UpdateMessage
/// - **Reaction Management**: ReactToMessage, RemoveReaction
///
/// All events support real-time group chat functionality with features like
/// message editing, deletion, and emoji reactions.
///
/// All events extend [MessageEvent] and implement [Equatable] for proper
/// equality comparison in the BLoC pattern.
import 'package:equatable/equatable.dart';
/// Base class for all message-related events.
///
/// All message events must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class MessageEvent extends Equatable {
const MessageEvent();
@@ -7,21 +27,59 @@ abstract class MessageEvent extends Equatable {
List<Object?> get props => [];
}
/// Event to load messages for a specific group with real-time updates.
///
/// This event initializes the message stream for a group, providing real-time
/// updates as new messages are sent, edited, or deleted. The stream will
/// continue until the bloc is closed or a new LoadMessages event is dispatched.
///
/// Args:
/// [groupId]: The unique identifier of the group whose messages should be loaded
class LoadMessages extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// Creates a LoadMessages event.
///
/// Args:
/// [groupId]: The group ID to load messages for
const LoadMessages(this.groupId);
@override
List<Object?> get props => [groupId];
}
/// Event to send a new message to a group chat.
///
/// This event creates and sends a new message to the specified group.
/// The message will be immediately visible to all group members through
/// the real-time message stream.
///
/// Args:
/// [groupId]: The unique identifier of the target group
/// [text]: The content of the message to send
/// [senderId]: The unique identifier of the user sending the message
/// [senderName]: The display name of the user sending the message
class SendMessage extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// The content of the message
final String text;
/// The unique identifier of the sender
final String senderId;
/// The display name of the sender
final String senderName;
/// Creates a SendMessage event.
///
/// Args:
/// [groupId]: The target group ID
/// [text]: The message content
/// [senderId]: The sender's user ID
/// [senderName]: The sender's display name
const SendMessage({
required this.groupId,
required this.text,
@@ -33,10 +91,27 @@ class SendMessage extends MessageEvent {
List<Object?> get props => [groupId, text, senderId, senderName];
}
/// Event to delete a message from the group chat.
///
/// This event permanently removes a message from the chat. The deletion
/// will be immediately reflected for all group members through the
/// real-time message stream. This action cannot be undone.
///
/// Args:
/// [groupId]: The unique identifier of the group containing the message
/// [messageId]: The unique identifier of the message to delete
class DeleteMessage extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// The unique identifier of the message to delete
final String messageId;
/// Creates a DeleteMessage event.
///
/// Args:
/// [groupId]: The group ID containing the message
/// [messageId]: The message ID to delete
const DeleteMessage({
required this.groupId,
required this.messageId,
@@ -46,11 +121,32 @@ class DeleteMessage extends MessageEvent {
List<Object?> get props => [groupId, messageId];
}
/// Event to update/edit an existing message in the group chat.
///
/// This event allows users to modify the content of a previously sent message.
/// The updated message will be immediately visible to all group members
/// through the real-time message stream, typically with an "edited" indicator.
///
/// Args:
/// [groupId]: The unique identifier of the group containing the message
/// [messageId]: The unique identifier of the message to update
/// [newText]: The new content for the message
class UpdateMessage extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// The unique identifier of the message to update
final String messageId;
/// The new content for the message
final String newText;
/// Creates an UpdateMessage event.
///
/// Args:
/// [groupId]: The group ID containing the message
/// [messageId]: The message ID to update
/// [newText]: The new message content
const UpdateMessage({
required this.groupId,
required this.messageId,
@@ -61,12 +157,37 @@ class UpdateMessage extends MessageEvent {
List<Object?> get props => [groupId, messageId, newText];
}
/// Event to add an emoji reaction to a message.
///
/// This event allows users to react to messages with emojis, providing
/// a quick way to express emotions or acknowledgment without sending
/// a full message. The reaction will be immediately visible to all group members.
///
/// Args:
/// [groupId]: The unique identifier of the group containing the message
/// [messageId]: The unique identifier of the message to react to
/// [userId]: The unique identifier of the user adding the reaction
/// [reaction]: The emoji or reaction string to add
class ReactToMessage extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// The unique identifier of the message
final String messageId;
/// The unique identifier of the user adding the reaction
final String userId;
/// The emoji or reaction string
final String reaction;
/// Creates a ReactToMessage event.
///
/// Args:
/// [groupId]: The group ID containing the message
/// [messageId]: The message ID to react to
/// [userId]: The user ID adding the reaction
/// [reaction]: The emoji/reaction to add
const ReactToMessage({
required this.groupId,
required this.messageId,
@@ -78,11 +199,32 @@ class ReactToMessage extends MessageEvent {
List<Object?> get props => [groupId, messageId, userId, reaction];
}
/// Event to remove a user's reaction from a message.
///
/// This event removes a previously added emoji reaction from a message.
/// Only the user who added the reaction can remove it. The removal will
/// be immediately reflected for all group members through the real-time stream.
///
/// Args:
/// [groupId]: The unique identifier of the group containing the message
/// [messageId]: The unique identifier of the message with the reaction
/// [userId]: The unique identifier of the user removing their reaction
class RemoveReaction extends MessageEvent {
/// The unique identifier of the group
final String groupId;
/// The unique identifier of the message
final String messageId;
/// The unique identifier of the user removing the reaction
final String userId;
/// Creates a RemoveReaction event.
///
/// Args:
/// [groupId]: The group ID containing the message
/// [messageId]: The message ID with the reaction
/// [userId]: The user ID removing the reaction
const RemoveReaction({
required this.groupId,
required this.messageId,

View File

@@ -1,6 +1,27 @@
/// States for message-related operations in the MessageBloc.
///
/// This file defines all possible states that the MessageBloc can emit in response
/// to message-related events. The states represent different phases of message
/// operations including loading, success, and error states for real-time chat functionality.
///
/// State Categories:
/// - **Initial State**: MessageInitial
/// - **Loading States**: MessageLoading
/// - **Success States**: MessagesLoaded
/// - **Error States**: MessageError
///
/// The states support real-time chat features including message streaming,
/// reactions, editing, and deletion within group conversations.
///
/// All states extend [MessageState] and implement [Equatable] for proper
/// equality comparison and state change detection in the BLoC pattern.
import 'package:equatable/equatable.dart';
import '../../models/message.dart';
/// Base class for all message-related states.
///
/// All message states must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class MessageState extends Equatable {
const MessageState();
@@ -8,14 +29,41 @@ abstract class MessageState extends Equatable {
List<Object?> get props => [];
}
/// The initial state of the MessageBloc before any operations are performed.
///
/// This is the default state when the bloc is first created and represents
/// a clean slate with no message data loaded or operations in progress.
class MessageInitial extends MessageState {}
/// State indicating that a message operation is currently in progress.
///
/// This state is emitted when the bloc is performing operations like
/// loading messages or initializing the real-time message stream.
/// UI components can use this state to show loading indicators.
class MessageLoading extends MessageState {}
/// State containing a list of successfully loaded messages for a group.
///
/// This state is emitted when messages have been successfully retrieved
/// from the repository, either through initial loading or real-time updates.
/// The state includes both the messages and the group ID for context.
/// The UI can use this data to display the chat conversation.
///
/// Properties:
/// [messages]: List of Message objects in the conversation
/// [groupId]: The unique identifier of the group these messages belong to
class MessagesLoaded extends MessageState {
/// The list of messages in the conversation
final List<Message> messages;
/// The unique identifier of the group
final String groupId;
/// Creates a MessagesLoaded state.
///
/// Args:
/// [messages]: The list of messages to include in this state
/// [groupId]: The group ID these messages belong to
const MessagesLoaded({
required this.messages,
required this.groupId,
@@ -25,9 +73,23 @@ class MessagesLoaded extends MessageState {
List<Object?> get props => [messages, groupId];
}
/// State indicating that a message operation has failed.
///
/// This state is emitted when any message operation encounters an error,
/// such as failing to load messages, send a message, or perform other
/// message-related operations. It contains an error message that can be
/// displayed to the user to explain what went wrong.
///
/// Properties:
/// [message]: An error message describing what went wrong
class MessageError extends MessageState {
/// The error message to display to the user
final String message;
/// Creates a MessageError state.
///
/// Args:
/// [message]: The error message to display
const MessageError(this.message);
@override

View File

@@ -4,23 +4,39 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'theme_event.dart';
import 'theme_state.dart';
/// BLoC for managing application theme preferences.
///
/// This BLoC handles theme-related events and manages the application's
/// theme mode (light, dark, or system). It persists theme preferences
/// using SharedPreferences for consistency across app sessions.
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
/// Creates a new [ThemeBloc] with default theme state.
///
/// Registers event handlers for theme changes and loading saved preferences.
ThemeBloc() : super(const ThemeState()) {
on<ThemeChanged>(_onThemeChanged);
on<ThemeLoadRequested>(_onThemeLoadRequested);
}
/// Handles [ThemeChanged] events.
///
/// Updates the theme mode and persists the preference to local storage.
/// This ensures the theme choice is remembered across app restarts.
Future<void> _onThemeChanged(
ThemeChanged event,
Emitter<ThemeState> emit,
) async {
emit(state.copyWith(themeMode: event.themeMode));
// Sauvegarder la préférence
// Save the theme preference to persistent storage
final prefs = await SharedPreferences.getInstance();
await prefs.setString('themeMode', event.themeMode.toString());
}
/// Handles [ThemeLoadRequested] events.
///
/// Loads the saved theme preference from SharedPreferences and applies it.
/// If no preference is saved, the theme remains as system default.
Future<void> _onThemeLoadRequested(
ThemeLoadRequested event,
Emitter<ThemeState> emit,

View File

@@ -1,20 +1,35 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
/// Abstract base class for all theme-related events.
///
/// This class extends [Equatable] to enable value equality for event comparison.
/// All theme events in the application should inherit from this class.
abstract class ThemeEvent extends Equatable {
/// Creates a new [ThemeEvent].
const ThemeEvent();
@override
List<Object?> get props => [];
}
/// Event to request a theme change.
///
/// This event is dispatched when the user wants to change the application theme
/// mode (light, dark, or system). The new theme mode is persisted and applied immediately.
class ThemeChanged extends ThemeEvent {
/// The new theme mode to apply.
final ThemeMode themeMode;
/// Creates a new [ThemeChanged] event with the specified [themeMode].
const ThemeChanged({required this.themeMode});
@override
List<Object?> get props => [themeMode];
}
/// Event to request loading of saved theme preferences.
///
/// This event is typically dispatched when the app starts to restore
/// the user's previously selected theme preference.
class ThemeLoadRequested extends ThemeEvent {}

View File

@@ -1,15 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
/// State class for theme management.
///
/// This class represents the current theme state of the application,
/// including the selected theme mode and provides utility methods
/// for theme-related operations.
class ThemeState extends Equatable {
/// The current theme mode of the application.
final ThemeMode themeMode;
/// Creates a new [ThemeState] with the specified [themeMode].
///
/// Defaults to [ThemeMode.system] if no theme mode is provided.
const ThemeState({this.themeMode = ThemeMode.system});
/// Whether the current theme mode is explicitly set to dark.
///
/// Returns true only if the theme mode is [ThemeMode.dark].
/// Returns false for [ThemeMode.light] and [ThemeMode.system].
bool get isDarkMode {
return themeMode == ThemeMode.dark;
}
/// Creates a copy of this state with optionally modified properties.
///
/// Allows updating the theme mode while preserving other state properties.
/// Useful for state transitions in the theme BLoC.
ThemeState copyWith({ThemeMode? themeMode}) {
return ThemeState(
themeMode: themeMode ?? this.themeMode,

View File

@@ -1,3 +1,40 @@
/// A BLoC (Business Logic Component) that manages trip-related operations.
///
/// This bloc handles all trip operations including creation, updates, deletion,
/// and loading trips for users. It provides real-time updates through streams
/// and manages the trip lifecycle with proper state transitions.
///
/// The bloc processes these main events:
/// - [LoadTripsByUserId]: Loads all trips for a specific user with real-time updates
/// - [TripCreateRequested]: Creates a new trip and reloads the user's trip list
/// - [TripUpdateRequested]: Updates an existing trip and refreshes the list
/// - [TripDeleteRequested]: Deletes a trip and refreshes the list
/// - [ResetTrips]: Resets the trip state and cancels subscriptions
///
/// Dependencies:
/// - [TripRepository]: Repository for trip data operations
///
/// State Management:
/// The bloc maintains the current user ID to enable automatic list refreshing
/// after operations like create, update, or delete. This ensures the UI stays
/// in sync with the latest data.
///
/// Example usage:
/// ```dart
/// final tripBloc = TripBloc(tripRepository);
///
/// // Load trips for a user
/// tripBloc.add(LoadTripsByUserId(userId: 'userId123'));
///
/// // Create a new trip
/// tripBloc.add(TripCreateRequested(trip: newTrip));
///
/// // Update a trip
/// tripBloc.add(TripUpdateRequested(trip: updatedTrip));
///
/// // Delete a trip
/// tripBloc.add(TripDeleteRequested(tripId: 'tripId456'));
/// ```
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/models/trip.dart';
@@ -5,12 +42,24 @@ import 'trip_event.dart';
import 'trip_state.dart';
import '../../repositories/trip_repository.dart';
/// BLoC that manages trip-related operations and state.
class TripBloc extends Bloc<TripEvent, TripState> {
/// Repository for trip data operations
final TripRepository _repository;
/// Subscription to trip stream for real-time updates
StreamSubscription? _tripsSubscription;
/// Current user ID for automatic list refreshing after operations
String? _currentUserId;
/// Constructor for TripBloc.
///
/// Initializes the bloc with the trip repository and sets up event handlers
/// for all trip-related operations.
///
/// Args:
/// [_repository]: Repository for trip data operations
TripBloc(this._repository) : super(TripInitial()) {
on<LoadTripsByUserId>(_onLoadTripsByUserId);
on<TripCreateRequested>(_onTripCreateRequested);
@@ -20,6 +69,15 @@ class TripBloc extends Bloc<TripEvent, TripState> {
on<ResetTrips>(_onResetTrips);
}
/// Handles [LoadTripsByUserId] events.
///
/// Loads all trips for a specific user with real-time updates via stream subscription.
/// Stores the user ID for future automatic refreshing after operations and cancels
/// any existing subscription to prevent memory leaks.
///
/// Args:
/// [event]: The LoadTripsByUserId event containing the user ID
/// [emit]: State emitter function
Future<void> _onLoadTripsByUserId(
LoadTripsByUserId event,
Emitter<TripState> emit,
@@ -39,6 +97,14 @@ class TripBloc extends Bloc<TripEvent, TripState> {
);
}
/// Handles [_TripsUpdated] events.
///
/// Processes real-time updates from the trip stream and emits the
/// updated trip list to the UI.
///
/// Args:
/// [event]: The _TripsUpdated event containing the updated trip list
/// [emit]: State emitter function
void _onTripsUpdated(
_TripsUpdated event,
Emitter<TripState> emit,
@@ -46,6 +112,15 @@ class TripBloc extends Bloc<TripEvent, TripState> {
emit(TripLoaded(event.trips));
}
/// Handles [TripCreateRequested] events.
///
/// Creates a new trip and automatically refreshes the user's trip list
/// to show the newly created trip. Includes a delay to allow the creation
/// to complete before refreshing.
///
/// Args:
/// [event]: The TripCreateRequested event containing the trip data
/// [emit]: State emitter function
Future<void> _onTripCreateRequested(
TripCreateRequested event,
Emitter<TripState> emit,
@@ -63,27 +138,45 @@ class TripBloc extends Bloc<TripEvent, TripState> {
}
} catch (e) {
emit(TripError('Erreur lors de la création: $e'));
emit(TripError('Error during creation: $e'));
}
}
/// Handles [TripUpdateRequested] events.
///
/// Updates an existing trip and automatically refreshes the user's trip list
/// to show the updated information. Includes a delay to allow the update
/// to complete before refreshing.
///
/// Args:
/// [event]: The TripUpdateRequested event containing the updated trip data
/// [emit]: State emitter function
Future<void> _onTripUpdateRequested(
TripUpdateRequested event,
Emitter<TripState> emit,
) async {
try {
await _repository.updateTrip(event.trip.id!, event.trip);
emit(const TripOperationSuccess('Voyage mis à jour avec succès'));
emit(const TripOperationSuccess('Trip updated successfully'));
await Future.delayed(const Duration(milliseconds: 500));
if (_currentUserId != null) {
add(LoadTripsByUserId(userId: _currentUserId!));
}
} catch (e) {
emit(TripError('Erreur lors de la mise à jour: $e'));
emit(TripError('Error during update: $e'));
}
}
/// Handles [TripDeleteRequested] events.
///
/// Deletes a trip and automatically refreshes the user's trip list
/// to remove the deleted trip from the UI. Includes a delay to allow
/// the deletion to complete before refreshing.
///
/// Args:
/// [event]: The TripDeleteRequested event containing the trip ID to delete
/// [emit]: State emitter function
Future<void> _onTripDeleteRequested(
TripDeleteRequested event,
Emitter<TripState> emit,
@@ -91,7 +184,7 @@ class TripBloc extends Bloc<TripEvent, TripState> {
try {
await _repository.deleteTrip(event.tripId);
emit(const TripOperationSuccess('Voyage supprimé avec succès'));
emit(const TripOperationSuccess('Trip deleted successfully'));
await Future.delayed(const Duration(milliseconds: 500));
if (_currentUserId != null) {
@@ -99,10 +192,19 @@ class TripBloc extends Bloc<TripEvent, TripState> {
}
} catch (e) {
emit(TripError('Erreur lors de la suppression: $e'));
emit(TripError('Error during deletion: $e'));
}
}
/// Handles [ResetTrips] events.
///
/// Resets the trip state to initial and cleans up resources.
/// Cancels the trip stream subscription and clears the current user ID.
/// This is useful for user logout or when switching contexts.
///
/// Args:
/// [event]: The ResetTrips event
/// [emit]: State emitter function
Future<void> _onResetTrips(
ResetTrips event,
Emitter<TripState> emit,
@@ -112,6 +214,10 @@ class TripBloc extends Bloc<TripEvent, TripState> {
emit(TripInitial());
}
/// Cleans up resources when the bloc is closed.
///
/// Cancels the trip stream subscription to prevent memory leaks
/// and ensure proper disposal of resources.
@override
Future<void> close() {
_tripsSubscription?.cancel();
@@ -119,9 +225,18 @@ class TripBloc extends Bloc<TripEvent, TripState> {
}
}
/// Private event for handling real-time trip updates from streams.
///
/// This internal event is used to process updates from the trip stream
/// subscription and emit appropriate states based on the received data.
class _TripsUpdated extends TripEvent {
/// List of trips received from the stream
final List<Trip> trips;
/// Creates a _TripsUpdated event.
///
/// Args:
/// [trips]: List of trips from the stream update
const _TripsUpdated(this.trips);
@override

View File

@@ -1,6 +1,26 @@
/// Events for trip-related operations in the TripBloc.
///
/// This file defines all possible events that can be dispatched to the TripBloc
/// to trigger trip-related state changes and operations such as loading trips,
/// creating trips, updating trip information, and performing CRUD operations.
///
/// Event Categories:
/// - **Loading Events**: LoadTripsByUserId
/// - **CRUD Operations**: TripCreateRequested, TripUpdateRequested, TripDeleteRequested
/// - **State Management**: ResetTrips
///
/// The events support complete trip lifecycle management with automatic
/// list refreshing after operations to maintain UI consistency.
///
/// All events extend [TripEvent] and implement [Equatable] for proper
/// equality comparison in the BLoC pattern.
import 'package:equatable/equatable.dart';
import '../../models/trip.dart';
/// Base class for all trip-related events.
///
/// All trip events must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class TripEvent extends Equatable {
const TripEvent();
@@ -8,40 +28,98 @@ abstract class TripEvent extends Equatable {
List<Object?> get props => [];
}
/// Event to load all trips associated with a specific user.
///
/// This event retrieves all trips where the user is a participant or organizer,
/// providing a comprehensive view of the user's travel activities. The loading
/// uses real-time streams to keep the trip list updated automatically.
///
/// Args:
/// [userId]: The unique identifier of the user whose trips should be loaded
class LoadTripsByUserId extends TripEvent {
/// The unique identifier of the user
final String userId;
/// Creates a LoadTripsByUserId event.
///
/// Args:
/// [userId]: The user ID to load trips for
const LoadTripsByUserId({required this.userId});
@override
List<Object?> get props => [userId];
}
/// Event to request creation of a new trip.
///
/// This event creates a new trip with the provided information and automatically
/// refreshes the user's trip list to show the newly created trip. The operation
/// includes validation and proper error handling.
///
/// Args:
/// [trip]: The trip object containing all the trip information to create
class TripCreateRequested extends TripEvent {
/// The trip object to create
final Trip trip;
/// Creates a TripCreateRequested event.
///
/// Args:
/// [trip]: The trip object to create
const TripCreateRequested({required this.trip});
@override
List<Object?> get props => [trip];
}
/// Event to request updating an existing trip.
///
/// This event updates an existing trip with new information and automatically
/// refreshes the user's trip list to reflect the changes. The trip must have
/// a valid ID for the update operation to succeed.
///
/// Args:
/// [trip]: The trip object with updated information (must include valid ID)
class TripUpdateRequested extends TripEvent {
/// The trip object with updated information
final Trip trip;
/// Creates a TripUpdateRequested event.
///
/// Args:
/// [trip]: The updated trip object (must have valid ID)
const TripUpdateRequested({required this.trip});
@override
List<Object?> get props => [trip];
}
/// Event to reset the trip state and clean up resources.
///
/// This event resets the TripBloc to its initial state, cancels any active
/// stream subscriptions, and clears the current user context. This is typically
/// used during user logout or when switching between different user contexts.
class ResetTrips extends TripEvent {
/// Creates a ResetTrips event.
const ResetTrips();
}
/// Event to request deletion of a trip.
///
/// This event permanently deletes a trip and all associated data including
/// groups, expenses, and messages. This is a destructive operation that cannot
/// be undone. The user's trip list is automatically refreshed after deletion.
///
/// Args:
/// [tripId]: The unique identifier of the trip to delete
class TripDeleteRequested extends TripEvent {
/// The unique identifier of the trip to delete
final String tripId;
/// Creates a TripDeleteRequested event.
///
/// Args:
/// [tripId]: The ID of the trip to delete
const TripDeleteRequested({required this.tripId});
@override

View File

@@ -1,6 +1,27 @@
/// States for trip-related operations in the TripBloc.
///
/// This file defines all possible states that the TripBloc can emit in response
/// to trip-related events. The states represent different phases of trip
/// operations including loading, success, error, and data states for trip management.
///
/// State Categories:
/// - **Initial State**: TripInitial
/// - **Loading States**: TripLoading
/// - **Success States**: TripLoaded, TripCreated, TripOperationSuccess
/// - **Error States**: TripError
///
/// The states support complete trip lifecycle management with proper feedback
/// for create, read, update, and delete operations.
///
/// All states extend [TripState] and implement [Equatable] for proper
/// equality comparison and state change detection in the BLoC pattern.
import 'package:equatable/equatable.dart';
import '../../models/trip.dart';
/// Base class for all trip-related states.
///
/// All trip states must extend this class and implement the [props] getter
/// for proper equality comparison in the BLoC pattern.
abstract class TripState extends Equatable {
const TripState();
@@ -8,44 +29,111 @@ abstract class TripState extends Equatable {
List<Object?> get props => [];
}
/// The initial state of the TripBloc before any operations are performed.
///
/// This is the default state when the bloc is first created and represents
/// a clean slate with no trip data loaded or operations in progress.
class TripInitial extends TripState {}
/// State indicating that a trip operation is currently in progress.
///
/// This state is emitted when the bloc is performing operations like
/// loading trips, creating trips, updating trip information, or deleting trips.
/// UI components can use this state to show loading indicators.
class TripLoading extends TripState {}
/// State containing a list of successfully loaded trips.
///
/// This state is emitted when trips have been successfully retrieved
/// from the repository, either through initial loading or real-time updates.
/// The UI can use this data to display the list of available trips for the user.
///
/// Properties:
/// [trips]: List of Trip objects that were loaded
class TripLoaded extends TripState {
/// The list of loaded trips
final List<Trip> trips;
/// Creates a TripLoaded state.
///
/// Args:
/// [trips]: The list of trips to include in this state
const TripLoaded(this.trips);
@override
List<Object?> get props => [trips];
}
/// State indicating successful trip creation.
///
/// This state is emitted when a new trip has been successfully created.
/// It contains the ID of the newly created trip and a success message
/// that can be displayed to the user. This state is typically followed
/// by automatic reloading of the user's trip list.
///
/// Properties:
/// [tripId]: The unique identifier of the newly created trip
/// [message]: A success message to display to the user
class TripCreated extends TripState {
/// The unique identifier of the newly created trip
final String tripId;
/// Success message for the user
final String message;
/// Creates a TripCreated state.
///
/// Args:
/// [tripId]: The ID of the newly created trip
/// [message]: Optional success message (defaults to "Trip created successfully")
const TripCreated({
required this.tripId,
this.message = 'Voyage créé avec succès',
this.message = 'Trip created successfully',
});
@override
List<Object?> get props => [tripId, message];
}
/// State indicating successful completion of a trip operation.
///
/// This state is emitted when operations like updating or deleting trips
/// have completed successfully. It contains a message that can be displayed
/// to inform the user of the successful operation. This state is typically
/// followed by automatic reloading of the user's trip list.
///
/// Properties:
/// [message]: A success message describing the completed operation
class TripOperationSuccess extends TripState {
/// The success message to display to the user
final String message;
/// Creates a TripOperationSuccess state.
///
/// Args:
/// [message]: The success message to display
const TripOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
/// State indicating that a trip operation has failed.
///
/// This state is emitted when any trip operation encounters an error.
/// It contains an error message that can be displayed to the user to
/// explain what went wrong during the operation.
///
/// Properties:
/// [message]: An error message describing what went wrong
class TripError extends TripState {
/// The error message to display to the user
final String message;
/// Creates a TripError state.
///
/// Args:
/// [message]: The error message to display
const TripError(this.message);
@override

View File

@@ -4,10 +4,21 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'user_event.dart' as event;
import 'user_state.dart' as state;
/// BLoC for managing user data and operations.
///
/// This BLoC handles user-related operations including loading user data,
/// updating user information, and managing user state throughout the application.
/// It coordinates with Firebase Auth and Firestore to manage user data persistence.
class UserBloc extends Bloc<event.UserEvent, state.UserState> {
/// Firebase Auth instance for authentication operations.
final FirebaseAuth _auth = FirebaseAuth.instance;
/// Firestore instance for user data operations.
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
/// Creates a new [UserBloc] with initial state.
///
/// Registers event handlers for all user-related events.
UserBloc() : super(state.UserInitial()) {
on<event.UserInitialized>(_onUserInitialized);
on<event.LoadUser>(_onLoadUser);
@@ -15,6 +26,11 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
on<event.UserLoggedOut>(_onUserLoggedOut);
}
/// Handles [UserInitialized] events.
///
/// Initializes the current authenticated user's data by fetching it from Firestore.
/// If the user doesn't exist in Firestore, creates a default user document.
/// This is typically called when the app starts or after successful authentication.
Future<void> _onUserInitialized(
event.UserInitialized event,
Emitter<state.UserState> emit,
@@ -25,18 +41,18 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
final currentUser = _auth.currentUser;
if (currentUser == null) {
emit(state.UserError('Aucun utilisateur connecté'));
emit(state.UserError('No user currently authenticated'));
return;
}
// Récupérer les données utilisateur depuis Firestore
// Fetch user data from Firestore
final userDoc = await _firestore
.collection('users')
.doc(currentUser.uid)
.get();
if (!userDoc.exists) {
// Créer un utilisateur par défaut si non existant
// Create a default user if it doesn't exist
final defaultUser = state.UserModel(
id: currentUser.uid,
email: currentUser.email ?? '',
@@ -57,10 +73,14 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
emit(state.UserLoaded(user));
}
} catch (e) {
emit(state.UserError('Erreur lors du chargement de l\'utilisateur: $e'));
emit(state.UserError('Error loading user: $e'));
}
}
/// Handles [LoadUser] events.
///
/// Loads a specific user's data from Firestore by their user ID.
/// This is useful when you need to display information about other users.
Future<void> _onLoadUser(
event.LoadUser event,
Emitter<state.UserState> emit,
@@ -80,13 +100,18 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
});
emit(state.UserLoaded(user));
} else {
emit(state.UserError('Utilisateur non trouvé'));
emit(state.UserError('User not found'));
}
} catch (e) {
emit(state.UserError('Erreur lors du chargement: $e'));
emit(state.UserError('Error loading user: $e'));
}
}
/// Handles [UserUpdated] events.
///
/// Updates the current user's data in Firestore with the provided information.
/// Only updates the fields specified in the userData map, allowing for partial updates.
/// After successful update, reloads the user data to reflect changes.
Future<void> _onUserUpdated(
event.UserUpdated event,
Emitter<state.UserState> emit,
@@ -112,11 +137,15 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
emit(state.UserLoaded(updatedUser));
} catch (e) {
emit(state.UserError('Erreur lors de la mise à jour: $e'));
emit(state.UserError('Error updating user: $e'));
}
}
}
/// Handles [UserLoggedOut] events.
///
/// Resets the user bloc state to initial when the user logs out.
/// This clears any cached user data from the application state.
Future<void> _onUserLoggedOut(
event.UserLoggedOut event,
Emitter<state.UserState> emit,

View File

@@ -1,24 +1,47 @@
/// Abstract base class for all user-related events.
///
/// All user events in the application should inherit from this class.
/// This provides a common interface for user-related operations.
abstract class UserEvent {}
/// Event to initialize the current user's data.
///
/// This event is typically dispatched when the app starts or when
/// a user signs in to load their profile information from Firestore.
class UserInitialized extends UserEvent {}
class UserLoaded extends UserEvent {
/// Event to load a specific user's data by their ID.
///
/// This event is used to fetch user information from Firestore
/// when you need to display or work with a specific user's data.
class LoadUser extends UserEvent {
/// The ID of the user to load.
final String userId;
UserLoaded(this.userId);
/// Creates a [LoadUser] event with the specified [userId].
LoadUser(this.userId);
}
/// Event to update the current user's data.
///
/// This event is dispatched when the user modifies their profile
/// information and the changes need to be saved to Firestore.
class UserUpdated extends UserEvent {
/// Map containing the user data fields to update.
///
/// Only the fields present in this map will be updated in Firestore.
/// This allows for partial updates of user information.
final Map<String, dynamic> userData;
/// Creates a [UserUpdated] event with the specified [userData].
UserUpdated(this.userData);
}
/// Event to handle user logout.
///
/// This event is dispatched when the user signs out of the application,
/// clearing their data from the user bloc state.
class UserLoggedOut extends UserEvent {
/// Creates a [UserLoggedOut] event.
UserLoggedOut();
}
class LoadUser extends UserEvent {
final String userId;
LoadUser(this.userId);
}

View File

@@ -1,39 +1,81 @@
import 'package:equatable/equatable.dart';
/// Abstract base class for all user-related states.
///
/// This class extends [Equatable] to enable value equality for state comparison.
/// All user states in the application should inherit from this class.
abstract class UserState extends Equatable {
@override
List<Object?> get props => [];
}
/// Initial state of the user bloc.
///
/// This state represents the initial state before any user operations
/// have been performed or when the user has logged out.
class UserInitial extends UserState {}
/// State indicating that a user operation is in progress.
///
/// This state is used to show loading indicators during user data
/// operations like loading, updating, or initializing user information.
class UserLoading extends UserState {}
/// State indicating that user data has been successfully loaded.
///
/// This state contains the loaded user information and is used
/// throughout the app to access current user data.
class UserLoaded extends UserState {
/// The loaded user data.
final UserModel user;
/// Creates a [UserLoaded] state with the given [user] data.
UserLoaded(this.user);
@override
List<Object?> get props => [user];
}
/// State indicating that a user operation has failed.
///
/// This state contains an error message that can be displayed to the user
/// when user operations fail.
class UserError extends UserState {
/// The error message describing what went wrong.
final String message;
/// Creates a [UserError] state with the given error [message].
UserError(this.message);
@override
List<Object?> get props => [message];
}
// Modèle utilisateur simple
/// Simple user model for representing user data in the application.
///
/// This model contains basic user information and provides methods for
/// serialization/deserialization with Firestore operations.
/// Simple user model for representing user data in the application.
///
/// This model contains basic user information and provides methods for
/// serialization/deserialization with Firestore operations.
class UserModel {
/// Unique identifier for the user (Firebase UID).
final String id;
/// User's email address.
final String email;
/// User's first name.
final String prenom;
/// User's last name (optional).
final String? nom;
/// Creates a new [UserModel] instance.
///
/// [id], [email], and [prenom] are required fields.
/// [nom] is optional and can be null.
UserModel({
required this.id,
required this.email,
@@ -41,6 +83,10 @@ class UserModel {
this.nom,
});
/// Creates a [UserModel] instance from a JSON map.
///
/// Handles null values gracefully by providing default values.
/// [prenom] defaults to 'Voyageur' (Traveler) if not provided.
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] ?? '',
@@ -50,6 +96,9 @@ class UserModel {
);
}
/// Converts the [UserModel] instance to a JSON map.
///
/// Useful for storing user data in Firestore or other JSON-based operations.
Map<String, dynamic> toJson() {
return {
'id': id,