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
This commit is contained in:
d6soft
2025-06-05 17:02:11 +02:00
parent e5ab857913
commit 86a9a35594
32 changed files with 10561 additions and 9982 deletions

View File

@@ -9,6 +9,295 @@ import 'package:retry/retry.dart';
import 'package:universal_html/html.dart' as html;
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) {
synchronized(_lock, () {
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');
}
// Fonction synchronized simple pour éviter les imports supplémentaires
static T synchronized<T>(Object lock, T Function() computation) {
return computation();
}
// === TOUTES LES MÉTHODES EXISTANTES RESTENT IDENTIQUES ===
// Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL
String _determineEnvironment() {
if (!kIsWeb) {
// En mode non-web, utiliser l'environnement de développement par défaut
return 'DEV';
}
final currentUrl = html.window.location.href.toLowerCase();
if (currentUrl.contains('dapp.geosector.fr')) {
return 'DEV';
} else if (currentUrl.contains('rapp.geosector.fr')) {
return 'REC';
} else {
return 'PROD';
}
}
// Configure l'URL de base API et l'identifiant d'application selon l'environnement
void _configureEnvironment() {
final env = _determineEnvironment();
switch (env) {
case 'DEV':
_baseUrl = AppKeys.baseApiUrlDev;
_appIdentifier = AppKeys.appIdentifierDev;
break;
case 'REC':
_baseUrl = AppKeys.baseApiUrlRec;
_appIdentifier = AppKeys.appIdentifierRec;
break;
default: // PROD
_baseUrl = AppKeys.baseApiUrlProd;
_appIdentifier = AppKeys.appIdentifierProd;
}
debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl');
}
// Définir l'ID de session
void setSessionId(String? sessionId) {
_sessionId = sessionId;
}
// Obtenir l'environnement actuel (utile pour le débogage)
String getCurrentEnvironment() {
return _determineEnvironment();
}
// Obtenir l'URL API actuelle (utile pour le débogage)
String getCurrentApiUrl() {
return _baseUrl;
}
// Obtenir l'identifiant d'application actuel (utile pour le débogage)
String getCurrentAppIdentifier() {
return _appIdentifier;
}
// Vérifier la connectivité réseau
Future<bool> hasInternetConnection() async {
final connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
// Méthode POST générique
Future<Response> post(String path, {dynamic data}) async {
try {
return await _dio.post(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode GET générique
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
return await _dio.get(path, queryParameters: queryParameters);
} catch (e) {
rethrow;
}
}
// Méthode PUT générique
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode DELETE générique
Future<Response> delete(String path) async {
try {
return await _dio.delete(path);
} catch (e) {
rethrow;
}
}
// Authentification avec PHP session
Future<Map<String, dynamic>> login(String username, String password, {required String type}) async {
try {
final response = await _dio.post(AppKeys.loginEndpoint, data: {
'username': username,
'password': password,
'type': type, // Ajouter le type de connexion (user ou admin)
});
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
final status = data['status'] as String?;
// Afficher le message en cas d'erreur
if (status != 'success') {
final message = data['message'] as String?;
debugPrint('Erreur d\'authentification: $message');
}
// Si le statut est 'success', récupérer le session_id
if (status == 'success' && data.containsKey('session_id')) {
final sessionId = data['session_id'];
// Définir la session pour les futures requêtes
if (sessionId != null) {
setSessionId(sessionId);
}
}
return data;
} catch (e) {
rethrow;
}
}
// Déconnexion
Future<void> logout() async {
try {
if (_sessionId != null) {
await _dio.post(AppKeys.logoutEndpoint);
_sessionId = null;
}
} catch (e) {
// Même en cas d'erreur, on réinitialise la session
_sessionId = null;
rethrow;
}
}
// Utilisateurs
Future<List<UserModel>> getUsers() async {
try {
final response = await retry(
() => _dio.get('/users'),
retryIf: (e) => e is SocketException || e is TimeoutException,
);
return (response.data as List).map((json) => UserModel.fromJson(json)).toList();
} catch (e) {
// Gérer les erreurs
rethrow;
}
}
Future<UserModel> getUserById(int id) async {
try {
final response = await _dio.get('/users/$id');
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> createUser(UserModel user) async {
try {
final response = await _dio.post('/users', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> updateUser(UserModel user) async {
try {
final response = await _dio.put('/users/${user.id}', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> deleteUser(String id) async {
try {
await _dio.delete('/users/$id');
} catch (e) {
rethrow;
}
}
// Espace réservé pour les futures méthodes de gestion des profils
// Espace réservé pour les futures méthodes de gestion des données
// Synchronisation en batch
Future<Map<String, dynamic>> syncData({
List<UserModel>? users,
}) async {
try {
final Map<String, dynamic> payload = {
if (users != null) 'users': users.map((u) => u.toJson()).toList(),
};
final response = await _dio.post('/sync', data: payload);
return response.data;
} catch (e) {
rethrow;
}
}
// Méthode de nettoyage pour les tests
static void reset() {
_instance = null;
}
}
final Dio _dio = Dio();
late final String _baseUrl;
late final String _appIdentifier;

View File

@@ -0,0 +1,144 @@
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;
}
}

View File

@@ -0,0 +1,156 @@
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';
import 'package:geosector_app/core/services/current_amicale_service.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;
}
}

View File

@@ -22,8 +22,7 @@ class HiveResetService {
/// Réinitialise complètement Hive et recrée les boîtes nécessaires
static Future<bool> resetAndRecreateHiveBoxes() async {
try {
debugPrint(
'HiveResetService: Début de la réinitialisation complète de Hive');
debugPrint('HiveResetService: Début de la réinitialisation complète de Hive');
// Approche plus radicale pour le web : supprimer directement IndexedDB
if (kIsWeb) {
@@ -68,8 +67,7 @@ class HiveResetService {
// Rouvrir les boîtes essentielles
await _reopenEssentialBoxes();
debugPrint(
'HiveResetService: Réinitialisation complète terminée avec succès');
debugPrint('HiveResetService: Réinitialisation complète terminée avec succès');
return true;
} catch (e) {
debugPrint('HiveResetService: Erreur lors de la réinitialisation: $e');
@@ -80,7 +78,7 @@ class HiveResetService {
/// Ferme toutes les boîtes Hive ouvertes
static Future<void> _closeAllBoxes() async {
final boxNames = [
AppKeys.usersBoxName,
AppKeys.userBoxName,
AppKeys.amicaleBoxName,
AppKeys.clientsBoxName,
AppKeys.operationsBoxName,
@@ -137,7 +135,7 @@ class HiveResetService {
debugPrint('HiveResetService: Réouverture des boîtes essentielles');
// Ouvrir les boîtes essentielles au démarrage
await Hive.openBox<UserModel>(AppKeys.usersBoxName);
await Hive.openBox<UserModel>(AppKeys.userBoxName);
await Hive.openBox<AmicaleModel>(AppKeys.amicaleBoxName);
await Hive.openBox<ClientModel>(AppKeys.clientsBoxName);
await Hive.openBox(AppKeys.settingsBoxName);

View File

@@ -12,8 +12,7 @@ class HiveWebFix {
if (!kIsWeb) return;
try {
debugPrint(
'HiveWebFix: Nettoyage sécurisé des boîtes Hive en version web');
debugPrint('HiveWebFix: Nettoyage sécurisé des boîtes Hive en version web');
// Liste des boîtes à nettoyer
final boxesToClean = [
@@ -36,16 +35,14 @@ class HiveWebFix {
await box.clear();
debugPrint('HiveWebFix: Boîte $boxName nettoyée avec succès');
} else {
debugPrint(
'HiveWebFix: La boîte $boxName n\'est pas ouverte, ouverture temporaire');
debugPrint('HiveWebFix: La boîte $boxName n\'est pas ouverte, ouverture temporaire');
final box = await Hive.openBox(boxName);
await box.clear();
await box.close();
debugPrint('HiveWebFix: Boîte $boxName nettoyée et fermée');
}
} catch (e) {
debugPrint(
'HiveWebFix: Erreur lors du nettoyage de la boîte $boxName: $e');
debugPrint('HiveWebFix: Erreur lors du nettoyage de la boîte $boxName: $e');
}
}
@@ -65,14 +62,13 @@ class HiveWebFix {
// Vérifier si IndexedDB est accessible
final isIndexedDBAvailable = js.context.hasProperty('indexedDB');
if (!isIndexedDBAvailable) {
debugPrint(
'HiveWebFix: IndexedDB n\'est pas disponible dans ce navigateur');
debugPrint('HiveWebFix: IndexedDB n\'est pas disponible dans ce navigateur');
return false;
}
// Liste des boîtes essentielles
final essentialBoxes = [
AppKeys.usersBoxName,
AppKeys.userBoxName,
AppKeys.settingsBoxName,
];
@@ -80,8 +76,7 @@ class HiveWebFix {
for (final boxName in essentialBoxes) {
try {
if (!Hive.isBoxOpen(boxName)) {
debugPrint(
'HiveWebFix: Ouverture de la boîte essentielle $boxName');
debugPrint('HiveWebFix: Ouverture de la boîte essentielle $boxName');
await Hive.openBox(boxName);
}
@@ -89,15 +84,13 @@ class HiveWebFix {
final box = Hive.box(boxName);
// Tenter une opération simple pour vérifier l'intégrité
final length = box.length;
debugPrint(
'HiveWebFix: Boîte $boxName accessible avec $length éléments');
debugPrint('HiveWebFix: Boîte $boxName accessible avec $length éléments');
} catch (e) {
debugPrint('HiveWebFix: Erreur d\'accès à la boîte $boxName: $e');
// Tenter de réparer en réinitialisant Hive
try {
debugPrint(
'HiveWebFix: Tentative de réparation de la boîte $boxName');
debugPrint('HiveWebFix: Tentative de réparation de la boîte $boxName');
// Fermer la boîte si elle est ouverte
if (Hive.isBoxOpen(boxName)) {
await Hive.box(boxName).close();
@@ -107,8 +100,7 @@ class HiveWebFix {
await Hive.openBox(boxName);
debugPrint('HiveWebFix: Boîte $boxName réparée avec succès');
} catch (repairError) {
debugPrint(
'HiveWebFix: Échec de la réparation de la boîte $boxName: $repairError');
debugPrint('HiveWebFix: Échec de la réparation de la boîte $boxName: $repairError');
return false;
}
}
@@ -132,7 +124,7 @@ class HiveWebFix {
// Fermer toutes les boîtes ouvertes
final boxesToClose = [
AppKeys.usersBoxName,
AppKeys.userBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,