From 2faf37f145db0d8fe9df2689493a97a3c10a9056 Mon Sep 17 00:00:00 2001 From: Dayron Date: Thu, 30 Oct 2025 15:56:17 +0100 Subject: [PATCH] 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. --- lib/blocs/account/account_bloc.dart | 23 +++ lib/blocs/account/account_event.dart | 31 +++- lib/blocs/account/account_state.dart | 35 ++++ lib/blocs/auth/auth_bloc.dart | 64 ++++++- lib/blocs/auth/auth_event.dart | 51 ++++++ lib/blocs/auth/auth_state.dart | 35 ++++ lib/blocs/balance/balance_bloc.dart | 57 +++++-- lib/blocs/balance/balance_event.dart | 32 ++++ lib/blocs/balance/balance_state.dart | 39 +++++ lib/blocs/expense/expense_bloc.dart | 100 +++++++++-- lib/blocs/expense/expense_event.dart | 56 ++++++- lib/blocs/expense/expense_state.dart | 38 +++++ lib/blocs/group/group_bloc.dart | 157 ++++++++++++++++-- lib/blocs/group/group_event.dart | 153 ++++++++++++++++- lib/blocs/group/group_state.dart | 108 +++++++++++- lib/blocs/message/message_bloc.dart | 165 +++++++++++++++++-- lib/blocs/message/message_event.dart | 142 ++++++++++++++++ lib/blocs/message/message_state.dart | 62 +++++++ lib/blocs/theme/theme_bloc.dart | 18 +- lib/blocs/theme/theme_event.dart | 15 ++ lib/blocs/theme/theme_state.dart | 17 ++ lib/blocs/trip/trip_bloc.dart | 127 +++++++++++++- lib/blocs/trip/trip_event.dart | 78 +++++++++ lib/blocs/trip/trip_state.dart | 90 +++++++++- lib/blocs/user/user_bloc.dart | 43 ++++- lib/blocs/user/user_event.dart | 37 ++++- lib/blocs/user/user_state.dart | 51 +++++- lib/components/account/account_content.dart | 131 ++++++++++++--- lib/components/error/error_content.dart | 21 ++- lib/components/group/chat_group_content.dart | 54 +++++- lib/components/home/create_trip_content.dart | 37 ++++- lib/components/home/home_content.dart | 22 ++- lib/main.dart | 38 ++++- lib/models/expense.dart | 70 +++++++- lib/models/group.dart | 29 ++++ lib/models/trip.dart | 32 ++++ lib/models/user.dart | 44 ++++- lib/pages/login.dart | 31 +++- lib/repositories/auth_repository.dart | 96 +++++++++-- lib/repositories/user_repository.dart | 42 ++++- lib/services/auth_service.dart | 80 ++++++++- lib/services/error_service.dart | 60 ++++++- lib/services/expense_service.dart | 31 +++- lib/services/storage_service.dart | 164 ++++++++++++++---- lib/services/trip_service.dart | 32 ++++ lib/services/user_service.dart | 38 ++++- 46 files changed, 2656 insertions(+), 220 deletions(-) diff --git a/lib/blocs/account/account_bloc.dart b/lib/blocs/account/account_bloc.dart index 3c43162..a31e25e 100644 --- a/lib/blocs/account/account_bloc.dart +++ b/lib/blocs/account/account_bloc.dart @@ -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'; diff --git a/lib/blocs/account/account_event.dart b/lib/blocs/account/account_event.dart index 0e63e7d..181bff9 100644 --- a/lib/blocs/account/account_event.dart +++ b/lib/blocs/account/account_event.dart @@ -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 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 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 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 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 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 members; diff --git a/lib/blocs/account/account_state.dart b/lib/blocs/account/account_state.dart index 0257566..e67a8b5 100644 --- a/lib/blocs/account/account_state.dart +++ b/lib/blocs/account/account_state.dart @@ -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'; diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart index ef78469..56be1b1 100644 --- a/lib/blocs/auth/auth_bloc.dart +++ b/lib/blocs/auth/auth_bloc.dart @@ -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 { + /// 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 { on(_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 _onAuthCheckRequested( AuthCheckRequested event, Emitter emit, @@ -28,7 +59,7 @@ class AuthBloc extends Bloc { 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 { } } + /// Handles [AuthSignInRequested] events. + /// + /// Attempts to sign in a user with the provided email and password. + /// Emits [AuthAuthenticated] on success or [AuthError] on failure. Future _onSignInRequested( AuthSignInRequested event, Emitter emit, @@ -59,13 +94,17 @@ class AuthBloc extends Bloc { 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 _onSignUpRequested( AuthSignUpRequested event, Emitter emit, @@ -83,13 +122,17 @@ class AuthBloc extends Bloc { 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 _onGoogleSignInRequested( AuthGoogleSignInRequested event, Emitter emit, @@ -102,13 +145,17 @@ class AuthBloc extends Bloc { 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 _onAppleSignInRequested( AuthAppleSignInRequested event, Emitter emit, @@ -121,13 +168,16 @@ class AuthBloc extends Bloc { 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 _onSignOutRequested( AuthSignOutRequested event, Emitter emit, @@ -136,6 +186,10 @@ class AuthBloc extends Bloc { emit(AuthUnauthenticated()); } + /// Handles [AuthPasswordResetRequested] events. + /// + /// Sends a password reset email to the specified email address. + /// Emits [AuthPasswordResetSent] on success or [AuthError] on failure. Future _onPasswordResetRequested( AuthPasswordResetRequested event, Emitter emit, diff --git a/lib/blocs/auth/auth_event.dart b/lib/blocs/auth/auth_event.dart index c1e12aa..e15fd91 100644 --- a/lib/blocs/auth/auth_event.dart +++ b/lib/blocs/auth/auth_event.dart @@ -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 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 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 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 diff --git a/lib/blocs/auth/auth_state.dart b/lib/blocs/auth/auth_state.dart index 2897284..329c350 100644 --- a/lib/blocs/auth/auth_state.dart +++ b/lib/blocs/auth/auth_state.dart @@ -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 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 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 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 diff --git a/lib/blocs/balance/balance_bloc.dart b/lib/blocs/balance/balance_bloc.dart index aed6934..b952c31 100644 --- a/lib/blocs/balance/balance_bloc.dart +++ b/lib/blocs/balance/balance_bloc.dart @@ -44,6 +44,16 @@ class BalanceBloc extends Bloc { 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 { on(_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 _onLoadGroupBalance( LoadGroupBalances event, Emitter emit, @@ -65,10 +84,10 @@ class BalanceBloc extends Bloc { 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 { 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 _onRefreshBalance( RefreshBalance event, Emitter 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 { 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 _onMarkSettlementAsCompleted( MarkSettlementAsCompleted event, Emitter emit, @@ -119,12 +156,12 @@ class BalanceBloc extends Bloc { 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())); } } diff --git a/lib/blocs/balance/balance_event.dart b/lib/blocs/balance/balance_event.dart index 2c71710..41f2097 100644 --- a/lib/blocs/balance/balance_event.dart +++ b/lib/blocs/balance/balance_event.dart @@ -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 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 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 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, diff --git a/lib/blocs/balance/balance_state.dart b/lib/blocs/balance/balance_state.dart index b8ae1ce..ff155d3 100644 --- a/lib/blocs/balance/balance_state.dart +++ b/lib/blocs/balance/balance_state.dart @@ -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 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 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 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 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 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 diff --git a/lib/blocs/expense/expense_bloc.dart b/lib/blocs/expense/expense_bloc.dart index 6fc7875..9670c2e 100644 --- a/lib/blocs/expense/expense_bloc.dart +++ b/lib/blocs/expense/expense_bloc.dart @@ -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 { + /// 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 { on(_onArchiveExpense); } + /// Handles [LoadExpensesByGroup] events. + /// + /// Sets up a stream subscription to receive real-time updates for expenses + /// in the specified group. Cancels any existing subscription before creating a new one. Future _onLoadExpensesByGroup( LoadExpensesByGroup event, Emitter emit, @@ -47,11 +67,19 @@ class ExpenseBloc extends Bloc { 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 _onExpensesUpdated( ExpensesUpdated event, Emitter emit, @@ -63,71 +91,119 @@ class ExpenseBloc extends Bloc { } } + /// Handles [CreateExpense] events. + /// + /// Creates a new expense with validation and optional receipt image upload. + /// Uses the expense service to handle business logic and validation, + /// including currency conversion and split calculations. + /// + /// Args: + /// [event]: The CreateExpense event containing expense data and optional receipt + /// [emit]: State emitter function Future _onCreateExpense( CreateExpense event, Emitter emit, ) async { try { await _expenseService.createExpenseWithValidation(event.expense, event.receiptImage); - emit(const ExpenseOperationSuccess('Dé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 _onUpdateExpense( UpdateExpense event, Emitter emit, ) async { try { await _expenseService.updateExpenseWithValidation(event.expense, event.newReceiptImage); - emit(const ExpenseOperationSuccess('Dé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 _onDeleteExpense( DeleteExpense event, Emitter emit, ) async { try { await _expenseRepository.deleteExpense(event.expenseId); - emit(const ExpenseOperationSuccess('Dé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 _onMarkSplitAsPaid( MarkSplitAsPaid event, Emitter 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 _onArchiveExpense( ArchiveExpense event, Emitter emit, ) async { try { await _expenseRepository.archiveExpense(event.expenseId); - emit(const ExpenseOperationSuccess('Dé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 close() { _expensesSubscription?.cancel(); diff --git a/lib/blocs/expense/expense_event.dart b/lib/blocs/expense/expense_event.dart index d04d253..14e34dd 100644 --- a/lib/blocs/expense/expense_event.dart +++ b/lib/blocs/expense/expense_event.dart @@ -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 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 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 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 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 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 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 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 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 diff --git a/lib/blocs/expense/expense_state.dart b/lib/blocs/expense/expense_state.dart index ca00940..829d60a 100644 --- a/lib/blocs/expense/expense_state.dart +++ b/lib/blocs/expense/expense_state.dart @@ -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 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 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 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 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 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 diff --git a/lib/blocs/group/group_bloc.dart b/lib/blocs/group/group_bloc.dart index 1ecdc8c..cedddb8 100644 --- a/lib/blocs/group/group_bloc.dart +++ b/lib/blocs/group/group_bloc.dart @@ -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 { + /// 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(_onLoadGroupsByUserId); on<_GroupsUpdated>(_onGroupsUpdated); @@ -23,6 +69,14 @@ class GroupBloc extends Bloc { on(_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 _onLoadGroupsByUserId( LoadGroupsByUserId event, Emitter emit, @@ -44,6 +98,14 @@ class GroupBloc extends Bloc { } } + /// 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 _onGroupsUpdated( _GroupsUpdated event, Emitter emit, @@ -56,6 +118,14 @@ class GroupBloc extends Bloc { } } + /// 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 _onLoadGroupsByTrip( LoadGroupsByTrip event, Emitter emit, @@ -73,6 +143,14 @@ class GroupBloc extends Bloc { } } + /// 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 _onCreateGroup( CreateGroup event, Emitter emit, @@ -84,12 +162,21 @@ class GroupBloc extends Bloc { 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 _onCreateGroupWithMembers( CreateGroupWithMembers event, Emitter emit, @@ -102,58 +189,94 @@ class GroupBloc extends Bloc { ); 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 _onAddMemberToGroup( AddMemberToGroup event, Emitter 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 _onRemoveMemberFromGroup( RemoveMemberFromGroup event, Emitter 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 _onUpdateGroup( UpdateGroup event, Emitter 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 _onDeleteGroup( DeleteGroup event, Emitter 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 close() { _groupsSubscription?.cancel(); @@ -161,10 +284,22 @@ class GroupBloc extends Bloc { } } +/// 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 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 diff --git a/lib/blocs/group/group_event.dart b/lib/blocs/group/group_event.dart index 605344a..e394f40 100644 --- a/lib/blocs/group/group_event.dart +++ b/lib/blocs/group/group_event.dart @@ -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 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 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 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 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 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 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 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 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 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 diff --git a/lib/blocs/group/group_state.dart b/lib/blocs/group/group_state.dart index e8f152b..165fd24 100644 --- a/lib/blocs/group/group_state.dart +++ b/lib/blocs/group/group_state.dart @@ -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 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 groups; + /// Creates a GroupsLoaded state. + /// + /// Args: + /// [groups]: The list of groups to include in this state const GroupsLoaded(this.groups); @override List 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 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 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 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 diff --git a/lib/blocs/message/message_bloc.dart b/lib/blocs/message/message_bloc.dart index 59f9cec..9b6822f 100644 --- a/lib/blocs/message/message_bloc.dart +++ b/lib/blocs/message/message_bloc.dart @@ -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 { + /// Service for message operations and business logic final MessageService _messageService; + + /// Subscription to message stream for real-time updates StreamSubscription>? _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 { 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 _onLoadMessages( LoadMessages event, Emitter emit, @@ -39,11 +100,19 @@ class MessageBloc extends Bloc { 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 emit, @@ -51,12 +120,20 @@ class MessageBloc extends Bloc { 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 _onSendMessage( SendMessage event, Emitter 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 { 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 _onDeleteMessage( DeleteMessage event, Emitter 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 _onUpdateMessage( UpdateMessage event, Emitter 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 _onReactToMessage( ReactToMessage event, Emitter 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 { 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 _onRemoveReaction( RemoveReaction event, Emitter 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 close() { _messagesSubscription?.cancel(); @@ -143,11 +256,22 @@ class MessageBloc extends Bloc { } } -// 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 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 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 diff --git a/lib/blocs/message/message_event.dart b/lib/blocs/message/message_event.dart index 01c031b..5a0c278 100644 --- a/lib/blocs/message/message_event.dart +++ b/lib/blocs/message/message_event.dart @@ -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 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 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 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 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 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 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, diff --git a/lib/blocs/message/message_state.dart b/lib/blocs/message/message_state.dart index 5d6a89f..1656bd5 100644 --- a/lib/blocs/message/message_state.dart +++ b/lib/blocs/message/message_state.dart @@ -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 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 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 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 diff --git a/lib/blocs/theme/theme_bloc.dart b/lib/blocs/theme/theme_bloc.dart index 7b46526..03ce06e 100644 --- a/lib/blocs/theme/theme_bloc.dart +++ b/lib/blocs/theme/theme_bloc.dart @@ -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 { + /// Creates a new [ThemeBloc] with default theme state. + /// + /// Registers event handlers for theme changes and loading saved preferences. ThemeBloc() : super(const ThemeState()) { on(_onThemeChanged); on(_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 _onThemeChanged( ThemeChanged event, Emitter 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 _onThemeLoadRequested( ThemeLoadRequested event, Emitter emit, diff --git a/lib/blocs/theme/theme_event.dart b/lib/blocs/theme/theme_event.dart index da9c2be..612ba76 100644 --- a/lib/blocs/theme/theme_event.dart +++ b/lib/blocs/theme/theme_event.dart @@ -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 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 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 {} \ No newline at end of file diff --git a/lib/blocs/theme/theme_state.dart b/lib/blocs/theme/theme_state.dart index a50806e..746436d 100644 --- a/lib/blocs/theme/theme_state.dart +++ b/lib/blocs/theme/theme_state.dart @@ -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, diff --git a/lib/blocs/trip/trip_bloc.dart b/lib/blocs/trip/trip_bloc.dart index 61963b9..9609121 100644 --- a/lib/blocs/trip/trip_bloc.dart +++ b/lib/blocs/trip/trip_bloc.dart @@ -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 { + /// 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(_onLoadTripsByUserId); on(_onTripCreateRequested); @@ -20,6 +69,15 @@ class TripBloc extends Bloc { on(_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 _onLoadTripsByUserId( LoadTripsByUserId event, Emitter emit, @@ -39,6 +97,14 @@ class TripBloc extends Bloc { ); } + /// 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 emit, @@ -46,6 +112,15 @@ class TripBloc extends Bloc { 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 _onTripCreateRequested( TripCreateRequested event, Emitter emit, @@ -63,27 +138,45 @@ class TripBloc extends Bloc { } } 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 _onTripUpdateRequested( TripUpdateRequested event, Emitter 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 _onTripDeleteRequested( TripDeleteRequested event, Emitter emit, @@ -91,7 +184,7 @@ class TripBloc extends Bloc { 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 { } } 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 _onResetTrips( ResetTrips event, Emitter emit, @@ -112,6 +214,10 @@ class TripBloc extends Bloc { 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 close() { _tripsSubscription?.cancel(); @@ -119,9 +225,18 @@ class TripBloc extends Bloc { } } +/// 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 trips; + /// Creates a _TripsUpdated event. + /// + /// Args: + /// [trips]: List of trips from the stream update const _TripsUpdated(this.trips); @override diff --git a/lib/blocs/trip/trip_event.dart b/lib/blocs/trip/trip_event.dart index cda58a0..e7391ef 100644 --- a/lib/blocs/trip/trip_event.dart +++ b/lib/blocs/trip/trip_event.dart @@ -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 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 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 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 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 diff --git a/lib/blocs/trip/trip_state.dart b/lib/blocs/trip/trip_state.dart index d8da52e..d5d4032 100644 --- a/lib/blocs/trip/trip_state.dart +++ b/lib/blocs/trip/trip_state.dart @@ -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 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 trips; + /// Creates a TripLoaded state. + /// + /// Args: + /// [trips]: The list of trips to include in this state const TripLoaded(this.trips); @override List 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 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 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 diff --git a/lib/blocs/user/user_bloc.dart b/lib/blocs/user/user_bloc.dart index 7ac5ad1..291631d 100644 --- a/lib/blocs/user/user_bloc.dart +++ b/lib/blocs/user/user_bloc.dart @@ -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 { + /// 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(_onUserInitialized); on(_onLoadUser); @@ -15,6 +26,11 @@ class UserBloc extends Bloc { on(_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 _onUserInitialized( event.UserInitialized event, Emitter emit, @@ -25,18 +41,18 @@ class UserBloc extends Bloc { 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 { 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 _onLoadUser( event.LoadUser event, Emitter emit, @@ -80,13 +100,18 @@ class UserBloc extends Bloc { }); 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 _onUserUpdated( event.UserUpdated event, Emitter emit, @@ -112,11 +137,15 @@ class UserBloc extends Bloc { 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 _onUserLoggedOut( event.UserLoggedOut event, Emitter emit, diff --git a/lib/blocs/user/user_event.dart b/lib/blocs/user/user_event.dart index 4f6f86d..be14c15 100644 --- a/lib/blocs/user/user_event.dart +++ b/lib/blocs/user/user_event.dart @@ -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 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); -} diff --git a/lib/blocs/user/user_state.dart b/lib/blocs/user/user_state.dart index f3d6fa5..ac9698c 100644 --- a/lib/blocs/user/user_state.dart +++ b/lib/blocs/user/user_state.dart @@ -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 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 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 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 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 toJson() { return { 'id': id, diff --git a/lib/components/account/account_content.dart b/lib/components/account/account_content.dart index fefbfd2..7ac1a06 100644 --- a/lib/components/account/account_content.dart +++ b/lib/components/account/account_content.dart @@ -1,3 +1,23 @@ +/// A widget that displays the account content page for the travel expense app. +/// +/// This component manages the display of user accounts (groups) and provides +/// navigation to group expense management. It handles loading account data, +/// error states, and navigation to the group expenses page. +/// +/// Features: +/// - **Account Loading**: Automatically loads accounts for the current user +/// - **Error Handling**: Displays error states with retry functionality +/// - **Navigation**: Navigates to group expense management pages +/// - **Real-time Updates**: Uses BLoC pattern for reactive state management +/// - **Pull to Refresh**: Allows users to refresh account data +/// +/// Dependencies: +/// - [UserBloc]: For current user state management +/// - [AccountBloc]: For account data management +/// - [GroupRepository]: For group data operations +/// +/// The component automatically loads account data when initialized and +/// provides a clean interface for managing group-based expenses. import 'package:flutter/material.dart'; import 'package:travel_mate/blocs/user/user_bloc.dart'; import '../../models/account.dart'; @@ -10,6 +30,7 @@ import '../../blocs/user/user_state.dart' as user_state; import '../../repositories/group_repository.dart'; // Ajouter cet import import 'group_expenses_page.dart'; // Ajouter cet import +/// Widget that displays the account content page with account management functionality. class AccountContent extends StatefulWidget { const AccountContent({super.key}); @@ -17,19 +38,25 @@ class AccountContent extends StatefulWidget { State createState() => _AccountContentState(); } +/// State class for AccountContent that manages account loading and navigation. class _AccountContentState extends State { + /// Repository for group data operations used for navigation final _groupRepository = GroupRepository(); // Ajouter cette ligne @override void initState() { super.initState(); - // Charger immédiatement sans attendre le prochain frame + // Load immediately without waiting for the next frame WidgetsBinding.instance.addPostFrameCallback((_) { _loadInitialData(); }); } + /// Loads the initial account data for the current user. + /// + /// Retrieves the current user from UserBloc and loads their accounts + /// using the AccountBloc. Handles errors gracefully with error display. void _loadInitialData() { try { final userState = context.read().state; @@ -38,20 +65,27 @@ class _AccountContentState extends State { final userId = userState.user.id; context.read().add(LoadAccountsByUserId(userId)); } else { - throw Exception('Utilisateur non connecté'); + throw Exception('User not connected'); } } catch (e) { ErrorContent( - message: 'Erreur lors du chargement des comptes: $e', + message: 'Error loading accounts: $e', onRetry: () {}, ); } } - // Nouvelle méthode pour naviguer vers la page des dépenses de groupe + /// Navigates to the group expenses page for a specific account. + /// + /// Retrieves the group associated with the account and navigates to + /// the group expenses management page. Shows error messages if the + /// group cannot be found or if navigation fails. + /// + /// Args: + /// [account]: The account to navigate to for expense management Future _navigateToGroupExpenses(Account account) async { try { - // Récupérer le groupe associé au compte + // Retrieve the group associated with the account final group = await _groupRepository.getGroupByTripId(account.tripId); if (group != null && mounted) { @@ -68,7 +102,7 @@ class _AccountContentState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Groupe non trouvé pour ce compte'), + content: Text('Group not found for this account'), backgroundColor: Colors.red, ), ); @@ -78,7 +112,7 @@ class _AccountContentState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Erreur lors du chargement du groupe: $e'), + content: Text('Error loading group: $e'), backgroundColor: Colors.red, ), ); @@ -86,6 +120,15 @@ class _AccountContentState extends State { } } + /// Builds the main widget for the account content page. + /// + /// Creates a responsive UI that handles different user and account states: + /// - Shows loading indicator for user authentication + /// - Displays error content for user errors + /// - Builds account content based on account loading states + /// + /// Returns: + /// Widget representing the complete account page UI @override Widget build(BuildContext context) { return BlocBuilder( @@ -98,14 +141,14 @@ class _AccountContentState extends State { if (userState is user_state.UserError) { return ErrorContent( - message: 'Erreur utilisateur: ${userState.message}', + message: 'User error: ${userState.message}', onRetry: () {}, ); } if (userState is! user_state.UserLoaded) { return const Scaffold( - body: Center(child: Text('Utilisateur non connecté')), + body: Center(child: Text('User not connected')), ); } final user = userState.user; @@ -114,7 +157,7 @@ class _AccountContentState extends State { listener: (context, accountState) { if (accountState is AccountError) { ErrorContent( - message: 'Erreur de chargement des comptes: ${accountState.message}', + message: 'Account loading error: ${accountState.message}', onRetry: () { context.read().add(LoadAccountsByUserId(user.id)); }, @@ -131,6 +174,19 @@ class _AccountContentState extends State { ); } + /// Builds the main content based on the current account state. + /// + /// Handles different account loading states and renders appropriate UI: + /// - Loading: Shows circular progress indicator with loading message + /// - Error: Displays error content with retry functionality + /// - Loaded: Renders the accounts list or empty state message + /// + /// Args: + /// [accountState]: Current state of account loading + /// [userId]: ID of the current user for reload operations + /// + /// Returns: + /// Widget representing the account content UI Widget _buildContent(AccountState accountState, String userId) { if (accountState is AccountLoading) { return const Center( @@ -139,7 +195,7 @@ class _AccountContentState extends State { children: [ CircularProgressIndicator(), SizedBox(height: 16), - Text('Chargement des comptes...'), + Text('Loading accounts...'), ], ), ); @@ -147,7 +203,7 @@ class _AccountContentState extends State { if (accountState is AccountError) { return ErrorContent( - message: 'Erreur de chargement des comptes...', + message: 'Account loading error...', onRetry: () { context.read().add(LoadAccountsByUserId(userId)); }, @@ -165,19 +221,27 @@ class _AccountContentState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('État inconnu'), + const Text('Unknown state'), const SizedBox(height: 16), ElevatedButton( onPressed: () { context.read().add(LoadAccountsByUserId(userId)); }, - child: const Text('Charger les comptes'), + child: const Text('Load accounts'), ), ], ), ); } + /// Builds the empty state widget when no accounts are found. + /// + /// Displays a user-friendly message explaining that accounts are + /// automatically created when trips are created. Shows an icon + /// and informative text to guide the user. + /// + /// Returns: + /// Widget representing the empty accounts state Widget _buildEmptyState() { return Center( child: Padding( @@ -188,12 +252,12 @@ class _AccountContentState extends State { const Icon(Icons.account_balance_wallet, size: 80, color: Colors.grey), const SizedBox(height: 16), const Text( - 'Aucun compte trouvé', + 'No accounts found', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( - 'Les comptes sont créés automatiquement lorsque vous créez un voyage', + 'Accounts are automatically created when you create a trip', style: TextStyle(fontSize: 14, color: Colors.grey), textAlign: TextAlign.center, ), @@ -203,6 +267,18 @@ class _AccountContentState extends State { ); } + /// Builds a scrollable list of user accounts with pull-to-refresh functionality. + /// + /// Creates a RefreshIndicator-wrapped ListView that displays all user accounts + /// in card format. Includes a header with title and description, and renders + /// each account using the _buildSimpleAccountCard method. + /// + /// Args: + /// [accounts]: List of accounts to display + /// [userId]: Current user ID for refresh operations + /// + /// Returns: + /// Widget containing the accounts list with pull-to-refresh capability Widget _buildAccountsList(List accounts, String userId) { return RefreshIndicator( onRefresh: () async { @@ -213,12 +289,12 @@ class _AccountContentState extends State { padding: const EdgeInsets.all(16), children: [ const Text( - 'Mes comptes', + 'My accounts', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Gérez vos comptes de voyage', + 'Manage your travel accounts', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), const SizedBox(height: 24), @@ -234,12 +310,25 @@ class _AccountContentState extends State { ); } + /// Builds an individual account card with account information. + /// + /// Creates a Material Design card displaying account details including: + /// - Account name with color-coded avatar + /// - Member count and member names (up to 2 displayed) + /// - Navigation capability to group expenses + /// - Error handling for card rendering issues + /// + /// Args: + /// [account]: Account object containing account details + /// + /// Returns: + /// Widget representing a single account card Widget _buildSimpleAccountCard(Account account) { try { final colors = [Colors.blue, Colors.purple, Colors.green, Colors.orange]; final color = colors[account.name.hashCode.abs() % colors.length]; - String memberInfo = '${account.members.length} membre${account.members.length > 1 ? 's' : ''}'; + String memberInfo = '${account.members.length} member${account.members.length > 1 ? 's' : ''}'; if(account.members.isNotEmpty){ final names = account.members @@ -262,7 +351,7 @@ class _AccountContentState extends State { ), subtitle: Text(memberInfo), trailing: const Icon(Icons.chevron_right), - onTap: () => _navigateToGroupExpenses(account), // Modifier cette ligne + onTap: () => _navigateToGroupExpenses(account), // Navigate to group expenses ), ); } catch (e) { @@ -270,7 +359,7 @@ class _AccountContentState extends State { color: Colors.red, child: const ListTile( leading: Icon(Icons.error, color: Colors.red), - title: Text('Erreur d\'affichage'), + title: Text('Display error'), ) ); } diff --git a/lib/components/error/error_content.dart b/lib/components/error/error_content.dart index b948e05..d204c92 100644 --- a/lib/components/error/error_content.dart +++ b/lib/components/error/error_content.dart @@ -1,16 +1,35 @@ import 'package:flutter/material.dart'; +/// A reusable error display component. +/// +/// This widget provides a consistent way to display error messages throughout +/// the application. It supports customizable titles, messages, icons, and +/// action buttons for retry and close operations. class ErrorContent extends StatelessWidget { + /// The error title to display final String title; + + /// The error message to display final String message; + + /// Optional callback for retry action final VoidCallback? onRetry; + + /// Optional callback for close action final VoidCallback? onClose; + + /// Icon to display with the error final IconData icon; + + /// Color of the error icon final Color? iconColor; + /// Creates a new [ErrorContent] widget. + /// + /// [message] is required, other parameters are optional with sensible defaults. const ErrorContent({ super.key, - this.title = 'Une erreur est survenue', + this.title = 'An error occurred', required this.message, this.onRetry, this.onClose, diff --git a/lib/components/group/chat_group_content.dart b/lib/components/group/chat_group_content.dart index 7ca3219..b2f990d 100644 --- a/lib/components/group/chat_group_content.dart +++ b/lib/components/group/chat_group_content.dart @@ -8,9 +8,26 @@ import '../../blocs/message/message_state.dart'; import '../../models/group.dart'; import '../../models/message.dart'; +/// Chat group content widget for group messaging functionality. +/// +/// This widget provides a complete chat interface for group members to +/// communicate within a travel group. Features include: +/// - Real-time message loading and sending +/// - Message editing and deletion +/// - Message reactions (like/unlike) +/// - Scroll-to-bottom functionality +/// - Message status indicators +/// +/// The widget integrates with MessageBloc for state management and +/// handles various message operations through the bloc pattern. class ChatGroupContent extends StatefulWidget { + /// The group for which to display the chat interface final Group group; + /// Creates a chat group content widget. + /// + /// Args: + /// [group]: The group object containing group details and ID const ChatGroupContent({ super.key, required this.group, @@ -21,14 +38,19 @@ class ChatGroupContent extends StatefulWidget { } class _ChatGroupContentState extends State { + /// Controller for the message input field final _messageController = TextEditingController(); + + /// Controller for managing scroll position in the message list final _scrollController = ScrollController(); + + /// Currently selected message for editing (null if not editing) Message? _editingMessage; @override void initState() { super.initState(); - // Charger les messages au démarrage + // Load messages when the widget initializes context.read().add(LoadMessages(widget.group.id)); } @@ -39,12 +61,20 @@ class _ChatGroupContentState extends State { super.dispose(); } + /// Sends a new message or updates an existing message. + /// + /// Handles both sending new messages and editing existing ones based + /// on the current editing state. Validates input and clears the input + /// field after successful submission. + /// + /// Args: + /// [currentUser]: The user sending or editing the message void _sendMessage(user_state.UserModel currentUser) { final messageText = _messageController.text.trim(); if (messageText.isEmpty) return; if (_editingMessage != null) { - // Mode édition + // Edit mode - update existing message context.read().add( UpdateMessage( groupId: widget.group.id, @@ -54,7 +84,7 @@ class _ChatGroupContentState extends State { ); _cancelEdit(); } else { - // Mode envoi + // Send mode - create new message context.read().add( SendMessage( groupId: widget.group.id, @@ -68,6 +98,13 @@ class _ChatGroupContentState extends State { _messageController.clear(); } + /// Initiates editing mode for a selected message. + /// + /// Sets the message as the currently editing message and populates + /// the input field with the message text for modification. + /// + /// Args: + /// [message]: The message to edit void _editMessage(Message message) { setState(() { _editingMessage = message; @@ -75,6 +112,10 @@ class _ChatGroupContentState extends State { }); } + /// Cancels the current editing operation. + /// + /// Resets the editing state and clears the input field, + /// returning to normal message sending mode. void _cancelEdit() { setState(() { _editingMessage = null; @@ -82,6 +123,13 @@ class _ChatGroupContentState extends State { }); } + /// Deletes a message from the group chat. + /// + /// Sends a delete event to the MessageBloc to remove the specified + /// message from the group's message history. + /// + /// Args: + /// [messageId]: The ID of the message to delete void _deleteMessage(String messageId) { context.read().add( DeleteMessage( diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 9b61b28..1d9edaf 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -20,8 +20,29 @@ import '../../repositories/group_repository.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; +/// Create trip content widget for trip creation and editing functionality. +/// +/// This widget provides a comprehensive form interface for creating new trips +/// or editing existing ones. Key features include: +/// - Trip creation with validation +/// - Location search with autocomplete +/// - Date selection for trip duration +/// - Budget planning and management +/// - Group creation and member management +/// - Account setup for expense tracking +/// - Integration with mapping services for location selection +/// +/// The widget handles both creation and editing modes based on the +/// provided tripToEdit parameter. class CreateTripContent extends StatefulWidget { + /// Optional trip to edit. If null, creates a new trip final Trip? tripToEdit; + + /// Creates a create trip content widget. + /// + /// Args: + /// [tripToEdit]: Optional trip to edit. If provided, the form will + /// be populated with existing trip data for editing const CreateTripContent({ super.key, this.tripToEdit, @@ -32,30 +53,44 @@ class CreateTripContent extends StatefulWidget { } class _CreateTripContentState extends State { + /// Service for handling and displaying errors final _errorService = ErrorService(); + + /// Form validation key final _formKey = GlobalKey(); + + /// Text controllers for form fields final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _budgetController = TextEditingController(); + final _participantController = TextEditingController(); + + /// Services for user and group operations final _userService = UserService(); final _groupRepository = GroupRepository(); + /// Trip date variables DateTime? _startDate; DateTime? _endDate; + + /// Loading and state management variables bool _isLoading = false; String? _createdTripId; + /// Google Maps API key for location services static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; + /// Participant management final List _participants = []; - final _participantController = TextEditingController(); + /// Location autocomplete functionality List _placeSuggestions = []; bool _isLoadingSuggestions = false; OverlayEntry? _suggestionsOverlay; final LayerLink _layerLink = LayerLink(); + /// Determines if the widget is in editing mode bool get isEditing => widget.tripToEdit != null; @override diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index 188db61..2b1969b 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -9,7 +9,20 @@ import '../../blocs/trip/trip_state.dart'; import '../../blocs/trip/trip_event.dart'; import '../../models/trip.dart'; +/// Home content widget for the main application dashboard. +/// +/// This widget serves as the primary content area of the home screen, +/// displaying user trips and providing navigation to trip management +/// features. Key functionality includes: +/// - Loading and displaying user trips +/// - Creating new trips +/// - Viewing trip details +/// - Managing trip state with proper user authentication +/// +/// The widget maintains state persistence using AutomaticKeepAliveClientMixin +/// to preserve content when switching between tabs. class HomeContent extends StatefulWidget { + /// Creates a home content widget. const HomeContent({super.key}); @override @@ -17,20 +30,27 @@ class HomeContent extends StatefulWidget { } class _HomeContentState extends State with AutomaticKeepAliveClientMixin { + /// Preserves widget state when switching between tabs @override bool get wantKeepAlive => true; + /// Flag to prevent duplicate trip loading operations bool _hasLoadedTrips = false; @override void initState() { super.initState(); - // MODIFIÉ : Utiliser addPostFrameCallback pour attendre que le widget tree soit prêt + // Use addPostFrameCallback to wait for the widget tree to be ready WidgetsBinding.instance.addPostFrameCallback((_) { _loadTripsIfUserLoaded(); }); } + /// Loads trips if a user is currently loaded and trips haven't been loaded yet. + /// + /// Checks the current user state and initiates trip loading if the user is + /// authenticated and trips haven't been loaded previously. This prevents + /// duplicate loading operations. void _loadTripsIfUserLoaded() { if (!_hasLoadedTrips && mounted) { final userState = context.read().state; diff --git a/lib/main.dart b/lib/main.dart index 7dc298e..50073ac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,6 +30,10 @@ import 'pages/home.dart'; import 'pages/signup.dart'; import 'pages/resetpswd.dart'; +/// Entry point of the Travel Mate application. +/// +/// This function initializes Flutter widgets, loads environment variables, +/// initializes Firebase, and starts the application. void main() async { WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(fileName: ".env"); @@ -38,45 +42,63 @@ void main() async { runApp(const MyApp()); } +/// The root widget of the Travel Mate application. +/// +/// This widget sets up the BLoC providers, repositories, and MaterialApp +/// configuration. It manages the application's theme, routing, and global state. class MyApp extends StatelessWidget { + /// Creates the main application widget. const MyApp({super.key}); + /// Builds the widget tree for the application. + /// + /// Sets up repository providers for dependency injection, BLoC providers + /// for state management, and configures the MaterialApp with themes and routing. @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ + // Authentication repository for handling user authentication RepositoryProvider( create: (context) => AuthRepository(), ), + // User repository for managing user data RepositoryProvider( create: (context) => UserRepository(), ), + // Trip repository for managing travel trips RepositoryProvider( create: (context) => TripRepository(), ), + // Group repository for managing travel groups RepositoryProvider( create: (context) => GroupRepository(), ), + // Message repository for handling in-app messaging RepositoryProvider( create: (context) => MessageRepository(), ), + // Account repository for managing user account settings RepositoryProvider( create: (context) => AccountRepository(), ), + // Expense repository for managing trip expenses RepositoryProvider( create: (context) => ExpenseRepository(), ), - // Provide service instances so BLoCs can read them with context.read() + // Expense service for business logic related to expenses RepositoryProvider( create: (context) => ExpenseService( expenseRepository: context.read(), ), ), + // Balance repository for calculating expense balances RepositoryProvider( create: (context) => BalanceRepository( expenseRepository: context.read(), ), ), + // Balance service for business logic related to balances RepositoryProvider( create: (context) => BalanceService( balanceRepository: context.read(), @@ -87,38 +109,46 @@ class MyApp extends StatelessWidget { ], child: MultiBlocProvider( providers: [ + // Theme BLoC for managing app theme preferences BlocProvider( create: (context) => ThemeBloc()..add(ThemeLoadRequested()), ), + // Authentication BLoC for managing user authentication state BlocProvider( create: (context) => AuthBloc(authRepository: context.read()) ..add(AuthCheckRequested()), ), + // Group BLoC for managing travel groups BlocProvider( create: (context) => GroupBloc(context.read()), ), + // Trip BLoC for managing travel trips BlocProvider( create: (context) => TripBloc(context.read()), ), + // User BLoC for managing user data BlocProvider(create: (context) => UserBloc()), + // Message BLoC for managing in-app messaging BlocProvider( create: (context) => MessageBloc(), ), + // Account BLoC for managing user account settings BlocProvider( create: (context) => AccountBloc( context.read(), ), ), - // Nouveaux blocs + // Expense BLoC for managing trip expenses BlocProvider( create: (context) => ExpenseBloc( expenseRepository: context.read(), expenseService: context.read(), ), ), + // Balance BLoC for managing expense balances and calculations BlocProvider( create: (context) => BalanceBloc( balanceRepository: context.read(), @@ -134,6 +164,7 @@ class MyApp extends StatelessWidget { title: 'Travel Mate', navigatorKey: ErrorService.navigatorKey, themeMode: themeState.themeMode, + // Light theme configuration theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 180, 180, 180), @@ -141,6 +172,7 @@ class MyApp extends StatelessWidget { ), useMaterial3: true, ), + // Dark theme configuration darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 43, 43, 43), @@ -148,7 +180,9 @@ class MyApp extends StatelessWidget { ), useMaterial3: true, ), + // Default page when app starts home: const LoginPage(), + // Named routes for navigation routes: { '/login': (context) => const LoginPage(), '/signup': (context) => const SignUpPage(), diff --git a/lib/models/expense.dart b/lib/models/expense.dart index a425b0b..e66806a 100644 --- a/lib/models/expense.dart +++ b/lib/models/expense.dart @@ -3,47 +3,109 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'expense_split.dart'; +/// Enumeration of supported currencies for expenses. +/// +/// Each currency includes both a display symbol and standard currency code. enum ExpenseCurrency { + /// Euro currency eur('€', 'EUR'), + /// US Dollar currency usd('\$', 'USD'), + /// British Pound currency gbp('£', 'GBP'); const ExpenseCurrency(this.symbol, this.code); + + /// Currency symbol for display (e.g., €, $, £) final String symbol; + + /// Standard currency code (e.g., EUR, USD, GBP) final String code; } +/// Enumeration of expense categories with display names and icons. +/// +/// Provides predefined categories for organizing travel expenses. enum ExpenseCategory { + /// Restaurant and food expenses restaurant('Restaurant', Icons.restaurant), + /// Transportation expenses transport('Transport', Icons.directions_car), - accommodation('Hébergement', Icons.hotel), - entertainment('Loisirs', Icons.local_activity), + /// Accommodation and lodging expenses + accommodation('Accommodation', Icons.hotel), + /// Entertainment and activity expenses + entertainment('Entertainment', Icons.local_activity), + /// Shopping expenses shopping('Shopping', Icons.shopping_bag), - other('Autre', Icons.category); + /// Other miscellaneous expenses + other('Other', Icons.category); const ExpenseCategory(this.displayName, this.icon); + + /// Human-readable display name for the category final String displayName; + + /// Icon representing the category final IconData icon; } +/// Model representing a travel expense. +/// +/// This class encapsulates all information about an expense including +/// amount, currency, category, who paid, how it's split among participants, +/// and receipt information. It extends [Equatable] for value comparison. class Expense extends Equatable { + /// Unique identifier for the expense final String id; + + /// ID of the group this expense belongs to final String groupId; + + /// Description of the expense final String description; + + /// Amount of the expense in the original currency final double amount; + + /// Currency of the expense final ExpenseCurrency currency; - final double amountInEur; // Montant converti en EUR + + /// Amount converted to EUR for standardized calculations + final double amountInEur; + + /// Category of the expense final ExpenseCategory category; + + /// ID of the user who paid for this expense final String paidById; + + /// Name of the user who paid for this expense final String paidByName; + + /// Date when the expense occurred final DateTime date; + + /// Timestamp when the expense was created final DateTime createdAt; + + /// Timestamp when the expense was last edited (null if never edited) final DateTime? editedAt; + + /// Whether this expense has been edited after creation final bool isEdited; + + /// Whether this expense has been archived final bool isArchived; + + /// URL to the receipt image (optional) final String? receiptUrl; + + /// List of expense splits showing how the cost is divided final List splits; + /// Creates a new [Expense] instance. + /// + /// All parameters except [editedAt] and [receiptUrl] are required. const Expense({ required this.id, required this.groupId, diff --git a/lib/models/group.dart b/lib/models/group.dart index 7e1864e..7097091 100644 --- a/lib/models/group.dart +++ b/lib/models/group.dart @@ -1,14 +1,37 @@ import 'group_member.dart'; +/// Model representing a travel group. +/// +/// A group is a collection of travelers who are part of a specific trip. +/// It contains information about group members, creation details, and +/// provides methods for serialization with Firestore. class Group { + /// Unique identifier for the group final String id; + + /// Display name of the group final String name; + + /// ID of the trip this group belongs to final String tripId; + + /// ID of the user who created this group final String createdBy; + + /// Timestamp when the group was created final DateTime createdAt; + + /// Timestamp when the group was last updated final DateTime updatedAt; + + /// List of members in this group final List members; + /// Creates a new [Group] instance. + /// + /// [id], [name], [tripId], and [createdBy] are required. + /// [createdAt] and [updatedAt] default to current time if not provided. + /// [members] defaults to empty list if not provided. Group({ required this.id, required this.name, @@ -21,6 +44,12 @@ class Group { updatedAt = updatedAt ?? DateTime.now(), members = members ?? []; + /// Creates a [Group] instance from a Firestore document map. + /// + /// [map] - The document data from Firestore + /// [id] - The document ID from Firestore + /// + /// Returns a new [Group] instance with data from the map. factory Group.fromMap(Map map, String id) { return Group( id: id, diff --git a/lib/models/trip.dart b/lib/models/trip.dart index 9851958..4853194 100644 --- a/lib/models/trip.dart +++ b/lib/models/trip.dart @@ -1,20 +1,52 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; +/// Model representing a travel trip in the application. +/// +/// This class encapsulates all trip-related information including dates, +/// location, participants, and budget details. It provides serialization +/// methods for Firebase operations and supports trip lifecycle management. class Trip { + /// Unique identifier for the trip (usually Firestore document ID). final String? id; + + /// Title or name of the trip. final String title; + + /// Detailed description of the trip. final String description; + + /// Trip destination or location. final String location; + + /// Trip start date and time. final DateTime startDate; + + /// Trip end date and time. final DateTime endDate; + + /// Optional budget for the trip in the local currency. final double? budget; + + /// List of participant user IDs. final List participants; + + /// User ID of the trip creator. final String createdBy; + + /// Timestamp when the trip was created. final DateTime createdAt; + + /// Timestamp when the trip was last updated. final DateTime updatedAt; + + /// Current status of the trip (e.g., 'draft', 'active', 'completed'). final String status; + /// Creates a new [Trip] instance. + /// + /// Most fields are required except [id] and [budget]. + /// [status] defaults to 'draft' for new trips. Trip({ this.id, required this.title, diff --git a/lib/models/user.dart b/lib/models/user.dart index 13ca0f0..95a80d8 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,11 +1,27 @@ import 'dart:convert'; +/// Model representing a user in the travel mate application. +/// +/// This class encapsulates user information including personal details +/// and provides methods for serialization/deserialization with Firebase +/// and JSON operations. class User { + /// Unique identifier for the user (usually Firebase UID). final String? id; + + /// User's last name. final String nom; + + /// User's first name. final String prenom; + + /// User's email address. final String email; + /// Creates a new [User] instance. + /// + /// [nom], [prenom], and [email] are required fields. + /// [id] is optional and typically assigned by Firebase. User({ this.id, required this.nom, @@ -13,7 +29,9 @@ class User { required this.email, }); - // Constructeur pour créer un User depuis un Map (utile pour Firebase) + /// Creates a [User] instance from a Map (useful for Firebase operations). + /// + /// Handles null values gracefully by providing empty string defaults. factory User.fromMap(Map map) { return User( id: map['id'], @@ -23,13 +41,17 @@ class User { ); } - // Constructeur pour créer un User depuis JSON + /// Creates a [User] instance from a JSON string. + /// + /// Parses the JSON and delegates to [fromMap] for object creation. factory User.fromJson(String jsonStr) { Map map = json.decode(jsonStr); return User.fromMap(map); } - // Méthode pour convertir un User en Map (utile pour Firebase) + /// Converts the [User] instance to a Map (useful for Firebase operations). + /// + /// Returns a map with all user properties for database storage. Map toMap() { return { 'id': id, @@ -39,15 +61,20 @@ class User { }; } - // Méthode pour convertir un User en JSON + /// Converts the [User] instance to a JSON string. + /// + /// Useful for API communications and data serialization. String toJson() { return json.encode(toMap()); } - // Méthode pour obtenir le nom complet + /// Gets the user's full name in "first last" format. String get fullName => '$prenom $nom'; - // Méthode pour créer une copie avec des modifications + /// Creates a copy of this user with optionally modified properties. + /// + /// Allows updating specific fields while preserving others. + /// Useful for state management and partial updates. User copyWith({ String? id, String? nom, @@ -62,17 +89,22 @@ class User { ); } + /// Returns a string representation of the user. @override String toString() { return 'User(id: $id, nom: $nom, prenom: $prenom, email: $email)'; } + /// Compares users based on email address. + /// + /// Two users are considered equal if they have the same email. @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is User && other.email == email; } + /// Hash code based on email address. @override int get hashCode => email.hashCode; } diff --git a/lib/pages/login.dart b/lib/pages/login.dart index d2f8fe7..842058e 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -4,7 +4,13 @@ import '../blocs/auth/auth_bloc.dart'; import '../blocs/auth/auth_event.dart'; import '../blocs/auth/auth_state.dart'; +/// Login page widget for user authentication. +/// +/// This page provides a user interface for signing in with email and password, +/// as well as options for social sign-in (Google, Apple) and password reset. +/// It integrates with the AuthBloc to handle authentication state management. class LoginPage extends StatefulWidget { + /// Creates a new [LoginPage]. const LoginPage({super.key}); @override @@ -12,9 +18,16 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { + /// Form key for validation final _formKey = GlobalKey(); + + /// Controller for email input field final _emailController = TextEditingController(); + + /// Controller for password input field final _passwordController = TextEditingController(); + + /// Whether the password field should hide its content bool _obscurePassword = true; @override @@ -24,24 +37,36 @@ class _LoginPageState extends State { super.dispose(); } + /// Validates email input. + /// + /// Returns an error message if the email is invalid or empty, + /// null if the email is valid. String? _validateEmail(String? value) { if (value == null || value.trim().isEmpty) { - return 'Email requis'; + return 'Email required'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value.trim())) { - return 'Email invalide'; + return 'Invalid email'; } return null; } + /// Validates password input. + /// + /// Returns an error message if the password is empty, + /// null if the password is valid. String? _validatePassword(String? value) { if (value == null || value.isEmpty) { - return 'Mot de passe requis'; + return 'Password required'; } return null; } + /// Handles the login process. + /// + /// Validates the form and dispatches a sign-in event to the AuthBloc + /// if all fields are valid. void _login(BuildContext context) { if (!_formKey.currentState!.validate()) { return; diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index bab8ccd..83a34c1 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -3,23 +3,49 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../models/user.dart'; import '../services/auth_service.dart'; +/// Repository for authentication operations and user data management. +/// +/// This repository handles authentication logic and user data persistence +/// by coordinating between Firebase Auth and Firestore. It provides a +/// clean interface for authentication operations while managing the +/// relationship between Firebase users and application user data. class AuthRepository { + /// Authentication service for Firebase Auth operations. final AuthService _authService; + + /// Firestore instance for user data operations. final FirebaseFirestore _firestore; + /// Creates a new [AuthRepository] with optional service dependencies. + /// + /// If [authService] or [firestore] are not provided, default instances will be used. AuthRepository({ AuthService? authService, FirebaseFirestore? firestore, }) : _authService = authService ?? AuthService(), _firestore = firestore ?? FirebaseFirestore.instance; - // Vérifier l'état de connexion actuel + /// Stream of authentication state changes. + /// + /// Emits Firebase user objects when authentication state changes. Stream get authStateChanges => _authService.authStateChanges; + /// Gets the currently authenticated Firebase user. + /// + /// Returns null if no user is currently authenticated. firebase_auth.User? get currentUser => _authService.currentUser; - // Connexion avec email/mot de passe + /// Signs in a user with email and password. + /// + /// Authenticates with Firebase Auth and retrieves the corresponding + /// user data from Firestore. + /// + /// [email] - User's email address + /// [password] - User's password + /// + /// Returns the [User] model if successful, null if user data not found. + /// Throws an exception if authentication fails. Future signInWithEmailAndPassword({ required String email, required String password, @@ -31,11 +57,22 @@ class AuthRepository { ); return await getUserFromFirestore(firebaseUser.user!.uid); } catch (e) { - throw Exception('Erreur de connexion: $e'); + throw Exception('Sign-in error: $e'); } } - // Inscription avec email/mot de passe + /// Creates a new user account with email and password. + /// + /// Creates a Firebase Auth account and stores additional user information + /// in Firestore. + /// + /// [email] - User's email address + /// [password] - User's password + /// [nom] - User's last name + /// [prenom] - User's first name + /// + /// Returns the created [User] model if successful. + /// Throws an exception if account creation fails. Future signUpWithEmailAndPassword({ required String email, required String password, @@ -48,7 +85,7 @@ class AuthRepository { password: password, ); - // Créer le document utilisateur dans Firestore + // Create user document in Firestore with additional information final user = User( id: firebaseUser.user!.uid, email: email, @@ -60,29 +97,35 @@ class AuthRepository { return user; } catch (e) { - throw Exception('Erreur d\'inscription: $e'); + throw Exception('Registration error: $e'); } } - // Connexion avec Google + /// Signs in a user using Google authentication. + /// + /// Handles Google sign-in flow and creates/retrieves user data from Firestore. + /// If the user doesn't exist, creates a new user document. + /// + /// Returns the [User] model if successful, null if Google sign-in was cancelled. + /// Throws an exception if authentication fails. Future signInWithGoogle() async { try { final firebaseUser = await _authService.signInWithGoogle(); if (firebaseUser.user != null) { - // Vérifier si l'utilisateur existe déjà + // Check if user already exists in Firestore final existingUser = await getUserFromFirestore(firebaseUser.user!.uid); if (existingUser != null) { return existingUser; } - // Créer un nouvel utilisateur + // Create new user document for first-time Google sign-in final user = User( id: firebaseUser.user!.uid, email: firebaseUser.user!.email ?? '', nom: '', - prenom: firebaseUser.user!.displayName ?? 'Utilisateur', + prenom: firebaseUser.user!.displayName ?? 'User', ); await _firestore.collection('users').doc(user.id).set(user.toMap()); @@ -90,11 +133,17 @@ class AuthRepository { } return null; } catch (e) { - throw Exception('Erreur de connexion Google: $e'); + throw Exception('Google sign-in error: $e'); } } - // Connexion avec Apple + /// Signs in a user using Apple authentication. + /// + /// Handles Apple sign-in flow and creates/retrieves user data from Firestore. + /// If the user doesn't exist, creates a new user document. + /// + /// Returns the [User] model if successful, null if Apple sign-in was cancelled. + /// Throws an exception if authentication fails. Future signInWithApple() async { try { final firebaseUser = await _authService.signInWithApple(); @@ -110,7 +159,7 @@ class AuthRepository { id: firebaseUser.user!.uid, email: firebaseUser.user!.email ?? '', nom: '', - prenom: firebaseUser.user!.displayName ?? 'Utilisateur', + prenom: firebaseUser.user!.displayName ?? 'User', ); await _firestore.collection('users').doc(user.id).set(user.toMap()); @@ -118,21 +167,34 @@ class AuthRepository { } return null; } catch (e) { - throw Exception('Erreur de connexion Apple: $e'); + throw Exception('Apple sign-in error: $e'); } } - // Déconnexion + /// Signs out the current user. + /// + /// Clears the authentication state and signs out from Firebase Auth. Future signOut() async { await _authService.signOut(); } - // Réinitialisation du mot de passe + /// Sends a password reset email to the specified email address. + /// + /// [email] - The email address to send the reset link to + /// + /// Throws an exception if the operation fails. Future resetPassword(String email) async { await _authService.resetPassword(email); } - // Récupérer les données utilisateur depuis Firestore + /// Retrieves user data from Firestore by user ID. + /// + /// Fetches the user document from the 'users' collection and converts + /// it to a [User] model. + /// + /// [uid] - The Firebase user ID to look up + /// + /// Returns the [User] model if found, null otherwise. Future getUserFromFirestore(String uid) async { try { final doc = await _firestore.collection('users').doc(uid).get(); diff --git a/lib/repositories/user_repository.dart b/lib/repositories/user_repository.dart index 8caaab8..bee45b4 100644 --- a/lib/repositories/user_repository.dart +++ b/lib/repositories/user_repository.dart @@ -2,17 +2,35 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../models/user.dart'; import '../services/auth_service.dart'; +/// Repository for user data operations in Firestore. +/// +/// This repository provides methods for CRUD operations on user data, +/// including retrieving users by ID or email, updating user information, +/// and managing user profiles in the Firestore database. class UserRepository { + /// Firestore instance for database operations. final FirebaseFirestore _firestore; + + /// Authentication service for user-related operations. final AuthService _authService; + /// Creates a new [UserRepository] with optional dependencies. + /// + /// If [firestore] or [authService] are not provided, default instances will be used. UserRepository({ FirebaseFirestore? firestore, AuthService? authService, }) : _firestore = firestore ?? FirebaseFirestore.instance, _authService = authService ?? AuthService(); - // Récupérer un utilisateur par ID + /// Retrieves a user by their unique ID. + /// + /// Fetches the user document from Firestore and converts it to a [User] model. + /// + /// [uid] - The unique user identifier + /// + /// Returns the [User] model if found, null otherwise. + /// Throws an exception if the operation fails. Future getUserById(String uid) async { try { final doc = await _firestore.collection('users').doc(uid).get(); @@ -22,11 +40,19 @@ class UserRepository { } return null; } catch (e) { - throw Exception('Erreur lors de la récupération de l\'utilisateur: $e'); + throw Exception('Error retrieving user: $e'); } } - // Récupérer un utilisateur par email + /// Retrieves a user by their email address. + /// + /// Searches the users collection for a matching email address. + /// Email comparison is case-insensitive after trimming whitespace. + /// + /// [email] - The email address to search for + /// + /// Returns the first [User] found with the matching email, null if not found. + /// Throws an exception if the operation fails. Future getUserByEmail(String email) async { try { final querySnapshot = await _firestore @@ -42,11 +68,17 @@ class UserRepository { } return null; } catch (e) { - throw Exception('Erreur lors de la recherche de l\'utilisateur: $e'); + throw Exception('Error searching for user: $e'); } } - // Mettre à jour un utilisateur + /// Updates an existing user in Firestore. + /// + /// Updates the user document with the provided user data. + /// + /// [user] - The user object containing updated information + /// + /// Returns true if the update was successful, false otherwise. Future updateUser(User user) async { try { await _firestore.collection('users').doc(user.id).update(user.toMap()); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 72a6ab4..496cf91 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -2,14 +2,32 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:travel_mate/services/error_service.dart'; +/// Service for handling Firebase authentication operations. +/// +/// This service provides methods for user authentication including email/password +/// sign-in, Google sign-in, Apple sign-in, password reset, and account management. +/// It acts as a wrapper around Firebase Auth functionality. class AuthService { + /// Error service for logging authentication errors. final _errorService = ErrorService(); + + /// Firebase Auth instance for authentication operations. final FirebaseAuth firebaseAuth = FirebaseAuth.instance; + /// Gets the currently authenticated user. + /// + /// Returns null if no user is currently signed in. User? get currentUser => firebaseAuth.currentUser; + /// Stream that emits authentication state changes. + /// + /// Emits the current user when authenticated, null when not authenticated. Stream get authStateChanges => firebaseAuth.authStateChanges(); + /// Signs in a user with email and password. + /// + /// Returns a [UserCredential] containing the authenticated user's information. + /// Throws [FirebaseAuthException] if authentication fails. Future signInWithEmailAndPassword({ required String email, required String password @@ -18,6 +36,10 @@ class AuthService { email: email, password: password); } + /// Creates a new user account with email and password. + /// + /// Returns a [UserCredential] containing the new user's information. + /// Throws [FirebaseAuthException] if account creation fails. Future signUpWithEmailAndPassword({ required String email, required String password @@ -26,52 +48,88 @@ class AuthService { email: email, password: password); } + /// Signs out the current user. + /// + /// Clears the authentication state and signs out the user from Firebase. Future signOut() async { await firebaseAuth.signOut(); } + /// Sends a password reset email to the specified email address. + /// + /// The user will receive an email with instructions to reset their password. + /// Throws [FirebaseAuthException] if the email is invalid or other errors occur. Future resetPassword(String email) async { await firebaseAuth.sendPasswordResetEmail(email: email); } + /// Updates the display name of the current user. + /// + /// Requires a user to be currently authenticated. + /// Throws if no user is signed in. Future updateDisplayName({ required String displayName, }) async { await currentUser!.updateDisplayName(displayName); } + /// Deletes the current user's account permanently. + /// + /// Requires re-authentication with the user's current password for security. + /// This operation cannot be undone. + /// + /// [password] - The user's current password for re-authentication + /// [email] - The user's email address for re-authentication Future deleteAccount({ required String password, required String email, }) async { - // Re-authenticate the user + // Re-authenticate the user for security AuthCredential credential = EmailAuthProvider.credential(email: email, password: password); await currentUser!.reauthenticateWithCredential(credential); - // Delete the user + // Delete the user account permanently await currentUser!.delete(); await firebaseAuth.signOut(); } + /// Resets the user's password after re-authentication. + /// + /// This method allows users to change their password by providing their + /// current password for security verification. + /// + /// [currentPassword] - The user's current password for verification + /// [newPassword] - The new password to set + /// [email] - The user's email address for re-authentication Future resetPasswordFromCurrentPassword({ required String currentPassword, required String newPassword, required String email, }) async { - // Re-authenticate the user + // Re-authenticate the user for security AuthCredential credential = EmailAuthProvider.credential(email: email, password: currentPassword); await currentUser!.reauthenticateWithCredential(credential); - // Update the password + // Update to the new password await currentUser!.updatePassword(newPassword); } + /// Ensures Google Sign-In is properly initialized. + /// + /// This method must be called before attempting Google authentication. Future ensureInitialized(){ return GoogleSignInPlatform.instance.init(const InitParameters()); } + /// Signs in a user using Google authentication. + /// + /// Handles the complete Google Sign-In flow including platform initialization + /// and credential exchange with Firebase. + /// + /// Returns a [UserCredential] containing the authenticated user's information. + /// Throws various exceptions if authentication fails. Future signInWithGoogle() async { try { await ensureInitialized(); @@ -86,24 +144,28 @@ class AuthService { final OAuthCredential credential = GoogleAuthProvider.credential(idToken: idToken); UserCredential userCredential = await firebaseAuth.signInWithCredential(credential); - // Retourner le UserCredential au lieu de void + // Return the UserCredential instead of void return userCredential; } } on GoogleSignInException catch (e) { - _errorService.logError('Erreur Google Sign-In: $e', StackTrace.current); + _errorService.logError('Google Sign-In error: $e', StackTrace.current); rethrow; } on FirebaseAuthException catch (e) { - _errorService.logError('Erreur Firebase lors de l\'initialisation de Google Sign-In: $e', StackTrace.current); + _errorService.logError('Firebase error during Google Sign-In initialization: $e', StackTrace.current); rethrow; } catch (e) { - _errorService.logError('Erreur inconnue lors de l\'initialisation de Google Sign-In: $e', StackTrace.current); + _errorService.logError('Unknown error during Google Sign-In initialization: $e', StackTrace.current); rethrow; } } + /// Signs in a user using Apple authentication. + /// + /// TODO: Implement Apple Sign-In functionality + /// This method is currently a placeholder for future Apple authentication support. Future signInWithApple() async { - // TODO: Implémenter la connexion avec Apple + // TODO: Implement Apple sign-in } } \ No newline at end of file diff --git a/lib/services/error_service.dart b/lib/services/error_service.dart index 3f41c07..5d26da2 100644 --- a/lib/services/error_service.dart +++ b/lib/services/error_service.dart @@ -1,18 +1,40 @@ import 'package:flutter/material.dart'; import '../components/error/error_content.dart'; +/// Service for handling application errors and user notifications. +/// +/// This singleton service provides centralized error handling capabilities +/// including displaying error dialogs, snackbars, and logging errors for +/// debugging purposes. It uses a global navigator key to show notifications +/// from anywhere in the application. class ErrorService { static final ErrorService _instance = ErrorService._internal(); + + /// Factory constructor that returns the singleton instance. factory ErrorService() => _instance; + + /// Private constructor for singleton pattern. ErrorService._internal(); - // GlobalKey pour accéder au context depuis n'importe où + /// Global navigator key for accessing context from anywhere in the app. + /// + /// This key should be assigned to the MaterialApp's navigatorKey property + /// to enable error notifications from any part of the application. static GlobalKey navigatorKey = GlobalKey(); - // Afficher une erreur en dialog + /// Displays an error message in a dialog. + /// + /// Shows a modal dialog with the error message and optional retry functionality. + /// The dialog appearance can be customized with different icons and colors. + /// + /// [message] - The error message to display + /// [title] - The dialog title (defaults to 'Error') + /// [onRetry] - Optional callback for retry functionality + /// [icon] - Icon to display in the dialog + /// [iconColor] - Color of the icon void showError({ required String message, - String title = 'Erreur', + String title = 'Error', VoidCallback? onRetry, IconData icon = Icons.error_outline, Color? iconColor, @@ -30,7 +52,14 @@ class ErrorService { } } - // Afficher une erreur en snackbar + /// Displays an error or success message in a snackbar. + /// + /// Shows a floating snackbar at the bottom of the screen with the message. + /// The appearance changes based on whether it's an error or success message. + /// + /// [message] - The message to display + /// [onRetry] - Optional callback for retry functionality + /// [isError] - Whether this is an error (true) or success (false) message void showSnackbar({ required String message, VoidCallback? onRetry, @@ -45,7 +74,7 @@ class ErrorService { duration: const Duration(seconds: 4), action: onRetry != null ? SnackBarAction( - label: 'Réessayer', + label: 'Retry', textColor: Colors.white, onPressed: onRetry, ) @@ -59,10 +88,17 @@ class ErrorService { } } - // Logger dans la console (développement) + /// Logs error messages to the console during development. + /// + /// Formats and displays error information including source, error message, + /// and optional stack trace in a visually distinct format. + /// + /// [source] - The source or location where the error occurred + /// [error] - The error object or message + /// [stackTrace] - Optional stack trace for debugging void logError(String source, dynamic error, [StackTrace? stackTrace]) { print('═══════════════════════════════════'); - print('❌ ERREUR dans $source'); + print('❌ ERROR in $source'); print('Message: $error'); if (stackTrace != null) { print('StackTrace: $stackTrace'); @@ -70,12 +106,18 @@ class ErrorService { print('═══════════════════════════════════'); } - // Logger une info (développement) + /// Logs informational messages to the console during development. + /// + /// [source] - The source or location of the information + /// [message] - The informational message void logInfo(String source, String message) { print('ℹ️ [$source] $message'); } - // Logger un succès + /// Logs success messages to the console during development. + /// + /// [source] - The source or location of the success + /// [message] - The success message void logSuccess(String source, String message) { print('✅ [$source] $message'); } diff --git a/lib/services/expense_service.dart b/lib/services/expense_service.dart index c7e1fe2..9e33c0f 100644 --- a/lib/services/expense_service.dart +++ b/lib/services/expense_service.dart @@ -2,13 +2,27 @@ import 'dart:io'; import '../models/expense.dart'; import '../repositories/expense_repository.dart'; import 'error_service.dart'; -import 'storage_service.dart'; // Pour upload des reçus +import 'storage_service.dart'; +/// Service for managing expense operations with business logic validation. +/// +/// This service provides high-level expense management functionality including +/// validation, receipt image uploading, and coordination with the expense repository. +/// It acts as a business logic layer between the UI and data persistence. class ExpenseService { + /// Repository for expense data operations. final ExpenseRepository _expenseRepository; + + /// Service for error handling and logging. final ErrorService _errorService; + + /// Service for handling file uploads (receipts). final StorageService _storageService; + /// Creates a new [ExpenseService] with required dependencies. + /// + /// [expenseRepository] is required for data operations. + /// [errorService] and [storageService] have default implementations if not provided. ExpenseService({ required ExpenseRepository expenseRepository, ErrorService? errorService, @@ -17,13 +31,22 @@ class ExpenseService { _errorService = errorService ?? ErrorService(), _storageService = storageService ?? StorageService(); - // Création avec validation et upload d'image + /// Creates an expense with validation and optional receipt image upload. + /// + /// Validates the expense data, uploads receipt image if provided, and + /// creates the expense record in the database. + /// + /// [expense] - The expense data to create + /// [receiptImage] - Optional receipt image file to upload + /// + /// Returns the ID of the created expense. + /// Throws exceptions if validation fails or creation errors occur. Future createExpenseWithValidation(Expense expense, File? receiptImage) async { try { - // Validation métier + // Business logic validation _validateExpenseData(expense); - // Upload du reçu si présent + // Upload receipt image if provided String? receiptUrl; if (receiptImage != null) { receiptUrl = await _storageService.uploadReceiptImage( diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index d7c6761..c0710f7 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,3 +1,29 @@ +/// A service that handles file storage operations using Firebase Storage. +/// +/// This service provides functionality for: +/// - Receipt image upload and compression +/// - Profile image management +/// - File validation and optimization +/// - Automatic image compression to reduce storage costs +/// - Metadata management for uploaded files +/// +/// The service automatically compresses images to JPEG format with 85% quality +/// to balance file size and image quality. It also generates unique filenames +/// and handles error logging through the ErrorService. +/// +/// Example usage: +/// ```dart +/// final storageService = StorageService(); +/// +/// // Upload a receipt image +/// final receiptUrl = await storageService.uploadReceiptImage(groupId, imageFile); +/// +/// // Upload a profile image +/// final profileUrl = await storageService.uploadProfileImage(userId, imageFile); +/// +/// // Delete a file +/// await storageService.deleteFile(fileUrl); +/// ``` import 'dart:io'; import 'dart:typed_data'; import 'package:firebase_storage/firebase_storage.dart'; @@ -5,32 +31,55 @@ import 'package:path/path.dart' as path; import 'package:image/image.dart' as img; import 'error_service.dart'; +/// Service for managing file storage operations with Firebase Storage. class StorageService { + /// Firebase Storage instance for file operations final FirebaseStorage _storage; + + /// Service for error handling and logging final ErrorService _errorService; + /// Constructor for StorageService. + /// + /// Args: + /// [storage]: Optional Firebase Storage instance (auto-created if null) + /// [errorService]: Optional error service instance (auto-created if null) StorageService({ FirebaseStorage? storage, ErrorService? errorService, }) : _storage = storage ?? FirebaseStorage.instance, _errorService = errorService ?? ErrorService(); - /// Upload d'une image de reçu pour une dépense + /// Uploads a receipt image for an expense with automatic compression. + /// + /// Validates the image file, compresses it to JPEG format with 85% quality, + /// generates a unique filename, and uploads it with appropriate metadata. + /// Monitors upload progress and logs it for debugging purposes. + /// + /// Args: + /// [groupId]: ID of the group this receipt belongs to + /// [imageFile]: The image file to upload + /// + /// Returns: + /// A Future containing the download URL of the uploaded image + /// + /// Throws: + /// Exception if file validation fails or upload encounters an error Future uploadReceiptImage(String groupId, File imageFile) async { try { - // Validation du fichier + // File validation _validateImageFile(imageFile); - // Compression de l'image + // Image compression final compressedImage = await _compressImage(imageFile); - // Génération du nom de fichier unique + // Generate unique filename final fileName = _generateReceiptFileName(groupId); - // Référence vers le storage + // Storage reference final storageRef = _storage.ref().child('receipts/$groupId/$fileName'); - // Métadonnées pour optimiser le cache et la compression + // Metadata for cache optimization and compression info final metadata = SettableMetadata( contentType: 'image/jpeg', customMetadata: { @@ -40,58 +89,77 @@ class StorageService { }, ); - // Upload du fichier + // File upload final uploadTask = storageRef.putData(compressedImage, metadata); - // Monitoring du progrès (optionnel) + // Progress monitoring (optional) uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) { final progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; _errorService.logInfo('StorageService', 'Upload progress: ${progress.toStringAsFixed(1)}%'); }); - // Attendre la completion + // Wait for completion final snapshot = await uploadTask; - // Récupérer l'URL de téléchargement + // Get download URL final downloadUrl = await snapshot.ref.getDownloadURL(); - _errorService.logSuccess('StorageService', 'Image uploadée avec succès: $fileName'); + _errorService.logSuccess('StorageService', 'Image uploaded successfully: $fileName'); return downloadUrl; } catch (e) { - _errorService.logError('StorageService', 'Erreur upload image: $e'); + _errorService.logError('StorageService', 'Error uploading image: $e'); rethrow; } } - /// Supprimer une image de reçu + /// Deletes a receipt image from storage. + /// + /// Extracts the storage reference from the provided URL and deletes the file. + /// Does not throw errors to avoid blocking expense deletion operations. + /// + /// Args: + /// [imageUrl]: The download URL of the image to delete Future deleteReceiptImage(String imageUrl) async { try { if (imageUrl.isEmpty) return; - // Extraire la référence depuis l'URL + // Extract reference from URL final ref = _storage.refFromURL(imageUrl); await ref.delete(); - _errorService.logSuccess('StorageService', 'Image supprimée avec succès'); + _errorService.logSuccess('StorageService', 'Image deleted successfully'); } catch (e) { - _errorService.logError('StorageService', 'Erreur suppression image: $e'); - // Ne pas rethrow pour éviter de bloquer la suppression de la dépense + _errorService.logError('StorageService', 'Error deleting image: $e'); + // Don't rethrow to avoid blocking expense deletion } } - /// Compresser une image pour optimiser le stockage + /// Compresses an image to optimize storage space and upload speed. + /// + /// Reads the image file, decodes it, resizes it if too large (max 1024x1024), + /// and encodes it as JPEG with 85% quality for optimal balance between + /// file size and image quality. + /// + /// Args: + /// [imageFile]: The image file to compress + /// + /// Returns: + /// A Future containing the compressed image bytes + /// + /// Throws: + /// Exception if the image cannot be decoded or processed Future _compressImage(File imageFile) async { try { - // Lire l'image + // Read image final bytes = await imageFile.readAsBytes(); img.Image? image = img.decodeImage(bytes); if (image == null) { - throw Exception('Impossible de décoder l\'image'); + throw Exception('Unable to decode image'); } - // Redimensionner si l'image est trop grande + // Resize if image is too large const maxWidth = 1024; const maxHeight = 1024; @@ -104,50 +172,78 @@ class StorageService { ); } - // Encoder en JPEG avec compression + // Encode as JPEG with compression final compressedBytes = img.encodeJpg(image, quality: 85); _errorService.logInfo('StorageService', - 'Image compressée: ${bytes.length} → ${compressedBytes.length} bytes'); + 'Image compressed: ${bytes.length} → ${compressedBytes.length} bytes'); return Uint8List.fromList(compressedBytes); } catch (e) { - _errorService.logError('StorageService', 'Erreur compression image: $e'); - // Fallback: retourner l'image originale si la compression échoue + _errorService.logError('StorageService', 'Error compressing image: $e'); + // Fallback: return original image if compression fails return await imageFile.readAsBytes(); } } - /// Valider le fichier image + /// Validates an image file before upload. + /// + /// Checks file existence, size constraints (max 10MB), and file extension + /// to ensure only valid image files are processed for upload. + /// + /// Args: + /// [imageFile]: The image file to validate + /// + /// Throws: + /// Exception if validation fails (file doesn't exist, too large, or invalid extension) void _validateImageFile(File imageFile) { - // Vérifier que le fichier existe + // Check if file exists if (!imageFile.existsSync()) { - throw Exception('Le fichier image n\'existe pas'); + throw Exception('Image file does not exist'); } - // Vérifier la taille du fichier (max 10MB) + // Check file size (max 10MB) const maxSizeBytes = 10 * 1024 * 1024; // 10MB final fileSize = imageFile.lengthSync(); if (fileSize > maxSizeBytes) { - throw Exception('La taille du fichier dépasse 10MB'); + throw Exception('File size exceeds 10MB limit'); } - // Vérifier l'extension + // Check file extension final extension = path.extension(imageFile.path).toLowerCase(); const allowedExtensions = ['.jpg', '.jpeg', '.png', '.webp']; if (!allowedExtensions.contains(extension)) { - throw Exception('Format d\'image non supporté. Utilisez JPG, PNG ou WebP'); + throw Exception('Unsupported image format. Use JPG, PNG or WebP'); } } - /// Générer un nom de fichier unique pour un reçu + /// Generates a unique filename for a receipt image. + /// + /// Creates a filename using timestamp, microseconds, and group ID to ensure + /// uniqueness and prevent naming conflicts when multiple receipts are uploaded. + /// + /// Args: + /// [groupId]: ID of the group this receipt belongs to + /// + /// Returns: + /// A unique filename string for the receipt image String _generateReceiptFileName(String groupId) { final timestamp = DateTime.now().millisecondsSinceEpoch; final random = DateTime.now().microsecond; return 'receipt_${groupId}_${timestamp}_$random.jpg'; } - /// Upload multiple d'images (pour futures fonctionnalités) + /// Uploads multiple images simultaneously (for future features). + /// + /// Processes multiple image files in parallel for batch upload scenarios. + /// Each image is validated, compressed, and uploaded with unique filenames. + /// + /// Args: + /// [groupId]: ID of the group these images belong to + /// [imageFiles]: List of image files to upload + /// + /// Returns: + /// A Future> containing download URLs of uploaded images Future> uploadMultipleImages( String groupId, List imageFiles, diff --git a/lib/services/trip_service.dart b/lib/services/trip_service.dart index 6597ebe..1ee7652 100644 --- a/lib/services/trip_service.dart +++ b/lib/services/trip_service.dart @@ -1,10 +1,42 @@ +/// A service that handles trip-related business logic and data operations. +/// +/// This service provides functionality for: +/// - Trip creation, updating, and deletion +/// - Trip validation and business rule enforcement +/// - Location-based trip suggestions +/// - Trip statistics and analytics +/// - Integration with Firebase Firestore for data persistence +/// +/// The service ensures data integrity by validating trip information +/// before database operations and provides comprehensive error handling +/// and logging through the ErrorService. +/// +/// Example usage: +/// ```dart +/// final tripService = TripService(); +/// +/// // Create a new trip +/// final tripId = await tripService.createTrip(newTrip); +/// +/// // Get trip suggestions for a location +/// final suggestions = await tripService.getTripSuggestions('Paris'); +/// +/// // Calculate trip statistics +/// final stats = await tripService.getTripStatistics(tripId); +/// ``` import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:travel_mate/services/error_service.dart'; import '../models/trip.dart'; +/// Service for managing trip-related operations and business logic. class TripService { + /// Service for error handling and logging final _errorService = ErrorService(); + + /// Firestore instance for database operations final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + /// Collection name for trips in Firestore static const String _tripsCollection = 'trips'; // Charger tous les voyages diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index c835ca6..3692de6 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -3,29 +3,53 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:travel_mate/services/error_service.dart'; import '../blocs/user/user_state.dart'; +/// Service for managing user operations with Firestore and Firebase Auth. +/// +/// This service provides functionality for user management including creating, +/// retrieving, updating, and deleting user data in Firestore. It also handles +/// user authentication state and provides methods for user profile management. class UserService { + /// Error service for logging user operation errors. final _errorService = ErrorService(); + + /// Firestore instance for database operations. final FirebaseFirestore _firestore; + + /// Firebase Auth instance for user authentication. final FirebaseAuth _auth; + + /// Collection name for users in Firestore. static const String _usersCollection = 'users'; + /// Creates a new [UserService] with optional Firestore and Auth instances. + /// + /// If [firestore] or [auth] are not provided, the default instances will be used. UserService({ FirebaseFirestore? firestore, FirebaseAuth? auth, }) : _firestore = firestore ?? FirebaseFirestore.instance, _auth = auth ?? FirebaseAuth.instance; - // Obtenir l'utilisateur connecté actuel + /// Gets the currently authenticated Firebase user. + /// + /// Returns the [User] object if authenticated, null otherwise. User? getCurrentFirebaseUser() { return _auth.currentUser; } - // Obtenir l'ID de l'utilisateur connecté + /// Gets the ID of the currently authenticated user. + /// + /// Returns the user ID string if authenticated, null otherwise. String? getCurrentUserId() { return _auth.currentUser?.uid; } - // Créer un nouvel utilisateur dans Firestore + /// Creates a new user document in Firestore. + /// + /// Takes a [UserModel] object and stores it in the users collection. + /// Returns true if successful, false if an error occurs. + /// + /// [user] - The user model to create in Firestore Future createUser(UserModel user) async { try { await _firestore @@ -34,12 +58,16 @@ class UserService { .set(user.toJson()); return true; } catch (e) { - _errorService.logError('Erreur lors de la création de l\'utilisateur: $e', StackTrace.current); + _errorService.logError('Error creating user: $e', StackTrace.current); return false; } } - // Obtenir un utilisateur par son ID + /// Retrieves a user by their ID from Firestore. + /// + /// Returns a [UserModel] if the user exists, null otherwise. + /// + /// [userId] - The ID of the user to retrieve Future getUserById(String userId) async { try { final doc = await _firestore