Files
geo/app/PLAN2-APP.md
d6soft 7e6431b5aa 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
2025-06-05 17:02:11 +02:00

43 KiB
Raw Blame History

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)

# 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)

# 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
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)

# 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 :

// 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 :

Future<void> _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<UserModel>(AppKeys.usersBoxNameOld);
      final newBox = await Hive.openBox<UserModel>(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 :

// Avant
Box<UserModel> get _userBox => Hive.box<UserModel>(AppKeys.usersBoxName);

// Après
Box<UserModel> get _userBox => Hive.box<UserModel>(AppKeys.userBoxName);

Phase 3: Création du nouveau ApiService Singleton (45 min)

Tâche 3.1: Backup du code existant (5 min)

# 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 :

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<void> 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<String, String>.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 :

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<void> 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<void> updateUser(UserModel updatedUser) async {
    _currentUser = updatedUser;
    await _saveToHive();
    notifyListeners();
    debugPrint('👤 Utilisateur mis à jour: ${updatedUser.email}');
  }

  Future<void> clearUser() async {
    final userEmail = _currentUser?.email;
    _currentUser = null;
    await _clearFromHive();
    notifyListeners();
    debugPrint('👤 Utilisateur effacé: $userEmail');
  }

  // === PERSISTENCE HIVE (nouvelle Box user) ===
  Future<void> _saveToHive() async {
    try {
      if (_currentUser != null) {
        final box = Hive.box<UserModel>(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<void> _clearFromHive() async {
    try {
      final box = Hive.box<UserModel>(AppKeys.userBoxName); // Nouvelle Box
      await box.clear();
      debugPrint('🗑️ Box user effacée');
    } catch (e) {
      debugPrint('❌ Erreur effacement utilisateur Hive: $e');
    }
  }

  Future<void> loadFromHive() async {
    try {
      final box = Hive.box<UserModel>(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<void> 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 :

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<void> setAmicale(AmicaleModel? amicale) async {
    _currentAmicale = amicale;
    await _saveToHive();
    notifyListeners();
    debugPrint('🏢 Amicale définie: ${amicale?.name ?? 'null'}');
  }

  Future<void> updateAmicale(AmicaleModel updatedAmicale) async {
    _currentAmicale = updatedAmicale;
    await _saveToHive();
    notifyListeners();
    debugPrint('🏢 Amicale mise à jour: ${updatedAmicale.name}');
  }

  Future<void> clearAmicale() async {
    final amicaleName = _currentAmicale?.name;
    _currentAmicale = null;
    await _clearFromHive();
    notifyListeners();
    debugPrint('🏢 Amicale effacée: $amicaleName');
  }

  // === AUTO-LOAD BASÉ SUR L'UTILISATEUR ===
  Future<void> loadUserAmicale() async {
    final user = CurrentUserService.instance.currentUser;
    if (user?.fkEntite != null) {
      await loadAmicaleById(user!.fkEntite!);
    } else {
      await clearAmicale();
    }
  }

  Future<void> loadAmicaleById(int amicaleId) async {
    try {
      final box = Hive.box<AmicaleModel>(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<void> _saveToHive() async {
    try {
      if (_currentAmicale != null) {
        final box = Hive.box<AmicaleModel>(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<void> _clearFromHive() async {
    try {
      final box = Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
      await box.clear();
      debugPrint('🗑️ Box amicale effacée');
    } catch (e) {
      debugPrint('❌ Erreur effacement amicale Hive: $e');
    }
  }

  Future<void> loadFromHive() async {
   try {
      final box = Hive.box<AmicaleModel>(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 :

Future<void> _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 :

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)

# 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 :

// Création avec injection
final apiService = ApiService();
final userRepository = UserRepository(apiService);

Après :

// 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 :

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<bool> 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<bool> 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<void> 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 :

class LoginPage extends StatefulWidget {
  // Plus besoin d'injection
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  // Utilisation directe des services
  Future<void> _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 :

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<void> _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 :

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 :

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 :

GoRoute(
  path: '/admin',
  builder: (context, state) => AdminPage(
    userRepository: UserRepository(apiService),
    amicaleRepository: AmicaleRepository(apiService),
  ),
)

Après :

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 :

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)

# 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

# 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

// Accès global à l'amicale courante
final amicale = CurrentAmicaleService.instance.currentAmicale;
final hasGps = CurrentAmicaleService.instance.hasGpsCoordinates;

ApiService

// 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