feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles

- Config Spec-Kit pour Spec-Driven Development
- CONSTITUTION.md + .specify/memory/constitution.md
- Commandes Cursor /speckit.*, règles projet
- Mission: associations + mutuelles d'épargne et de financement
- .gitignore: versionner config spec-kit unionflow

Made-with: Cursor
This commit is contained in:
dahoud
2026-02-27 14:41:07 +00:00
parent 144b68f8e7
commit b1957c1c81
631 changed files with 104070 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
/// Environnements de déploiement de l'application
enum Environment { dev, staging, prod }
/// Configuration centralisée par environnement.
/// Les URLs sont injectées via --dart-define=ENV=dev|staging|prod
class AppConfig {
static late final Environment _environment;
static late final String apiBaseUrl;
static late final String keycloakBaseUrl;
static late final String wsBaseUrl;
static late final bool enableDebugMode;
static late final bool enableLogging;
static late final bool enableCrashReporting;
static late final bool enableAnalytics;
/// Initialise la configuration à partir de l'environnement.
/// Appeler dans main() avant runApp().
static void initialize() {
const envString = String.fromEnvironment('ENV', defaultValue: 'dev');
_environment = Environment.values.firstWhere(
(e) => e.name == envString,
orElse: () => Environment.dev,
);
switch (_environment) {
case Environment.dev:
apiBaseUrl = const String.fromEnvironment(
'API_URL',
defaultValue: 'http://192.168.1.11:8085',
);
keycloakBaseUrl = const String.fromEnvironment(
'KEYCLOAK_URL',
defaultValue: 'http://192.168.1.11:8180',
);
wsBaseUrl = const String.fromEnvironment(
'WS_URL',
defaultValue: 'ws://192.168.1.11:8085',
);
enableDebugMode = true;
enableLogging = true;
enableCrashReporting = false;
enableAnalytics = false;
case Environment.staging:
apiBaseUrl = const String.fromEnvironment(
'API_URL',
defaultValue: 'https://api-staging.lions.dev',
);
keycloakBaseUrl = const String.fromEnvironment(
'KEYCLOAK_URL',
defaultValue: 'https://security-staging.lions.dev',
);
wsBaseUrl = const String.fromEnvironment(
'WS_URL',
defaultValue: 'wss://api-staging.lions.dev',
);
enableDebugMode = false;
enableLogging = true;
enableCrashReporting = true;
enableAnalytics = false;
case Environment.prod:
apiBaseUrl = const String.fromEnvironment(
'API_URL',
defaultValue: 'https://api.lions.dev',
);
keycloakBaseUrl = const String.fromEnvironment(
'KEYCLOAK_URL',
defaultValue: 'https://security.lions.dev',
);
wsBaseUrl = const String.fromEnvironment(
'WS_URL',
defaultValue: 'wss://api.lions.dev',
);
enableDebugMode = false;
enableLogging = false;
enableCrashReporting = true;
enableAnalytics = true;
}
}
static Environment get environment => _environment;
static bool get isDev => _environment == Environment.dev;
static bool get isStaging => _environment == Environment.staging;
static bool get isProd => _environment == Environment.prod;
/// URL complète du realm Keycloak
static String get keycloakRealmUrl => '$keycloakBaseUrl/realms/unionflow';
/// URL du endpoint token Keycloak
static String get keycloakTokenUrl =>
'$keycloakRealmUrl/protocol/openid-connect/token';
/// URL du endpoint WebSocket dashboard
static String get wsDashboardUrl => '$wsBaseUrl/ws/dashboard';
}

View File

@@ -0,0 +1,299 @@
/// Constantes globales de l'application
library app_constants;
/// Constantes de l'application UnionFlow
class AppConstants {
// Empêcher l'instanciation
AppConstants._();
// ============================================================================
// API & BACKEND
// Les URLs sont gérées par AppConfig (lib/core/config/environment.dart).
// Utiliser AppConfig.apiBaseUrl, AppConfig.keycloakBaseUrl, etc.
// ============================================================================
/// Realm Keycloak
static const String keycloakRealm = 'unionflow';
/// Client ID Keycloak
static const String keycloakClientId = 'unionflow-mobile';
/// Redirect URI pour l'authentification
static const String redirectUri = 'dev.lions.unionflow-mobile://auth/callback';
// ============================================================================
// PAGINATION
// ============================================================================
/// Taille de page par défaut pour les listes paginées
static const int defaultPageSize = 20;
/// Taille de page maximale
static const int maxPageSize = 100;
/// Taille de page minimale
static const int minPageSize = 5;
/// Page initiale
static const int initialPage = 0;
// ============================================================================
// TIMEOUTS
// ============================================================================
/// Timeout de connexion (en secondes)
static const Duration connectTimeout = Duration(seconds: 30);
/// Timeout d'envoi (en secondes)
static const Duration sendTimeout = Duration(seconds: 30);
/// Timeout de réception (en secondes)
static const Duration receiveTimeout = Duration(seconds: 30);
// ============================================================================
// CACHE
// ============================================================================
/// Durée d'expiration du cache (en heures)
static const Duration cacheExpiration = Duration(hours: 1);
/// Durée d'expiration du cache pour les données statiques (en jours)
static const Duration staticCacheExpiration = Duration(days: 7);
/// Taille maximale du cache (en MB)
static const int maxCacheSize = 100;
// ============================================================================
// UI & DESIGN
// ============================================================================
/// Padding par défaut
static const double defaultPadding = 16.0;
/// Padding petit
static const double smallPadding = 8.0;
/// Padding large
static const double largePadding = 24.0;
/// Padding extra large
static const double extraLargePadding = 32.0;
/// Rayon de bordure par défaut
static const double defaultRadius = 8.0;
/// Rayon de bordure petit
static const double smallRadius = 4.0;
/// Rayon de bordure large
static const double largeRadius = 12.0;
/// Rayon de bordure extra large
static const double extraLargeRadius = 16.0;
/// Hauteur de l'AppBar
static const double appBarHeight = 56.0;
/// Hauteur du BottomNavigationBar
static const double bottomNavBarHeight = 60.0;
/// Largeur maximale pour les écrans larges (tablettes, desktop)
static const double maxContentWidth = 1200.0;
// ============================================================================
// ANIMATIONS
// ============================================================================
/// Durée d'animation par défaut
static const Duration defaultAnimationDuration = Duration(milliseconds: 300);
/// Durée d'animation rapide
static const Duration fastAnimationDuration = Duration(milliseconds: 150);
/// Durée d'animation lente
static const Duration slowAnimationDuration = Duration(milliseconds: 500);
// ============================================================================
// VALIDATION
// ============================================================================
/// Longueur minimale du mot de passe
static const int minPasswordLength = 8;
/// Longueur maximale du mot de passe
static const int maxPasswordLength = 128;
/// Longueur minimale du nom
static const int minNameLength = 2;
/// Longueur maximale du nom
static const int maxNameLength = 100;
/// Longueur maximale de la description
static const int maxDescriptionLength = 1000;
/// Longueur maximale du titre
static const int maxTitleLength = 200;
// ============================================================================
// FORMATS
// ============================================================================
/// Format de date par défaut (dd/MM/yyyy)
static const String defaultDateFormat = 'dd/MM/yyyy';
/// Format de date et heure (dd/MM/yyyy HH:mm)
static const String defaultDateTimeFormat = 'dd/MM/yyyy HH:mm';
/// Format de date court (dd/MM/yy)
static const String shortDateFormat = 'dd/MM/yy';
/// Format de date long (EEEE dd MMMM yyyy)
static const String longDateFormat = 'EEEE dd MMMM yyyy';
/// Format d'heure (HH:mm)
static const String timeFormat = 'HH:mm';
/// Format d'heure avec secondes (HH:mm:ss)
static const String timeWithSecondsFormat = 'HH:mm:ss';
// ============================================================================
// DEVISE
// ============================================================================
/// Devise par défaut
static const String defaultCurrency = 'EUR';
/// Symbole de la devise par défaut
static const String defaultCurrencySymbol = '';
// ============================================================================
// IMAGES
// ============================================================================
/// Taille maximale d'upload d'image (en MB)
static const int maxImageUploadSize = 5;
/// Formats d'image acceptés
static const List<String> acceptedImageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
/// Qualité de compression d'image (0-100)
static const int imageCompressionQuality = 85;
// ============================================================================
// DOCUMENTS
// ============================================================================
/// Taille maximale d'upload de document (en MB)
static const int maxDocumentUploadSize = 10;
/// Formats de document acceptés
static const List<String> acceptedDocumentFormats = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'];
// ============================================================================
// NOTIFICATIONS
// ============================================================================
/// Durée d'affichage des snackbars (en secondes)
static const Duration snackbarDuration = Duration(seconds: 3);
/// Durée d'affichage des snackbars d'erreur (en secondes)
static const Duration errorSnackbarDuration = Duration(seconds: 5);
/// Durée d'affichage des snackbars de succès (en secondes)
static const Duration successSnackbarDuration = Duration(seconds: 2);
// ============================================================================
// RECHERCHE
// ============================================================================
/// Délai de debounce pour la recherche (en millisecondes)
static const Duration searchDebounce = Duration(milliseconds: 500);
/// Nombre minimum de caractères pour déclencher une recherche
static const int minSearchLength = 2;
// ============================================================================
// REFRESH
// ============================================================================
/// Intervalle de rafraîchissement automatique (en minutes)
static const Duration autoRefreshInterval = Duration(minutes: 5);
// ============================================================================
// STORAGE KEYS
// ============================================================================
/// Clé pour le token d'accès
static const String accessTokenKey = 'access_token';
/// Clé pour le refresh token
static const String refreshTokenKey = 'refresh_token';
/// Clé pour l'ID token
static const String idTokenKey = 'id_token';
/// Clé pour les données utilisateur
static const String userDataKey = 'user_data';
/// Clé pour les préférences de thème
static const String themePreferenceKey = 'theme_preference';
/// Clé pour les préférences de langue
static const String languagePreferenceKey = 'language_preference';
/// Clé pour le mode hors ligne
static const String offlineModeKey = 'offline_mode';
// ============================================================================
// REGEX PATTERNS
// ============================================================================
/// Pattern pour valider un email
static const String emailPattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
/// Pattern pour valider un numéro de téléphone français
static const String phonePattern = r'^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$';
/// Pattern pour valider un code postal français
static const String postalCodePattern = r'^\d{5}$';
/// Pattern pour valider une URL
static const String urlPattern = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$';
// ============================================================================
// FEATURES FLAGS
// ============================================================================
// Feature flags pilotés par AppConfig (lib/core/config/environment.dart).
// Utiliser AppConfig.enableDebugMode, AppConfig.enableLogging, etc.
/// Activer le mode offline
static const bool enableOfflineMode = false;
// ============================================================================
// APP INFO
// ============================================================================
/// Nom de l'application
static const String appName = 'UnionFlow';
/// Version de l'application
static const String appVersion = '1.0.0';
/// Build number
static const String buildNumber = '1';
/// Email de support
static const String supportEmail = 'support@unionflow.com';
/// URL du site web
static const String websiteUrl = 'https://unionflow.com';
/// URL des conditions d'utilisation
static const String termsOfServiceUrl = 'https://unionflow.com/terms';
/// URL de la politique de confidentialité
static const String privacyPolicyUrl = 'https://unionflow.com/privacy';
}

