Files
geo/app/PLAN2-APP.md
d6soft 86a9a35594 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

1538 lines
43 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** :
- [x] Créer la branche `singletons`
- [x] Sauvegarder les fichiers critiques
- [x] Documenter le début de la refactorisation
- [x] 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** :
- [x] Lister tous les fichiers utilisant ApiService
- [x] Identifier les patterns d'injection actuels
- [x] Noter les accès aux données utilisateur/amicale
- [x] Documenter les méthodes utilisées
- [x] Analyser les Box Hive users/amicale
- [x] 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** :
- [x] Changer `usersBoxName` en `userBoxName`
- [x] Ajouter une constante de migration si nécessaire
- [x] 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** :
- [x] Remplacer `AppKeys.usersBoxName` par `AppKeys.userBoxName`
- [x] Modifier l'ouverture de la Box dans `_openEssentialHiveBoxes()`
- [ ] Ajouter logique de migration depuis l'ancienne Box si nécessaire
**Code à modifier** :
```dart
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** :
- [x] Remplacer toutes les occurrences de `AppKeys.usersBoxName` par `AppKeys.userBoxName`
- [x] Modifier les getters de Box
- [x] Tester que la compilation passe
**Code à modifier** :
```dart
// 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)
```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<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** :
```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<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** :
```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<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** :
```dart
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** :
```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<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** :
```dart
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** :
```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<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** :
```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
```