Enhance model and service documentation with detailed comments and descriptions
- Updated Group, Trip, User, and other model classes to include comprehensive documentation for better understanding and maintainability. - Improved error handling and logging in services, including AuthService, ErrorService, and StorageService. - Added validation and business logic explanations in ExpenseService and TripService. - Refactored method comments to follow a consistent format across the codebase. - Translated error messages and comments from French to English for consistency.
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/account.dart';
|
||||
import '../../models/group_member.dart';
|
||||
import 'package:travel_mate/models/account.dart';
|
||||
import 'package:travel_mate/models/group_member.dart';
|
||||
|
||||
|
||||
/// Abstract base class for account-related events in the BLoC pattern.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value comparison for events.
|
||||
/// All account events should inherit from this class and implement the [props] getter.
|
||||
abstract class AccountEvent extends Equatable {
|
||||
const AccountEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Event to load accounts associated with a specific user ID.
|
||||
///
|
||||
/// This event is dispatched when the application needs to fetch accounts
|
||||
/// for a given user, typically for displaying user-specific account data.
|
||||
class LoadAccountsByUserId extends AccountEvent {
|
||||
final String userId;
|
||||
|
||||
@@ -18,6 +27,10 @@ class LoadAccountsByUserId extends AccountEvent {
|
||||
List<Object?> get props => [userId];
|
||||
}
|
||||
|
||||
/// Event to load accounts associated with a specific trip.
|
||||
///
|
||||
/// This event is dispatched when the application needs to fetch accounts
|
||||
/// related to a particular trip, such as for trip expense management.
|
||||
class LoadAccountsByTrip extends AccountEvent {
|
||||
final String tripId;
|
||||
|
||||
@@ -27,6 +40,10 @@ class LoadAccountsByTrip extends AccountEvent {
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
/// Event to create a new account.
|
||||
///
|
||||
/// This event is dispatched when a new account needs to be created,
|
||||
/// passing the [Account] object containing the account details.
|
||||
class CreateAccount extends AccountEvent {
|
||||
final Account account;
|
||||
|
||||
@@ -36,6 +53,10 @@ class CreateAccount extends AccountEvent {
|
||||
List<Object?> get props => [account];
|
||||
}
|
||||
|
||||
/// Event to update an existing account.
|
||||
///
|
||||
/// This event is dispatched when an account needs to be modified,
|
||||
/// requiring the [accountId] to identify the account and the updated [Account] object.
|
||||
class UpdateAccount extends AccountEvent {
|
||||
final String accountId;
|
||||
final Account account;
|
||||
@@ -49,6 +70,10 @@ class UpdateAccount extends AccountEvent {
|
||||
List<Object?> get props => [accountId, account];
|
||||
}
|
||||
|
||||
/// Event to create a new account along with its group members.
|
||||
///
|
||||
/// This event is dispatched when creating an account that includes
|
||||
/// a list of [GroupMember] objects, such as for shared trip accounts.
|
||||
class CreateAccountWithMembers extends AccountEvent {
|
||||
final Account account;
|
||||
final List<GroupMember> members;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
/// Business Logic Component for managing authentication state.
|
||||
///
|
||||
/// The [AuthBloc] handles authentication-related events and manages the
|
||||
/// authentication state throughout the application. It coordinates with
|
||||
/// the [AuthRepository] to perform authentication operations and emits
|
||||
/// appropriate states based on the results.
|
||||
///
|
||||
/// Supported authentication methods:
|
||||
/// - Email and password authentication
|
||||
/// - Google Sign-In
|
||||
/// - Apple Sign-In
|
||||
/// - Password reset functionality
|
||||
///
|
||||
/// This bloc handles the following events:
|
||||
/// - [AuthCheckRequested]: Verifies current authentication status
|
||||
/// - [AuthSignInRequested]: Processes email/password sign-in
|
||||
/// - [AuthSignUpRequested]: Processes user registration
|
||||
/// - [AuthGoogleSignInRequested]: Processes Google authentication
|
||||
/// - [AuthAppleSignInRequested]: Processes Apple authentication
|
||||
/// - [AuthSignOutRequested]: Processes user sign-out
|
||||
/// - [AuthPasswordResetRequested]: Processes password reset requests
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../repositories/auth_repository.dart';
|
||||
import 'auth_event.dart';
|
||||
import 'auth_state.dart';
|
||||
|
||||
/// BLoC for managing authentication state and operations.
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
/// Repository for authentication operations.
|
||||
final AuthRepository _authRepository;
|
||||
|
||||
/// Creates an [AuthBloc] with the provided [authRepository].
|
||||
///
|
||||
/// The bloc starts in the [AuthInitial] state and registers event handlers
|
||||
/// for all supported authentication events.
|
||||
AuthBloc({required AuthRepository authRepository})
|
||||
: _authRepository = authRepository,
|
||||
super(AuthInitial()) {
|
||||
@@ -18,6 +45,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
on<AuthPasswordResetRequested>(_onPasswordResetRequested);
|
||||
}
|
||||
|
||||
/// Handles [AuthCheckRequested] events.
|
||||
///
|
||||
/// Checks if a user is currently authenticated and emits the appropriate state.
|
||||
/// If a user is found, attempts to fetch user data from Firestore.
|
||||
Future<void> _onAuthCheckRequested(
|
||||
AuthCheckRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -28,7 +59,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
final currentUser = _authRepository.currentUser;
|
||||
|
||||
if (currentUser != null) {
|
||||
// Récupérer les données utilisateur depuis Firestore
|
||||
// Fetch user data from Firestore
|
||||
final user = await _authRepository.getUserFromFirestore(currentUser.uid);
|
||||
|
||||
if (user != null) {
|
||||
@@ -44,6 +75,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AuthSignInRequested] events.
|
||||
///
|
||||
/// Attempts to sign in a user with the provided email and password.
|
||||
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
|
||||
Future<void> _onSignInRequested(
|
||||
AuthSignInRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -59,13 +94,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
if (user != null) {
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} else {
|
||||
emit(const AuthError(message: 'Email ou mot de passe incorrect'));
|
||||
emit(const AuthError(message: 'Invalid email or password'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AuthSignUpRequested] events.
|
||||
///
|
||||
/// Attempts to create a new user account with the provided information.
|
||||
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
|
||||
Future<void> _onSignUpRequested(
|
||||
AuthSignUpRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -83,13 +122,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
if (user != null) {
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} else {
|
||||
emit(const AuthError(message: 'Erreur lors de l\'inscription'));
|
||||
emit(const AuthError(message: 'Registration failed'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AuthGoogleSignInRequested] events.
|
||||
///
|
||||
/// Attempts to sign in the user using Google authentication.
|
||||
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
|
||||
Future<void> _onGoogleSignInRequested(
|
||||
AuthGoogleSignInRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -102,13 +145,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
if (user != null) {
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} else {
|
||||
emit(const AuthError(message: 'Connexion Google annulée'));
|
||||
emit(const AuthError(message: 'Google sign-in cancelled'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AuthAppleSignInRequested] events.
|
||||
///
|
||||
/// Attempts to sign in the user using Apple authentication.
|
||||
/// Emits [AuthAuthenticated] on success or [AuthError] on failure.
|
||||
Future<void> _onAppleSignInRequested(
|
||||
AuthAppleSignInRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -121,13 +168,16 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
if (user != null) {
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} else {
|
||||
emit(const AuthError(message: 'Connexion Apple annulée'));
|
||||
emit(const AuthError(message: 'Apple sign-in cancelled'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AuthSignOutRequested] events.
|
||||
///
|
||||
/// Signs out the current user and emits [AuthUnauthenticated].
|
||||
Future<void> _onSignOutRequested(
|
||||
AuthSignOutRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
@@ -136,6 +186,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
|
||||
/// Handles [AuthPasswordResetRequested] events.
|
||||
///
|
||||
/// Sends a password reset email to the specified email address.
|
||||
/// Emits [AuthPasswordResetSent] on success or [AuthError] on failure.
|
||||
Future<void> _onPasswordResetRequested(
|
||||
AuthPasswordResetRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Abstract base class for all authentication-related events.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for event comparison.
|
||||
/// All authentication events in the application should inherit from this class.
|
||||
abstract class AuthEvent extends Equatable {
|
||||
/// Creates a new [AuthEvent].
|
||||
const AuthEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to check the current authentication status.
|
||||
///
|
||||
/// This event is typically dispatched when the app starts to determine
|
||||
/// if a user is already authenticated.
|
||||
class AuthCheckRequested extends AuthEvent {}
|
||||
|
||||
/// Event to request user sign-in with email and password.
|
||||
///
|
||||
/// This event contains the user's credentials and triggers the authentication
|
||||
/// process when dispatched to the [AuthBloc].
|
||||
class AuthSignInRequested extends AuthEvent {
|
||||
/// The user's email address.
|
||||
final String email;
|
||||
|
||||
/// The user's password.
|
||||
final String password;
|
||||
|
||||
/// Creates a new [AuthSignInRequested] event.
|
||||
///
|
||||
/// Both [email] and [password] are required parameters.
|
||||
const AuthSignInRequested({
|
||||
required this.email,
|
||||
required this.password,
|
||||
@@ -22,12 +41,26 @@ class AuthSignInRequested extends AuthEvent {
|
||||
List<Object?> get props => [email, password];
|
||||
}
|
||||
|
||||
/// Event to request user registration with email, password, and personal information.
|
||||
///
|
||||
/// This event contains all necessary information to create a new user account
|
||||
/// and triggers the registration process when dispatched to the [AuthBloc].
|
||||
class AuthSignUpRequested extends AuthEvent {
|
||||
/// The user's email address.
|
||||
final String email;
|
||||
|
||||
/// The user's password.
|
||||
final String password;
|
||||
|
||||
/// The user's last name.
|
||||
final String nom;
|
||||
|
||||
/// The user's first name.
|
||||
final String prenom;
|
||||
|
||||
/// Creates a new [AuthSignUpRequested] event.
|
||||
///
|
||||
/// All parameters are required for user registration.
|
||||
const AuthSignUpRequested({
|
||||
required this.email,
|
||||
required this.password,
|
||||
@@ -39,15 +72,33 @@ class AuthSignUpRequested extends AuthEvent {
|
||||
List<Object?> get props => [email, password, nom, prenom];
|
||||
}
|
||||
|
||||
/// Event to request user sign-in using Google authentication.
|
||||
///
|
||||
/// This event triggers the Google sign-in flow when dispatched to the [AuthBloc].
|
||||
class AuthGoogleSignInRequested extends AuthEvent {}
|
||||
|
||||
/// Event to request user sign-in using Apple authentication.
|
||||
///
|
||||
/// This event triggers the Apple sign-in flow when dispatched to the [AuthBloc].
|
||||
class AuthAppleSignInRequested extends AuthEvent {}
|
||||
|
||||
/// Event to request user sign-out.
|
||||
///
|
||||
/// This event triggers the sign-out process and clears the user session
|
||||
/// when dispatched to the [AuthBloc].
|
||||
class AuthSignOutRequested extends AuthEvent {}
|
||||
|
||||
/// Event to request a password reset for a user account.
|
||||
///
|
||||
/// This event triggers the password reset process by sending a reset email
|
||||
/// to the specified email address.
|
||||
class AuthPasswordResetRequested extends AuthEvent {
|
||||
/// The email address to send the password reset link to.
|
||||
final String email;
|
||||
|
||||
/// Creates a new [AuthPasswordResetRequested] event.
|
||||
///
|
||||
/// The [email] parameter is required.
|
||||
const AuthPasswordResetRequested({required this.email});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,40 +1,75 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/user.dart';
|
||||
|
||||
/// Abstract base class for all authentication states.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for state comparison.
|
||||
/// All authentication states in the application should inherit from this class.
|
||||
abstract class AuthState extends Equatable {
|
||||
/// Creates a new [AuthState].
|
||||
const AuthState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Initial state of the authentication bloc.
|
||||
///
|
||||
/// This state represents the initial state before any authentication
|
||||
/// actions have been performed.
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
/// State indicating that an authentication operation is in progress.
|
||||
///
|
||||
/// This state is used to show loading indicators during authentication
|
||||
/// processes like sign-in, sign-up, or sign-out.
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
/// State indicating that a user is successfully authenticated.
|
||||
///
|
||||
/// This state contains the authenticated user's information and is
|
||||
/// used throughout the app to access user data.
|
||||
class AuthAuthenticated extends AuthState {
|
||||
/// The authenticated user.
|
||||
final User user;
|
||||
|
||||
/// Creates an [AuthAuthenticated] state with the given [user].
|
||||
const AuthAuthenticated({required this.user});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [user];
|
||||
}
|
||||
|
||||
/// State indicating that no user is currently authenticated.
|
||||
///
|
||||
/// This state is used when a user is not signed in or has signed out
|
||||
/// of the application.
|
||||
class AuthUnauthenticated extends AuthState {}
|
||||
|
||||
/// State indicating that an authentication error has occurred.
|
||||
///
|
||||
/// This state contains an error message that can be displayed to the user
|
||||
/// when authentication operations fail.
|
||||
class AuthError extends AuthState {
|
||||
/// The error message describing what went wrong.
|
||||
final String message;
|
||||
|
||||
/// Creates an [AuthError] state with the given error [message].
|
||||
const AuthError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// State indicating that a password reset email has been sent.
|
||||
///
|
||||
/// This state is used to confirm to the user that a password reset
|
||||
/// email has been successfully sent to their email address.
|
||||
class AuthPasswordResetSent extends AuthState {
|
||||
/// The email address to which the reset link was sent.
|
||||
final String email;
|
||||
|
||||
/// Creates an [AuthPasswordResetSent] state with the given [email].
|
||||
const AuthPasswordResetSent({required this.email});
|
||||
|
||||
@override
|
||||
|
||||
@@ -44,6 +44,16 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
final BalanceService _balanceService;
|
||||
final ErrorService _errorService;
|
||||
|
||||
/// Constructor for BalanceBloc.
|
||||
///
|
||||
/// Initializes the bloc with required repositories and optional services.
|
||||
/// Sets up event handlers for balance-related operations.
|
||||
///
|
||||
/// Args:
|
||||
/// [balanceRepository]: Repository for balance data operations
|
||||
/// [expenseRepository]: Repository for expense data operations
|
||||
/// [balanceService]: Optional service for balance calculations (auto-created if null)
|
||||
/// [errorService]: Optional service for error handling (auto-created if null)
|
||||
BalanceBloc({
|
||||
required BalanceRepository balanceRepository,
|
||||
required ExpenseRepository expenseRepository,
|
||||
@@ -58,6 +68,15 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
on<MarkSettlementAsCompleted>(_onMarkSettlementAsCompleted);
|
||||
}
|
||||
|
||||
/// Handles [LoadGroupBalances] events.
|
||||
///
|
||||
/// Loads and calculates user balances for a specific group along with
|
||||
/// optimal settlement recommendations. This provides a complete overview
|
||||
/// of who owes money to whom and the most efficient payment strategy.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The LoadGroupBalances event containing the group ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onLoadGroupBalance(
|
||||
LoadGroupBalances event,
|
||||
Emitter<BalanceState> emit,
|
||||
@@ -65,10 +84,10 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
try {
|
||||
emit(BalanceLoading());
|
||||
|
||||
// Calculer les balances du groupe
|
||||
// Calculate group user balances
|
||||
final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
|
||||
|
||||
// Calculer les règlements optimisés
|
||||
// Calculate optimal settlements
|
||||
final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
|
||||
|
||||
emit(GroupBalancesLoaded(
|
||||
@@ -76,25 +95,34 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
settlements: settlements,
|
||||
));
|
||||
} catch (e) {
|
||||
_errorService.logError('BalanceBloc', 'Erreur chargement balance: $e');
|
||||
_errorService.logError('BalanceBloc', 'Error loading balance: $e');
|
||||
emit(BalanceError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [RefreshBalance] events.
|
||||
///
|
||||
/// Refreshes the balance data for a group while trying to maintain the current
|
||||
/// state when possible to provide a smoother user experience. Only shows loading
|
||||
/// state if there's no existing balance data.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The RefreshBalance event containing the group ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onRefreshBalance(
|
||||
RefreshBalance event,
|
||||
Emitter<BalanceState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Garde l'état actuel pendant le refresh si possible
|
||||
// Keep current state during refresh if possible
|
||||
if (state is! GroupBalancesLoaded) {
|
||||
emit(BalanceLoading());
|
||||
}
|
||||
|
||||
// Calculer les balances du groupe
|
||||
// Calculate group user balances
|
||||
final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
|
||||
|
||||
// Calculer les règlements optimisés
|
||||
// Calculate optimal settlements
|
||||
final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
|
||||
|
||||
emit(GroupBalancesLoaded(
|
||||
@@ -102,11 +130,20 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
settlements: settlements,
|
||||
));
|
||||
} catch (e) {
|
||||
_errorService.logError('BalanceBloc', 'Erreur refresh balance: $e');
|
||||
_errorService.logError('BalanceBloc', 'Error refreshing balance: $e');
|
||||
emit(BalanceError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [MarkSettlementAsCompleted] events.
|
||||
///
|
||||
/// Records a settlement transaction between two users, marking that
|
||||
/// a debt has been paid. This updates the balance calculations and
|
||||
/// automatically refreshes the group balance data to reflect the change.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The MarkSettlementAsCompleted event containing settlement details
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onMarkSettlementAsCompleted(
|
||||
MarkSettlementAsCompleted event,
|
||||
Emitter<BalanceState> emit,
|
||||
@@ -119,12 +156,12 @@ class BalanceBloc extends Bloc<BalanceEvent, BalanceState> {
|
||||
amount: event.amount,
|
||||
);
|
||||
|
||||
emit(const BalanceOperationSuccess('Règlement marqué comme effectué'));
|
||||
emit(const BalanceOperationSuccess('Settlement marked as completed'));
|
||||
|
||||
// Recharger la balance après le règlement
|
||||
// Reload balance after settlement
|
||||
add(RefreshBalance(event.groupId));
|
||||
} catch (e) {
|
||||
_errorService.logError('BalanceBloc', 'Erreur mark settlement: $e');
|
||||
_errorService.logError('BalanceBloc', 'Error marking settlement: $e');
|
||||
emit(BalanceError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,68 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Abstract base class for all balance-related events.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for event comparison.
|
||||
/// All balance events in the application should inherit from this class.
|
||||
abstract class BalanceEvent extends Equatable {
|
||||
/// Creates a new [BalanceEvent].
|
||||
const BalanceEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to load balance information for a specific group.
|
||||
///
|
||||
/// This event triggers the loading of user balances and settlements
|
||||
/// for all members of the specified group. It calculates who owes
|
||||
/// money to whom based on shared expenses.
|
||||
class LoadGroupBalances extends BalanceEvent {
|
||||
/// The ID of the group to load balances for.
|
||||
final String groupId;
|
||||
|
||||
/// Creates a [LoadGroupBalances] event for the specified [groupId].
|
||||
const LoadGroupBalances(this.groupId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [groupId];
|
||||
}
|
||||
|
||||
/// Event to refresh balance calculations for a group.
|
||||
///
|
||||
/// This event recalculates all balances and settlements for a group,
|
||||
/// typically called after expenses are added, modified, or deleted.
|
||||
class RefreshBalance extends BalanceEvent {
|
||||
/// The ID of the group to refresh balances for.
|
||||
final String groupId;
|
||||
|
||||
/// Creates a [RefreshBalance] event for the specified [groupId].
|
||||
const RefreshBalance(this.groupId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId];
|
||||
}
|
||||
|
||||
/// Event to mark a settlement as completed between two users.
|
||||
///
|
||||
/// This event is dispatched when one user pays back money they owe
|
||||
/// to another user, updating the balance calculations accordingly.
|
||||
class MarkSettlementAsCompleted extends BalanceEvent {
|
||||
/// The ID of the group where the settlement occurred.
|
||||
final String groupId;
|
||||
|
||||
/// The ID of the user who is paying the debt.
|
||||
final String fromUserId;
|
||||
|
||||
/// The ID of the user who is receiving the payment.
|
||||
final String toUserId;
|
||||
|
||||
/// The amount being settled.
|
||||
final double amount;
|
||||
|
||||
/// Creates a [MarkSettlementAsCompleted] event with the settlement details.
|
||||
///
|
||||
/// All parameters are required to properly record the settlement.
|
||||
const MarkSettlementAsCompleted({
|
||||
required this.groupId,
|
||||
required this.fromUserId,
|
||||
|
||||
@@ -2,21 +2,48 @@ import 'package:equatable/equatable.dart';
|
||||
import '../../models/settlement.dart';
|
||||
import '../../models/user_balance.dart';
|
||||
|
||||
/// Abstract base class for all balance-related states.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for state comparison.
|
||||
/// All balance states in the application should inherit from this class.
|
||||
abstract class BalanceState extends Equatable {
|
||||
/// Creates a new [BalanceState].
|
||||
const BalanceState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Initial state of the balance bloc.
|
||||
///
|
||||
/// This state represents the initial state before any balance
|
||||
/// operations have been performed.
|
||||
class BalanceInitial extends BalanceState {}
|
||||
|
||||
/// State indicating that a balance operation is in progress.
|
||||
///
|
||||
/// This state is used to show loading indicators during balance
|
||||
/// calculations, data loading, or settlement operations.
|
||||
class BalanceLoading extends BalanceState {}
|
||||
|
||||
/// State indicating that group balances have been successfully loaded.
|
||||
///
|
||||
/// This state contains the calculated balances for all group members
|
||||
/// and the list of settlements needed to balance all debts.
|
||||
class GroupBalancesLoaded extends BalanceState {
|
||||
/// List of user balances showing how much each user owes or is owed.
|
||||
///
|
||||
/// Positive balances indicate the user is owed money,
|
||||
/// negative balances indicate the user owes money.
|
||||
final List<UserBalance> balances;
|
||||
|
||||
/// List of settlements required to balance all debts in the group.
|
||||
///
|
||||
/// Each settlement represents a payment that needs to be made
|
||||
/// from one user to another to settle shared expenses.
|
||||
final List<Settlement> settlements;
|
||||
|
||||
/// Creates a [GroupBalancesLoaded] state with the calculated data.
|
||||
const GroupBalancesLoaded({
|
||||
required this.balances,
|
||||
required this.settlements,
|
||||
@@ -26,18 +53,30 @@ class GroupBalancesLoaded extends BalanceState {
|
||||
List<Object> get props => [balances, settlements];
|
||||
}
|
||||
|
||||
/// State indicating that a balance operation has completed successfully.
|
||||
///
|
||||
/// This state is used to show success messages after operations like
|
||||
/// marking settlements as completed or refreshing balance calculations.
|
||||
class BalanceOperationSuccess extends BalanceState {
|
||||
/// Success message to display to the user.
|
||||
final String message;
|
||||
|
||||
/// Creates a [BalanceOperationSuccess] state with the given [message].
|
||||
const BalanceOperationSuccess(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// State indicating that a balance operation has failed.
|
||||
///
|
||||
/// This state contains an error message that can be displayed to the user
|
||||
/// when balance operations fail.
|
||||
class BalanceError extends BalanceState {
|
||||
/// The error message describing what went wrong.
|
||||
final String message;
|
||||
|
||||
/// Creates a [BalanceError] state with the given error [message].
|
||||
const BalanceError(this.message);
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,13 +6,29 @@ import '../../services/error_service.dart';
|
||||
import 'expense_event.dart';
|
||||
import 'expense_state.dart';
|
||||
|
||||
|
||||
/// BLoC for managing expense operations and state.
|
||||
///
|
||||
/// This BLoC handles expense-related operations including loading expenses,
|
||||
/// creating new expenses, updating existing ones, deleting expenses, and
|
||||
/// managing expense splits. It coordinates with the expense repository and
|
||||
/// service to provide business logic and data persistence.
|
||||
class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
|
||||
/// Repository for expense data operations.
|
||||
final ExpenseRepository _expenseRepository;
|
||||
|
||||
/// Service for expense business logic and validation.
|
||||
final ExpenseService _expenseService;
|
||||
|
||||
/// Service for error handling and logging.
|
||||
final ErrorService _errorService;
|
||||
|
||||
/// Subscription to the expenses stream for real-time updates.
|
||||
StreamSubscription? _expensesSubscription;
|
||||
|
||||
/// Creates a new [ExpenseBloc] with required dependencies.
|
||||
///
|
||||
/// [expenseRepository] is required for data operations.
|
||||
/// [expenseService] and [errorService] have default implementations if not provided.
|
||||
ExpenseBloc({
|
||||
required ExpenseRepository expenseRepository,
|
||||
ExpenseService? expenseService,
|
||||
@@ -31,6 +47,10 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
|
||||
on<ArchiveExpense>(_onArchiveExpense);
|
||||
}
|
||||
|
||||
/// Handles [LoadExpensesByGroup] events.
|
||||
///
|
||||
/// Sets up a stream subscription to receive real-time updates for expenses
|
||||
/// in the specified group. Cancels any existing subscription before creating a new one.
|
||||
Future<void> _onLoadExpensesByGroup(
|
||||
LoadExpensesByGroup event,
|
||||
Emitter<ExpenseState> emit,
|
||||
@@ -47,11 +67,19 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
|
||||
onError: (error) => add(ExpensesUpdated([], error: error.toString())),
|
||||
);
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseBloc', 'Erreur chargement expenses: $e');
|
||||
_errorService.logError('ExpenseBloc', 'Error loading expenses: $e');
|
||||
emit(ExpenseError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [ExpensesUpdated] events.
|
||||
///
|
||||
/// Processes real-time updates from the expense stream, either emitting
|
||||
/// the updated expense list or an error state if the stream encountered an error.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The ExpensesUpdated event containing expenses or error information
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onExpensesUpdated(
|
||||
ExpensesUpdated event,
|
||||
Emitter<ExpenseState> emit,
|
||||
@@ -63,71 +91,119 @@ class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [CreateExpense] events.
|
||||
///
|
||||
/// Creates a new expense with validation and optional receipt image upload.
|
||||
/// Uses the expense service to handle business logic and validation,
|
||||
/// including currency conversion and split calculations.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The CreateExpense event containing expense data and optional receipt
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onCreateExpense(
|
||||
CreateExpense event,
|
||||
Emitter<ExpenseState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _expenseService.createExpenseWithValidation(event.expense, event.receiptImage);
|
||||
emit(const ExpenseOperationSuccess('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<void> _onUpdateExpense(
|
||||
UpdateExpense event,
|
||||
Emitter<ExpenseState> 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<void> _onDeleteExpense(
|
||||
DeleteExpense event,
|
||||
Emitter<ExpenseState> 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<void> _onMarkSplitAsPaid(
|
||||
MarkSplitAsPaid event,
|
||||
Emitter<ExpenseState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _expenseRepository.markSplitAsPaid(event.expenseId, event.userId);
|
||||
emit(const ExpenseOperationSuccess('Paiement marqué comme effectué'));
|
||||
emit(const ExpenseOperationSuccess('Payment marked as completed'));
|
||||
} catch (e) {
|
||||
_errorService.logError('ExpenseBloc', 'Erreur mark split paid: $e');
|
||||
_errorService.logError('ExpenseBloc', 'Error marking split as paid: $e');
|
||||
emit(ExpenseError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [ArchiveExpense] events.
|
||||
///
|
||||
/// Archives an expense, moving it out of the active expense list
|
||||
/// while preserving it for historical records and audit purposes.
|
||||
/// Archived expenses are not included in current balance calculations.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The ArchiveExpense event containing the expense ID to archive
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onArchiveExpense(
|
||||
ArchiveExpense event,
|
||||
Emitter<ExpenseState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _expenseRepository.archiveExpense(event.expenseId);
|
||||
emit(const ExpenseOperationSuccess('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<void> close() {
|
||||
_expensesSubscription?.cancel();
|
||||
|
||||
@@ -2,26 +2,45 @@ import 'package:equatable/equatable.dart';
|
||||
import '../../models/expense.dart';
|
||||
import 'dart:io';
|
||||
|
||||
/// Abstract base class for all expense-related events.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for event comparison.
|
||||
/// All expense events in the application should inherit from this class.
|
||||
abstract class ExpenseEvent extends Equatable {
|
||||
/// Creates a new [ExpenseEvent].
|
||||
const ExpenseEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to load all expenses for a specific group.
|
||||
///
|
||||
/// This event triggers the loading of all expenses associated with
|
||||
/// the specified group, setting up a stream to receive real-time updates.
|
||||
class LoadExpensesByGroup extends ExpenseEvent {
|
||||
/// The ID of the group to load expenses for.
|
||||
final String groupId;
|
||||
|
||||
/// Creates a [LoadExpensesByGroup] event for the specified [groupId].
|
||||
const LoadExpensesByGroup(this.groupId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId];
|
||||
}
|
||||
|
||||
/// Event to create a new expense.
|
||||
///
|
||||
/// This event is dispatched when a user wants to add a new expense
|
||||
/// to a group, optionally including a receipt image.
|
||||
class CreateExpense extends ExpenseEvent {
|
||||
/// The expense data to create.
|
||||
final Expense expense;
|
||||
|
||||
/// Optional receipt image file to upload with the expense.
|
||||
final File? receiptImage;
|
||||
|
||||
/// Creates a [CreateExpense] event with the expense data and optional receipt.
|
||||
const CreateExpense({
|
||||
required this.expense,
|
||||
this.receiptImage,
|
||||
@@ -31,10 +50,18 @@ class CreateExpense extends ExpenseEvent {
|
||||
List<Object?> get props => [expense, receiptImage];
|
||||
}
|
||||
|
||||
/// Event to update an existing expense.
|
||||
///
|
||||
/// This event is dispatched when a user modifies an existing expense,
|
||||
/// optionally changing the receipt image.
|
||||
class UpdateExpense extends ExpenseEvent {
|
||||
/// The updated expense data.
|
||||
final Expense expense;
|
||||
|
||||
/// Optional new receipt image file to replace the existing one.
|
||||
final File? newReceiptImage;
|
||||
|
||||
/// Creates an [UpdateExpense] event with updated expense data.
|
||||
const UpdateExpense({
|
||||
required this.expense,
|
||||
this.newReceiptImage,
|
||||
@@ -44,19 +71,33 @@ class UpdateExpense extends ExpenseEvent {
|
||||
List<Object?> get props => [expense, newReceiptImage];
|
||||
}
|
||||
|
||||
/// Event to delete an expense.
|
||||
///
|
||||
/// This event is dispatched when a user wants to permanently
|
||||
/// remove an expense from the group.
|
||||
class DeleteExpense extends ExpenseEvent {
|
||||
/// The ID of the expense to delete.
|
||||
final String expenseId;
|
||||
|
||||
/// Creates a [DeleteExpense] event for the specified [expenseId].
|
||||
const DeleteExpense(this.expenseId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [expenseId];
|
||||
}
|
||||
|
||||
/// Event to mark a user's split of an expense as paid.
|
||||
///
|
||||
/// This event is used when a user has paid their portion of
|
||||
/// a shared expense to the person who originally paid for it.
|
||||
class MarkSplitAsPaid extends ExpenseEvent {
|
||||
/// The ID of the expense containing the split.
|
||||
final String expenseId;
|
||||
|
||||
/// The ID of the user whose split is being marked as paid.
|
||||
final String userId;
|
||||
|
||||
/// Creates a [MarkSplitAsPaid] event for the specified expense and user.
|
||||
const MarkSplitAsPaid({
|
||||
required this.expenseId,
|
||||
required this.userId,
|
||||
@@ -66,20 +107,33 @@ class MarkSplitAsPaid extends ExpenseEvent {
|
||||
List<Object?> get props => [expenseId, userId];
|
||||
}
|
||||
|
||||
/// Event to archive an expense.
|
||||
///
|
||||
/// This event moves an expense to an archived state, hiding it
|
||||
/// from the main expense list while preserving it for history.
|
||||
class ArchiveExpense extends ExpenseEvent {
|
||||
/// The ID of the expense to archive.
|
||||
final String expenseId;
|
||||
|
||||
/// Creates an [ArchiveExpense] event for the specified [expenseId].
|
||||
const ArchiveExpense(this.expenseId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [expenseId];
|
||||
}
|
||||
|
||||
// Événement privé pour les mises à jour du stream
|
||||
/// Internal event for handling expense stream updates.
|
||||
///
|
||||
/// This is a private event used internally by the bloc to handle
|
||||
/// real-time updates from the Firestore stream.
|
||||
class ExpensesUpdated extends ExpenseEvent {
|
||||
/// The updated list of expenses from the stream.
|
||||
final List<Expense> expenses;
|
||||
|
||||
/// Optional error message if the stream encountered an error.
|
||||
final String? error;
|
||||
|
||||
/// Creates an [ExpensesUpdated] event with the expense list and optional error.
|
||||
const ExpensesUpdated(this.expenses, {this.error});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/expense.dart';
|
||||
|
||||
/// Abstract base class for all expense-related states.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for state comparison.
|
||||
/// All expense states in the application should inherit from this class.
|
||||
abstract class ExpenseState extends Equatable {
|
||||
/// Creates a new [ExpenseState].
|
||||
const ExpenseState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Initial state of the expense bloc.
|
||||
///
|
||||
/// This state represents the initial state before any expense
|
||||
/// operations have been performed.
|
||||
class ExpenseInitial extends ExpenseState {}
|
||||
|
||||
/// State indicating that an expense operation is in progress.
|
||||
///
|
||||
/// This state is used to show loading indicators during expense
|
||||
/// operations like loading, creating, updating, or deleting expenses.
|
||||
class ExpenseLoading extends ExpenseState {}
|
||||
|
||||
/// State indicating that expenses have been successfully loaded.
|
||||
///
|
||||
/// This state contains the list of expenses for a group and
|
||||
/// exchange rates for currency conversion calculations.
|
||||
class ExpensesLoaded extends ExpenseState {
|
||||
/// List of expenses for the current group.
|
||||
final List<Expense> expenses;
|
||||
|
||||
/// Exchange rates for currency conversion.
|
||||
///
|
||||
/// Maps currency codes to their exchange rates relative to EUR.
|
||||
/// Used for converting different currencies to a common base for calculations.
|
||||
final Map<String, double> exchangeRates;
|
||||
|
||||
/// Creates an [ExpensesLoaded] state with expenses and exchange rates.
|
||||
///
|
||||
/// [exchangeRates] defaults to common rates if not provided.
|
||||
const ExpensesLoaded({
|
||||
required this.expenses,
|
||||
this.exchangeRates = const {'EUR': 1.0, 'USD': 0.85, 'GBP': 1.15},
|
||||
@@ -25,18 +51,30 @@ class ExpensesLoaded extends ExpenseState {
|
||||
List<Object?> get props => [expenses, exchangeRates];
|
||||
}
|
||||
|
||||
/// State indicating that an expense operation has completed successfully.
|
||||
///
|
||||
/// This state is used to show success messages after operations like
|
||||
/// creating, updating, deleting, or archiving expenses.
|
||||
class ExpenseOperationSuccess extends ExpenseState {
|
||||
/// Success message to display to the user.
|
||||
final String message;
|
||||
|
||||
/// Creates an [ExpenseOperationSuccess] state with the given [message].
|
||||
const ExpenseOperationSuccess(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// State indicating that an expense operation has failed.
|
||||
///
|
||||
/// This state contains an error message that can be displayed to the user
|
||||
/// when expense operations fail.
|
||||
class ExpenseError extends ExpenseState {
|
||||
/// The error message describing what went wrong.
|
||||
final String message;
|
||||
|
||||
/// Creates an [ExpenseError] state with the given error [message].
|
||||
const ExpenseError(this.message);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
/// A BLoC (Business Logic Component) that manages group-related operations.
|
||||
///
|
||||
/// This bloc handles all group operations including creation, updates, member management,
|
||||
/// and loading groups for users or trips. It provides real-time updates through streams
|
||||
/// and manages the relationship between users, groups, and trips.
|
||||
///
|
||||
/// The bloc processes these main events:
|
||||
/// - [LoadGroupsByUserId]: Loads all groups for a specific user with real-time updates
|
||||
/// - [LoadGroupsByTrip]: Loads the group associated with a specific trip
|
||||
/// - [CreateGroup]: Creates a new group without members
|
||||
/// - [CreateGroupWithMembers]: Creates a new group with initial members
|
||||
/// - [AddMemberToGroup]: Adds a member to an existing group
|
||||
/// - [RemoveMemberFromGroup]: Removes a member from a group
|
||||
/// - [UpdateGroup]: Updates group information
|
||||
/// - [DeleteGroup]: Deletes a group
|
||||
///
|
||||
/// Dependencies:
|
||||
/// - [GroupRepository]: Repository for group data operations
|
||||
/// - [ErrorService]: Service for error logging and handling
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final groupBloc = GroupBloc(groupRepository);
|
||||
///
|
||||
/// // Load groups for a user
|
||||
/// groupBloc.add(LoadGroupsByUserId('userId123'));
|
||||
///
|
||||
/// // Create a new group with members
|
||||
/// groupBloc.add(CreateGroupWithMembers(
|
||||
/// group: newGroup,
|
||||
/// members: [member1, member2],
|
||||
/// ));
|
||||
/// ```
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:travel_mate/services/error_service.dart';
|
||||
@@ -6,11 +39,24 @@ import 'group_state.dart';
|
||||
import '../../repositories/group_repository.dart';
|
||||
import '../../models/group.dart';
|
||||
|
||||
/// BLoC that manages group-related operations and state.
|
||||
class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
/// Repository for group data operations
|
||||
final GroupRepository _repository;
|
||||
|
||||
/// Subscription to group stream for real-time updates
|
||||
StreamSubscription? _groupsSubscription;
|
||||
|
||||
/// Service for error handling and logging
|
||||
final _errorService = ErrorService();
|
||||
|
||||
/// Constructor for GroupBloc.
|
||||
///
|
||||
/// Initializes the bloc with the group repository and sets up event handlers
|
||||
/// for all group-related operations.
|
||||
///
|
||||
/// Args:
|
||||
/// [_repository]: Repository for group data operations
|
||||
GroupBloc(this._repository) : super(GroupInitial()) {
|
||||
on<LoadGroupsByUserId>(_onLoadGroupsByUserId);
|
||||
on<_GroupsUpdated>(_onGroupsUpdated);
|
||||
@@ -23,6 +69,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
on<DeleteGroup>(_onDeleteGroup);
|
||||
}
|
||||
|
||||
/// Handles [LoadGroupsByUserId] events.
|
||||
///
|
||||
/// Loads all groups for a specific user with real-time updates via stream subscription.
|
||||
/// Cancels any existing subscription before creating a new one to prevent memory leaks.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The LoadGroupsByUserId event containing the user ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onLoadGroupsByUserId(
|
||||
LoadGroupsByUserId event,
|
||||
Emitter<GroupState> emit,
|
||||
@@ -44,6 +98,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [_GroupsUpdated] events.
|
||||
///
|
||||
/// Processes real-time updates from the group stream, either emitting
|
||||
/// the updated group list or an error state if the stream encountered an error.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The _GroupsUpdated event containing groups or error information
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onGroupsUpdated(
|
||||
_GroupsUpdated event,
|
||||
Emitter<GroupState> emit,
|
||||
@@ -56,6 +118,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [LoadGroupsByTrip] events.
|
||||
///
|
||||
/// Loads the group associated with a specific trip. Since each trip typically
|
||||
/// has one primary group, this returns a single group or an empty list.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The LoadGroupsByTrip event containing the trip ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onLoadGroupsByTrip(
|
||||
LoadGroupsByTrip event,
|
||||
Emitter<GroupState> emit,
|
||||
@@ -73,6 +143,14 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [CreateGroup] events.
|
||||
///
|
||||
/// Creates a new group without any initial members. The group creator
|
||||
/// can add members later using AddMemberToGroup events.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The CreateGroup event containing the group data
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onCreateGroup(
|
||||
CreateGroup event,
|
||||
Emitter<GroupState> emit,
|
||||
@@ -84,12 +162,21 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
members: [],
|
||||
);
|
||||
emit(GroupCreated(groupId: groupId));
|
||||
emit(const GroupOperationSuccess('Groupe créé avec succès'));
|
||||
emit(const GroupOperationSuccess('Group created successfully'));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de la création: $e'));
|
||||
emit(GroupError('Error during creation: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [CreateGroupWithMembers] events.
|
||||
///
|
||||
/// Creates a new group with an initial set of members. This is useful
|
||||
/// for setting up complete groups in one operation, such as when
|
||||
/// planning a trip with known participants.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The CreateGroupWithMembers event containing group data and member list
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onCreateGroupWithMembers(
|
||||
CreateGroupWithMembers event,
|
||||
Emitter<GroupState> emit,
|
||||
@@ -102,58 +189,94 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
);
|
||||
emit(GroupCreated(groupId: groupId));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de la création: $e'));
|
||||
emit(GroupError('Error during creation: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [AddMemberToGroup] events.
|
||||
///
|
||||
/// Adds a new member to an existing group. The member will be able to
|
||||
/// participate in group expenses and access group features.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The AddMemberToGroup event containing group ID and member data
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onAddMemberToGroup(
|
||||
AddMemberToGroup event,
|
||||
Emitter<GroupState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.addMember(event.groupId, event.member);
|
||||
emit(const GroupOperationSuccess('Membre ajouté'));
|
||||
emit(const GroupOperationSuccess('Member added'));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de l\'ajout: $e'));
|
||||
emit(GroupError('Error during addition: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [RemoveMemberFromGroup] events.
|
||||
///
|
||||
/// Removes a member from a group. This will affect expense calculations
|
||||
/// and the member will no longer have access to group features.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The RemoveMemberFromGroup event containing group ID and user ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onRemoveMemberFromGroup(
|
||||
RemoveMemberFromGroup event,
|
||||
Emitter<GroupState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.removeMember(event.groupId, event.userId);
|
||||
emit(const GroupOperationSuccess('Membre supprimé'));
|
||||
emit(const GroupOperationSuccess('Member removed'));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de la suppression: $e'));
|
||||
emit(GroupError('Error during removal: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [UpdateGroup] events.
|
||||
///
|
||||
/// Updates group information such as name, description, or settings.
|
||||
/// Member lists are managed through separate add/remove member events.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The UpdateGroup event containing group ID and updated group data
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onUpdateGroup(
|
||||
UpdateGroup event,
|
||||
Emitter<GroupState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.updateGroup(event.groupId, event.group);
|
||||
emit(const GroupOperationSuccess('Groupe mis à jour'));
|
||||
emit(const GroupOperationSuccess('Group updated'));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de la mise à jour: $e'));
|
||||
emit(GroupError('Error during update: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [DeleteGroup] events.
|
||||
///
|
||||
/// Permanently deletes a group and all associated data. This action
|
||||
/// cannot be undone and will affect all group members and expenses.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The DeleteGroup event containing the trip ID to delete
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onDeleteGroup(
|
||||
DeleteGroup event,
|
||||
Emitter<GroupState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.deleteGroup(event.tripId);
|
||||
emit(const GroupOperationSuccess('Groupe supprimé'));
|
||||
emit(const GroupOperationSuccess('Group deleted'));
|
||||
} catch (e) {
|
||||
emit(GroupError('Erreur lors de la suppression: $e'));
|
||||
emit(GroupError('Error during deletion: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up resources when the bloc is closed.
|
||||
///
|
||||
/// Cancels the group stream subscription to prevent memory leaks
|
||||
/// and ensure proper disposal of resources.
|
||||
@override
|
||||
Future<void> close() {
|
||||
_groupsSubscription?.cancel();
|
||||
@@ -161,10 +284,22 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Private event for handling real-time group updates from streams.
|
||||
///
|
||||
/// This internal event is used to process updates from the group stream
|
||||
/// subscription and emit appropriate states based on the received data.
|
||||
class _GroupsUpdated extends GroupEvent {
|
||||
/// List of groups received from the stream
|
||||
final List<Group> groups;
|
||||
|
||||
/// Error message if the stream encountered an error
|
||||
final String? error;
|
||||
|
||||
/// Creates a _GroupsUpdated event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groups]: List of groups from the stream update
|
||||
/// [error]: Optional error message if stream failed
|
||||
const _GroupsUpdated(this.groups, {this.error});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
/// Events for group-related operations in the GroupBloc.
|
||||
///
|
||||
/// This file defines all possible events that can be dispatched to the GroupBloc
|
||||
/// to trigger group-related state changes and operations such as loading groups,
|
||||
/// creating groups, managing members, and performing CRUD operations.
|
||||
///
|
||||
/// Event Categories:
|
||||
/// - **Loading Events**: LoadGroupsByUserId, LoadGroupsByTrip
|
||||
/// - **Creation Events**: CreateGroup, CreateGroupWithMembers
|
||||
/// - **Member Management**: AddMemberToGroup, RemoveMemberFromGroup
|
||||
/// - **Modification Events**: UpdateGroup, DeleteGroup
|
||||
///
|
||||
/// All events extend [GroupEvent] and implement [Equatable] for proper
|
||||
/// equality comparison in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/group.dart';
|
||||
import '../../models/group_member.dart';
|
||||
|
||||
/// Base class for all group-related events.
|
||||
///
|
||||
/// All group events must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class GroupEvent extends Equatable {
|
||||
const GroupEvent();
|
||||
|
||||
@@ -9,41 +27,96 @@ abstract class GroupEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// NOUVEAU : Charger les groupes par userId
|
||||
/// Event to load all groups associated with a specific user.
|
||||
///
|
||||
/// This is the primary method for loading groups, as it retrieves all groups
|
||||
/// where the user is a member, providing a comprehensive view of the user's
|
||||
/// group memberships across different trips and activities.
|
||||
///
|
||||
/// Args:
|
||||
/// [userId]: The unique identifier of the user whose groups should be loaded
|
||||
class LoadGroupsByUserId extends GroupEvent {
|
||||
/// The unique identifier of the user
|
||||
final String userId;
|
||||
|
||||
/// Creates a LoadGroupsByUserId event.
|
||||
///
|
||||
/// Args:
|
||||
/// [userId]: The user ID to load groups for
|
||||
const LoadGroupsByUserId(this.userId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId];
|
||||
}
|
||||
|
||||
// Charger les groupes d'un voyage (conservé pour compatibilité)
|
||||
// Load groups for a trip (maintained for compatibility)
|
||||
/// Event to load groups associated with a specific trip.
|
||||
///
|
||||
/// This event is maintained for backward compatibility and specific use cases
|
||||
/// where you need to load groups within the context of a particular trip.
|
||||
/// Most trips have one primary group, but some may have multiple sub-groups.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The unique identifier of the trip whose groups should be loaded
|
||||
class LoadGroupsByTrip extends GroupEvent {
|
||||
/// The unique identifier of the trip
|
||||
final String tripId;
|
||||
|
||||
/// Creates a LoadGroupsByTrip event.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The trip ID to load groups for
|
||||
const LoadGroupsByTrip(this.tripId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
// Créer un groupe simple
|
||||
// Create a simple group
|
||||
/// Event to create a new group without any initial members.
|
||||
///
|
||||
/// This creates a basic group structure that can be populated with members
|
||||
/// later using AddMemberToGroup events. Useful when setting up a group
|
||||
/// before knowing all participants.
|
||||
///
|
||||
/// Args:
|
||||
/// [group]: The group object containing basic information (name, description, etc.)
|
||||
class CreateGroup extends GroupEvent {
|
||||
/// The group to be created
|
||||
final Group group;
|
||||
|
||||
/// Creates a CreateGroup event.
|
||||
///
|
||||
/// Args:
|
||||
/// [group]: The group object to create
|
||||
const CreateGroup(this.group);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [group];
|
||||
}
|
||||
|
||||
// Créer un groupe avec ses membres
|
||||
// Create a group with its members
|
||||
/// Event to create a new group with an initial set of members.
|
||||
///
|
||||
/// This is the preferred method for group creation when you know all or most
|
||||
/// of the participants upfront. It creates the group and adds all specified
|
||||
/// members in a single operation, ensuring data consistency.
|
||||
///
|
||||
/// Args:
|
||||
/// [group]: The group object containing basic information
|
||||
/// [members]: List of initial members to add to the group
|
||||
class CreateGroupWithMembers extends GroupEvent {
|
||||
/// The group to be created
|
||||
final Group group;
|
||||
|
||||
/// Initial members to add to the group
|
||||
final List<GroupMember> members;
|
||||
|
||||
/// Creates a CreateGroupWithMembers event.
|
||||
///
|
||||
/// Args:
|
||||
/// [group]: The group object to create
|
||||
/// [members]: List of initial group members
|
||||
const CreateGroupWithMembers({
|
||||
required this.group,
|
||||
required this.members,
|
||||
@@ -53,43 +126,107 @@ class CreateGroupWithMembers extends GroupEvent {
|
||||
List<Object?> get props => [group, members];
|
||||
}
|
||||
|
||||
// Ajouter un membre
|
||||
// Add a member
|
||||
/// Event to add a new member to an existing group.
|
||||
///
|
||||
/// This allows for dynamic group expansion by adding users to groups after
|
||||
/// creation. The new member will gain access to group features like expenses,
|
||||
/// messages, and shared resources.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the target group
|
||||
/// [member]: The group member object containing user information and role
|
||||
class AddMemberToGroup extends GroupEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The member to add to the group
|
||||
final GroupMember member;
|
||||
|
||||
/// Creates an AddMemberToGroup event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID to add the member to
|
||||
/// [member]: The group member to add
|
||||
const AddMemberToGroup(this.groupId, this.member);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId, member];
|
||||
}
|
||||
|
||||
// Supprimer un membre
|
||||
// Remove a member
|
||||
/// Event to remove a member from a group.
|
||||
///
|
||||
/// This removes a user from the group, revoking their access to group features.
|
||||
/// The removal will affect expense calculations and the user will no longer
|
||||
/// receive group-related notifications or updates.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group
|
||||
/// [userId]: The unique identifier of the user to remove
|
||||
class RemoveMemberFromGroup extends GroupEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The unique identifier of the user to remove
|
||||
final String userId;
|
||||
|
||||
/// Creates a RemoveMemberFromGroup event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID to remove the member from
|
||||
/// [userId]: The user ID to remove
|
||||
const RemoveMemberFromGroup(this.groupId, this.userId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId, userId];
|
||||
}
|
||||
|
||||
// Mettre à jour un groupe
|
||||
// Update a group
|
||||
/// Event to update an existing group's information.
|
||||
///
|
||||
/// This allows modification of group properties such as name, description,
|
||||
/// settings, or other metadata. Member management is handled through
|
||||
/// separate add/remove member events.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group to update
|
||||
/// [group]: The updated group object with new information
|
||||
class UpdateGroup extends GroupEvent {
|
||||
/// The unique identifier of the group to update
|
||||
final String groupId;
|
||||
|
||||
/// The updated group object
|
||||
final Group group;
|
||||
|
||||
/// Creates an UpdateGroup event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID to update
|
||||
/// [group]: The updated group object
|
||||
const UpdateGroup(this.groupId, this.group);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId, group];
|
||||
}
|
||||
|
||||
// Supprimer un groupe
|
||||
// Delete a group
|
||||
/// Event to permanently delete a group.
|
||||
///
|
||||
/// This is a destructive operation that removes the group and all associated
|
||||
/// data. This action cannot be undone and will affect all group members.
|
||||
/// Consider archiving instead of deleting for historical records.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The unique identifier of the trip whose group should be deleted
|
||||
class DeleteGroup extends GroupEvent {
|
||||
/// The unique identifier of the trip (used to identify the group)
|
||||
final String tripId;
|
||||
|
||||
/// Creates a DeleteGroup event.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The trip ID whose group should be deleted
|
||||
const DeleteGroup(this.tripId);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/// States for group-related operations in the GroupBloc.
|
||||
///
|
||||
/// This file defines all possible states that the GroupBloc can emit in response
|
||||
/// to group-related events. The states represent different phases of group
|
||||
/// operations including loading, success, error, and data states.
|
||||
///
|
||||
/// State Categories:
|
||||
/// - **Initial State**: GroupInitial
|
||||
/// - **Loading States**: GroupLoading
|
||||
/// - **Success States**: GroupsLoaded, GroupLoaded, GroupCreated, GroupOperationSuccess
|
||||
/// - **Error States**: GroupError
|
||||
///
|
||||
/// All states extend [GroupState] and implement [Equatable] for proper
|
||||
/// equality comparison and state change detection in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/group.dart';
|
||||
|
||||
/// Base class for all group-related states.
|
||||
///
|
||||
/// All group states must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class GroupState extends Equatable {
|
||||
const GroupState();
|
||||
|
||||
@@ -8,54 +26,132 @@ abstract class GroupState extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// État initial
|
||||
// Initial state
|
||||
/// The initial state of the GroupBloc before any operations are performed.
|
||||
///
|
||||
/// This is the default state when the bloc is first created and represents
|
||||
/// a clean slate with no group data loaded or operations in progress.
|
||||
class GroupInitial extends GroupState {}
|
||||
|
||||
// Chargement
|
||||
// Loading
|
||||
/// State indicating that a group operation is currently in progress.
|
||||
///
|
||||
/// This state is emitted when the bloc is performing operations like
|
||||
/// loading groups, creating groups, or updating group information.
|
||||
/// UI components can use this state to show loading indicators.
|
||||
class GroupLoading extends GroupState {}
|
||||
|
||||
// Groupes chargés
|
||||
// Groups loaded
|
||||
/// State containing a list of successfully loaded groups.
|
||||
///
|
||||
/// This state is emitted when groups have been successfully retrieved
|
||||
/// from the repository, either through initial loading or real-time updates.
|
||||
/// The UI can use this data to display the list of available groups.
|
||||
///
|
||||
/// Properties:
|
||||
/// [groups]: List of Group objects that were loaded
|
||||
class GroupsLoaded extends GroupState {
|
||||
/// The list of loaded groups
|
||||
final List<Group> groups;
|
||||
|
||||
/// Creates a GroupsLoaded state.
|
||||
///
|
||||
/// Args:
|
||||
/// [groups]: The list of groups to include in this state
|
||||
const GroupsLoaded(this.groups);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groups];
|
||||
}
|
||||
|
||||
/// Alternative state for loaded groups (used in some contexts).
|
||||
///
|
||||
/// This state serves a similar purpose to GroupsLoaded but may be used
|
||||
/// in different contexts or for backward compatibility.
|
||||
///
|
||||
/// Properties:
|
||||
/// [groups]: List of Group objects that were loaded
|
||||
class GroupLoaded extends GroupState {
|
||||
/// The list of loaded groups
|
||||
final List<Group> groups;
|
||||
|
||||
/// Creates a GroupLoaded state.
|
||||
///
|
||||
/// Args:
|
||||
/// [groups]: The list of groups to include in this state
|
||||
const GroupLoaded(this.groups);
|
||||
}
|
||||
|
||||
/// State indicating successful group creation.
|
||||
///
|
||||
/// This state is emitted when a new group has been successfully created.
|
||||
/// It contains the ID of the newly created group and an optional success message
|
||||
/// that can be displayed to the user.
|
||||
///
|
||||
/// Properties:
|
||||
/// [groupId]: The unique identifier of the newly created group
|
||||
/// [message]: A success message to display to the user
|
||||
class GroupCreated extends GroupState {
|
||||
/// The unique identifier of the newly created group
|
||||
final String groupId;
|
||||
|
||||
/// Success message for the user
|
||||
final String message;
|
||||
|
||||
/// Creates a GroupCreated state.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The ID of the newly created group
|
||||
/// [message]: Optional success message (defaults to "Group created successfully")
|
||||
const GroupCreated({
|
||||
required this.groupId,
|
||||
this.message = 'Groupe créé avec succès',
|
||||
this.message = 'Group created successfully',
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId, message];
|
||||
}
|
||||
|
||||
// Succès d'une opération
|
||||
// Operation success
|
||||
/// State indicating successful completion of a group operation.
|
||||
///
|
||||
/// This state is emitted when operations like updating, deleting, or
|
||||
/// member management have completed successfully. It contains a message
|
||||
/// that can be displayed to inform the user of the successful operation.
|
||||
///
|
||||
/// Properties:
|
||||
/// [message]: A success message describing the completed operation
|
||||
class GroupOperationSuccess extends GroupState {
|
||||
/// The success message to display to the user
|
||||
final String message;
|
||||
|
||||
/// Creates a GroupOperationSuccess state.
|
||||
///
|
||||
/// Args:
|
||||
/// [message]: The success message to display
|
||||
const GroupOperationSuccess(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
// Erreur
|
||||
// Error
|
||||
/// State indicating that a group operation has failed.
|
||||
///
|
||||
/// This state is emitted when any group operation encounters an error.
|
||||
/// It contains an error message that can be displayed to the user to
|
||||
/// explain what went wrong.
|
||||
///
|
||||
/// Properties:
|
||||
/// [message]: An error message describing what went wrong
|
||||
class GroupError extends GroupState {
|
||||
/// The error message to display to the user
|
||||
final String message;
|
||||
|
||||
/// Creates a GroupError state.
|
||||
///
|
||||
/// Args:
|
||||
/// [message]: The error message to display
|
||||
const GroupError(this.message);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
/// A BLoC (Business Logic Component) that manages message-related operations for group chats.
|
||||
///
|
||||
/// This bloc handles all messaging functionality including sending, updating, deleting messages,
|
||||
/// and managing reactions. It provides real-time updates through streams and manages the
|
||||
/// relationship between users and group conversations.
|
||||
///
|
||||
/// The bloc processes these main events:
|
||||
/// - [LoadMessages]: Loads messages for a group with real-time updates
|
||||
/// - [SendMessage]: Sends a new message to a group chat
|
||||
/// - [DeleteMessage]: Deletes a message from the chat
|
||||
/// - [UpdateMessage]: Updates/edits an existing message
|
||||
/// - [ReactToMessage]: Adds an emoji reaction to a message
|
||||
/// - [RemoveReaction]: Removes a user's reaction from a message
|
||||
///
|
||||
/// Dependencies:
|
||||
/// - [MessageService]: Service for message operations and business logic
|
||||
/// - [MessageRepository]: Repository for message data operations
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final messageBloc = MessageBloc();
|
||||
///
|
||||
/// // Load messages for a group
|
||||
/// messageBloc.add(LoadMessages('groupId123'));
|
||||
///
|
||||
/// // Send a message
|
||||
/// messageBloc.add(SendMessage(
|
||||
/// groupId: 'groupId123',
|
||||
/// text: 'Hello everyone!',
|
||||
/// senderId: 'userId123',
|
||||
/// senderName: 'John Doe',
|
||||
/// ));
|
||||
///
|
||||
/// // React to a message
|
||||
/// messageBloc.add(ReactToMessage(
|
||||
/// groupId: 'groupId123',
|
||||
/// messageId: 'msgId456',
|
||||
/// userId: 'userId123',
|
||||
/// reaction: '👍',
|
||||
/// ));
|
||||
/// ```
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/message.dart';
|
||||
@@ -6,10 +47,22 @@ import '../../repositories/message_repository.dart';
|
||||
import 'message_event.dart';
|
||||
import 'message_state.dart';
|
||||
|
||||
/// BLoC that manages message-related operations and real-time chat state.
|
||||
class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
/// Service for message operations and business logic
|
||||
final MessageService _messageService;
|
||||
|
||||
/// Subscription to message stream for real-time updates
|
||||
StreamSubscription<List<Message>>? _messagesSubscription;
|
||||
|
||||
/// Constructor for MessageBloc.
|
||||
///
|
||||
/// Initializes the bloc with an optional message service. If no service is provided,
|
||||
/// creates a default MessageService with MessageRepository. Sets up event handlers
|
||||
/// for all message-related operations.
|
||||
///
|
||||
/// Args:
|
||||
/// [messageService]: Optional service for message operations (auto-created if null)
|
||||
MessageBloc({MessageService? messageService})
|
||||
: _messageService = messageService ?? MessageService(
|
||||
messageRepository: MessageRepository(),
|
||||
@@ -24,6 +77,14 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
on<_MessagesUpdated>(_onMessagesUpdated);
|
||||
}
|
||||
|
||||
/// Handles [LoadMessages] events.
|
||||
///
|
||||
/// Loads messages for a specific group with real-time updates via stream subscription.
|
||||
/// Cancels any existing subscription before creating a new one to prevent memory leaks.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The LoadMessages event containing the group ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onLoadMessages(
|
||||
LoadMessages event,
|
||||
Emitter<MessageState> emit,
|
||||
@@ -39,11 +100,19 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
add(_MessagesUpdated(messages: messages, groupId: event.groupId));
|
||||
},
|
||||
onError: (error) {
|
||||
add(_MessagesError('Erreur lors du chargement des messages: $error'));
|
||||
add(_MessagesError('Error loading messages: $error'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles [_MessagesUpdated] events.
|
||||
///
|
||||
/// Processes real-time updates from the message stream, emitting the
|
||||
/// updated message list with the associated group ID.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The _MessagesUpdated event containing messages and group ID
|
||||
/// [emit]: State emitter function
|
||||
void _onMessagesUpdated(
|
||||
_MessagesUpdated event,
|
||||
Emitter<MessageState> emit,
|
||||
@@ -51,12 +120,20 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
emit(MessagesLoaded(messages: event.messages, groupId: event.groupId));
|
||||
}
|
||||
|
||||
/// Handles [SendMessage] events.
|
||||
///
|
||||
/// Sends a new message to a group chat. The stream subscription will
|
||||
/// automatically update the UI with the new message, so no state is emitted here.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The SendMessage event containing message details
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onSendMessage(
|
||||
SendMessage event,
|
||||
Emitter<MessageState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Juste effectuer l'action, le stream mettra à jour
|
||||
// Just perform the action, the stream will update
|
||||
await _messageService.sendMessage(
|
||||
groupId: event.groupId,
|
||||
text: event.text,
|
||||
@@ -64,50 +141,74 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
senderName: event.senderName,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(MessageError('Erreur lors de l\'envoi du message: $e'));
|
||||
emit(MessageError('Error sending message: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [DeleteMessage] events.
|
||||
///
|
||||
/// Deletes a message from the group chat. The Firestore stream will
|
||||
/// automatically update the UI, so no state is emitted here unless there's an error.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The DeleteMessage event containing group ID and message ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onDeleteMessage(
|
||||
DeleteMessage event,
|
||||
Emitter<MessageState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Ne pas émettre d'état, juste effectuer l'action
|
||||
// Le stream Firestore mettra à jour automatiquement
|
||||
// Don't emit state, just perform the action
|
||||
// The Firestore stream will update automatically
|
||||
await _messageService.deleteMessage(
|
||||
groupId: event.groupId,
|
||||
messageId: event.messageId,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(MessageError('Erreur lors de la suppression du message: $e'));
|
||||
emit(MessageError('Error deleting message: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [UpdateMessage] events.
|
||||
///
|
||||
/// Updates/edits an existing message in the group chat. The Firestore stream will
|
||||
/// automatically update the UI with the edited message, so no state is emitted here.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The UpdateMessage event containing message ID and new text
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onUpdateMessage(
|
||||
UpdateMessage event,
|
||||
Emitter<MessageState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Ne pas émettre d'état, juste effectuer l'action
|
||||
// Le stream Firestore mettra à jour automatiquement
|
||||
// Don't emit state, just perform the action
|
||||
// The Firestore stream will update automatically
|
||||
await _messageService.updateMessage(
|
||||
groupId: event.groupId,
|
||||
messageId: event.messageId,
|
||||
newText: event.newText,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(MessageError('Erreur lors de la modification du message: $e'));
|
||||
emit(MessageError('Error updating message: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [ReactToMessage] events.
|
||||
///
|
||||
/// Adds an emoji reaction to a message. The Firestore stream will
|
||||
/// automatically update the UI with the new reaction, so no state is emitted here.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The ReactToMessage event containing message ID, user ID, and reaction
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onReactToMessage(
|
||||
ReactToMessage event,
|
||||
Emitter<MessageState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Ne pas émettre d'état, juste effectuer l'action
|
||||
// Le stream Firestore mettra à jour automatiquement
|
||||
// Don't emit state, just perform the action
|
||||
// The Firestore stream will update automatically
|
||||
await _messageService.reactToMessage(
|
||||
groupId: event.groupId,
|
||||
messageId: event.messageId,
|
||||
@@ -115,27 +216,39 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
reaction: event.reaction,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(MessageError('Erreur lors de l\'ajout de la réaction: $e'));
|
||||
emit(MessageError('Error adding reaction: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [RemoveReaction] events.
|
||||
///
|
||||
/// Removes a user's reaction from a message. The Firestore stream will
|
||||
/// automatically update the UI with the removed reaction, so no state is emitted here.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The RemoveReaction event containing message ID and user ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onRemoveReaction(
|
||||
RemoveReaction event,
|
||||
Emitter<MessageState> emit,
|
||||
) async {
|
||||
try {
|
||||
// Ne pas émettre d'état, juste effectuer l'action
|
||||
// Le stream Firestore mettra à jour automatiquement
|
||||
// Don't emit state, just perform the action
|
||||
// The Firestore stream will update automatically
|
||||
await _messageService.removeReaction(
|
||||
groupId: event.groupId,
|
||||
messageId: event.messageId,
|
||||
userId: event.userId,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(MessageError('Erreur lors de la suppression de la réaction: $e'));
|
||||
emit(MessageError('Error removing reaction: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up resources when the bloc is closed.
|
||||
///
|
||||
/// Cancels the message stream subscription to prevent memory leaks
|
||||
/// and ensure proper disposal of resources.
|
||||
@override
|
||||
Future<void> close() {
|
||||
_messagesSubscription?.cancel();
|
||||
@@ -143,11 +256,22 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
|
||||
}
|
||||
}
|
||||
|
||||
// Events internes
|
||||
/// Private event for handling real-time message updates from streams.
|
||||
///
|
||||
/// This internal event is used to process updates from the message stream
|
||||
/// subscription and emit appropriate states based on the received data.
|
||||
class _MessagesUpdated extends MessageEvent {
|
||||
/// List of messages received from the stream
|
||||
final List<Message> messages;
|
||||
|
||||
/// Group ID associated with the messages
|
||||
final String groupId;
|
||||
|
||||
/// Creates a _MessagesUpdated event.
|
||||
///
|
||||
/// Args:
|
||||
/// [messages]: List of messages from the stream update
|
||||
/// [groupId]: ID of the group these messages belong to
|
||||
const _MessagesUpdated({
|
||||
required this.messages,
|
||||
required this.groupId,
|
||||
@@ -157,9 +281,18 @@ class _MessagesUpdated extends MessageEvent {
|
||||
List<Object?> get props => [messages, groupId];
|
||||
}
|
||||
|
||||
/// Private event for handling message stream errors.
|
||||
///
|
||||
/// This internal event is used to process errors from the message stream
|
||||
/// subscription and emit appropriate error states.
|
||||
class _MessagesError extends MessageEvent {
|
||||
/// Error message from the stream
|
||||
final String error;
|
||||
|
||||
/// Creates a _MessagesError event.
|
||||
///
|
||||
/// Args:
|
||||
/// [error]: Error message from the stream failure
|
||||
const _MessagesError(this.error);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
/// Events for message-related operations in the MessageBloc.
|
||||
///
|
||||
/// This file defines all possible events that can be dispatched to the MessageBloc
|
||||
/// to trigger message-related state changes and operations such as loading messages,
|
||||
/// sending messages, managing reactions, and performing CRUD operations on chat messages.
|
||||
///
|
||||
/// Event Categories:
|
||||
/// - **Loading Events**: LoadMessages
|
||||
/// - **Message Operations**: SendMessage, DeleteMessage, UpdateMessage
|
||||
/// - **Reaction Management**: ReactToMessage, RemoveReaction
|
||||
///
|
||||
/// All events support real-time group chat functionality with features like
|
||||
/// message editing, deletion, and emoji reactions.
|
||||
///
|
||||
/// All events extend [MessageEvent] and implement [Equatable] for proper
|
||||
/// equality comparison in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Base class for all message-related events.
|
||||
///
|
||||
/// All message events must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class MessageEvent extends Equatable {
|
||||
const MessageEvent();
|
||||
|
||||
@@ -7,21 +27,59 @@ abstract class MessageEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to load messages for a specific group with real-time updates.
|
||||
///
|
||||
/// This event initializes the message stream for a group, providing real-time
|
||||
/// updates as new messages are sent, edited, or deleted. The stream will
|
||||
/// continue until the bloc is closed or a new LoadMessages event is dispatched.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group whose messages should be loaded
|
||||
class LoadMessages extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// Creates a LoadMessages event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID to load messages for
|
||||
const LoadMessages(this.groupId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId];
|
||||
}
|
||||
|
||||
/// Event to send a new message to a group chat.
|
||||
///
|
||||
/// This event creates and sends a new message to the specified group.
|
||||
/// The message will be immediately visible to all group members through
|
||||
/// the real-time message stream.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the target group
|
||||
/// [text]: The content of the message to send
|
||||
/// [senderId]: The unique identifier of the user sending the message
|
||||
/// [senderName]: The display name of the user sending the message
|
||||
class SendMessage extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The content of the message
|
||||
final String text;
|
||||
|
||||
/// The unique identifier of the sender
|
||||
final String senderId;
|
||||
|
||||
/// The display name of the sender
|
||||
final String senderName;
|
||||
|
||||
/// Creates a SendMessage event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The target group ID
|
||||
/// [text]: The message content
|
||||
/// [senderId]: The sender's user ID
|
||||
/// [senderName]: The sender's display name
|
||||
const SendMessage({
|
||||
required this.groupId,
|
||||
required this.text,
|
||||
@@ -33,10 +91,27 @@ class SendMessage extends MessageEvent {
|
||||
List<Object?> get props => [groupId, text, senderId, senderName];
|
||||
}
|
||||
|
||||
/// Event to delete a message from the group chat.
|
||||
///
|
||||
/// This event permanently removes a message from the chat. The deletion
|
||||
/// will be immediately reflected for all group members through the
|
||||
/// real-time message stream. This action cannot be undone.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group containing the message
|
||||
/// [messageId]: The unique identifier of the message to delete
|
||||
class DeleteMessage extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The unique identifier of the message to delete
|
||||
final String messageId;
|
||||
|
||||
/// Creates a DeleteMessage event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID containing the message
|
||||
/// [messageId]: The message ID to delete
|
||||
const DeleteMessage({
|
||||
required this.groupId,
|
||||
required this.messageId,
|
||||
@@ -46,11 +121,32 @@ class DeleteMessage extends MessageEvent {
|
||||
List<Object?> get props => [groupId, messageId];
|
||||
}
|
||||
|
||||
/// Event to update/edit an existing message in the group chat.
|
||||
///
|
||||
/// This event allows users to modify the content of a previously sent message.
|
||||
/// The updated message will be immediately visible to all group members
|
||||
/// through the real-time message stream, typically with an "edited" indicator.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group containing the message
|
||||
/// [messageId]: The unique identifier of the message to update
|
||||
/// [newText]: The new content for the message
|
||||
class UpdateMessage extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The unique identifier of the message to update
|
||||
final String messageId;
|
||||
|
||||
/// The new content for the message
|
||||
final String newText;
|
||||
|
||||
/// Creates an UpdateMessage event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID containing the message
|
||||
/// [messageId]: The message ID to update
|
||||
/// [newText]: The new message content
|
||||
const UpdateMessage({
|
||||
required this.groupId,
|
||||
required this.messageId,
|
||||
@@ -61,12 +157,37 @@ class UpdateMessage extends MessageEvent {
|
||||
List<Object?> get props => [groupId, messageId, newText];
|
||||
}
|
||||
|
||||
/// Event to add an emoji reaction to a message.
|
||||
///
|
||||
/// This event allows users to react to messages with emojis, providing
|
||||
/// a quick way to express emotions or acknowledgment without sending
|
||||
/// a full message. The reaction will be immediately visible to all group members.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group containing the message
|
||||
/// [messageId]: The unique identifier of the message to react to
|
||||
/// [userId]: The unique identifier of the user adding the reaction
|
||||
/// [reaction]: The emoji or reaction string to add
|
||||
class ReactToMessage extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The unique identifier of the message
|
||||
final String messageId;
|
||||
|
||||
/// The unique identifier of the user adding the reaction
|
||||
final String userId;
|
||||
|
||||
/// The emoji or reaction string
|
||||
final String reaction;
|
||||
|
||||
/// Creates a ReactToMessage event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID containing the message
|
||||
/// [messageId]: The message ID to react to
|
||||
/// [userId]: The user ID adding the reaction
|
||||
/// [reaction]: The emoji/reaction to add
|
||||
const ReactToMessage({
|
||||
required this.groupId,
|
||||
required this.messageId,
|
||||
@@ -78,11 +199,32 @@ class ReactToMessage extends MessageEvent {
|
||||
List<Object?> get props => [groupId, messageId, userId, reaction];
|
||||
}
|
||||
|
||||
/// Event to remove a user's reaction from a message.
|
||||
///
|
||||
/// This event removes a previously added emoji reaction from a message.
|
||||
/// Only the user who added the reaction can remove it. The removal will
|
||||
/// be immediately reflected for all group members through the real-time stream.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The unique identifier of the group containing the message
|
||||
/// [messageId]: The unique identifier of the message with the reaction
|
||||
/// [userId]: The unique identifier of the user removing their reaction
|
||||
class RemoveReaction extends MessageEvent {
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// The unique identifier of the message
|
||||
final String messageId;
|
||||
|
||||
/// The unique identifier of the user removing the reaction
|
||||
final String userId;
|
||||
|
||||
/// Creates a RemoveReaction event.
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: The group ID containing the message
|
||||
/// [messageId]: The message ID with the reaction
|
||||
/// [userId]: The user ID removing the reaction
|
||||
const RemoveReaction({
|
||||
required this.groupId,
|
||||
required this.messageId,
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
/// States for message-related operations in the MessageBloc.
|
||||
///
|
||||
/// This file defines all possible states that the MessageBloc can emit in response
|
||||
/// to message-related events. The states represent different phases of message
|
||||
/// operations including loading, success, and error states for real-time chat functionality.
|
||||
///
|
||||
/// State Categories:
|
||||
/// - **Initial State**: MessageInitial
|
||||
/// - **Loading States**: MessageLoading
|
||||
/// - **Success States**: MessagesLoaded
|
||||
/// - **Error States**: MessageError
|
||||
///
|
||||
/// The states support real-time chat features including message streaming,
|
||||
/// reactions, editing, and deletion within group conversations.
|
||||
///
|
||||
/// All states extend [MessageState] and implement [Equatable] for proper
|
||||
/// equality comparison and state change detection in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/message.dart';
|
||||
|
||||
/// Base class for all message-related states.
|
||||
///
|
||||
/// All message states must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class MessageState extends Equatable {
|
||||
const MessageState();
|
||||
|
||||
@@ -8,14 +29,41 @@ abstract class MessageState extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// The initial state of the MessageBloc before any operations are performed.
|
||||
///
|
||||
/// This is the default state when the bloc is first created and represents
|
||||
/// a clean slate with no message data loaded or operations in progress.
|
||||
class MessageInitial extends MessageState {}
|
||||
|
||||
/// State indicating that a message operation is currently in progress.
|
||||
///
|
||||
/// This state is emitted when the bloc is performing operations like
|
||||
/// loading messages or initializing the real-time message stream.
|
||||
/// UI components can use this state to show loading indicators.
|
||||
class MessageLoading extends MessageState {}
|
||||
|
||||
/// State containing a list of successfully loaded messages for a group.
|
||||
///
|
||||
/// This state is emitted when messages have been successfully retrieved
|
||||
/// from the repository, either through initial loading or real-time updates.
|
||||
/// The state includes both the messages and the group ID for context.
|
||||
/// The UI can use this data to display the chat conversation.
|
||||
///
|
||||
/// Properties:
|
||||
/// [messages]: List of Message objects in the conversation
|
||||
/// [groupId]: The unique identifier of the group these messages belong to
|
||||
class MessagesLoaded extends MessageState {
|
||||
/// The list of messages in the conversation
|
||||
final List<Message> messages;
|
||||
|
||||
/// The unique identifier of the group
|
||||
final String groupId;
|
||||
|
||||
/// Creates a MessagesLoaded state.
|
||||
///
|
||||
/// Args:
|
||||
/// [messages]: The list of messages to include in this state
|
||||
/// [groupId]: The group ID these messages belong to
|
||||
const MessagesLoaded({
|
||||
required this.messages,
|
||||
required this.groupId,
|
||||
@@ -25,9 +73,23 @@ class MessagesLoaded extends MessageState {
|
||||
List<Object?> get props => [messages, groupId];
|
||||
}
|
||||
|
||||
/// State indicating that a message operation has failed.
|
||||
///
|
||||
/// This state is emitted when any message operation encounters an error,
|
||||
/// such as failing to load messages, send a message, or perform other
|
||||
/// message-related operations. It contains an error message that can be
|
||||
/// displayed to the user to explain what went wrong.
|
||||
///
|
||||
/// Properties:
|
||||
/// [message]: An error message describing what went wrong
|
||||
class MessageError extends MessageState {
|
||||
/// The error message to display to the user
|
||||
final String message;
|
||||
|
||||
/// Creates a MessageError state.
|
||||
///
|
||||
/// Args:
|
||||
/// [message]: The error message to display
|
||||
const MessageError(this.message);
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,23 +4,39 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'theme_event.dart';
|
||||
import 'theme_state.dart';
|
||||
|
||||
/// BLoC for managing application theme preferences.
|
||||
///
|
||||
/// This BLoC handles theme-related events and manages the application's
|
||||
/// theme mode (light, dark, or system). It persists theme preferences
|
||||
/// using SharedPreferences for consistency across app sessions.
|
||||
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
|
||||
/// Creates a new [ThemeBloc] with default theme state.
|
||||
///
|
||||
/// Registers event handlers for theme changes and loading saved preferences.
|
||||
ThemeBloc() : super(const ThemeState()) {
|
||||
on<ThemeChanged>(_onThemeChanged);
|
||||
on<ThemeLoadRequested>(_onThemeLoadRequested);
|
||||
}
|
||||
|
||||
/// Handles [ThemeChanged] events.
|
||||
///
|
||||
/// Updates the theme mode and persists the preference to local storage.
|
||||
/// This ensures the theme choice is remembered across app restarts.
|
||||
Future<void> _onThemeChanged(
|
||||
ThemeChanged event,
|
||||
Emitter<ThemeState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(themeMode: event.themeMode));
|
||||
|
||||
// Sauvegarder la préférence
|
||||
// Save the theme preference to persistent storage
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('themeMode', event.themeMode.toString());
|
||||
}
|
||||
|
||||
/// Handles [ThemeLoadRequested] events.
|
||||
///
|
||||
/// Loads the saved theme preference from SharedPreferences and applies it.
|
||||
/// If no preference is saved, the theme remains as system default.
|
||||
Future<void> _onThemeLoadRequested(
|
||||
ThemeLoadRequested event,
|
||||
Emitter<ThemeState> emit,
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Abstract base class for all theme-related events.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for event comparison.
|
||||
/// All theme events in the application should inherit from this class.
|
||||
abstract class ThemeEvent extends Equatable {
|
||||
/// Creates a new [ThemeEvent].
|
||||
const ThemeEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to request a theme change.
|
||||
///
|
||||
/// This event is dispatched when the user wants to change the application theme
|
||||
/// mode (light, dark, or system). The new theme mode is persisted and applied immediately.
|
||||
class ThemeChanged extends ThemeEvent {
|
||||
/// The new theme mode to apply.
|
||||
final ThemeMode themeMode;
|
||||
|
||||
/// Creates a new [ThemeChanged] event with the specified [themeMode].
|
||||
const ThemeChanged({required this.themeMode});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [themeMode];
|
||||
}
|
||||
|
||||
/// Event to request loading of saved theme preferences.
|
||||
///
|
||||
/// This event is typically dispatched when the app starts to restore
|
||||
/// the user's previously selected theme preference.
|
||||
class ThemeLoadRequested extends ThemeEvent {}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
/// A BLoC (Business Logic Component) that manages trip-related operations.
|
||||
///
|
||||
/// This bloc handles all trip operations including creation, updates, deletion,
|
||||
/// and loading trips for users. It provides real-time updates through streams
|
||||
/// and manages the trip lifecycle with proper state transitions.
|
||||
///
|
||||
/// The bloc processes these main events:
|
||||
/// - [LoadTripsByUserId]: Loads all trips for a specific user with real-time updates
|
||||
/// - [TripCreateRequested]: Creates a new trip and reloads the user's trip list
|
||||
/// - [TripUpdateRequested]: Updates an existing trip and refreshes the list
|
||||
/// - [TripDeleteRequested]: Deletes a trip and refreshes the list
|
||||
/// - [ResetTrips]: Resets the trip state and cancels subscriptions
|
||||
///
|
||||
/// Dependencies:
|
||||
/// - [TripRepository]: Repository for trip data operations
|
||||
///
|
||||
/// State Management:
|
||||
/// The bloc maintains the current user ID to enable automatic list refreshing
|
||||
/// after operations like create, update, or delete. This ensures the UI stays
|
||||
/// in sync with the latest data.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final tripBloc = TripBloc(tripRepository);
|
||||
///
|
||||
/// // Load trips for a user
|
||||
/// tripBloc.add(LoadTripsByUserId(userId: 'userId123'));
|
||||
///
|
||||
/// // Create a new trip
|
||||
/// tripBloc.add(TripCreateRequested(trip: newTrip));
|
||||
///
|
||||
/// // Update a trip
|
||||
/// tripBloc.add(TripUpdateRequested(trip: updatedTrip));
|
||||
///
|
||||
/// // Delete a trip
|
||||
/// tripBloc.add(TripDeleteRequested(tripId: 'tripId456'));
|
||||
/// ```
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:travel_mate/models/trip.dart';
|
||||
@@ -5,12 +42,24 @@ import 'trip_event.dart';
|
||||
import 'trip_state.dart';
|
||||
import '../../repositories/trip_repository.dart';
|
||||
|
||||
|
||||
/// BLoC that manages trip-related operations and state.
|
||||
class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
/// Repository for trip data operations
|
||||
final TripRepository _repository;
|
||||
|
||||
/// Subscription to trip stream for real-time updates
|
||||
StreamSubscription? _tripsSubscription;
|
||||
|
||||
/// Current user ID for automatic list refreshing after operations
|
||||
String? _currentUserId;
|
||||
|
||||
/// Constructor for TripBloc.
|
||||
///
|
||||
/// Initializes the bloc with the trip repository and sets up event handlers
|
||||
/// for all trip-related operations.
|
||||
///
|
||||
/// Args:
|
||||
/// [_repository]: Repository for trip data operations
|
||||
TripBloc(this._repository) : super(TripInitial()) {
|
||||
on<LoadTripsByUserId>(_onLoadTripsByUserId);
|
||||
on<TripCreateRequested>(_onTripCreateRequested);
|
||||
@@ -20,6 +69,15 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
on<ResetTrips>(_onResetTrips);
|
||||
}
|
||||
|
||||
/// Handles [LoadTripsByUserId] events.
|
||||
///
|
||||
/// Loads all trips for a specific user with real-time updates via stream subscription.
|
||||
/// Stores the user ID for future automatic refreshing after operations and cancels
|
||||
/// any existing subscription to prevent memory leaks.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The LoadTripsByUserId event containing the user ID
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onLoadTripsByUserId(
|
||||
LoadTripsByUserId event,
|
||||
Emitter<TripState> emit,
|
||||
@@ -39,6 +97,14 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles [_TripsUpdated] events.
|
||||
///
|
||||
/// Processes real-time updates from the trip stream and emits the
|
||||
/// updated trip list to the UI.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The _TripsUpdated event containing the updated trip list
|
||||
/// [emit]: State emitter function
|
||||
void _onTripsUpdated(
|
||||
_TripsUpdated event,
|
||||
Emitter<TripState> emit,
|
||||
@@ -46,6 +112,15 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
emit(TripLoaded(event.trips));
|
||||
}
|
||||
|
||||
/// Handles [TripCreateRequested] events.
|
||||
///
|
||||
/// Creates a new trip and automatically refreshes the user's trip list
|
||||
/// to show the newly created trip. Includes a delay to allow the creation
|
||||
/// to complete before refreshing.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The TripCreateRequested event containing the trip data
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onTripCreateRequested(
|
||||
TripCreateRequested event,
|
||||
Emitter<TripState> emit,
|
||||
@@ -63,27 +138,45 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(TripError('Erreur lors de la création: $e'));
|
||||
emit(TripError('Error during creation: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [TripUpdateRequested] events.
|
||||
///
|
||||
/// Updates an existing trip and automatically refreshes the user's trip list
|
||||
/// to show the updated information. Includes a delay to allow the update
|
||||
/// to complete before refreshing.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The TripUpdateRequested event containing the updated trip data
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onTripUpdateRequested(
|
||||
TripUpdateRequested event,
|
||||
Emitter<TripState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.updateTrip(event.trip.id!, event.trip);
|
||||
emit(const TripOperationSuccess('Voyage mis à jour avec succès'));
|
||||
emit(const TripOperationSuccess('Trip updated successfully'));
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (_currentUserId != null) {
|
||||
add(LoadTripsByUserId(userId: _currentUserId!));
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(TripError('Erreur lors de la mise à jour: $e'));
|
||||
emit(TripError('Error during update: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [TripDeleteRequested] events.
|
||||
///
|
||||
/// Deletes a trip and automatically refreshes the user's trip list
|
||||
/// to remove the deleted trip from the UI. Includes a delay to allow
|
||||
/// the deletion to complete before refreshing.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The TripDeleteRequested event containing the trip ID to delete
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onTripDeleteRequested(
|
||||
TripDeleteRequested event,
|
||||
Emitter<TripState> emit,
|
||||
@@ -91,7 +184,7 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
try {
|
||||
await _repository.deleteTrip(event.tripId);
|
||||
|
||||
emit(const TripOperationSuccess('Voyage supprimé avec succès'));
|
||||
emit(const TripOperationSuccess('Trip deleted successfully'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (_currentUserId != null) {
|
||||
@@ -99,10 +192,19 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(TripError('Erreur lors de la suppression: $e'));
|
||||
emit(TripError('Error during deletion: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [ResetTrips] events.
|
||||
///
|
||||
/// Resets the trip state to initial and cleans up resources.
|
||||
/// Cancels the trip stream subscription and clears the current user ID.
|
||||
/// This is useful for user logout or when switching contexts.
|
||||
///
|
||||
/// Args:
|
||||
/// [event]: The ResetTrips event
|
||||
/// [emit]: State emitter function
|
||||
Future<void> _onResetTrips(
|
||||
ResetTrips event,
|
||||
Emitter<TripState> emit,
|
||||
@@ -112,6 +214,10 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
emit(TripInitial());
|
||||
}
|
||||
|
||||
/// Cleans up resources when the bloc is closed.
|
||||
///
|
||||
/// Cancels the trip stream subscription to prevent memory leaks
|
||||
/// and ensure proper disposal of resources.
|
||||
@override
|
||||
Future<void> close() {
|
||||
_tripsSubscription?.cancel();
|
||||
@@ -119,9 +225,18 @@ class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Private event for handling real-time trip updates from streams.
|
||||
///
|
||||
/// This internal event is used to process updates from the trip stream
|
||||
/// subscription and emit appropriate states based on the received data.
|
||||
class _TripsUpdated extends TripEvent {
|
||||
/// List of trips received from the stream
|
||||
final List<Trip> trips;
|
||||
|
||||
/// Creates a _TripsUpdated event.
|
||||
///
|
||||
/// Args:
|
||||
/// [trips]: List of trips from the stream update
|
||||
const _TripsUpdated(this.trips);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
/// Events for trip-related operations in the TripBloc.
|
||||
///
|
||||
/// This file defines all possible events that can be dispatched to the TripBloc
|
||||
/// to trigger trip-related state changes and operations such as loading trips,
|
||||
/// creating trips, updating trip information, and performing CRUD operations.
|
||||
///
|
||||
/// Event Categories:
|
||||
/// - **Loading Events**: LoadTripsByUserId
|
||||
/// - **CRUD Operations**: TripCreateRequested, TripUpdateRequested, TripDeleteRequested
|
||||
/// - **State Management**: ResetTrips
|
||||
///
|
||||
/// The events support complete trip lifecycle management with automatic
|
||||
/// list refreshing after operations to maintain UI consistency.
|
||||
///
|
||||
/// All events extend [TripEvent] and implement [Equatable] for proper
|
||||
/// equality comparison in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/trip.dart';
|
||||
|
||||
/// Base class for all trip-related events.
|
||||
///
|
||||
/// All trip events must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class TripEvent extends Equatable {
|
||||
const TripEvent();
|
||||
|
||||
@@ -8,40 +28,98 @@ abstract class TripEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to load all trips associated with a specific user.
|
||||
///
|
||||
/// This event retrieves all trips where the user is a participant or organizer,
|
||||
/// providing a comprehensive view of the user's travel activities. The loading
|
||||
/// uses real-time streams to keep the trip list updated automatically.
|
||||
///
|
||||
/// Args:
|
||||
/// [userId]: The unique identifier of the user whose trips should be loaded
|
||||
class LoadTripsByUserId extends TripEvent {
|
||||
/// The unique identifier of the user
|
||||
final String userId;
|
||||
|
||||
/// Creates a LoadTripsByUserId event.
|
||||
///
|
||||
/// Args:
|
||||
/// [userId]: The user ID to load trips for
|
||||
const LoadTripsByUserId({required this.userId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId];
|
||||
}
|
||||
|
||||
/// Event to request creation of a new trip.
|
||||
///
|
||||
/// This event creates a new trip with the provided information and automatically
|
||||
/// refreshes the user's trip list to show the newly created trip. The operation
|
||||
/// includes validation and proper error handling.
|
||||
///
|
||||
/// Args:
|
||||
/// [trip]: The trip object containing all the trip information to create
|
||||
class TripCreateRequested extends TripEvent {
|
||||
/// The trip object to create
|
||||
final Trip trip;
|
||||
|
||||
/// Creates a TripCreateRequested event.
|
||||
///
|
||||
/// Args:
|
||||
/// [trip]: The trip object to create
|
||||
const TripCreateRequested({required this.trip});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [trip];
|
||||
}
|
||||
|
||||
/// Event to request updating an existing trip.
|
||||
///
|
||||
/// This event updates an existing trip with new information and automatically
|
||||
/// refreshes the user's trip list to reflect the changes. The trip must have
|
||||
/// a valid ID for the update operation to succeed.
|
||||
///
|
||||
/// Args:
|
||||
/// [trip]: The trip object with updated information (must include valid ID)
|
||||
class TripUpdateRequested extends TripEvent {
|
||||
/// The trip object with updated information
|
||||
final Trip trip;
|
||||
|
||||
/// Creates a TripUpdateRequested event.
|
||||
///
|
||||
/// Args:
|
||||
/// [trip]: The updated trip object (must have valid ID)
|
||||
const TripUpdateRequested({required this.trip});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [trip];
|
||||
}
|
||||
|
||||
/// Event to reset the trip state and clean up resources.
|
||||
///
|
||||
/// This event resets the TripBloc to its initial state, cancels any active
|
||||
/// stream subscriptions, and clears the current user context. This is typically
|
||||
/// used during user logout or when switching between different user contexts.
|
||||
class ResetTrips extends TripEvent {
|
||||
/// Creates a ResetTrips event.
|
||||
const ResetTrips();
|
||||
}
|
||||
|
||||
/// Event to request deletion of a trip.
|
||||
///
|
||||
/// This event permanently deletes a trip and all associated data including
|
||||
/// groups, expenses, and messages. This is a destructive operation that cannot
|
||||
/// be undone. The user's trip list is automatically refreshed after deletion.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The unique identifier of the trip to delete
|
||||
class TripDeleteRequested extends TripEvent {
|
||||
/// The unique identifier of the trip to delete
|
||||
final String tripId;
|
||||
|
||||
/// Creates a TripDeleteRequested event.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The ID of the trip to delete
|
||||
const TripDeleteRequested({required this.tripId});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
/// States for trip-related operations in the TripBloc.
|
||||
///
|
||||
/// This file defines all possible states that the TripBloc can emit in response
|
||||
/// to trip-related events. The states represent different phases of trip
|
||||
/// operations including loading, success, error, and data states for trip management.
|
||||
///
|
||||
/// State Categories:
|
||||
/// - **Initial State**: TripInitial
|
||||
/// - **Loading States**: TripLoading
|
||||
/// - **Success States**: TripLoaded, TripCreated, TripOperationSuccess
|
||||
/// - **Error States**: TripError
|
||||
///
|
||||
/// The states support complete trip lifecycle management with proper feedback
|
||||
/// for create, read, update, and delete operations.
|
||||
///
|
||||
/// All states extend [TripState] and implement [Equatable] for proper
|
||||
/// equality comparison and state change detection in the BLoC pattern.
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/trip.dart';
|
||||
|
||||
/// Base class for all trip-related states.
|
||||
///
|
||||
/// All trip states must extend this class and implement the [props] getter
|
||||
/// for proper equality comparison in the BLoC pattern.
|
||||
abstract class TripState extends Equatable {
|
||||
const TripState();
|
||||
|
||||
@@ -8,44 +29,111 @@ abstract class TripState extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// The initial state of the TripBloc before any operations are performed.
|
||||
///
|
||||
/// This is the default state when the bloc is first created and represents
|
||||
/// a clean slate with no trip data loaded or operations in progress.
|
||||
class TripInitial extends TripState {}
|
||||
|
||||
/// State indicating that a trip operation is currently in progress.
|
||||
///
|
||||
/// This state is emitted when the bloc is performing operations like
|
||||
/// loading trips, creating trips, updating trip information, or deleting trips.
|
||||
/// UI components can use this state to show loading indicators.
|
||||
class TripLoading extends TripState {}
|
||||
|
||||
/// State containing a list of successfully loaded trips.
|
||||
///
|
||||
/// This state is emitted when trips have been successfully retrieved
|
||||
/// from the repository, either through initial loading or real-time updates.
|
||||
/// The UI can use this data to display the list of available trips for the user.
|
||||
///
|
||||
/// Properties:
|
||||
/// [trips]: List of Trip objects that were loaded
|
||||
class TripLoaded extends TripState {
|
||||
/// The list of loaded trips
|
||||
final List<Trip> trips;
|
||||
|
||||
/// Creates a TripLoaded state.
|
||||
///
|
||||
/// Args:
|
||||
/// [trips]: The list of trips to include in this state
|
||||
const TripLoaded(this.trips);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [trips];
|
||||
}
|
||||
|
||||
/// State indicating successful trip creation.
|
||||
///
|
||||
/// This state is emitted when a new trip has been successfully created.
|
||||
/// It contains the ID of the newly created trip and a success message
|
||||
/// that can be displayed to the user. This state is typically followed
|
||||
/// by automatic reloading of the user's trip list.
|
||||
///
|
||||
/// Properties:
|
||||
/// [tripId]: The unique identifier of the newly created trip
|
||||
/// [message]: A success message to display to the user
|
||||
class TripCreated extends TripState {
|
||||
/// The unique identifier of the newly created trip
|
||||
final String tripId;
|
||||
|
||||
/// Success message for the user
|
||||
final String message;
|
||||
|
||||
/// Creates a TripCreated state.
|
||||
///
|
||||
/// Args:
|
||||
/// [tripId]: The ID of the newly created trip
|
||||
/// [message]: Optional success message (defaults to "Trip created successfully")
|
||||
const TripCreated({
|
||||
required this.tripId,
|
||||
this.message = 'Voyage créé avec succès',
|
||||
this.message = 'Trip created successfully',
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tripId, message];
|
||||
}
|
||||
|
||||
/// State indicating successful completion of a trip operation.
|
||||
///
|
||||
/// This state is emitted when operations like updating or deleting trips
|
||||
/// have completed successfully. It contains a message that can be displayed
|
||||
/// to inform the user of the successful operation. This state is typically
|
||||
/// followed by automatic reloading of the user's trip list.
|
||||
///
|
||||
/// Properties:
|
||||
/// [message]: A success message describing the completed operation
|
||||
class TripOperationSuccess extends TripState {
|
||||
/// The success message to display to the user
|
||||
final String message;
|
||||
|
||||
/// Creates a TripOperationSuccess state.
|
||||
///
|
||||
/// Args:
|
||||
/// [message]: The success message to display
|
||||
const TripOperationSuccess(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// State indicating that a trip operation has failed.
|
||||
///
|
||||
/// This state is emitted when any trip operation encounters an error.
|
||||
/// It contains an error message that can be displayed to the user to
|
||||
/// explain what went wrong during the operation.
|
||||
///
|
||||
/// Properties:
|
||||
/// [message]: An error message describing what went wrong
|
||||
class TripError extends TripState {
|
||||
/// The error message to display to the user
|
||||
final String message;
|
||||
|
||||
/// Creates a TripError state.
|
||||
///
|
||||
/// Args:
|
||||
/// [message]: The error message to display
|
||||
const TripError(this.message);
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,10 +4,21 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'user_event.dart' as event;
|
||||
import 'user_state.dart' as state;
|
||||
|
||||
/// BLoC for managing user data and operations.
|
||||
///
|
||||
/// This BLoC handles user-related operations including loading user data,
|
||||
/// updating user information, and managing user state throughout the application.
|
||||
/// It coordinates with Firebase Auth and Firestore to manage user data persistence.
|
||||
class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
/// Firebase Auth instance for authentication operations.
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
/// Firestore instance for user data operations.
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
/// Creates a new [UserBloc] with initial state.
|
||||
///
|
||||
/// Registers event handlers for all user-related events.
|
||||
UserBloc() : super(state.UserInitial()) {
|
||||
on<event.UserInitialized>(_onUserInitialized);
|
||||
on<event.LoadUser>(_onLoadUser);
|
||||
@@ -15,6 +26,11 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
on<event.UserLoggedOut>(_onUserLoggedOut);
|
||||
}
|
||||
|
||||
/// Handles [UserInitialized] events.
|
||||
///
|
||||
/// Initializes the current authenticated user's data by fetching it from Firestore.
|
||||
/// If the user doesn't exist in Firestore, creates a default user document.
|
||||
/// This is typically called when the app starts or after successful authentication.
|
||||
Future<void> _onUserInitialized(
|
||||
event.UserInitialized event,
|
||||
Emitter<state.UserState> emit,
|
||||
@@ -25,18 +41,18 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
final currentUser = _auth.currentUser;
|
||||
|
||||
if (currentUser == null) {
|
||||
emit(state.UserError('Aucun utilisateur connecté'));
|
||||
emit(state.UserError('No user currently authenticated'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les données utilisateur depuis Firestore
|
||||
// Fetch user data from Firestore
|
||||
final userDoc = await _firestore
|
||||
.collection('users')
|
||||
.doc(currentUser.uid)
|
||||
.get();
|
||||
|
||||
if (!userDoc.exists) {
|
||||
// Créer un utilisateur par défaut si non existant
|
||||
// Create a default user if it doesn't exist
|
||||
final defaultUser = state.UserModel(
|
||||
id: currentUser.uid,
|
||||
email: currentUser.email ?? '',
|
||||
@@ -57,10 +73,14 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
emit(state.UserLoaded(user));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.UserError('Erreur lors du chargement de l\'utilisateur: $e'));
|
||||
emit(state.UserError('Error loading user: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [LoadUser] events.
|
||||
///
|
||||
/// Loads a specific user's data from Firestore by their user ID.
|
||||
/// This is useful when you need to display information about other users.
|
||||
Future<void> _onLoadUser(
|
||||
event.LoadUser event,
|
||||
Emitter<state.UserState> emit,
|
||||
@@ -80,13 +100,18 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
});
|
||||
emit(state.UserLoaded(user));
|
||||
} else {
|
||||
emit(state.UserError('Utilisateur non trouvé'));
|
||||
emit(state.UserError('User not found'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.UserError('Erreur lors du chargement: $e'));
|
||||
emit(state.UserError('Error loading user: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [UserUpdated] events.
|
||||
///
|
||||
/// Updates the current user's data in Firestore with the provided information.
|
||||
/// Only updates the fields specified in the userData map, allowing for partial updates.
|
||||
/// After successful update, reloads the user data to reflect changes.
|
||||
Future<void> _onUserUpdated(
|
||||
event.UserUpdated event,
|
||||
Emitter<state.UserState> emit,
|
||||
@@ -112,11 +137,15 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
||||
|
||||
emit(state.UserLoaded(updatedUser));
|
||||
} catch (e) {
|
||||
emit(state.UserError('Erreur lors de la mise à jour: $e'));
|
||||
emit(state.UserError('Error updating user: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [UserLoggedOut] events.
|
||||
///
|
||||
/// Resets the user bloc state to initial when the user logs out.
|
||||
/// This clears any cached user data from the application state.
|
||||
Future<void> _onUserLoggedOut(
|
||||
event.UserLoggedOut event,
|
||||
Emitter<state.UserState> emit,
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
/// Abstract base class for all user-related events.
|
||||
///
|
||||
/// All user events in the application should inherit from this class.
|
||||
/// This provides a common interface for user-related operations.
|
||||
abstract class UserEvent {}
|
||||
|
||||
/// Event to initialize the current user's data.
|
||||
///
|
||||
/// This event is typically dispatched when the app starts or when
|
||||
/// a user signs in to load their profile information from Firestore.
|
||||
class UserInitialized extends UserEvent {}
|
||||
|
||||
class UserLoaded extends UserEvent {
|
||||
/// Event to load a specific user's data by their ID.
|
||||
///
|
||||
/// This event is used to fetch user information from Firestore
|
||||
/// when you need to display or work with a specific user's data.
|
||||
class LoadUser extends UserEvent {
|
||||
/// The ID of the user to load.
|
||||
final String userId;
|
||||
|
||||
UserLoaded(this.userId);
|
||||
/// Creates a [LoadUser] event with the specified [userId].
|
||||
LoadUser(this.userId);
|
||||
}
|
||||
|
||||
/// Event to update the current user's data.
|
||||
///
|
||||
/// This event is dispatched when the user modifies their profile
|
||||
/// information and the changes need to be saved to Firestore.
|
||||
class UserUpdated extends UserEvent {
|
||||
/// Map containing the user data fields to update.
|
||||
///
|
||||
/// Only the fields present in this map will be updated in Firestore.
|
||||
/// This allows for partial updates of user information.
|
||||
final Map<String, dynamic> userData;
|
||||
|
||||
/// Creates a [UserUpdated] event with the specified [userData].
|
||||
UserUpdated(this.userData);
|
||||
}
|
||||
|
||||
/// Event to handle user logout.
|
||||
///
|
||||
/// This event is dispatched when the user signs out of the application,
|
||||
/// clearing their data from the user bloc state.
|
||||
class UserLoggedOut extends UserEvent {
|
||||
/// Creates a [UserLoggedOut] event.
|
||||
UserLoggedOut();
|
||||
}
|
||||
class LoadUser extends UserEvent {
|
||||
final String userId;
|
||||
|
||||
LoadUser(this.userId);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,81 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Abstract base class for all user-related states.
|
||||
///
|
||||
/// This class extends [Equatable] to enable value equality for state comparison.
|
||||
/// All user states in the application should inherit from this class.
|
||||
abstract class UserState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Initial state of the user bloc.
|
||||
///
|
||||
/// This state represents the initial state before any user operations
|
||||
/// have been performed or when the user has logged out.
|
||||
class UserInitial extends UserState {}
|
||||
|
||||
/// State indicating that a user operation is in progress.
|
||||
///
|
||||
/// This state is used to show loading indicators during user data
|
||||
/// operations like loading, updating, or initializing user information.
|
||||
class UserLoading extends UserState {}
|
||||
|
||||
/// State indicating that user data has been successfully loaded.
|
||||
///
|
||||
/// This state contains the loaded user information and is used
|
||||
/// throughout the app to access current user data.
|
||||
class UserLoaded extends UserState {
|
||||
/// The loaded user data.
|
||||
final UserModel user;
|
||||
|
||||
/// Creates a [UserLoaded] state with the given [user] data.
|
||||
UserLoaded(this.user);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [user];
|
||||
}
|
||||
|
||||
/// State indicating that a user operation has failed.
|
||||
///
|
||||
/// This state contains an error message that can be displayed to the user
|
||||
/// when user operations fail.
|
||||
class UserError extends UserState {
|
||||
/// The error message describing what went wrong.
|
||||
final String message;
|
||||
|
||||
/// Creates a [UserError] state with the given error [message].
|
||||
UserError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
// Modèle utilisateur simple
|
||||
/// Simple user model for representing user data in the application.
|
||||
///
|
||||
/// This model contains basic user information and provides methods for
|
||||
/// serialization/deserialization with Firestore operations.
|
||||
/// Simple user model for representing user data in the application.
|
||||
///
|
||||
/// This model contains basic user information and provides methods for
|
||||
/// serialization/deserialization with Firestore operations.
|
||||
class UserModel {
|
||||
/// Unique identifier for the user (Firebase UID).
|
||||
final String id;
|
||||
|
||||
/// User's email address.
|
||||
final String email;
|
||||
|
||||
/// User's first name.
|
||||
final String prenom;
|
||||
|
||||
/// User's last name (optional).
|
||||
final String? nom;
|
||||
|
||||
/// Creates a new [UserModel] instance.
|
||||
///
|
||||
/// [id], [email], and [prenom] are required fields.
|
||||
/// [nom] is optional and can be null.
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
@@ -41,6 +83,10 @@ class UserModel {
|
||||
this.nom,
|
||||
});
|
||||
|
||||
/// Creates a [UserModel] instance from a JSON map.
|
||||
///
|
||||
/// Handles null values gracefully by providing default values.
|
||||
/// [prenom] defaults to 'Voyageur' (Traveler) if not provided.
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
id: json['id'] ?? '',
|
||||
@@ -50,6 +96,9 @@ class UserModel {
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts the [UserModel] instance to a JSON map.
|
||||
///
|
||||
/// Useful for storing user data in Firestore or other JSON-based operations.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
|
||||
Reference in New Issue
Block a user