- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
45 KiB
Executable File
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
usersBoxNameenuserBoxName - Ajouter une constante de migration si nécessaire
- Documenter le changement
Code modifié :
// 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: Correction des modèles et préparation (60 min) ✅
Tâche 2.1: Correction MembreModel selon les vrais champs (30 min) ✅
Fichier modifié : app/lib/core/data/models/membre_model.dart
Actions :
-
Corriger les champs selon les spécifications réelles :
final int idint? fkEntitefinal int roleint? fkTitreString? nameString? firstNameString? usernameString? sectNamefinal String emailString? phoneString? mobileDateTime? dateNaissanceDateTime? dateEmbauchefinal DateTime createdAtbool isActive
-
Adapter les annotations Hive @HiveField
-
Corriger fromJson() et toJson()
-
Mettre à jour copyWith()
Tâche 2.2: Correction ClientModel avec champs manquants (15 min) ✅
Fichier modifié : app/lib/core/data/models/client_model.dart
Actions :
- Ajouter les champs manqués :
chkStripe,createdAt,updatedAt - Mettre à jour les annotations Hive
- Corriger fromJson() et toJson()
- Mettre à jour copyWith()
Tâche 2.3: Correction des repositories selon les vrais modèles (15 min) ✅
Fichiers modifiés :
-
app/lib/core/repositories/membre_repository.dart -
app/lib/core/repositories/client_repository.dartActions : -
Adapter MembreRepository pour les vrais champs (
isActiveau lieu dechkActive, etc.) -
Corriger les méthodes create/update pour éviter les reconstructions manuelles
-
Utiliser copyWith() correctement
-
Simplifier la logique de création API
-
Corriger ClientRepository de la même manière
Phase 3: Renommage et migration de la Hive Box (30 min)
Tâche 3.1: Mise à jour main.dart pour la nouvelle Box (15 min)
Fichier à modifier : app/lib/main.dart
Actions :
- Remplacer
AppKeys.usersBoxNameparAppKeys.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 3.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.usersBoxNameparAppKeys.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 4: Création du nouveau ApiService Singleton (45 min)
Tâche 4.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 4.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 5: Création des Services Singleton Utilisateur/Amicale (90 min)
Tâche 5.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 5.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 6: Modification du main.dart (20 min)
Tâche 6.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 7: Commit intermédiaire de sécurité (10 min)
Tâche 7.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 8: Modification de l'App principale (20 min)
Tâche 8.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 9: Refactorisation des Repositories (90 min)
Tâche 9.1: UserRepository - Refactorisation majeure (40 min)
Fichier : app/lib/core/repositories/user_repository.dart
Actions :
- Supprimer ApiService du constructeur
- Utiliser
ApiService.instancedans 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 9.2: AmicaleRepository (20 min)
Fichier : app/lib/core/repositories/amicale_repository.dart
Actions :
- Supprimer ApiService du constructeur
- Utiliser
ApiService.instancedans les méthodes - Simplifier l'accès à l'amicale courante
- Intégrer avec CurrentAmicaleService
Tâche 9.3: MembreRepository (15 min)
Fichier : app/lib/core/repositories/membre_repository.dart
Actions :
- Supprimer ApiService du constructeur
- Utiliser
ApiService.instancedans les méthodes - Utiliser CurrentUserService pour les vérifications de permissions
Tâche 9.4: Autres repositories (15 min)
Fichiers à traiter :
client_repository.dartoperation_repository.dartsector_repository.dartpassage_repository.dart
Actions par repository :
- Supprimer injection ApiService
- Remplacer par
ApiService.instance - Utiliser CurrentUserService pour les données utilisateur
Phase 10: Modification des Pages principales (120 min)
Tâche 10.1: Pages d'authentification (30 min)
Fichiers :
app/lib/presentation/auth/login_page.dartapp/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 10.2: Dashboard pages (30 min)
Fichiers :
app/lib/presentation/admin/admin_dashboard_page.dartapp/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 10.3: Pages de gestion (30 min)
Fichiers :
app/lib/presentation/admin/admin_amicale_page.dartapp/lib/presentation/admin/admin_statistics_page.dartapp/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 10.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 11: Modification des Widgets (90 min)
Tâche 11.1: Création de widgets d'information (30 min)
Nouveaux widgets à créer :
app/lib/presentation/widgets/user/user_info_widget.dartapp/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 11.2: Widgets de tableaux (30 min)
Fichiers :
app/lib/presentation/widgets/tables/amicale_table_widget.dartapp/lib/presentation/widgets/tables/membre_table_widget.dartapp/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 11.3: Widgets de formulaires (30 min)
Fichiers :
app/lib/presentation/widgets/forms/amicale_form.dartapp/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 12: Mise à jour du Router et Navigation (45 min)
Tâche 12.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 12.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 13: Tests et validation (60 min)
Tâche 13.1: Tests de compilation (15 min)
Actions :
flutter pub getflutter analyze- Corriger erreurs de compilation
- Vérifier warnings
Tâche 13.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 13.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 14: Tests unitaires (45 min)
Tâche 14.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 14.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 14.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 15: Optimisations et nettoyage (45 min)
Tâche 15.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 15.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 16: Commit final et documentation (30 min)
Tâche 16.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é
✅ MembreRepository corrigé selon vrais champs modèle
✅ ClientRepository corrigé selon vrais champs modèle
✅ Tous les repositories utilisent ApiService.instance
Models:
✅ MembreModel corrigé avec les vrais champs
✅ ClientModel complété avec champs manquants
✅ Méthodes create/update simplifiées dans repositories
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é
- Champs de modèles corrigés selon spécifications
MIGRATION: Automatique au démarrage de l'app"
# Push de la branche
git push origin singletons
Tâche 16.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');
Corrections modèles
MembreModel
- Corrigé selon les vrais champs :
isActive,role, etc. - Plus de champs inventés
ClientModel
- Ajout champs manquants :
chkStripe,createdAt,updatedAt
Migration
- Box
usersrenommée enuser(migration automatique) - Plus d'injection de dépendances dans les constructeurs
- Widgets réactifs avec ListenableBuilder
- Repositories simplifiés
---
## 📁 Structure finale des fichiers modifiés/créés
### ✅ Fichiers déjà traités