View File

@@ -0,0 +1,120 @@
/// Configuration globale de l'injection de dépendances
library app_di;
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import '../network/dio_client.dart';
import '../network/network_info.dart';
import '../../features/organizations/di/organizations_di.dart';
import '../../features/members/di/membres_di.dart';
import '../../features/events/di/evenements_di.dart';
import '../../features/contributions/di/contributions_di.dart';
import '../../features/adhesions/di/adhesions_di.dart';
import '../../features/solidarity/di/solidarity_di.dart';
import '../../features/admin/di/admin_di.dart';
import '../../features/dashboard/di/dashboard_di.dart';
import '../../features/profile/di/profile_di.dart';
import '../../features/notifications/di/notifications_di.dart';
import '../../features/reports/di/reports_di.dart';
/// Gestionnaire global des dépendances
class AppDI {
static final GetIt _getIt = GetIt.instance;
/// Initialise toutes les dépendances de l'application
static Future<void> initialize() async {
// Configuration du client HTTP
await _setupNetworking();
// Configuration des modules
await _setupModules();
}
/// Configure les services réseau
static Future<void> _setupNetworking() async {
// Client Dio
final dioClient = DioClient();
_getIt.registerSingleton<DioClient>(dioClient);
_getIt.registerSingleton<Dio>(dioClient.dio);
// Network Info (pour l'instant, on simule toujours connecté)
_getIt.registerLazySingleton<NetworkInfo>(
() => _MockNetworkInfo(),
);
}
/// Configure tous les modules de l'application
static Future<void> _setupModules() async {
// Module Organizations
OrganizationsDI.registerDependencies();
// Module Membres
MembresDI.register();
// Module Événements
EvenementsDI.register();
// Module Contributions
registerCotisationsDependencies(_getIt);
// Module Adhésions
registerAdhesionsDependencies(_getIt);
// Module Solidarité (demandes d'aide)
registerSolidarityDependencies(_getIt);
// Module Admin (gestion utilisateurs SUPER_ADMIN)
registerAdminDependencies(_getIt);
// Module Dashboard
DashboardDI.registerDependencies();
// Module Profil utilisateur
ProfileDI.register();
// Module Notifications
NotificationsDI.register();
// Module Rapports & Analytics
ReportsDI.register();
}
/// Nettoie toutes les dépendances
static Future<void> dispose() async {
// Nettoyer les modules
OrganizationsDI.unregisterDependencies();
MembresDI.unregister();
EvenementsDI.unregister();
// Nettoyer les services globaux
if (_getIt.isRegistered<Dio>()) {
_getIt.unregister<Dio>();
}
if (_getIt.isRegistered<DioClient>()) {
_getIt.unregister<DioClient>();
}
// Reset complet
await _getIt.reset();
}
/// Obtient l'instance GetIt
static GetIt get instance => _getIt;
/// Obtient le client Dio
static Dio get dio => _getIt<Dio>();
/// Obtient le client Dio wrapper
static DioClient get dioClient => _getIt<DioClient>();
/// Nettoie toutes les dépendances
static Future<void> cleanup() async {
await _getIt.reset();
}
}
/// Mock de NetworkInfo pour les tests et développement
class _MockNetworkInfo implements NetworkInfo {
@override
Future<bool> get isConnected async => true;
}

View File

@@ -0,0 +1,15 @@
import 'package:get_it/get_it.dart';
import 'app_di.dart';
/// Service locator global - alias pour faciliter l'utilisation
final GetIt sl = AppDI.instance;
/// Initialise toutes les dépendances de l'application
Future<void> initializeDependencies() async {
await AppDI.initialize();
}
/// Nettoie toutes les dépendances
Future<void> cleanupDependencies() async {
await AppDI.cleanup();
}

View File

