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