- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1614 lines
45 KiB
Markdown
Executable File
1614 lines
45 KiB
Markdown
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) ✅
|
||
|
||
```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 modifié** :
|
||
|
||
```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** :
|
||
|
||
- [x] Créer une liste des repositories à modifier
|
||
- [x] Identifier les pages/widgets accédant aux données utilisateur
|
||
- [x] Planifier l'ordre de modification (dépendances)
|
||
- [x] Préparer la stratégie de tests
|
||
- [x] Définir l'architecture des nouveaux services
|
||
- [x] 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** :
|
||
|
||
- [x] Corriger les champs selon les spécifications réelles :
|
||
|
||
- `final int id`
|
||
- `int? fkEntite`
|
||
- `final int role`
|
||
- `int? fkTitre`
|
||
- `String? name`
|
||
- `String? firstName`
|
||
- `String? username`
|
||
- `String? sectName`
|
||
- `final String email`
|
||
- `String? phone`
|
||
- `String? mobile`
|
||
- `DateTime? dateNaissance`
|
||
- `DateTime? dateEmbauche`
|
||
- `final DateTime createdAt`
|
||
- `bool isActive`
|
||
|
||
- [x] Adapter les annotations Hive @HiveField
|
||
- [x] Corriger fromJson() et toJson()
|
||
- [x] 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** :
|
||
|
||
- [x] Ajouter les champs manqués : `chkStripe`, `createdAt`, `updatedAt`
|
||
- [x] Mettre à jour les annotations Hive
|
||
- [x] Corriger fromJson() et toJson()
|
||
- [x] 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.dart`
|
||
**Actions** :
|
||
|
||
- [x] Adapter MembreRepository pour les vrais champs (`isActive` au lieu de `chkActive`, etc.)
|
||
- [x] Corriger les méthodes create/update pour éviter les reconstructions manuelles
|
||
- [x] Utiliser copyWith() correctement
|
||
- [x] Simplifier la logique de création API
|
||
- [x] 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.usersBoxName` par `AppKeys.userBoxName`
|
||
- [ ] Modifier l'ouverture de la Box dans `_openEssentialHiveBoxes()`
|
||
- [ ] Ajouter logique de migration depuis l'ancienne Box si nécessaire
|
||
|
||
**Code à modifier** :
|
||
|
||
```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 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.usersBoxName` par `AppKeys.userBoxName`
|
||
- [ ] Modifier les getters de Box
|
||
- [ ] 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 4: Création du nouveau ApiService Singleton (45 min)
|
||
|
||
#### Tâche 4.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 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** :
|
||
|
||
```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 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** :
|
||
|
||
```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 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** :
|
||
|
||
```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 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** :
|
||
|
||
```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 7: Commit intermédiaire de sécurité (10 min)
|
||
|
||
#### Tâche 7.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 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** :
|
||
|
||
```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 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.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 9.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 9.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 9.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 10: Modification des Pages principales (120 min)
|
||
|
||
#### Tâche 10.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 10.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 10.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 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.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 11.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 11.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 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** :
|
||
|
||
```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 12.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 13: Tests et validation (60 min)
|
||
|
||
#### Tâche 13.1: Tests de compilation (15 min)
|
||
|
||
**Actions** :
|
||
|
||
- [ ] `flutter pub get`
|
||
- [ ] `flutter 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)
|
||
|
||
```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é
|
||
✅ 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`
|
||
|
||
````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');
|
||
```
|
||
|
||
## 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 `users` renommée en `user` (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
|
||
```
|