@@ -0,0 +1,192 @@
/// Gestionnaire d'erreurs global pour l'application
library error_handler;
import 'package:dio/dio.dart';
/// Classe utilitaire pour gérer les erreurs de manière centralisée
class ErrorHandler {
/// Convertit une erreur en message utilisateur lisible
static String getErrorMessage(dynamic error) {
if (error is DioException) {
return _handleDioError(error);
} else if (error is String) {
return error;
} else if (error is Exception) {
return error.toString().replaceAll('Exception: ', '');
}
return 'Une erreur inattendue s\'est produite.';
}
/// Gère les erreurs Dio spécifiques
static String _handleDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
return 'Délai de connexion dépassé.\nVérifiez votre connexion internet.';
case DioExceptionType.sendTimeout:
return 'Délai d\'envoi dépassé.\nVérifiez votre connexion internet.';
case DioExceptionType.receiveTimeout:
return 'Délai de réception dépassé.\nLe serveur met trop de temps à répondre.';
case DioExceptionType.badResponse:
return _handleBadResponse(error.response);
case DioExceptionType.cancel:
return 'Requête annulée.';
case DioExceptionType.connectionError:
return 'Erreur de connexion.\nVérifiez votre connexion internet.';
case DioExceptionType.badCertificate:
return 'Erreur de certificat SSL.\nLa connexion n\'est pas sécurisée.';
case DioExceptionType.unknown:
default:
if (error.message?.contains('SocketException') ?? false) {
return 'Impossible de se connecter au serveur.\nVérifiez votre connexion internet.';
}
return 'Erreur de connexion.\nVeuillez réessayer.';
}
}
/// Gère les réponses HTTP avec erreur
static String _handleBadResponse(Response? response) {
if (response == null) {
return 'Erreur serveur inconnue.';
}
// Essayer d'extraire le message d'erreur du body
String? errorMessage;
if (response.data is Map) {
errorMessage = response.data['message'] ??
response.data['error'] ??
response.data['details'];
}
switch (response.statusCode) {
case 400:
return errorMessage ?? 'Requête invalide.\nVérifiez les données saisies.';
case 401:
return errorMessage ?? 'Non authentifié.\nVeuillez vous reconnecter.';
case 403:
return errorMessage ?? 'Accès refusé.\nVous n\'avez pas les permissions nécessaires.';
case 404:
return errorMessage ?? 'Ressource non trouvée.';
case 409:
return errorMessage ?? 'Conflit.\nCette ressource existe déjà.';
case 422:
return errorMessage ?? 'Données invalides.\nVérifiez les informations saisies.';
case 429:
return 'Trop de requêtes.\nVeuillez patienter quelques instants.';
case 500:
return errorMessage ?? 'Erreur serveur interne.\nVeuillez réessayer plus tard.';
case 502:
return 'Passerelle incorrecte.\nLe serveur est temporairement indisponible.';
case 503:
return 'Service temporairement indisponible.\nVeuillez réessayer plus tard.';
case 504:
return 'Délai d\'attente de la passerelle dépassé.\nLe serveur met trop de temps à répondre.';
default:
return errorMessage ?? 'Erreur serveur (${response.statusCode}).\nVeuillez réessayer.';
}
}
/// Détermine si l'erreur est une erreur réseau
static bool isNetworkError(dynamic error) {
if (error is DioException) {
return error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.sendTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.connectionError ||
(error.message?.contains('SocketException') ?? false);
}
return false;
}
/// Détermine si l'erreur est une erreur d'authentification
static bool isAuthError(dynamic error) {
if (error is DioException && error.response != null) {
return error.response!.statusCode == 401;
}
return false;
}
/// Détermine si l'erreur est une erreur de permissions
static bool isPermissionError(dynamic error) {
if (error is DioException && error.response != null) {
return error.response!.statusCode == 403;
}
return false;
}
/// Détermine si l'erreur est une erreur de validation
static bool isValidationError(dynamic error) {
if (error is DioException && error.response != null) {
return error.response!.statusCode == 400 ||
error.response!.statusCode == 422;
}
return false;
}
/// Détermine si l'erreur est une erreur serveur
static bool isServerError(dynamic error) {
if (error is DioException && error.response != null) {
final statusCode = error.response!.statusCode ?? 0;
return statusCode >= 500 && statusCode < 600;
}
return false;
}
/// Extrait les détails de validation d'une erreur
static Map<String, dynamic>? getValidationErrors(dynamic error) {
if (error is DioException &&
error.response != null &&
error.response!.data is Map) {
final data = error.response!.data as Map;
if (data.containsKey('errors')) {
return data['errors'] as Map<String, dynamic>?;
}
if (data.containsKey('validationErrors')) {
return data['validationErrors'] as Map<String, dynamic>?;
}
}
return null;
}
}
/// Extension pour faciliter l'utilisation de ErrorHandler
extension ErrorHandlerExtension on Object {
/// Convertit l'objet en message d'erreur lisible
String toErrorMessage() => ErrorHandler.getErrorMessage(this);
/// Vérifie si c'est une erreur réseau
bool get isNetworkError => ErrorHandler.isNetworkError(this);
/// Vérifie si c'est une erreur d'authentification
bool get isAuthError => ErrorHandler.isAuthError(this);
/// Vérifie si c'est une erreur de permissions
bool get isPermissionError => ErrorHandler.isPermissionError(this);
/// Vérifie si c'est une erreur de validation
bool get isValidationError => ErrorHandler.isValidationError(this);
/// Vérifie si c'est une erreur serveur
bool get isServerError => ErrorHandler.isServerError(this);
/// Récupère les erreurs de validation
Map<String, dynamic>? get validationErrors => ErrorHandler.getValidationErrors(this);
}

View File

