# PLAN DE DÉVELOPPEMENT - ApiService Singleton + Services Utilisateur/Amicale v2.0 🎯 **Objectif** : Transformer l'ApiService actuel en singleton et créer des services singleton pour l'utilisateur et l'amicale courante, puis refactoriser toute l'application pour utiliser ces nouveaux patterns. --- ## 📋 Vue d'ensemble du projet ### 🎯 Objectifs principaux - ✅ Convertir ApiService en singleton thread-safe - ✅ Créer CurrentUserService singleton pour l'utilisateur connecté - ✅ Créer CurrentAmicaleService singleton pour l'amicale courante - ✅ Renommer la Hive Box "users" en "user" (plus logique pour un seul utilisateur) - ✅ Éliminer l'instanciation multiple des services - ✅ Centraliser la gestion des sessions et de l'authentification - ✅ Simplifier l'accès aux données utilisateur/amicale dans toute l'app - ✅ Optimiser les performances et la mémoire - ✅ Améliorer la maintenance et la testabilité ### 📊 Estimation - **Durée totale** : 1.5 journée (12h) - **Complexité** : Moyenne-Élevée - **Impact** : Application entière - **Risque** : Faible (refactoring) --- ## 🚀 SESSION DE TRAVAIL - PLANNING DÉTAILLÉ ### Phase 0: Préparation Git et environnement (15 min) #### Tâche 0.1: Création de la branche (5 min) ```bash # Créer et basculer sur la nouvelle branche git checkout -b singletons # Vérifier qu'on est sur la bonne branche git branch ``` #### Tâche 0.2: Backup et documentation (10 min) ```bash # Créer un backup des fichiers critiques mkdir -p backups/$(date +%Y%m%d_%H%M%S) cp app/lib/core/services/api_service.dart backups/$(date +%Y%m%d_%H%M%S)/ cp app/lib/core/repositories/user_repository.dart backups/$(date +%Y%m%d_%H%M%S)/ cp app/lib/core/constants/app_keys.dart backups/$(date +%Y%m%d_%H%M%S)/ cp app/lib/main.dart backups/$(date +%Y%m%d_%H%M%S)/ # Documenter le démarrage de la refactorisation echo "$(date): Début refactorisation singletons" >> refactoring.log ``` **Actions** : - [ ] Créer la branche `singletons` - [ ] Sauvegarder les fichiers critiques - [ ] Documenter le début de la refactorisation - [ ] Commit initial de la branche ```bash git add . git commit -m "feat: création branche singletons - début refactorisation - Sauvegarde des fichiers critiques - Préparation transformation ApiService en singleton - Préparation création CurrentUserService et CurrentAmicaleService - Objectif: renommer Box users -> user" ``` ### Phase 1: Préparation et analyse (45 min) #### Tâche 1.1: Audit du code existant (20 min) ```bash # Rechercher toutes les utilisations des services grep -r "ApiService" app/lib --include="*.dart" > audit_apiservice.txt grep -r "UserRepository" app/lib --include="*.dart" > audit_userrepository.txt grep -r "getCurrentUser" app/lib --include="*.dart" > audit_getcurrentuser.txt grep -r "currentUser" app/lib --include="*.dart" > audit_currentuser.txt grep -r "usersBoxName" app/lib --include="*.dart" > audit_usersbox.txt ``` **Actions** : - [ ] Lister tous les fichiers utilisant ApiService - [ ] Identifier les patterns d'injection actuels - [ ] Noter les accès aux données utilisateur/amicale - [ ] Documenter les méthodes utilisées - [ ] Analyser les Box Hive users/amicale - [ ] Identifier toutes les occurrences de "usersBoxName" #### Tâche 1.2: Modification app_keys.dart pour renommage Box (10 min) **Fichier à modifier** : `app/lib/core/constants/app_keys.dart` **Actions** : - [ ] Changer `usersBoxName` en `userBoxName` - [ ] Ajouter une constante de migration si nécessaire - [ ] Documenter le changement **Code à modifier** : ```dart // Avant static const String usersBoxName = 'users'; // Après static const String userBoxName = 'user'; // Box pour l'utilisateur unique connecté static const String usersBoxNameOld = 'users'; // Pour migration si nécessaire ``` #### Tâche 1.3: Planification de la refactorisation (15 min) **Actions** : - [ ] Créer une liste des repositories à modifier - [ ] Identifier les pages/widgets accédant aux données utilisateur - [ ] Planifier l'ordre de modification (dépendances) - [ ] Préparer la stratégie de tests - [ ] Définir l'architecture des nouveaux services - [ ] Planifier la migration de la Box users -> user --- ### Phase 2: Renommage et migration de la Hive Box (30 min) #### Tâche 2.1: Mise à jour main.dart pour la nouvelle Box (15 min) **Fichier à modifier** : `app/lib/main.dart` **Actions** : - [ ] Remplacer `AppKeys.usersBoxName` par `AppKeys.userBoxName` - [ ] Modifier l'ouverture de la Box dans `_openEssentialHiveBoxes()` - [ ] Ajouter logique de migration depuis l'ancienne Box si nécessaire **Code à modifier** : ```dart Future _openEssentialHiveBoxes() async { final boxesToOpen = [ {'name': AppKeys.userBoxName, 'type': 'UserModel'}, // Changé {'name': AppKeys.amicaleBoxName, 'type': 'AmicaleModel'}, // ... autres boxes ]; // Logique de migration si l'ancienne box existe try { if (Hive.isBoxOpen(AppKeys.usersBoxNameOld)) { final oldBox = Hive.box(AppKeys.usersBoxNameOld); final newBox = await Hive.openBox(AppKeys.userBoxName); // Migrer les données if (oldBox.isNotEmpty && newBox.isEmpty) { final userData = oldBox.get('current_user'); if (userData != null) { await newBox.put('current_user', userData); debugPrint('✅ Migration de users -> user réussie'); } } // Fermer et supprimer l'ancienne box await oldBox.close(); await Hive.deleteBoxFromDisk(AppKeys.usersBoxNameOld); debugPrint('✅ Ancienne box users supprimée'); } } catch (e) { debugPrint('⚠️ Erreur migration box users: $e'); } } ``` #### Tâche 2.2: Mise à jour UserRepository pour la nouvelle Box (15 min) **Fichier à modifier** : `app/lib/core/repositories/user_repository.dart` **Actions** : - [ ] Remplacer toutes les occurrences de `AppKeys.usersBoxName` par `AppKeys.userBoxName` - [ ] Modifier les getters de Box - [ ] Tester que la compilation passe **Code à modifier** : ```dart // Avant Box get _userBox => Hive.box(AppKeys.usersBoxName); // Après Box get _userBox => Hive.box(AppKeys.userBoxName); ``` --- ### Phase 3: Création du nouveau ApiService Singleton (45 min) #### Tâche 3.1: Backup du code existant (5 min) ```bash # Sauvegarder l'ApiService actuel cp app/lib/core/services/api_service.dart app/lib/core/services/api_service_backup.dart ``` #### Tâche 3.2: Refactorisation ApiService en Singleton (40 min) **Fichier à modifier** : `app/lib/core/services/api_service.dart` **Actions** : - [ ] Implémenter le pattern Singleton thread-safe - [ ] Ajouter méthode d'initialisation statique - [ ] Conserver toute la logique d'environnement existante - [ ] Améliorer la gestion des erreurs - [ ] Ajouter logging pour debug - [ ] Maintenir la compatibilité des méthodes existantes **Code à implémenter** : ```dart class ApiService { static ApiService? _instance; static final Object _lock = Object(); // ... propriétés existantes conservées ... final Dio _dio = Dio(); late final String _baseUrl; late final String _appIdentifier; String? _sessionId; // Singleton thread-safe static ApiService get instance { if (_instance == null) { throw Exception('ApiService non initialisé. Appelez initialize() d\'abord.'); } return _instance!; } static Future initialize() async { if (_instance == null) { _instance = ApiService._internal(); debugPrint('✅ ApiService singleton initialisé'); } } // Constructeur privé avec toute la logique existante ApiService._internal() { _configureEnvironment(); _dio.options.baseUrl = _baseUrl; _dio.options.connectTimeout = AppKeys.connectionTimeout; _dio.options.receiveTimeout = AppKeys.receiveTimeout; final headers = Map.from(AppKeys.defaultHeaders); headers['X-App-Identifier'] = _appIdentifier; _dio.options.headers.addAll(headers); _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { if (_sessionId != null) { options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId'; } handler.next(options); }, onError: (DioException error, handler) { if (error.response?.statusCode == 401) { _sessionId = null; } handler.next(error); }, )); debugPrint('🔗 ApiService configuré pour $_baseUrl'); } // Toutes les méthodes existantes restent identiques // (get, post, put, delete, login, logout, etc.) // Méthode de nettoyage pour les tests static void reset() { _instance = null; } } ``` --- ### Phase 4: Création des Services Singleton Utilisateur/Amicale (90 min) #### Tâche 4.1: Création CurrentUserService (45 min) **Fichier à créer** : `app/lib/core/services/current_user_service.dart` **Actions** : - [ ] Créer la classe singleton CurrentUserService - [ ] Implémenter la gestion du cache utilisateur - [ ] Ajouter méthodes de persistence avec Hive (nouvelle Box user) - [ ] Implémenter les getters utiles (role, permissions, etc.) - [ ] Ajouter gestion des sessions - [ ] Implémenter les méthodes de logout/login **Code à implémenter** : ```dart import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; class CurrentUserService extends ChangeNotifier { static CurrentUserService? _instance; static CurrentUserService get instance => _instance ??= CurrentUserService._internal(); CurrentUserService._internal(); UserModel? _currentUser; // === GETTERS === UserModel? get currentUser => _currentUser; bool get isLoggedIn => _currentUser?.hasValidSession ?? false; int get userRole => _currentUser?.role ?? 0; int? get userId => _currentUser?.id; String? get userEmail => _currentUser?.email; String? get userName => _currentUser?.name; String? get userFirstName => _currentUser?.firstName; String? get sessionId => _currentUser?.sessionId; int? get fkEntite => _currentUser?.fkEntite; String? get userPhone => _currentUser?.phone; String? get userMobile => _currentUser?.mobile; // Vérifications de rôles bool get isUser => userRole == 1; bool get isAdminAmicale => userRole == 2; bool get isSuperAdmin => userRole >= 3; bool get canAccessAdmin => isAdminAmicale || isSuperAdmin; // === SETTERS === Future setUser(UserModel? user) async { _currentUser = user; await _saveToHive(); notifyListeners(); debugPrint('👤 Utilisateur défini: ${user?.email ?? 'null'}'); // Auto-synchroniser l'amicale si l'utilisateur a une entité if (user?.fkEntite != null) { await CurrentAmicaleService.instance.loadUserAmicale(); } else { await CurrentAmicaleService.instance.clearAmicale(); } } Future updateUser(UserModel updatedUser) async { _currentUser = updatedUser; await _saveToHive(); notifyListeners(); debugPrint('👤 Utilisateur mis à jour: ${updatedUser.email}'); } Future clearUser() async { final userEmail = _currentUser?.email; _currentUser = null; await _clearFromHive(); notifyListeners(); debugPrint('👤 Utilisateur effacé: $userEmail'); } // === PERSISTENCE HIVE (nouvelle Box user) === Future _saveToHive() async { try { if (_currentUser != null) { final box = Hive.box(AppKeys.userBoxName); // Nouvelle Box await box.clear(); await box.put('current_user', _currentUser!); debugPrint('💾 Utilisateur sauvegardé dans Box user'); } } catch (e) { debugPrint('❌ Erreur sauvegarde utilisateur Hive: $e'); } } Future _clearFromHive() async { try { final box = Hive.box(AppKeys.userBoxName); // Nouvelle Box await box.clear(); debugPrint('🗑️ Box user effacée'); } catch (e) { debugPrint('❌ Erreur effacement utilisateur Hive: $e'); } } Future loadFromHive() async { try { final box = Hive.box(AppKeys.userBoxName); // Nouvelle Box final user = box.get('current_user'); if (user?.hasValidSession == true) { _currentUser = user; debugPrint('📥 Utilisateur chargé depuis Hive: ${user.email}'); } else { _currentUser = null; debugPrint('ℹ️ Aucun utilisateur valide trouvé dans Hive'); } notifyListeners(); } catch (e) { debugPrint('❌ Erreur chargement utilisateur depuis Hive: $e'); _currentUser = null; } } // === MÉTHODES UTILITAIRES === Future updateLastPath(String path) async { if (_currentUser != null) { await updateUser(_currentUser!.copyWith(lastPath: path)); } } String? getLastPath() => _currentUser?.lastPath; String getDefaultRoute() { if (!isLoggedIn) return '/'; return canAccessAdmin ? '/admin' : '/user'; } String getRoleLabel() { switch (userRole) { case 1: return 'Utilisateur'; case 2: return 'Admin Amicale'; case 3: return 'Super Admin'; default: return 'Inconnu'; } } bool hasPermission(String permission) { switch (permission) { case 'admin': return canAccessAdmin; case 'super_admin': return isSuperAdmin; case 'manage_amicale': return canAccessAdmin; case 'manage_users': return isSuperAdmin; default: return isLoggedIn; } } // === RESET POUR TESTS === static void reset() { _instance?._currentUser = null; _instance = null; } } ``` #### Tâche 4.2: Création CurrentAmicaleService (45 min) **Fichier à créer** : `app/lib/core/services/current_amicale_service.dart` **Actions** : - [ ] Créer la classe singleton CurrentAmicaleService - [ ] Implémenter la gestion du cache amicale - [ ] Ajouter méthodes de persistence avec Hive - [ ] Implémenter les getters utiles - [ ] Ajouter synchronisation avec CurrentUserService **Code à implémenter** : ```dart import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/amicale_model.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; class CurrentAmicaleService extends ChangeNotifier { static CurrentAmicaleService? _instance; static CurrentAmicaleService get instance => _instance ??= CurrentAmicaleService._internal(); CurrentAmicaleService._internal(); AmicaleModel? _currentAmicale; // === GETTERS === AmicaleModel? get currentAmicale => _currentAmicale; bool get hasAmicale => _currentAmicale != null; int? get amicaleId => _currentAmicale?.id; String? get amicaleName => _currentAmicale?.name; String? get amicaleEmail => _currentAmicale?.email; String? get amicalePhone => _currentAmicale?.phone; String? get amicaleMobile => _currentAmicale?.mobile; String? get amicaleAddress => _currentAmicale != null ? '${_currentAmicale!.adresse1} ${_currentAmicale!.adresse2}'.trim() : null; String? get amicaleFullAddress => _currentAmicale != null ? '${amicaleAddress ?? ''} ${_currentAmicale!.codePostal} ${_currentAmicale!.ville}'.trim() : null; bool get amicaleIsActive => _currentAmicale?.chkActive ?? false; bool get isClient => _currentAmicale?.fkType == 1; // Géolocalisation bool get hasGpsCoordinates => _currentAmicale?.gpsLat.isNotEmpty == true && _currentAmicale?.gpsLng.isNotEmpty == true; double? get latitude => hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLat) : null; double? get longitude => hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLng) : null; // === SETTERS === Future setAmicale(AmicaleModel? amicale) async { _currentAmicale = amicale; await _saveToHive(); notifyListeners(); debugPrint('🏢 Amicale définie: ${amicale?.name ?? 'null'}'); } Future updateAmicale(AmicaleModel updatedAmicale) async { _currentAmicale = updatedAmicale; await _saveToHive(); notifyListeners(); debugPrint('🏢 Amicale mise à jour: ${updatedAmicale.name}'); } Future clearAmicale() async { final amicaleName = _currentAmicale?.name; _currentAmicale = null; await _clearFromHive(); notifyListeners(); debugPrint('🏢 Amicale effacée: $amicaleName'); } // === AUTO-LOAD BASÉ SUR L'UTILISATEUR === Future loadUserAmicale() async { final user = CurrentUserService.instance.currentUser; if (user?.fkEntite != null) { await loadAmicaleById(user!.fkEntite!); } else { await clearAmicale(); } } Future loadAmicaleById(int amicaleId) async { try { final box = Hive.box(AppKeys.amicaleBoxName); final amicale = box.get('current_amicale'); if (amicale?.id == amicaleId) { _currentAmicale = amicale; debugPrint('📥 Amicale chargée depuis Hive: ${amicale.name}'); } else { // Si l'amicale n'est pas la bonne, la chercher ou l'effacer _currentAmicale = null; debugPrint('⚠️ Amicale ${amicaleId} non trouvée dans Hive'); } notifyListeners(); } catch (e) { debugPrint('❌ Erreur chargement amicale depuis Hive: $e'); _currentAmicale = null; } } // === PERSISTENCE HIVE === Future _saveToHive() async { try { if (_currentAmicale != null) { final box = Hive.box(AppKeys.amicaleBoxName); await box.clear(); await box.put('current_amicale', _currentAmicale!); debugPrint('💾 Amicale sauvegardée dans Hive'); } } catch (e) { debugPrint('❌ Erreur sauvegarde amicale Hive: $e'); } } Future _clearFromHive() async { try { final box = Hive.box(AppKeys.amicaleBoxName); await box.clear(); debugPrint('🗑️ Box amicale effacée'); } catch (e) { debugPrint('❌ Erreur effacement amicale Hive: $e'); } } Future loadFromHive() async { try { final box = Hive.box(AppKeys.amicaleBoxName); _currentAmicale = box.get('current_amicale'); if (_currentAmicale != null) { debugPrint('📥 Amicale chargée depuis Hive: ${_currentAmicale!.name}'); } else { debugPrint('ℹ️ Aucune amicale trouvée dans Hive'); } notifyListeners(); } catch (e) { debugPrint('❌ Erreur chargement amicale depuis Hive: $e'); _currentAmicale = null; } } // === RESET POUR TESTS === static void reset() { _instance?._currentAmicale = null; _instance = null; } } ``` --- ### Phase 5: Modification du main.dart (20 min) #### Tâche 5.1: Intégration dans main.dart (20 min) **Fichier à modifier** : `app/lib/main.dart` **Actions** : - [ ] Ajouter l'initialisation des nouveaux services dans `_initializeServices()` - [ ] Gérer les erreurs d'initialisation - [ ] Ajouter logging approprié - [ ] Charger les données au démarrage **Code à ajouter** : ```dart Future _initializeServices() async { try { // Initialiser ApiService en premier await ApiService.initialize(); debugPrint('✅ ApiService singleton initialisé'); // Les services CurrentUserService et CurrentAmicaleService s'initialisent automatiquement // au premier accès via le pattern singleton lazy debugPrint('✅ CurrentUserService prêt'); debugPrint('✅ CurrentAmicaleService prêt'); // Charger les données depuis Hive au démarrage await CurrentUserService.instance.loadFromHive(); await CurrentAmicaleService.instance.loadFromHive(); debugPrint('✅ Données utilisateur/amicale chargées depuis Hive'); await AppInfoService.initialize(); debugPrint('✅ Tous les services initialisés avec succès'); } catch (e) { debugPrint('❌ Erreur lors de l\'initialisation des services: $e'); rethrow; // Important pour arrêter l'app si les services critiques échouent } } ``` **Imports à ajouter** : ```dart import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:geosector_app/core/services/current_amicale_service.dart'; ``` --- ### Phase 6: Commit intermédiaire de sécurité (10 min) #### Tâche 6.1: Commit des services créés (10 min) ```bash # Ajouter tous les nouveaux fichiers git add app/lib/core/services/current_user_service.dart git add app/lib/core/services/current_amicale_service.dart git add app/lib/core/services/api_service.dart git add app/lib/core/constants/app_keys.dart git add app/lib/main.dart # Commit intermédiaire git commit -m "feat: création services singleton et renommage Box Services créés: ✅ CurrentUserService singleton pour utilisateur connecté ✅ CurrentAmicaleService singleton pour amicale courante ✅ ApiService transformé en singleton Box Hive: ✅ Renommage users -> user (plus logique) ✅ Migration automatique des données ✅ Services intégrés dans main.dart État: Services créés, prêt pour refactorisation repositories" ``` **Actions** : - [ ] Compiler et vérifier qu'il n'y a pas d'erreurs critiques - [ ] Faire le commit intermédiaire - [ ] Documenter l'avancement --- ### Phase 7: Modification de l'App principale (20 min) #### Tâche 7.1: Refactorisation app.dart (20 min) **Fichier à modifier** : `app/lib/app.dart` **Actions** : - [ ] Supprimer l'injection d'ApiService et UserRepository dans les constructeurs - [ ] Simplifier l'instanciation des repositories - [ ] Utiliser les services singleton - [ ] Simplifier les passages de paramètres **Avant** : ```dart // Création avec injection final apiService = ApiService(); final userRepository = UserRepository(apiService); ``` **Après** : ```dart // Utilisation des singletons (plus besoin de créer quoi que ce soit) // Les services sont accessibles globalement via .instance ``` --- ### Phase 8: Refactorisation des Repositories (90 min) #### Tâche 8.1: UserRepository - Refactorisation majeure (40 min) **Fichier** : `app/lib/core/repositories/user_repository.dart` **Actions** : - [ ] Supprimer ApiService du constructeur - [ ] Utiliser `ApiService.instance` dans les méthodes - [ ] Remplacer la gestion interne par CurrentUserService - [ ] Simplifier les méthodes d'accès aux données utilisateur - [ ] Déléguer la persistence à CurrentUserService - [ ] Mettre à jour toutes les références à la Box (userBoxName) **Refactorisation majeure** : ```dart class UserRepository extends ChangeNotifier { // Plus d'injection d'ApiService - constructeur vide UserRepository(); // === DÉLÉGATION AUX SERVICES === UserModel? get currentUser => CurrentUserService.instance.currentUser; bool get isLoggedIn => CurrentUserService.instance.isLoggedIn; int get userRole => CurrentUserService.instance.userRole; // Getters délégués bool get isUser => CurrentUserService.instance.isUser; bool get isAdminAmicale => CurrentUserService.instance.isAdminAmicale; bool get isSuperAdmin => CurrentUserService.instance.isSuperAdmin; int? get userId => CurrentUserService.instance.userId; // === LOGIN SIMPLIFIÉ === Future login(String username, String password, {required String type}) async { try { debugPrint('🔐 Tentative de connexion: $username'); final apiResult = await ApiService.instance.login(username, password, type: type); if (apiResult['status'] == 'success') { // Créer l'utilisateur final user = _processUserData( apiResult['user'], apiResult['session_id'], apiResult['session_expiry'] ); // Sauvegarder via le service (qui gérera automatiquement l'amicale) await CurrentUserService.instance.setUser(user); // Traiter l'amicale si présente dans la réponse if (apiResult['amicale'] != null) { final amicale = AmicaleModel.fromJson(apiResult['amicale']); await CurrentAmicaleService.instance.setAmicale(amicale); } // Traiter les autres données (opérations, secteurs, etc.) await _processLoginData(apiResult); debugPrint('✅ Connexion réussie'); return true; } debugPrint('❌ Connexion échouée: ${apiResult['message']}'); return false; } catch (e) { debugPrint('❌ Erreur connexion: $e'); return false; } } // === LOGOUT SIMPLIFIÉ === Future logout(BuildContext context) async { try { debugPrint('🚪 Déconnexion en cours...'); await ApiService.instance.logout(); await CurrentUserService.instance.clearUser(); await CurrentAmicaleService.instance.clearAmicale(); // Nettoyer toutes les autres données await _clearAllData(); if (context.mounted) { context.go('/'); } debugPrint('✅ Déconnexion réussie'); return true; } catch (e) { debugPrint('❌ Erreur déconnexion: $e'); if (context.mounted) { context.go('/'); // Forcer la redirection même en cas d'erreur } return false; } } // === NAVIGATION === void navigateAfterLogin(BuildContext context) { if (context.mounted) { final route = CurrentUserService.instance.getDefaultRoute(); context.go(route); } } // === MÉTHODES UTILITAIRES === Future updateLastPath(String path) async { await CurrentUserService.instance.updateLastPath(path); } String? getLastPath() => CurrentUserService.instance.getLastPath(); // Simplifier les getters d'amicale aussi AmicaleModel? getCurrentUserAmicale() => CurrentAmicaleService.instance.currentAmicale; // Les autres méthodes restent mais sont simplifiées... } ``` #### Tâche 8.2: AmicaleRepository (20 min) **Fichier** : `app/lib/core/repositories/amicale_repository.dart` **Actions** : - [ ] Supprimer ApiService du constructeur - [ ] Utiliser `ApiService.instance` dans les méthodes - [ ] Simplifier l'accès à l'amicale courante - [ ] Intégrer avec CurrentAmicaleService #### Tâche 8.3: MembreRepository (15 min) **Fichier** : `app/lib/core/repositories/membre_repository.dart` **Actions** : - [ ] Supprimer ApiService du constructeur - [ ] Utiliser `ApiService.instance` dans les méthodes - [ ] Utiliser CurrentUserService pour les vérifications de permissions #### Tâche 8.4: Autres repositories (15 min) **Fichiers à traiter** : - `client_repository.dart` - `operation_repository.dart` - `sector_repository.dart` - `passage_repository.dart` **Actions par repository** : - [ ] Supprimer injection ApiService - [ ] Remplacer par `ApiService.instance` - [ ] Utiliser CurrentUserService pour les données utilisateur --- ### Phase 9: Modification des Pages principales (120 min) #### Tâche 9.1: Pages d'authentification (30 min) **Fichiers** : - `app/lib/presentation/auth/login_page.dart` - `app/lib/presentation/auth/register_page.dart` **Actions** : - [ ] Supprimer UserRepository des constructeurs - [ ] Utiliser CurrentUserService directement - [ ] Simplifier la logique de navigation post-login **Exemple pour LoginPage** : ```dart class LoginPage extends StatefulWidget { // Plus besoin d'injection const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State { // Utilisation directe des services Future _handleLogin() async { final userRepo = UserRepository(); // Sans injection, constructeur vide final success = await userRepo.login(username, password, type: type); if (success && mounted) { // Navigation automatique basée sur le rôle final route = CurrentUserService.instance.getDefaultRoute(); context.go(route); } } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: CurrentUserService.instance, builder: (context, child) { // Redirection automatique si déjà connecté if (CurrentUserService.instance.isLoggedIn) { WidgetsBinding.instance.addPostFrameCallback((_) { final route = CurrentUserService.instance.getDefaultRoute(); context.go(route); }); } return Scaffold( // ... UI de login ); }, ); } } ``` #### Tâche 9.2: Dashboard pages (30 min) **Fichiers** : - `app/lib/presentation/admin/admin_dashboard_page.dart` - `app/lib/presentation/user/user_dashboard_page.dart` **Actions** : - [ ] Supprimer injections de services des constructeurs - [ ] Utiliser CurrentUserService et CurrentAmicaleService directement - [ ] Simplifier l'affichage des données utilisateur/amicale **Exemple pour AdminDashboardPage** : ```dart class AdminDashboardPage extends StatelessWidget { const AdminDashboardPage({super.key}); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: Listenable.merge([ CurrentUserService.instance, CurrentAmicaleService.instance, ]), builder: (context, child) { final userService = CurrentUserService.instance; final amicaleService = CurrentAmicaleService.instance; // Vérification d'authentification if (!userService.isLoggedIn) { WidgetsBinding.instance.addPostFrameCallback((_) { context.go('/'); }); return const SizedBox.shrink(); } return Scaffold( appBar: AppBar( title: Text('Bonjour ${userService.userFirstName ?? userService.userName}'), actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: () => _handleLogout(context), ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Widget d'informations utilisateur const UserInfoWidget(), const SizedBox(height: 16), // Widget d'informations amicale if (amicaleService.hasAmicale) const AmicaleInfoWidget(), const SizedBox(height: 24), // Actions selon le rôle if (userService.isSuperAdmin) _buildSuperAdminActions(context), if (userService.isAdminAmicale) _buildAmicaleAdminActions(context), ], ), ), ); }, ); } Future _handleLogout(BuildContext context) async { final userRepo = UserRepository(); await userRepo.logout(context); } // ... autres méthodes } ``` #### Tâche 9.3: Pages de gestion (30 min) **Fichiers** : - `app/lib/presentation/admin/admin_amicale_page.dart` - `app/lib/presentation/admin/admin_statistics_page.dart` - `app/lib/presentation/user/map_page.dart` **Actions** : - [ ] Supprimer paramètres UserRepository et ApiService - [ ] Utiliser les services singleton - [ ] Simplifier l'accès aux données #### Tâche 9.4: Pages formulaires (30 min) **Fichiers contenant des formulaires** : - Forms de création/édition - Pages de configuration - Pages de paramètres **Actions** : - [ ] Identifier tous les formulaires utilisant UserRepository - [ ] Remplacer par CurrentUserService/CurrentAmicaleService - [ ] Simplifier les validations de permissions --- ### Phase 10: Modification des Widgets (90 min) #### Tâche 10.1: Création de widgets d'information (30 min) **Nouveaux widgets à créer** : - `app/lib/presentation/widgets/user/user_info_widget.dart` - `app/lib/presentation/widgets/user/amicale_info_widget.dart` **Code UserInfoWidget** : ```dart import 'package:flutter/material.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; class UserInfoWidget extends StatelessWidget { const UserInfoWidget({super.key}); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: CurrentUserService.instance, builder: (context, child) { final userService = CurrentUserService.instance; final user = userService.currentUser; if (user == null) { return const SizedBox.shrink(); } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.person), const SizedBox(width: 8), Text( 'Informations utilisateur', style: Theme.of(context).textTheme.titleMedium, ), ], ), const SizedBox(height: 12), Text( '${user.firstName} ${user.name}', style: Theme.of(context).textTheme.titleLarge, ), Text(user.email), Text('Rôle: ${userService.getRoleLabel()}'), if (user.phone?.isNotEmpty == true) Text('Tél: ${user.phone}'), if (user.mobile?.isNotEmpty == true) Text('Mobile: ${user.mobile}'), ], ), ), ); }, ); } } ``` **Code AmicaleInfoWidget** : ```dart import 'package:flutter/material.dart'; import 'package:geosector_app/core/services/current_amicale_service.dart'; class AmicaleInfoWidget extends StatelessWidget { const AmicaleInfoWidget({super.key}); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: CurrentAmicaleService.instance, builder: (context, child) { final amicaleService = CurrentAmicaleService.instance; final amicale = amicaleService.currentAmicale; if (amicale == null) { return const SizedBox.shrink(); } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.business), const SizedBox(width: 8), Text( 'Mon amicale', style: Theme.of(context).textTheme.titleMedium, ), ], ), const SizedBox(height: 12), Text( amicale.name, style: Theme.of(context).textTheme.titleLarge, ), if (amicaleService.amicaleFullAddress?.isNotEmpty == true) Text(amicaleService.amicaleFullAddress!), if (amicale.email.isNotEmpty) Text('Email: ${amicale.email}'), if (amicale.phone.isNotEmpty) Text('Tél: ${amicale.phone}'), if (amicale.mobile.isNotEmpty) Text('Mobile: ${amicale.mobile}'), Row( children: [ Icon( amicale.chkActive ? Icons.check_circle : Icons.cancel, color: amicale.chkActive ? Colors.green : Colors.red, size: 16, ), const SizedBox(width: 4), Text(amicale.chkActive ? 'Active' : 'Inactive'), ], ), ], ), ), ); }, ); } } ``` #### Tâche 10.2: Widgets de tableaux (30 min) **Fichiers** : - `app/lib/presentation/widgets/tables/amicale_table_widget.dart` - `app/lib/presentation/widgets/tables/membre_table_widget.dart` - `app/lib/presentation/widgets/tables/user_table_widget.dart` **Actions** : - [ ] Supprimer injections de repositories des constructeurs - [ ] Utiliser CurrentUserService pour les vérifications de permissions - [ ] Simplifier la logique d'affichage conditionnel #### Tâche 10.3: Widgets de formulaires (30 min) **Fichiers** : - `app/lib/presentation/widgets/forms/amicale_form.dart` - `app/lib/presentation/widgets/forms/membre_form.dart` **Actions** : - [ ] Remplacer injections par services singleton - [ ] Utiliser CurrentUserService pour les validations - [ ] Simplifier les callbacks et la gestion d'état --- ### Phase 11: Mise à jour du Router et Navigation (45 min) #### Tâche 11.1: Configuration GoRouter (30 min) **Fichier** : `app/lib/core/routing/app_router.dart` **Actions** : - [ ] Supprimer injection de services dans les routes - [ ] Utiliser CurrentUserService pour les guards d'authentification - [ ] Simplifier la création des pages - [ ] Adapter le middleware d'authentification **Exemple de refactorisation** : **Avant** : ```dart GoRoute( path: '/admin', builder: (context, state) => AdminPage( userRepository: UserRepository(apiService), amicaleRepository: AmicaleRepository(apiService), ), ) ``` **Après** : ```dart GoRoute( path: '/admin', builder: (context, state) => const AdminPage(), redirect: (context, state) => AuthGuard.checkAdminAccess(), ) ``` #### Tâche 11.2: Création AuthGuard (15 min) **Fichier à créer** : `app/lib/core/routing/auth_guard.dart` **Code AuthGuard** : ```dart import 'package:geosector_app/core/services/current_user_service.dart'; class AuthGuard { static String? checkAuth() { final userService = CurrentUserService.instance; if (!userService.isLoggedIn) { return '/'; } return null; // Accès autorisé } static String? checkAdminAccess() { final userService = CurrentUserService.instance; if (!userService.isLoggedIn) { return '/'; } if (!userService.canAccessAdmin) { return '/user'; } return null; // Accès autorisé } static String? checkSuperAdminAccess() { final userService = CurrentUserService.instance; if (!userService.isLoggedIn) { return '/'; } if (!userService.isSuperAdmin) { return userService.canAccessAdmin ? '/admin' : '/user'; } return null; // Accès autorisé } } ``` --- ### Phase 12: Tests et validation (60 min) #### Tâche 12.1: Tests de compilation (15 min) **Actions** : - [ ] `flutter pub get` - [ ] `flutter analyze` - [ ] Corriger erreurs de compilation - [ ] Vérifier warnings #### Tâche 12.2: Tests fonctionnels de base (30 min) **Scénarios à tester** : - [ ] Démarrage de l'application - [ ] Authentification utilisateur avec différents rôles - [ ] Navigation entre pages - [ ] Affichage des données utilisateur/amicale - [ ] Déconnexion et reconnexion - [ ] Persistence des données au redémarrage - [ ] Migration de la Box users -> user #### Tâche 12.3: Tests des services singleton (15 min) **Actions** : - [ ] Tester CurrentUserService.instance - [ ] Tester CurrentAmicaleService.instance - [ ] Vérifier la persistence Hive avec nouvelle Box - [ ] Tester les listeners et notifications --- ### Phase 13: Tests unitaires (45 min) #### Tâche 13.1: Tests ApiService (15 min) **Fichier** : `test/core/services/api_service_test.dart` **Actions** : - [ ] Adapter tests existants pour le singleton - [ ] Tester initialisation - [ ] Tester thread-safety - [ ] Mocker les appels réseau #### Tâche 13.2: Tests CurrentUserService (15 min) **Fichier** : `test/core/services/current_user_service_test.dart` **Actions** : - [ ] Créer tests pour le singleton utilisateur - [ ] Tester setUser/clearUser - [ ] Tester persistence Hive avec nouvelle Box - [ ] Tester les getters de rôles #### Tâche 13.3: Tests CurrentAmicaleService (15 min) **Fichier** : `test/core/services/current_amicale_service_test.dart` **Actions** : - [ ] Créer tests pour le singleton amicale - [ ] Tester setAmicale/clearAmicale - [ ] Tester loadUserAmicale - [ ] Tester les getters utiles --- ### Phase 14: Optimisations et nettoyage (45 min) #### Tâche 14.1: Optimisation des performances (20 min) **Actions** : - [ ] Vérifier la réactivité des ListenableBuilder - [ ] Optimiser les notifications des services - [ ] Éliminer les rebuilds inutiles - [ ] Vérifier les memory leaks #### Tâche 14.2: Nettoyage du code (25 min) **Actions** : - [ ] Supprimer les imports inutiles - [ ] Nettoyer les constructeurs simplifiés - [ ] Uniformiser le code style - [ ] Supprimer les fichiers backup - [ ] Mettre à jour la documentation --- ### Phase 15: Commit final et documentation (30 min) #### Tâche 15.1: Commit final (15 min) ```bash # Ajouter tous les fichiers modifiés git add . # Commit final avec description complète git commit -m "feat: refactorisation complète vers architecture singleton 🚀 TRANSFORMATION MAJEURE: API & Services: ✅ ApiService transformé en singleton thread-safe ✅ CurrentUserService singleton pour utilisateur connecté ✅ CurrentAmicaleService singleton pour amicale courante Hive Box: ✅ Renommage users -> user (logique pour utilisateur unique) ✅ Migration automatique des données existantes ✅ Persistence optimisée dans les nouveaux services Repositories: ✅ UserRepository simplifié (plus d'injection ApiService) ✅ AmicaleRepository simplifié ✅ Tous les repositories utilisent ApiService.instance UI/UX: ✅ Pages sans injections de dépendances ✅ Widgets UserInfoWidget et AmicaleInfoWidget réactifs ✅ Navigation automatique basée sur les rôles ✅ ListenableBuilder pour réactivité en temps réel Architecture: ✅ Constructeurs ultra-simplifiés (plus de prop drilling) ✅ AuthGuard centralisé pour sécurité ✅ Code maintenable et performance optimisée Tests: ✅ Tests unitaires pour tous les nouveaux services ✅ Validation fonctionnelle complète BREAKING CHANGES: - Box Hive users renommée en user - Constructeurs de pages/widgets simplifiés - Pattern d'accès aux données utilisateur/amicale changé MIGRATION: Automatique au démarrage de l'app" # Push de la branche git push origin singletons ``` #### Tâche 15.2: Documentation finale (15 min) **Actions** : - [ ] Mettre à jour le README principal - [ ] Documenter les nouveaux services - [ ] Créer guide de migration pour l'équipe - [ ] Mettre à jour les commentaires dans le code **Documentation à créer** : `app/SINGLETONS_GUIDE.md` ````markdown # Guide des Services Singleton ## Vue d'ensemble Cette refactorisation introduit 3 services singleton pour simplifier l'architecture: ### CurrentUserService ```dart // Accès global à l'utilisateur connecté final user = CurrentUserService.instance.currentUser; final isAdmin = CurrentUserService.instance.canAccessAdmin; // Réactivité ListenableBuilder( listenable: CurrentUserService.instance, builder: (context, child) => Text(user?.name ?? 'Anonyme'), ) ``` ```` ### CurrentAmicaleService ```dart // Accès global à l'amicale courante final amicale = CurrentAmicaleService.instance.currentAmicale; final hasGps = CurrentAmicaleService.instance.hasGpsCoordinates; ``` ### ApiService ```dart // Singleton API final response = await ApiService.instance.get('/endpoint'); ``` ## Migration - Box `users` renommée en `user` (migration automatique) - Plus d'injection de dépendances dans les constructeurs - Widgets réactifs avec ListenableBuilder ``` --- ## 📁 Structure finale des fichiers modifiés/créés ```