feat: refactorisation majeure - DataLoadingService + UserRepository simplifié

 NOUVEAU SERVICE CRÉÉ:
- DataLoadingService: gère tout le chargement des données au login
- Sépare les responsabilités: UserRepository se concentre sur l'auth
- Simplification massive du code de connexion

 USERREPOSITORY REFACTORISÉ:
- Suppression de toute la logique de chargement de données (déplacée vers DataLoadingService)
- Délégation complète aux services singleton (CurrentUserService, CurrentAmicaleService)
- Constructeur ultra-simplifié (plus d'injection ApiService)
- Méthodes d'auth optimisées et clarifiées

 REPOSITORIES SIMPLIFIÉS:
- AmicaleRepository: constructeur sans paramètres, ApiService.instance
- ClientRepository: même pattern de simplification
- MembreRepository: suppression injection, getters sécurisés
- OperationRepository: utilisation ApiService.instance
- PassageRepository: simplification massive, nouveau pattern
- SectorRepository: optimisation et nouvelle structure

 ARCHITECTURE SINGLETONS:
- ApiService: pattern singleton thread-safe
- CurrentUserService: gestion utilisateur connecté + persistence Hive (Box user)
- CurrentAmicaleService: gestion amicale courante + auto-sync
- Box Hive 'users' renommée en 'user' avec migration automatique

 APP.DART & MAIN.DART:
- Suppression injections multiples dans repositories
- Intégration des services singleton dans main.dart
- Router simplifié avec CurrentUserService

État: Architecture singleton opérationnelle, prêt pour tests et widgets
This commit is contained in:
d6soft
2025-06-05 18:35:12 +02:00
parent 7e6431b5aa
commit 150016d772
13 changed files with 1755 additions and 2237 deletions

View File

@@ -342,38 +342,6 @@ class ApiService {
debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl');
}
ApiService() {
// Configurer l'environnement
_configureEnvironment();
// Configurer Dio
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = AppKeys.connectionTimeout;
_dio.options.receiveTimeout = AppKeys.receiveTimeout;
// Ajouter les en-têtes par défaut avec l'identifiant d'application adapté à l'environnement
final headers = Map<String, String>.from(AppKeys.defaultHeaders);
headers['X-App-Identifier'] = _appIdentifier;
_dio.options.headers.addAll(headers);
// Ajouter des intercepteurs pour l'authentification par session
_dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
// Ajouter le session_id comme token Bearer aux en-têtes si disponible
if (_sessionId != null) {
options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId';
}
return handler.next(options);
}, onError: (DioException error, handler) {
// Gérer les erreurs d'authentification (401)
if (error.response?.statusCode == 401) {
// Session expirée ou invalide
_sessionId = null;
}
return handler.next(error);
}));
}
// Définir l'ID de session
void setSessionId(String? sessionId) {
_sessionId = sessionId;
@@ -397,7 +365,7 @@ class ApiService {
// Vérifier la connectivité réseau
Future<bool> hasInternetConnection() async {
final connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
return connectivityResult.contains(ConnectivityResult.none) == false;
}
// Méthode POST générique

View File

@@ -0,0 +1,667 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/hive_web_fix.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_sector_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/repositories/client_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/chat/models/conversation_model.dart';
import 'package:geosector_app/chat/models/message_model.dart';
import 'package:geosector_app/core/models/loading_state.dart';
/// Service singleton pour gérer le chargement et la gestion des données au login
class DataLoadingService extends ChangeNotifier {
static DataLoadingService? _instance;
static DataLoadingService get instance => _instance ??= DataLoadingService._internal();
DataLoadingService._internal();
// État du chargement
LoadingState _loadingState = LoadingState.initial;
LoadingState get loadingState => _loadingState;
// Callback pour les mises à jour de progression
Function(LoadingState)? _progressCallback;
// Méthode pour définir un callback de progression
void setProgressCallback(Function(LoadingState)? callback) {
_progressCallback = callback;
}
// Mettre à jour l'état du chargement
void _updateLoadingState(LoadingState newState) {
_loadingState = newState;
notifyListeners();
_progressCallback?.call(newState);
}
// === GETTERS POUR LES BOXES ===
Box<OperationModel> get _operationBox => Hive.box<OperationModel>(AppKeys.operationsBoxName);
Box<SectorModel> get _sectorBox => Hive.box<SectorModel>(AppKeys.sectorsBoxName);
Box<PassageModel> get _passageBox => Hive.box<PassageModel>(AppKeys.passagesBoxName);
Box<MembreModel> get _membreBox => Hive.box<MembreModel>(AppKeys.membresBoxName);
Box<UserSectorModel> get _userSectorBox => Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
Box<AmicaleModel> get _amicaleBox => Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
Box<ConversationModel> get _chatConversationBox => Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
Box<MessageModel> get _chatMessageBox => Hive.box<MessageModel>(AppKeys.chatMessagesBoxName);
Box get _settingsBox => Hive.box(AppKeys.settingsBoxName);
// === NETTOYAGE ET PRÉPARATION DES DONNÉES ===
/// Nettoie toutes les données avant le login
Future<void> cleanDataBeforeLogin() async {
try {
_updateLoadingState(LoadingState.initial.copyWith(
progress: 0.05,
message: 'Nettoyage des données...',
stepDescription: 'Suppression des données obsolètes',
));
debugPrint('🧹 Début du nettoyage des données avant login...');
// Étape 1: Nettoyage des boîtes non référencées (5%)
await _cleanNonDefinedBoxes();
// Étape 2: Nettoyage sécurisé des données (10%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.10,
stepDescription: 'Préparation du stockage local',
));
if (kIsWeb) {
await HiveWebFix.safeCleanHiveBoxes(excludeBoxes: [AppKeys.userBoxName]);
} else if (Platform.isIOS) {
await _cleanHiveFilesOnIOS();
} else if (Platform.isAndroid) {
await _cleanHiveFilesOnAndroid();
}
// Étape 3: Recréation des boîtes (15%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.15,
stepDescription: 'Initialisation des bases de données',
));
await _clearAndRecreateBoxes();
await _initializeBoxes();
debugPrint('✅ Nettoyage des données terminé');
} catch (e) {
debugPrint('❌ Erreur lors du nettoyage des données: $e');
_updateLoadingState(LoadingState.error('Erreur lors du nettoyage: $e'));
rethrow;
}
}
/// Traite toutes les données reçues de l'API lors du login
Future<void> processLoginData(Map<String, dynamic> apiResult) async {
try {
debugPrint('📊 Début du traitement des données de login...');
// Étape 4: Traitement des clients (35%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.35,
stepDescription: 'Chargement des clients',
));
if (apiResult['clients'] != null) {
await _processClients(apiResult['clients']);
}
// Étape 5: Traitement des opérations (50%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.50,
stepDescription: 'Chargement des opérations',
));
if (apiResult['operations'] != null) {
await _processOperations(apiResult['operations']);
}
// Étape 6: Traitement des secteurs (65%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.65,
stepDescription: 'Chargement des secteurs',
));
if (apiResult['sectors'] != null) {
await _processSectors(apiResult['sectors']);
}
// Étape 7: Traitement des passages (75%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.75,
stepDescription: 'Chargement des passages',
));
if (apiResult['passages'] != null) {
await _processPassages(apiResult['passages']);
}
// Étape 8: Traitement des membres (85%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.85,
stepDescription: 'Chargement des membres',
));
final membresData = apiResult['membres'] ?? apiResult['members'];
if (membresData != null) {
await _processMembres(membresData);
}
// Étape 9: Traitement des associations utilisateurs-secteurs (95%)
_updateLoadingState(_loadingState.copyWith(
progress: 0.95,
stepDescription: 'Finalisation du chargement',
));
if (apiResult['users_sectors'] != null) {
await _processUserSectors(apiResult['users_sectors']);
}
// Étape finale: Chargement terminé (100%)
_updateLoadingState(LoadingState.completed.copyWith(
progress: 1.0,
message: 'Chargement terminé avec succès',
stepDescription: 'Données synchronisées',
));
debugPrint('✅ Traitement des données de login terminé');
_logDataSummary();
} catch (e) {
debugPrint('❌ Erreur lors du traitement des données de login: $e');
_updateLoadingState(LoadingState.error('Erreur lors du chargement: $e'));
rethrow;
}
}
/// Nettoie complètement toutes les données lors du logout
Future<void> cleanDataAfterLogout() async {
try {
debugPrint('🧹 Début du nettoyage complet après logout...');
await _deepCleanHiveBoxes();
debugPrint('✅ Nettoyage complet terminé');
} catch (e) {
debugPrint('❌ Erreur lors du nettoyage après logout: $e');
rethrow;
}
}
// === MÉTHODES PRIVÉES DE NETTOYAGE ===
Future<void> _cleanNonDefinedBoxes() async {
final nonDefinedBoxes = ['auth', 'locations', 'messages'];
for (final boxName in nonDefinedBoxes) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('Fermeture de la boîte non référencée: $boxName');
await Hive.box(boxName).close();
}
await Hive.deleteBoxFromDisk(boxName);
debugPrint('✅ Box $boxName supprimée');
} catch (e) {
debugPrint('⚠️ Erreur suppression box $boxName: $e');
}
}
}
Future<void> _clearAndRecreateBoxes() async {
try {
debugPrint('🔄 Recréation des boîtes Hive...');
final boxesToDelete = [
AppKeys.passagesBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.userSectorBoxName,
AppKeys.chatConversationsBoxName,
AppKeys.chatMessagesBoxName,
];
// Vider chaque boîte sans la fermer
for (final boxName in boxesToDelete) {
try {
if (Hive.isBoxOpen(boxName)) {
final box = Hive.box(boxName);
await box.clear();
debugPrint('✅ Box $boxName vidée');
} else {
await Hive.deleteBoxFromDisk(boxName);
debugPrint('✅ Box $boxName supprimée du disque');
}
} catch (e) {
debugPrint('⚠️ Erreur nettoyage box $boxName: $e');
}
}
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) {
debugPrint('❌ Erreur recréation des boîtes: $e');
rethrow;
}
}
Future<void> _initializeBoxes() async {
debugPrint('📦 Initialisation des boîtes Hive...');
final boxesToInit = [
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
AppKeys.membresBoxName,
AppKeys.userSectorBoxName,
AppKeys.chatConversationsBoxName,
AppKeys.chatMessagesBoxName,
];
for (final boxName in boxesToInit) {
await _ensureBoxIsOpen(boxName);
}
debugPrint('✅ Toutes les boîtes Hive sont ouvertes');
}
Future<void> _ensureBoxIsOpen(String boxName) async {
try {
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName...');
switch (boxName) {
case AppKeys.passagesBoxName:
await Hive.openBox<PassageModel>(boxName);
break;
case AppKeys.operationsBoxName:
await Hive.openBox<OperationModel>(boxName);
break;
case AppKeys.sectorsBoxName:
await Hive.openBox<SectorModel>(boxName);
break;
case AppKeys.membresBoxName:
await Hive.openBox<MembreModel>(boxName);
break;
case AppKeys.userSectorBoxName:
await Hive.openBox<UserSectorModel>(boxName);
break;
case AppKeys.chatConversationsBoxName:
await Hive.openBox<ConversationModel>(boxName);
break;
case AppKeys.chatMessagesBoxName:
await Hive.openBox<MessageModel>(boxName);
break;
default:
await Hive.openBox(boxName);
}
}
} catch (e) {
debugPrint('❌ Erreur ouverture box $boxName: $e');
throw Exception('Impossible d\'ouvrir la boîte $boxName: $e');
}
}
// === MÉTHODES DE TRAITEMENT DES DONNÉES ===
Future<void> _processClients(dynamic clientsData) async {
try {
debugPrint('👥 Traitement des clients...');
List<dynamic> clientsList;
if (clientsData is List) {
clientsList = clientsData;
} else if (clientsData is Map && clientsData.containsKey('data')) {
clientsList = clientsData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données clients non reconnu');
return;
}
if (clientsList.isNotEmpty) {
debugPrint('📊 Traitement de ${clientsList.length} clients de type 1');
final clientRepository = ClientRepository();
await clientRepository.processClientsData(clientsList);
debugPrint('✅ Clients traités via ClientRepository');
}
} catch (e) {
debugPrint('❌ Erreur traitement clients: $e');
}
}
Future<void> _processOperations(dynamic operationsData) async {
try {
debugPrint('⚙️ Traitement des opérations...');
if (operationsData == null) {
debugPrint(' Aucune donnée d\'opération à traiter');
return;
}
List<dynamic> operationsList;
if (operationsData is List) {
operationsList = operationsData;
} else if (operationsData is Map && operationsData.containsKey('data')) {
operationsList = operationsData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données d\'opérations non reconnu');
return;
}
await _operationBox.clear();
int count = 0;
for (final operationData in operationsList) {
try {
final operation = OperationModel.fromJson(operationData);
await _operationBox.put(operation.id, operation);
count++;
} catch (e) {
debugPrint('⚠️ Erreur traitement opération: $e');
}
}
debugPrint('$count opérations stockées');
} catch (e) {
debugPrint('❌ Erreur traitement opérations: $e');
}
}
Future<void> _processSectors(dynamic sectorsData) async {
try {
debugPrint('📍 Traitement des secteurs...');
if (sectorsData == null) {
debugPrint(' Aucune donnée de secteur à traiter');
return;
}
List<dynamic> sectorsList;
if (sectorsData is List) {
sectorsList = sectorsData;
} else if (sectorsData is Map && sectorsData.containsKey('data')) {
sectorsList = sectorsData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données de secteurs non reconnu');
return;
}
await _sectorBox.clear();
int count = 0;
for (final sectorData in sectorsList) {
try {
final sector = SectorModel.fromJson(sectorData);
await _sectorBox.put(sector.id, sector);
count++;
} catch (e) {
debugPrint('⚠️ Erreur traitement secteur: $e');
}
}
debugPrint('$count secteurs stockés');
} catch (e) {
debugPrint('❌ Erreur traitement secteurs: $e');
}
}
Future<void> _processPassages(dynamic passagesData) async {
try {
debugPrint('🚶 Traitement des passages...');
if (passagesData == null) {
debugPrint(' Aucune donnée de passage à traiter');
return;
}
List<dynamic> passagesList;
if (passagesData is List) {
passagesList = passagesData;
} else if (passagesData is Map && passagesData.containsKey('data')) {
passagesList = passagesData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données de passages non reconnu');
return;
}
await _passageBox.clear();
int count = 0;
for (final passageData in passagesList) {
try {
final passage = PassageModel.fromJson(passageData);
await _passageBox.put(passage.id, passage);
count++;
} catch (e) {
debugPrint('⚠️ Erreur traitement passage: $e');
}
}
debugPrint('$count passages stockés');
} catch (e) {
debugPrint('❌ Erreur traitement passages: $e');
}
}
Future<void> _processMembres(dynamic membresData) async {
try {
debugPrint('👤 Traitement des membres...');
if (membresData == null) {
debugPrint(' Aucune donnée de membre à traiter');
return;
}
List<dynamic> membresList;
if (membresData is List) {
membresList = membresData;
} else if (membresData is Map && membresData.containsKey('data')) {
membresList = membresData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données de membres non reconnu');
return;
}
await _membreBox.clear();
int count = 0;
for (final membreData in membresList) {
try {
final membre = MembreModel.fromJson(membreData);
await _membreBox.put(membre.id, membre);
count++;
} catch (e) {
debugPrint('⚠️ Erreur traitement membre: $e');
}
}
debugPrint('$count membres stockés');
} catch (e) {
debugPrint('❌ Erreur traitement membres: $e');
}
}
Future<void> _processUserSectors(dynamic userSectorsData) async {
try {
debugPrint('🔗 Traitement des associations utilisateurs-secteurs...');
if (userSectorsData == null) {
debugPrint(' Aucune association utilisateur-secteur à traiter');
return;
}
List<dynamic> userSectorsList;
if (userSectorsData is List) {
userSectorsList = userSectorsData;
} else if (userSectorsData is Map && userSectorsData.containsKey('data')) {
userSectorsList = userSectorsData['data'] as List<dynamic>;
} else {
debugPrint('⚠️ Format de données d\'associations non reconnu');
return;
}
await _userSectorBox.clear();
int count = 0;
for (final userSectorData in userSectorsList) {
try {
final userSector = UserSectorModel.fromJson(userSectorData);
await _userSectorBox.put('${userSector.id}_${userSector.fkSector}', userSector);
count++;
} catch (e) {
debugPrint('⚠️ Erreur traitement association: $e');
}
}
debugPrint('$count associations stockées');
} catch (e) {
debugPrint('❌ Erreur traitement associations: $e');
}
}
// === MÉTHODES DE NETTOYAGE PLATEFORME SPÉCIFIQUES ===
Future<void> _cleanHiveFilesOnIOS() async {
if (!kIsWeb && Platform.isIOS) {
try {
debugPrint('🍎 Nettoyage des fichiers Hive sur iOS...');
final appDir = await getApplicationDocumentsDirectory();
final hiveDir = Directory('${appDir.path}/hive');
if (await hiveDir.exists()) {
final entries = await hiveDir.list().toList();
for (var entry in entries) {
final name = entry.path.split('/').last;
if (!name.contains(AppKeys.userBoxName)) {
if (entry is Directory) {
await entry.delete(recursive: true);
} else if (entry is File) {
await entry.delete();
}
}
}
debugPrint('✅ Nettoyage iOS terminé');
}
} catch (e) {
debugPrint('❌ Erreur nettoyage iOS: $e');
}
}
}
Future<void> _cleanHiveFilesOnAndroid() async {
if (!kIsWeb && Platform.isAndroid) {
try {
debugPrint('🤖 Nettoyage des fichiers Hive sur Android...');
final appDir = await getApplicationDocumentsDirectory();
final entries = await appDir.list().toList();
int filesDeleted = 0;
for (var entry in entries) {
final name = entry.path.split('/').last;
if (name.endsWith('.hive') && !name.contains(AppKeys.userBoxName)) {
if (entry is File) {
await entry.delete();
filesDeleted++;
final lockFile = File('${entry.path}.lock');
if (await lockFile.exists()) {
await lockFile.delete();
}
}
}
}
debugPrint('✅ Nettoyage Android terminé. $filesDeleted fichiers supprimés');
} catch (e) {
debugPrint('❌ Erreur nettoyage Android: $e');
}
}
}
Future<void> _deepCleanHiveBoxes() async {
try {
debugPrint('🧹 Nettoyage profond des boîtes Hive...');
// Vider toutes les boîtes
final boxesToClean = [
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
AppKeys.membresBoxName,
AppKeys.userSectorBoxName,
AppKeys.amicaleBoxName,
AppKeys.chatConversationsBoxName,
AppKeys.chatMessagesBoxName,
AppKeys.settingsBoxName,
];
for (final boxName in boxesToClean) {
try {
if (Hive.isBoxOpen(boxName)) {
await Hive.box(boxName).clear();
debugPrint('✅ Box $boxName vidée');
}
} catch (e) {
debugPrint('⚠️ Erreur vidage box $boxName: $e');
}
}
// Nettoyage spécifique à la plateforme
if (kIsWeb) {
await HiveWebFix.resetHiveCompletely();
} else if (Platform.isIOS) {
await _cleanHiveFilesOnIOS();
} else if (Platform.isAndroid) {
await _cleanHiveFilesOnAndroid();
}
await Future.delayed(const Duration(milliseconds: 800));
debugPrint('✅ Nettoyage profond terminé');
} catch (e) {
debugPrint('❌ Erreur nettoyage profond: $e');
}
}
// === MÉTHODES UTILITAIRES ===
/// Affiche un résumé des données chargées
void _logDataSummary() {
try {
debugPrint('📊 === RÉSUMÉ DES DONNÉES CHARGÉES ===');
debugPrint('Opérations: ${_operationBox.length}');
debugPrint('Secteurs: ${_sectorBox.length}');
debugPrint('Passages: ${_passageBox.length}');
debugPrint('Membres: ${_membreBox.length}');
debugPrint('Associations User-Sector: ${_userSectorBox.length}');
debugPrint('Amicales: ${_amicaleBox.length}');
debugPrint('=================================');
} catch (e) {
debugPrint('⚠️ Erreur lors du résumé: $e');
}
}
/// Retourne un résumé des données pour l'UI
Map<String, int> getDataSummary() {
try {
return {
'operations': _operationBox.length,
'sectors': _sectorBox.length,
'passages': _passageBox.length,
'membres': _membreBox.length,
'userSectors': _userSectorBox.length,
'amicales': _amicaleBox.length,
};
} catch (e) {
debugPrint('⚠️ Erreur génération résumé: $e');
return {};
}
}
// === RESET POUR TESTS ===
static void reset() {
_instance = null;
}
}