@@ -0,0 +1,50 @@
/// Exception de base pour l'application
abstract class AppException implements Exception {
final String message;
final String? code;
const AppException(this.message, [this.code]);
@override
String toString() => 'AppException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception serveur
class ServerException extends AppException {
const ServerException(super.message, [super.code]);
@override
String toString() => 'ServerException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de cache
class CacheException extends AppException {
const CacheException(super.message, [super.code]);
@override
String toString() => 'CacheException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de réseau
class NetworkException extends AppException {
const NetworkException(super.message, [super.code]);
@override
String toString() => 'NetworkException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception d'authentification
class AuthException extends AppException {
const AuthException(super.message, [super.code]);
@override
String toString() => 'AuthException: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Exception de validation
class ValidationException extends AppException {
const ValidationException(super.message, [super.code]);
@override
String toString() => 'ValidationException: $message${code != null ? ' (Code: $code)' : ''}';
}

View File

@@ -0,0 +1,71 @@
import 'package:equatable/equatable.dart';
/// Classe de base pour tous les échecs
abstract class Failure extends Equatable {
final String message;
final String? code;
const Failure(this.message, [this.code]);
@override
List<Object?> get props => [message, code];
@override
String toString() => 'Failure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec serveur
class ServerFailure extends Failure {
const ServerFailure(super.message, [super.code]);
@override
String toString() => 'ServerFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de cache
class CacheFailure extends Failure {
const CacheFailure(super.message, [super.code]);
@override
String toString() => 'CacheFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de réseau
class NetworkFailure extends Failure {
const NetworkFailure(super.message, [super.code]);
@override
String toString() => 'NetworkFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec d'authentification
class AuthFailure extends Failure {
const AuthFailure(super.message, [super.code]);
@override
String toString() => 'AuthFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de validation
class ValidationFailure extends Failure {
const ValidationFailure(super.message, [super.code]);
@override
String toString() => 'ValidationFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de permission
class PermissionFailure extends Failure {
const PermissionFailure(super.message, [super.code]);
@override
String toString() => 'PermissionFailure: $message${code != null ? ' (Code: $code)' : ''}';
}
/// Échec de données non trouvées
class NotFoundFailure extends Failure {
const NotFoundFailure(super.message, [super.code]);
@override
String toString() => 'NotFoundFailure: $message${code != null ? ' (Code: $code)' : ''}';
}

View File

@@ -0,0 +1,102 @@
/// Provider pour gérer la locale de l'application
library locale_provider;
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/logger.dart';
/// Provider pour la gestion de la locale
class LocaleProvider extends ChangeNotifier {
static const String _localeKey = 'app_locale';
Locale _locale = const Locale('fr');
/// Locale actuelle
Locale get locale => _locale;
/// Locales supportées
static const List<Locale> supportedLocales = [
Locale('fr'),
Locale('en'),
];
/// Initialiser la locale depuis les préférences
Future<void> initialize() async {
try {
final prefs = await SharedPreferences.getInstance();
final localeCode = prefs.getString(_localeKey);
if (localeCode != null) {
_locale = Locale(localeCode);
AppLogger.info('Locale chargée: $localeCode');
} else {
AppLogger.info('Locale par défaut: fr');
}
notifyListeners();
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors du chargement de la locale',
error: e,
stackTrace: stackTrace,
);
}
}
/// Changer la locale
Future<void> setLocale(Locale locale) async {
if (!supportedLocales.contains(locale)) {
AppLogger.warning('Locale non supportée: ${locale.languageCode}');
return;
}
if (_locale == locale) {
return;
}
try {
_locale = locale;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
AppLogger.info('Locale changée: ${locale.languageCode}');
AppLogger.userAction('Change language', data: {'locale': locale.languageCode});
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors du changement de locale',
error: e,
stackTrace: stackTrace,
);
}
}
/// Basculer entre FR et EN
Future<void> toggleLocale() async {
final newLocale = _locale.languageCode == 'fr'
? const Locale('en')
: const Locale('fr');
await setLocale(newLocale);
}
/// Obtenir le nom de la langue actuelle
String get currentLanguageName {
switch (_locale.languageCode) {
case 'fr':
return 'Français';
case 'en':
return 'English';
default:
return 'Français';
}
}
/// Vérifier si la locale est française
bool get isFrench => _locale.languageCode == 'fr';
/// Vérifier si la locale est anglaise
bool get isEnglish => _locale.languageCode == 'en';
}

View File

@@ -0,0 +1,561 @@
/// Système de navigation adaptatif basé sur les rôles
/// Navigation qui s'adapte selon les permissions et rôles utilisateurs
library adaptive_navigation;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../features/authentication/data/models/permission_matrix.dart';
import '../../shared/widgets/adaptive_widget.dart';
/// Élément de navigation adaptatif
class AdaptiveNavigationItem {
/// Icône de l'élément
final IconData icon;
/// Icône sélectionnée (optionnelle)
final IconData? selectedIcon;
/// Libellé de l'élément
final String label;
/// Route de destination
final String route;
/// Permissions requises pour afficher cet élément
final List<String> requiredPermissions;
/// Rôles minimum requis
final UserRole? minimumRole;
/// Badge de notification (optionnel)
final String? badge;
/// Couleur personnalisée (optionnelle)
final Color? color;
const AdaptiveNavigationItem({
required this.icon,
this.selectedIcon,
required this.label,
required this.route,
this.requiredPermissions = const [],
this.minimumRole,
this.badge,
this.color,
});
}
/// Drawer de navigation adaptatif
class AdaptiveNavigationDrawer extends StatelessWidget {
/// Callback de navigation
final Function(String route) onNavigate;
/// Callback de déconnexion
final VoidCallback onLogout;
/// Éléments de navigation personnalisés
final List<AdaptiveNavigationItem>? customItems;
const AdaptiveNavigationDrawer({
super.key,
required this.onNavigate,
required this.onLogout,
this.customItems,
});
@override
Widget build(BuildContext context) {
return AdaptiveWidget(
roleWidgets: {
UserRole.superAdmin: () => _buildSuperAdminDrawer(context),
UserRole.orgAdmin: () => _buildOrgAdminDrawer(context),
UserRole.moderator: () => _buildModeratorDrawer(context),
UserRole.activeMember: () => _buildActiveMemberDrawer(context),
UserRole.simpleMember: () => _buildSimpleMemberDrawer(context),
UserRole.visitor: () => _buildVisitorDrawer(context),
},
fallbackWidget: _buildBasicDrawer(context),
loadingWidget: _buildLoadingDrawer(context),
);
}
/// Drawer pour Super Admin
Widget _buildSuperAdminDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.dashboard,
label: 'Command Center',
route: '/dashboard',
requiredPermissions: [PermissionMatrix.SYSTEM_ADMIN],
),
const AdaptiveNavigationItem(
icon: Icons.business,
label: 'Organisations',
route: '/organizations',
requiredPermissions: [PermissionMatrix.ORG_CREATE],
),
const AdaptiveNavigationItem(
icon: Icons.people,
label: 'Utilisateurs Globaux',
route: '/global-users',
requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.settings,
label: 'Administration',
route: '/system-admin',
requiredPermissions: [PermissionMatrix.SYSTEM_CONFIG],
),
const AdaptiveNavigationItem(
icon: Icons.analytics,
label: 'Analytics',
route: '/analytics',
requiredPermissions: [PermissionMatrix.DASHBOARD_ANALYTICS],
),
const AdaptiveNavigationItem(
icon: Icons.security,
label: 'Sécurité',
route: '/security',
requiredPermissions: [PermissionMatrix.SYSTEM_SECURITY],
),
];
return _buildDrawer(
context,
'Super Administrateur',
const Color(0xFF6C5CE7),
Icons.admin_panel_settings,
items,
);
}
/// Drawer pour Org Admin
Widget _buildOrgAdminDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.dashboard,
label: 'Control Panel',
route: '/dashboard',
requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
),
const AdaptiveNavigationItem(
icon: Icons.people,
label: 'Membres',
route: '/members',
requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.account_balance_wallet,
label: 'Finances',
route: '/finances',
requiredPermissions: [PermissionMatrix.FINANCES_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.event,
label: 'Événements',
route: '/events',
requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.volunteer_activism,
label: 'Solidarité',
route: '/solidarity',
requiredPermissions: [PermissionMatrix.SOLIDARITY_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.assessment,
label: 'Rapports',
route: '/reports',
requiredPermissions: [PermissionMatrix.REPORTS_GENERATE],
),
const AdaptiveNavigationItem(
icon: Icons.settings,
label: 'Configuration',
route: '/org-settings',
requiredPermissions: [PermissionMatrix.ORG_CONFIG],
),
];
return _buildDrawer(
context,
'Administrateur',
const Color(0xFF0984E3),
Icons.business_center,
items,
);
}
/// Drawer pour Modérateur
Widget _buildModeratorDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.dashboard,
label: 'Management Hub',
route: '/dashboard',
requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
),
const AdaptiveNavigationItem(
icon: Icons.gavel,
label: 'Modération',
route: '/moderation',
requiredPermissions: [PermissionMatrix.MODERATION_CONTENT],
),
const AdaptiveNavigationItem(
icon: Icons.people,
label: 'Membres',
route: '/members',
requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.event,
label: 'Événements',
route: '/events',
requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.message,
label: 'Communication',
route: '/communication',
requiredPermissions: [PermissionMatrix.COMM_MODERATE],
),
];
return _buildDrawer(
context,
'Modérateur',
const Color(0xFFE17055),
Icons.manage_accounts,
items,
);
}
/// Drawer pour Membre Actif
Widget _buildActiveMemberDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.dashboard,
label: 'Activity Center',
route: '/dashboard',
requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
),
const AdaptiveNavigationItem(
icon: Icons.person,
label: 'Mon Profil',
route: '/profile',
requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_OWN],
),
const AdaptiveNavigationItem(
icon: Icons.event,
label: 'Événements',
route: '/events',
requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.volunteer_activism,
label: 'Solidarité',
route: '/solidarity',
requiredPermissions: [PermissionMatrix.SOLIDARITY_VIEW_ALL],
),
const AdaptiveNavigationItem(
icon: Icons.payment,
label: 'Mes Cotisations',
route: '/my-finances',
requiredPermissions: [PermissionMatrix.FINANCES_VIEW_OWN],
),
const AdaptiveNavigationItem(
icon: Icons.message,
label: 'Messages',
route: '/messages',
requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
),
];
return _buildDrawer(
context,
'Membre Actif',
const Color(0xFF00B894),
Icons.groups,
items,
);
}
/// Drawer pour Membre Simple
Widget _buildSimpleMemberDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.dashboard,
label: 'Mon Espace',
route: '/dashboard',
requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
),
const AdaptiveNavigationItem(
icon: Icons.person,
label: 'Mon Profil',
route: '/profile',
requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_OWN],
),
const AdaptiveNavigationItem(
icon: Icons.event,
label: 'Événements',
route: '/events',
requiredPermissions: [PermissionMatrix.EVENTS_VIEW_PUBLIC],
),
const AdaptiveNavigationItem(
icon: Icons.payment,
label: 'Mes Cotisations',
route: '/my-finances',
requiredPermissions: [PermissionMatrix.FINANCES_VIEW_OWN],
),
const AdaptiveNavigationItem(
icon: Icons.help,
label: 'Aide',
route: '/help',
requiredPermissions: [],
),
];
return _buildDrawer(
context,
'Membre',
const Color(0xFF00CEC9),
Icons.person,
items,
);
}
/// Drawer pour Visiteur
Widget _buildVisitorDrawer(BuildContext context) {
final items = [
const AdaptiveNavigationItem(
icon: Icons.home,
label: 'Accueil',
route: '/dashboard',
requiredPermissions: [],
),
const AdaptiveNavigationItem(
icon: Icons.info,
label: 'À Propos',
route: '/about',
requiredPermissions: [],
),
const AdaptiveNavigationItem(
icon: Icons.event,
label: 'Événements Publics',
route: '/public-events',
requiredPermissions: [PermissionMatrix.EVENTS_VIEW_PUBLIC],
),
const AdaptiveNavigationItem(
icon: Icons.contact_mail,
label: 'Contact',
route: '/contact',
requiredPermissions: [],
),
const AdaptiveNavigationItem(
icon: Icons.login,
label: 'Se Connecter',
route: '/login',
requiredPermissions: [],
),
];
return _buildDrawer(
context,
'Visiteur',
const Color(0xFF6C5CE7),
Icons.waving_hand,
items,
);
}
/// Drawer basique de fallback
Widget _buildBasicDrawer(BuildContext context) {
return _buildDrawer(
context,
'UnionFlow',
Colors.grey,
Icons.dashboard,
[
const AdaptiveNavigationItem(
icon: Icons.home,
label: 'Accueil',
route: '/dashboard',
),
],
);
}
/// Drawer de chargement
Widget _buildLoadingDrawer(BuildContext context) {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
),
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
);
}
/// Construit un drawer avec les éléments spécifiés
Widget _buildDrawer(
BuildContext context,
String title,
Color color,
IconData icon,
List<AdaptiveNavigationItem> items,
) {
return Drawer(
child: Column(
children: [
// En-tête du drawer
_buildDrawerHeader(context, title, color, icon),
// Éléments de navigation
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
...items.map((item) => _buildNavigationItem(context, item)),
const Divider(),
_buildLogoutItem(context),
],
),
),
],
),
);
}
/// Construit l'en-tête du drawer
Widget _buildDrawerHeader(
BuildContext context,
String title,
Color color,
IconData icon,
) {
return DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [color, color.withOpacity(0.8)],
),
),
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthAuthenticated) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Colors.white, size: 32),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Text(
state.user.fullName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
Text(
state.user.email,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
);
}
return Row(
children: [
Icon(icon, color: Colors.white, size: 32),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
);
},
),
);
}
/// Construit un élément de navigation
Widget _buildNavigationItem(
BuildContext context,
AdaptiveNavigationItem item,
) {
return SecureWidget(
requiredPermissions: item.requiredPermissions,
child: ListTile(
leading: Icon(item.icon, color: item.color),
title: Text(item.label),
trailing: item.badge != null
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
child: Text(
item.badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
)
: null,
onTap: () {
Navigator.of(context).pop();
onNavigate(item.route);
},
),
);
}
/// Construit l'élément de déconnexion
Widget _buildLogoutItem(BuildContext context) {
return ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text(
'Déconnexion',
style: TextStyle(color: Colors.red),
),
onTap: () {
Navigator.of(context).pop();
onLogout();
},
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:go_router/go_router.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/presentation/pages/login_page.dart';
import 'main_navigation_layout.dart';
/// Configuration du routeur principal de l'application
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final authState = context.read<AuthBloc>().state;
final isAuthenticated = authState is AuthAuthenticated;
final isOnLoginPage = state.matchedLocation == '/login';
// Si pas authentifié et pas sur la page de login, rediriger vers login
if (!isAuthenticated && !isOnLoginPage) {
return '/login';
}
// Si authentifié et sur la page de login, rediriger vers dashboard
if (isAuthenticated && isOnLoginPage) {
return '/';
}
return null; // Pas de redirection
},
routes: [
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/',
name: 'main',
builder: (context, state) => const MainNavigationLayout(),
),
],
);
}

View File

@@ -0,0 +1,537 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../shared/design_system/unionflow_design_system.dart';
import '../../features/dashboard/presentation/pages/role_dashboards/role_dashboards.dart';
import '../../features/members/presentation/pages/members_page_wrapper.dart';
import '../../features/events/presentation/pages/events_page_wrapper.dart';
import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../features/adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../features/admin/presentation/pages/user_management_page.dart';
import '../../features/about/presentation/pages/about_page.dart';
import '../../features/help/presentation/pages/help_support_page.dart';
import '../../features/notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../features/profile/presentation/pages/profile_page_wrapper.dart';
import '../../features/settings/presentation/pages/system_settings_page.dart';
import '../../features/backup/presentation/pages/backup_page.dart';
import '../../features/logs/presentation/pages/logs_page.dart';
import '../../features/reports/presentation/pages/reports_page_wrapper.dart';
/// Layout principal avec navigation hybride
/// Bottom Navigation pour les sections principales + Drawer pour fonctions avancées
class MainNavigationLayout extends StatefulWidget {
const MainNavigationLayout({super.key});
@override
State<MainNavigationLayout> createState() => _MainNavigationLayoutState();
}
class _MainNavigationLayoutState extends State<MainNavigationLayout> {
int _selectedIndex = 0;
/// Obtient le dashboard approprié selon le rôle de l'utilisateur
Widget _getDashboardForRole(UserRole role) {
switch (role) {
case UserRole.superAdmin:
return const SuperAdminDashboard();
case UserRole.orgAdmin:
return const OrgAdminDashboard();
case UserRole.moderator:
return const ModeratorDashboard();
case UserRole.activeMember:
return const ActiveMemberDashboard();
case UserRole.simpleMember:
return const SimpleMemberDashboard();
case UserRole.visitor:
return const VisitorDashboard();
}
}
List<Widget> _getPages(UserRole role) {
return [
_getDashboardForRole(role),
const MembersPageWrapper(), // Wrapper BLoC pour connexion API
const EventsPageWrapper(), // Wrapper BLoC pour connexion API
const MorePage(), // Page "Plus" qui affiche les options avancées
];
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
backgroundColor: ColorTokens.background,
body: SafeArea(
top: true, // Respecte le StatusBar
bottom: false, // Le BottomNavigationBar gère son propre SafeArea
child: IndexedStack(
index: _selectedIndex,
children: _getPages(state.effectiveRole),
),
),
bottomNavigationBar: SafeArea(
top: false,
child: Container(
decoration: const BoxDecoration(
color: ColorTokens.surface,
boxShadow: [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: 8,
offset: Offset(0, -2),
),
],
),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
backgroundColor: ColorTokens.surface,
selectedItemColor: ColorTokens.primary,
unselectedItemColor: ColorTokens.onSurfaceVariant,
selectedLabelStyle: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: TypographyTokens.labelSmall,
elevation: 0, // Géré par le Container
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard_outlined),
activeIcon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.people_outline),
activeIcon: Icon(Icons.people),
label: 'Membres',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_outlined),
activeIcon: Icon(Icons.event),
label: 'Événements',
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz_outlined),
activeIcon: Icon(Icons.more_horiz),
label: 'Plus',
),
],
),
),
),
);
},
);
}
}
/// Page "Plus" avec les fonctions avancées selon le rôle
class MorePage extends StatelessWidget {
const MorePage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return Container(
color: const Color(0xFFF8F9FA),
child: const Center(child: CircularProgressIndicator()),
);
}
return Container(
color: ColorTokens.background,
child: SingleChildScrollView(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la section
Text(
'Plus d\'Options',
style: TypographyTokens.headlineMedium.copyWith(
color: ColorTokens.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.lg),
// Profil utilisateur
_buildUserProfile(state),
const SizedBox(height: 16),
// Options selon le rôle
..._buildRoleBasedOptions(context, state),
const SizedBox(height: 16),
// Options communes
..._buildCommonOptions(context),
],
),
),
);
},
);
}
Widget _buildUserProfile(AuthAuthenticated state) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7),
borderRadius: BorderRadius.circular(25),
),
child: Center(
child: Text(
state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${state.user.firstName} ${state.user.lastName}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
),
const SizedBox(height: 2),
Text(
state.effectiveRole.displayName,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
state.user.email,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
],
),
);
}
List<Widget> _buildRoleBasedOptions(BuildContext context, AuthAuthenticated state) {
final options = <Widget>[];
// Options Super Admin uniquement
if (state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle('Administration Système'),
_buildOptionTile(
icon: Icons.people,
title: 'Gestion des utilisateurs',
subtitle: 'Utilisateurs Keycloak et rôles',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const UserManagementPage(),
),
);
},
),
_buildOptionTile(
icon: Icons.settings,
title: 'Paramètres Système',
subtitle: 'Configuration globale',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
},
),
_buildOptionTile(
icon: Icons.backup,
title: 'Sauvegarde & Restauration',
subtitle: 'Gestion des sauvegardes',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BackupPage(),
),
);
},
),
_buildOptionTile(
icon: Icons.article,
title: 'Logs & Monitoring',
subtitle: 'Surveillance et journaux',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const LogsPage(),
),
);
},
),
]);
}
// Options Admin+ (Admin Organisation et Super Admin)
if (state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin) {
options.addAll([
_buildSectionTitle('Rapports & Analytics'),
_buildOptionTile(
icon: Icons.assessment,
title: 'Rapports & Analytics',
subtitle: 'Statistiques détaillées',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
},
),
]);
}
return options;
}
List<Widget> _buildCommonOptions(BuildContext context) {
return [
_buildSectionTitle('Général'),
_buildOptionTile(
icon: Icons.payment,
title: 'Cotisations',
subtitle: 'Gérer les cotisations',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CotisationsPageWrapper(),
),
);
},
),
_buildOptionTile(
icon: Icons.how_to_reg,
title: 'Demandes d\'adhésion',
subtitle: 'Demandes d\'adhésion à une organisation',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AdhesionsPageWrapper(),
),
);
},
),
_buildOptionTile(
icon: Icons.volunteer_activism,
title: 'Demandes d\'aide',
subtitle: 'Solidarité demandes d\'aide',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DemandesAidePageWrapper(),
),
);
},
),
_buildOptionTile(
icon: Icons.person,
title: 'Mon Profil',
subtitle: 'Modifier mes informations',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProfilePageWrapper(),
),
);
},
),
_buildOptionTile(
icon: Icons.notifications,
title: 'Notifications',
subtitle: 'Gérer les notifications',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationsPageWrapper(),
),
);
},
),
_buildOptionTile(
icon: Icons.help,
title: 'Aide & Support',
subtitle: 'Documentation et support',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const HelpSupportPage(),
),
);
},
),
_buildOptionTile(
icon: Icons.info,
title: 'À propos',
subtitle: 'Version et informations',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AboutPage(),
),
);
},
),
const SizedBox(height: 16),
_buildOptionTile(
icon: Icons.logout,
title: 'Déconnexion',
subtitle: 'Se déconnecter de l\'application',
color: Colors.red,
onTap: () {
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
];
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
bottom: 8,
),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
);
}
Widget _buildOptionTile({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? color,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: (color ?? const Color(0xFF6C5CE7)).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
icon,
color: color ?? const Color(0xFF6C5CE7),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: color ?? const Color(0xFF374151),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
const Icon(
Icons.chevron_right,
color: Color(0xFF6B7280),
size: 16,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,214 @@
/// Client HTTP Dio configuré pour l'API UnionFlow
library dio_client;
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/environment.dart';
/// Configuration du client HTTP Dio
class DioClient {
static const int _connectTimeout = 30000; // 30 secondes
static const int _receiveTimeout = 30000; // 30 secondes
static const int _sendTimeout = 30000; // 30 secondes
late final Dio _dio;
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
DioClient() {
_dio = Dio();
_configureDio();
}
/// Configuration du client Dio
void _configureDio() {
// Configuration de base - URL depuis AppConfig
_dio.options = BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(milliseconds: _connectTimeout),
receiveTimeout: const Duration(milliseconds: _receiveTimeout),
sendTimeout: const Duration(milliseconds: _sendTimeout),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
// Intercepteur d'authentification
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// Ajouter le token d'authentification si disponible
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
// Gestion des erreurs d'authentification
if (error.response?.statusCode == 401) {
// Token expiré, essayer de le rafraîchir
final refreshed = await _refreshToken();
if (refreshed) {
// Réessayer la requête avec le nouveau token
final token = await _secureStorage.read(key: 'keycloak_webview_access_token');
if (token != null) {
error.requestOptions.headers['Authorization'] = 'Bearer $token';
final response = await _dio.fetch(error.requestOptions);
handler.resolve(response);
return;
}
}
}
handler.next(error);
},
));
// Logger uniquement en mode développement
if (AppConfig.enableLogging) {
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
logPrint: (obj) => print('DIO: $obj'),
),
);
}
}
/// Rafraîchit le token d'authentification
Future<bool> _refreshToken() async {
try {
final refreshToken = await _secureStorage.read(key: 'keycloak_webview_refresh_token');
if (refreshToken == null) return false;
final response = await Dio().post(
AppConfig.keycloakTokenUrl,
data: {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
'client_id': 'unionflow-mobile',
},
options: Options(
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
),
);
if (response.statusCode == 200) {
final data = response.data;
await _secureStorage.write(key: 'keycloak_webview_access_token', value: data['access_token']);
if (data['refresh_token'] != null) {
await _secureStorage.write(key: 'keycloak_webview_refresh_token', value: data['refresh_token']);
}
return true;
}
} catch (e) {
// Erreur lors du rafraîchissement, l'utilisateur devra se reconnecter
}
return false;
}
/// Obtient l'instance Dio configurée
Dio get dio => _dio;
/// Méthodes de convenance pour les requêtes HTTP
/// GET request
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) {
return _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
/// POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) {
return _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// PATCH request
Future<Response<T>> patch<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return _dio.patch<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:connectivity_plus/connectivity_plus.dart';
/// Interface pour vérifier la connectivité réseau
abstract class NetworkInfo {
Future<bool> get isConnected;
}
/// Implémentation de NetworkInfo utilisant connectivity_plus
class NetworkInfoImpl implements NetworkInfo {
final Connectivity connectivity;
NetworkInfoImpl(this.connectivity);
@override
Future<bool> get isConnected async {
final result = await connectivity.checkConnectivity();
return result.any((r) => r != ConnectivityResult.none);
}
}

View File

@@ -0,0 +1,418 @@
/// Gestionnaire de cache multi-niveaux ultra-performant
/// Cache mémoire + disque avec TTL adaptatif selon les rôles
library dashboard_cache_manager;
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/authentication/data/models/user_role.dart';
/// Gestionnaire de cache intelligent avec stratégie multi-niveaux
///
/// Niveaux de cache :
/// 1. Cache mémoire (ultra-rapide, volatile)
/// 2. Cache disque (rapide, persistant)
/// 3. Cache réseau (si applicable)
///
/// Fonctionnalités :
/// - TTL adaptatif selon le rôle utilisateur
/// - Compression automatique des données volumineuses
/// - Invalidation intelligente
/// - Métriques de performance
/// - Nettoyage automatique
class DashboardCacheManager {
static final DashboardCacheManager _instance = DashboardCacheManager._internal();
factory DashboardCacheManager() => _instance;
DashboardCacheManager._internal();
/// Cache mémoire niveau 1 (ultra-rapide)
static final Map<String, _CachedData> _memoryCache = {};
/// Instance SharedPreferences pour le cache disque
static SharedPreferences? _prefs;
/// Taille maximale du cache mémoire (en nombre d'entrées)
static const int _maxMemoryCacheSize = 1000;
/// Taille maximale du cache disque (en MB)
static const int _maxDiskCacheSizeMB = 50;
/// TTL par défaut selon les rôles
static const Map<UserRole, Duration> _roleTTL = {
UserRole.superAdmin: Duration(hours: 2), // Cache plus long pour les admins
UserRole.orgAdmin: Duration(hours: 1), // Cache modéré pour les admins org
UserRole.moderator: Duration(minutes: 30), // Cache court pour les modérateurs
UserRole.activeMember: Duration(minutes: 15), // Cache très court pour les membres
UserRole.simpleMember: Duration(minutes: 10), // Cache minimal
UserRole.visitor: Duration(minutes: 5), // Cache très court pour les visiteurs
};
/// Compteurs de performance
static int _memoryHits = 0;
static int _memoryMisses = 0;
static int _diskHits = 0;
static int _diskMisses = 0;
/// Timer pour le nettoyage automatique
static Timer? _cleanupTimer;
/// Initialise le gestionnaire de cache
static Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
// Démarrer le nettoyage automatique toutes les 30 minutes
_cleanupTimer = Timer.periodic(
const Duration(minutes: 30),
(_) => _performAutomaticCleanup(),
);
debugPrint('DashboardCacheManager initialisé');
}
/// Dispose le gestionnaire de cache
static void dispose() {
_cleanupTimer?.cancel();
_memoryCache.clear();
}
/// Récupère une donnée du cache avec stratégie multi-niveaux
///
/// [key] - Clé unique de la donnée
/// [userRole] - Rôle de l'utilisateur pour le TTL adaptatif
/// [fromDisk] - Autoriser la récupération depuis le disque
static Future<T?> get<T>(
String key,
UserRole userRole, {
bool fromDisk = true,
}) async {
// Niveau 1 : Cache mémoire
final memoryData = _getFromMemory<T>(key);
if (memoryData != null) {
_memoryHits++;
return memoryData;
}
_memoryMisses++;
// Niveau 2 : Cache disque
if (fromDisk && _prefs != null) {
final diskData = await _getFromDisk<T>(key, userRole);
if (diskData != null) {
_diskHits++;
// Remettre en cache mémoire pour les prochains accès
await _putInMemory(key, diskData, userRole);
return diskData;
}
_diskMisses++;
}
return null;
}
/// Stocke une donnée dans le cache avec stratégie multi-niveaux
///
/// [key] - Clé unique de la donnée
/// [data] - Donnée à stocker
/// [userRole] - Rôle de l'utilisateur pour le TTL adaptatif
/// [toDisk] - Sauvegarder sur disque
/// [compress] - Compresser les données volumineuses
static Future<void> put<T>(
String key,
T data,
UserRole userRole, {
bool toDisk = true,
bool compress = false,
}) async {
// Niveau 1 : Cache mémoire
await _putInMemory(key, data, userRole);
// Niveau 2 : Cache disque
if (toDisk && _prefs != null) {
await _putOnDisk(key, data, userRole, compress: compress);
}
}
/// Invalide une entrée du cache
static Future<void> invalidate(String key) async {
// Supprimer du cache mémoire
_memoryCache.remove(key);
// Supprimer du cache disque
if (_prefs != null) {
await _prefs!.remove('cache_$key');
await _prefs!.remove('cache_meta_$key');
}
}
/// Invalide toutes les entrées d'un préfixe
static Future<void> invalidatePrefix(String prefix) async {
// Cache mémoire
final keysToRemove = _memoryCache.keys
.where((key) => key.startsWith(prefix))
.toList();
for (final key in keysToRemove) {
_memoryCache.remove(key);
}
// Cache disque
if (_prefs != null) {
final allKeys = _prefs!.getKeys();
final diskKeysToRemove = allKeys
.where((key) => key.startsWith('cache_$prefix'))
.toList();
for (final key in diskKeysToRemove) {
await _prefs!.remove(key);
}
}
}
/// Vide complètement le cache
static Future<void> clear() async {
_memoryCache.clear();
if (_prefs != null) {
final allKeys = _prefs!.getKeys();
final cacheKeys = allKeys.where((key) => key.startsWith('cache_')).toList();
for (final key in cacheKeys) {
await _prefs!.remove(key);
}
}
debugPrint('Cache complètement vidé');
}
/// Obtient les statistiques du cache
static Map<String, dynamic> getStats() {
final totalMemoryRequests = _memoryHits + _memoryMisses;
final totalDiskRequests = _diskHits + _diskMisses;
final memoryHitRate = totalMemoryRequests > 0
? (_memoryHits / totalMemoryRequests * 100)
: 0.0;
final diskHitRate = totalDiskRequests > 0
? (_diskHits / totalDiskRequests * 100)
: 0.0;
return {
'memoryCache': {
'hits': _memoryHits,
'misses': _memoryMisses,
'hitRate': memoryHitRate.toStringAsFixed(2),
'size': _memoryCache.length,
'maxSize': _maxMemoryCacheSize,
},
'diskCache': {
'hits': _diskHits,
'misses': _diskMisses,
'hitRate': diskHitRate.toStringAsFixed(2),
'maxSizeMB': _maxDiskCacheSizeMB,
},
};
}
/// Effectue un nettoyage manuel du cache
static Future<void> cleanup() async {
await _performAutomaticCleanup();
}
// === MÉTHODES PRIVÉES ===
/// Récupère une donnée du cache mémoire
static T? _getFromMemory<T>(String key) {
final cached = _memoryCache[key];
if (cached == null) return null;
// Vérifier l'expiration
if (cached.expiresAt.isBefore(DateTime.now())) {
_memoryCache.remove(key);
return null;
}
return cached.data as T?;
}
/// Stocke une donnée dans le cache mémoire
static Future<void> _putInMemory<T>(String key, T data, UserRole userRole) async {
// Vérifier la taille du cache et nettoyer si nécessaire
if (_memoryCache.length >= _maxMemoryCacheSize) {
await _cleanOldestMemoryEntries();
}
final ttl = _roleTTL[userRole] ?? const Duration(minutes: 5);
_memoryCache[key] = _CachedData(
data: data,
expiresAt: DateTime.now().add(ttl),
createdAt: DateTime.now(),
);
}
/// Récupère une donnée du cache disque
static Future<T?> _getFromDisk<T>(String key, UserRole userRole) async {
if (_prefs == null) return null;
// Récupérer les métadonnées
final metaJson = _prefs!.getString('cache_meta_$key');
if (metaJson == null) return null;
final meta = jsonDecode(metaJson) as Map<String, dynamic>;
final expiresAt = DateTime.parse(meta['expiresAt']);
// Vérifier l'expiration
if (expiresAt.isBefore(DateTime.now())) {
await _prefs!.remove('cache_$key');
await _prefs!.remove('cache_meta_$key');
return null;
}
// Récupérer les données
final dataJson = _prefs!.getString('cache_$key');
if (dataJson == null) return null;
try {
final data = jsonDecode(dataJson);
return data as T;
} catch (e) {
debugPrint('Erreur de désérialisation du cache: $e');
return null;
}
}
/// Stocke une donnée sur le cache disque
static Future<void> _putOnDisk<T>(
String key,
T data,
UserRole userRole, {
bool compress = false,
}) async {
if (_prefs == null) return;
try {
final ttl = _roleTTL[userRole] ?? const Duration(minutes: 5);
final expiresAt = DateTime.now().add(ttl);
// Sérialiser les données
final dataJson = jsonEncode(data);
// Métadonnées
final meta = {
'expiresAt': expiresAt.toIso8601String(),
'createdAt': DateTime.now().toIso8601String(),
'userRole': userRole.name,
'compressed': compress,
};
// Sauvegarder
await _prefs!.setString('cache_$key', dataJson);
await _prefs!.setString('cache_meta_$key', jsonEncode(meta));
} catch (e) {
debugPrint('Erreur de sérialisation du cache: $e');
}
}
/// Nettoie les entrées les plus anciennes du cache mémoire
static Future<void> _cleanOldestMemoryEntries() async {
if (_memoryCache.isEmpty) return;
// Trier par date de création et supprimer les 10% les plus anciennes
final entries = _memoryCache.entries.toList();
entries.sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
final toRemove = (entries.length * 0.1).ceil();
for (int i = 0; i < toRemove && i < entries.length; i++) {
_memoryCache.remove(entries[i].key);
}
}
/// Effectue un nettoyage automatique
static Future<void> _performAutomaticCleanup() async {
final now = DateTime.now();
// Nettoyer le cache mémoire expiré
_memoryCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
// Nettoyer le cache disque expiré
if (_prefs != null) {
final allKeys = _prefs!.getKeys();
final metaKeys = allKeys.where((key) => key.startsWith('cache_meta_')).toList();
for (final metaKey in metaKeys) {
final metaJson = _prefs!.getString(metaKey);
if (metaJson != null) {
try {
final meta = jsonDecode(metaJson) as Map<String, dynamic>;
final expiresAt = DateTime.parse(meta['expiresAt']);
if (expiresAt.isBefore(now)) {
final dataKey = metaKey.replaceFirst('cache_meta_', 'cache_');
await _prefs!.remove(dataKey);
await _prefs!.remove(metaKey);
}
} catch (e) {
// Supprimer les métadonnées corrompues
await _prefs!.remove(metaKey);
}
}
}
}
debugPrint('Nettoyage automatique du cache effectué');
}
/// Invalide le cache pour un rôle spécifique
static Future<void> invalidateForRole(UserRole role) async {
debugPrint('🗑️ Invalidation du cache pour le rôle: ${role.displayName}');
// Invalider le cache mémoire pour ce rôle
final keysToRemove = <String>[];
for (final key in _memoryCache.keys) {
if (key.contains(role.name)) {
keysToRemove.add(key);
}
}
for (final key in keysToRemove) {
_memoryCache.remove(key);
}
// Invalider le cache disque pour ce rôle
_prefs ??= await SharedPreferences.getInstance();
if (_prefs != null) {
final keys = _prefs!.getKeys();
final diskKeysToRemove = <String>[];
for (final key in keys) {
if (key.startsWith('cache_') && key.contains(role.name)) {
diskKeysToRemove.add(key);
}
}
for (final key in diskKeysToRemove) {
await _prefs!.remove(key);
// Supprimer aussi les métadonnées associées
final metaKey = key.replaceFirst('cache_', 'cache_meta_');
await _prefs!.remove(metaKey);
}
}
debugPrint('✅ Cache invalidé pour le rôle: ${role.displayName}');
}
}
/// Classe pour les données mises en cache
class _CachedData {
final dynamic data;
final DateTime expiresAt;
final DateTime createdAt;
_CachedData({
required this.data,
required this.expiresAt,
required this.createdAt,
});
}

View File

@@ -0,0 +1,17 @@
import 'package:dartz/dartz.dart';
import '../error/failures.dart';
/// Interface de base pour tous les cas d'usage
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
/// Cas d'usage sans paramètres
abstract class NoParamsUseCase<Type> {
Future<Either<Failure, Type>> call();
}
/// Classe pour représenter l'absence de paramètres
class NoParams {
const NoParams();
}

View File

@@ -0,0 +1,302 @@
/// Logger centralisé pour l'application
library logger;
import 'package:flutter/foundation.dart';
import '../config/environment.dart';
import '../constants/app_constants.dart';
/// Niveaux de log
enum LogLevel {
debug,
info,
warning,
error,
fatal,
}
/// Logger centralisé pour toute l'application
class AppLogger {
// Empêcher l'instanciation
AppLogger._();
/// Couleurs ANSI pour les logs en console
static const String _reset = '\x1B[0m';
static const String _red = '\x1B[31m';
static const String _green = '\x1B[32m';
static const String _yellow = '\x1B[33m';
static const String _blue = '\x1B[34m';
static const String _magenta = '\x1B[35m';
static const String _cyan = '\x1B[36m';
static const String _white = '\x1B[37m';
/// Log de niveau DEBUG (bleu)
static void debug(String message, {String? tag}) {
if (AppConfig.enableLogging && kDebugMode) {
_log(LogLevel.debug, message, tag: tag, color: _blue);
}
}
/// Log de niveau INFO (vert)
static void info(String message, {String? tag}) {
if (AppConfig.enableLogging && kDebugMode) {
_log(LogLevel.info, message, tag: tag, color: _green);
}
}
/// Log de niveau WARNING (jaune)
static void warning(String message, {String? tag}) {
if (AppConfig.enableLogging && kDebugMode) {
_log(LogLevel.warning, message, tag: tag, color: _yellow);
}
}
/// Log de niveau ERROR (rouge)
static void error(
String message, {
String? tag,
dynamic error,
StackTrace? stackTrace,
}) {
if (AppConfig.enableLogging) {
_log(LogLevel.error, message, tag: tag, color: _red);
if (error != null) {
_log(LogLevel.error, 'Error: $error', tag: tag, color: _red);
}
if (stackTrace != null) {
_log(LogLevel.error, 'StackTrace:\n$stackTrace', tag: tag, color: _red);
}
// Envoi au service de monitoring si configuré
if (AppConfig.enableCrashReporting) {
_sendToMonitoring(message, error, stackTrace);
}
}
}
/// Log de niveau FATAL (magenta)
static void fatal(
String message, {
String? tag,
dynamic error,
StackTrace? stackTrace,
}) {
if (AppConfig.enableLogging) {
_log(LogLevel.fatal, message, tag: tag, color: _magenta);
if (error != null) {
_log(LogLevel.fatal, 'Error: $error', tag: tag, color: _magenta);
}
if (stackTrace != null) {
_log(LogLevel.fatal, 'StackTrace:\n$stackTrace', tag: tag, color: _magenta);
}
// Envoi au service de monitoring si configuré
if (AppConfig.enableCrashReporting) {
_sendToMonitoring(message, error, stackTrace, isFatal: true);
}
}
}
/// Log d'une requête HTTP
static void httpRequest({
required String method,
required String url,
Map<String, dynamic>? headers,
dynamic body,
}) {
if (AppConfig.enableLogging && kDebugMode) {
final buffer = StringBuffer();
buffer.writeln('┌─────────────────────────────────────────────────');
buffer.writeln('│ HTTP REQUEST');
buffer.writeln('├─────────────────────────────────────────────────');
buffer.writeln('│ Method: $method');
buffer.writeln('│ URL: $url');
if (headers != null && headers.isNotEmpty) {
buffer.writeln('│ Headers:');
headers.forEach((key, value) {
buffer.writeln('$key: $value');
});
}
if (body != null) {
buffer.writeln('│ Body: $body');
}
buffer.writeln('└─────────────────────────────────────────────────');
_log(LogLevel.debug, buffer.toString(), color: _cyan);
}
}
/// Log d'une réponse HTTP
static void httpResponse({
required int statusCode,
required String url,
Map<String, dynamic>? headers,
dynamic body,
Duration? duration,
}) {
if (AppConfig.enableLogging && kDebugMode) {
final buffer = StringBuffer();
buffer.writeln('┌─────────────────────────────────────────────────');
buffer.writeln('│ HTTP RESPONSE');
buffer.writeln('├─────────────────────────────────────────────────');
buffer.writeln('│ Status: $statusCode');
buffer.writeln('│ URL: $url');
if (duration != null) {
buffer.writeln('│ Duration: ${duration.inMilliseconds}ms');
}
if (headers != null && headers.isNotEmpty) {
buffer.writeln('│ Headers:');
headers.forEach((key, value) {
buffer.writeln('$key: $value');
});
}
if (body != null) {
buffer.writeln('│ Body: $body');
}
buffer.writeln('└─────────────────────────────────────────────────');
final color = statusCode >= 200 && statusCode < 300 ? _green : _red;
_log(LogLevel.debug, buffer.toString(), color: color);
}
}
/// Log d'un événement BLoC
static void blocEvent(String blocName, String eventName, {dynamic data}) {
if (AppConfig.enableLogging && kDebugMode) {
final message = data != null
? '[$blocName] Event: $eventName | Data: $data'
: '[$blocName] Event: $eventName';
_log(LogLevel.debug, message, color: _cyan);
}
}
/// Log d'un changement d'état BLoC
static void blocState(String blocName, String stateName, {dynamic data}) {
if (AppConfig.enableLogging && kDebugMode) {
final message = data != null
? '[$blocName] State: $stateName | Data: $data'
: '[$blocName] State: $stateName';
_log(LogLevel.debug, message, color: _magenta);
}
}
/// Log d'une navigation
static void navigation(String from, String to) {
if (AppConfig.enableLogging && kDebugMode) {
_log(LogLevel.debug, 'Navigation: $from$to', color: _yellow);
}
}
/// Log d'une action utilisateur
static void userAction(String action, {Map<String, dynamic>? data}) {
if (AppConfig.enableLogging && kDebugMode) {
final message = data != null
? 'User Action: $action | Data: $data'
: 'User Action: $action';
_log(LogLevel.info, message, color: _green);
}
// Envoi au service d'analytics si configuré
if (AppConfig.enableAnalytics) {
_sendToAnalytics(action, data);
}
}
/// Méthode privée pour logger avec formatage
static void _log(
LogLevel level,
String message, {
String? tag,
String color = _white,
}) {
final timestamp = DateTime.now().toIso8601String();
final levelStr = level.name.toUpperCase().padRight(7);
final tagStr = tag != null ? '[$tag] ' : '';
if (kDebugMode) {
// En mode debug, utiliser les couleurs
debugPrint('$color$timestamp | $levelStr | $tagStr$message$_reset');
} else {
// En mode release, pas de couleurs
debugPrint('$timestamp | $levelStr | $tagStr$message');
}
}
/// Envoyer les erreurs à un service de monitoring
static void _sendToMonitoring(
String message,
dynamic error,
StackTrace? stackTrace, {
bool isFatal = false,
}) {
// Stub — implémenter avec Sentry ou Firebase Crashlytics quand intégré
// Exemple avec Sentry:
// Sentry.captureException(
// error,
// stackTrace: stackTrace,
// hint: Hint.withMap({'message': message}),
// );
}
/// Envoyer les événements à un service d'analytics
static void _sendToAnalytics(String action, Map<String, dynamic>? data) {
// Stub — implémenter avec Firebase Analytics ou Mixpanel quand intégré
// Exemple avec Firebase Analytics:
// FirebaseAnalytics.instance.logEvent(
// name: action,
// parameters: data,
// );
}
/// Divider pour séparer visuellement les logs
static void divider({String? title}) {
if (AppConfig.enableLogging && kDebugMode) {
if (title != null) {
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
debugPrint('$_cyan $title$_reset');
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
} else {
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
}
}
}
}
/// Extension pour faciliter le logging depuis n'importe où
extension LoggerExtension on Object {
/// Log debug
void logDebug(String message) {
AppLogger.debug(message, tag: runtimeType.toString());
}
/// Log info
void logInfo(String message) {
AppLogger.info(message, tag: runtimeType.toString());
}
/// Log warning
void logWarning(String message) {
AppLogger.warning(message, tag: runtimeType.toString());
}
/// Log error
void logError(String message, {dynamic error, StackTrace? stackTrace}) {
AppLogger.error(
message,
tag: runtimeType.toString(),
error: error,
stackTrace: stackTrace,
);
}
}