feat: Livraison version 3.0.6

- Amélioration de la gestion des entités et des utilisateurs
- Mise à jour des modèles Amicale et Client avec champs supplémentaires
- Ajout du service de logging et amélioration du chargement UI
- Refactoring des formulaires utilisateur et amicale
- Intégration de file_picker et image_picker pour la gestion des fichiers
- Amélioration de la gestion des membres et de leur suppression
- Optimisation des performances de l'API
- Mise à jour de la documentation technique

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-08 20:33:54 +02:00
parent 1018b86537
commit 0e98a94374
63 changed files with 104136 additions and 87983 deletions

View File

@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/logger_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
class MembreRepository extends ChangeNotifier {
@@ -19,7 +21,7 @@ class MembreRepository extends ChangeNotifier {
throw Exception('La boîte ${AppKeys.membresBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
}
_cachedMembreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
debugPrint('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
LoggerService.database('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
}
return _cachedMembreBox!;
}
@@ -120,7 +122,7 @@ class MembreRepository extends ChangeNotifier {
// === MÉTHODES API ===
// Créer un membre via l'API
Future<MembreModel?> createMembre(MembreModel membre) async {
Future<MembreModel?> createMembre(MembreModel membre, {String? password}) async {
_isLoading = true;
notifyListeners();
@@ -130,48 +132,97 @@ class MembreRepository extends ChangeNotifier {
final data = userModel.toJson();
data.remove('id'); // L'API génère l'ID
data.remove('created_at'); // L'API génère created_at
// Supprimer les champs de session qui ne doivent pas être envoyés
data.remove('session_id');
data.remove('session_expiry');
data.remove('last_path');
// Convertir is_active en chk_active pour l'API
if (data.containsKey('is_active')) {
data['chk_active'] = data['is_active'] ? 1 : 0;
data.remove('is_active');
}
// Convertir role en fk_role pour l'API
if (data.containsKey('role')) {
data['fk_role'] = data['role'];
data.remove('role');
}
// Ajouter le mot de passe si fourni (sera ignoré par l'API si chk_mdp_manuel=0)
if (password != null && password.isNotEmpty) {
data['password'] = password;
debugPrint('🔑 Mot de passe inclus dans la requête');
} else {
debugPrint('⚠️ Pas de mot de passe fourni');
}
// Vérifier la présence de l'username (sera ignoré par l'API si chk_username_manuel=0)
if (data.containsKey('username') && data['username'] != null && data['username'].toString().isNotEmpty) {
debugPrint('👤 Username inclus dans la requête: ${data['username']}');
} else {
debugPrint('⚠️ Username manquant ou vide dans la requête');
// Si pas d'username, s'assurer qu'il n'est pas envoyé du tout
data.remove('username');
}
LoggerService.api('Données envoyées à l\'API pour création membre: $data');
// Appeler l'API users
final response = await ApiService.instance.post('/users', data: data);
if (response.statusCode == 201) {
// Extraire l'ID de la réponse API
final responseData = response.data;
debugPrint('🎉 Réponse API création utilisateur: $responseData');
// Vérifier d'abord si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
// Si l'API retourne un status error, propager le message
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
// Si succès avec code 201
if (response.statusCode == 201 && responseData['status'] == 'success') {
debugPrint('🎉 Réponse API création utilisateur: $responseData');
// L'API retourne {"status":"success","message":"Utilisateur créé avec succès","id":"10027748"}
final userId = responseData['id'] is String ? int.parse(responseData['id']) : responseData['id'] as int;
// L'API retourne {"status":"success","message":"Utilisateur créé avec succès","id":"10027748"}
final userId = responseData['id'] is String ? int.parse(responseData['id']) : responseData['id'] as int;
// Créer le nouveau membre avec l'ID retourné par l'API
final createdMember = MembreModel(
id: userId,
fkEntite: membre.fkEntite,
role: membre.role,
fkTitre: membre.fkTitre,
name: membre.name,
firstName: membre.firstName,
username: membre.username,
sectName: membre.sectName,
email: membre.email,
phone: membre.phone,
mobile: membre.mobile,
dateNaissance: membre.dateNaissance,
dateEmbauche: membre.dateEmbauche,
createdAt: DateTime.now(),
isActive: membre.isActive,
);
// Créer le nouveau membre avec l'ID retourné par l'API
final createdMember = MembreModel(
id: userId,
fkEntite: membre.fkEntite,
role: membre.role,
fkTitre: membre.fkTitre,
name: membre.name,
firstName: membre.firstName,
username: membre.username,
sectName: membre.sectName,
email: membre.email,
phone: membre.phone,
mobile: membre.mobile,
dateNaissance: membre.dateNaissance,
dateEmbauche: membre.dateEmbauche,
createdAt: DateTime.now(),
isActive: membre.isActive,
);
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
await saveMembreBox(createdMember);
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
await saveMembreBox(createdMember);
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
return createdMember;
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
return createdMember;
}
}
debugPrint('Échec création membre - Code: ${response.statusCode}');
return null;
LoggerService.error('Échec création membre - Code: ${response.statusCode}');
throw Exception('Erreur lors de la création du membre');
} catch (e) {
debugPrint('❌ Erreur lors de la création du membre: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la création du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la création du membre');
}
rethrow; // Propager l'exception pour la gestion d'erreurs
} finally {
_isLoading = false;
@@ -180,27 +231,67 @@ class MembreRepository extends ChangeNotifier {
}
// Mettre à jour un membre via l'API
Future<bool> updateMembre(MembreModel membre) async {
Future<bool> updateMembre(MembreModel membre, {String? password}) async {
_isLoading = true;
notifyListeners();
try {
// Convertir en UserModel pour l'API
final userModel = membre.toUserModel();
final data = userModel.toJson();
// Supprimer les champs de session qui ne doivent pas être envoyés
data.remove('session_id');
data.remove('session_expiry');
data.remove('last_path');
// Convertir is_active en chk_active pour l'API
if (data.containsKey('is_active')) {
data['chk_active'] = data['is_active'] ? 1 : 0;
data.remove('is_active');
}
// Convertir role en fk_role pour l'API
if (data.containsKey('role')) {
data['fk_role'] = data['role'];
data.remove('role');
}
// Ajouter le mot de passe si fourni (sera ignoré par l'API si chk_mdp_manuel=0)
if (password != null && password.isNotEmpty) {
data['password'] = password;
debugPrint('🔑 Mot de passe inclus dans la requête de mise à jour');
} else {
debugPrint('⚠️ Pas de mot de passe fourni pour la mise à jour');
}
// L'username ne devrait pas être modifiable en update, mais on le garde pour l'API
if (data.containsKey('username') && data['username'] != null && data['username'].toString().isNotEmpty) {
debugPrint('👤 Username présent dans la requête de mise à jour: ${data['username']}');
} else {
debugPrint('⚠️ Username manquant dans la requête de mise à jour');
}
LoggerService.api('Données envoyées à l\'API pour mise à jour membre: $data');
// Appeler l'API users au lieu de membres
final response = await ApiService.instance.put('/users/${membre.id}', data: userModel.toJson());
final response = await ApiService.instance.put('/users/${membre.id}', data: data);
if (response.statusCode == 200) {
// Sauvegarder le membre mis à jour localement
await saveMembreBox(membre);
return true;
}
return false;
// Si on arrive ici, c'est que la requête a réussi (200)
// Sauvegarder le membre mis à jour localement
await saveMembreBox(membre);
return true;
} catch (e) {
debugPrint('Erreur lors de la mise à jour du membre: $e');
return false;
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la mise à jour du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la mise à jour du membre');
}
// Si c'est une DioException, elle sera automatiquement convertie en ApiException
// par ApiException.fromDioException() qui extrait le message de l'API
rethrow; // Propager l'exception pour que le message d'erreur soit affiché
} finally {
_isLoading = false;
notifyListeners();
@@ -215,13 +306,26 @@ class MembreRepository extends ChangeNotifier {
try {
final response = await ApiService.instance.post('/users/$membreId/reset-password');
// Vérifier si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
}
if (response.statusCode == 200) {
return true;
}
return false;
throw Exception('Erreur lors de la réinitialisation du mot de passe');
} catch (e) {
debugPrint('Erreur lors de la réinitialisation du mot de passe: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la réinitialisation du mot de passe: ${e.message}');
} else {
LoggerService.error('Erreur lors de la réinitialisation du mot de passe');
}
rethrow;
} finally {
_isLoading = false;
@@ -254,19 +358,32 @@ class MembreRepository extends ChangeNotifier {
endpoint += '?${queryParams.join('&')}';
}
debugPrint('🔗 DELETE endpoint: $endpoint');
LoggerService.api('DELETE endpoint: $endpoint');
final response = await ApiService.instance.delete(endpoint);
// Vérifier si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
}
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer le membre localement
await deleteMembreBox(membreId);
return true;
}
return false;
throw Exception('Erreur lors de la suppression du membre');
} catch (e) {
debugPrint('Erreur lors de la suppression du membre: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la suppression du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la suppression du membre');
}
rethrow;
} finally {
_isLoading = false;

View File

@@ -15,12 +15,10 @@ 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/presentation/widgets/loading_progress_overlay.dart';
import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart';
import 'package:geosector_app/core/models/loading_state.dart';
class UserRepository extends ChangeNotifier {
// Overlay pour afficher la progression du chargement
OverlayEntry? _progressOverlay;
bool _isLoading = false;
// Constructeur simplifié - plus d'injection d'ApiService
@@ -336,64 +334,39 @@ class UserRepository extends ChangeNotifier {
}
}
/// Connexion avec interface utilisateur et progression
Future<bool> loginWithUI(
/// Connexion avec spinner moderne (pour remplacer la barre de progression)
Future<bool> loginWithSpinner(
BuildContext context, String username, String password,
{required String type}) async {
OverlayEntry? spinOverlay;
try {
// Créer et afficher l'overlay de progression
_progressOverlay = LoadingProgressOverlayUtils.show(
// Déterminer le message selon le type de connexion
final message = type == 'admin'
? 'Connexion administrateur...'
: 'Connexion utilisateur...';
// Afficher le spinner moderne
spinOverlay = LoadingSpinOverlayUtils.show(
context: context,
message: 'Connexion en cours...',
progress: 0.0,
stepDescription: 'Préparation',
blurAmount: 5.0,
message: message,
blurAmount: 10.0,
showCard: true,
);
// Écouter les changements d'état du DataLoadingService
void listener() {
if (_progressOverlay != null) {
final loadingState = DataLoadingService.instance.loadingState;
LoadingProgressOverlayUtils.update(
overlayEntry: _progressOverlay!,
message: loadingState.message,
progress: loadingState.progress,
stepDescription: loadingState.stepDescription,
);
}
}
// Configurer le callback de progression
DataLoadingService.instance.setProgressCallback((_) => listener());
// Exécuter la connexion
final result = await login(username, password, type: type);
// Attendre un court instant pour que l'utilisateur voie le résultat
if (result) {
await Future.delayed(const Duration(milliseconds: 500));
} else {
await Future.delayed(const Duration(seconds: 2));
}
// Petit délai pour une meilleure UX
await Future.delayed(const Duration(milliseconds: 300));
// Supprimer l'overlay
if (_progressOverlay != null) {
_progressOverlay!.remove();
_progressOverlay = null;
}
// Nettoyer le callback
DataLoadingService.instance.setProgressCallback(null);
// Fermer le spinner
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
return result;
} catch (e) {
// En cas d'erreur, supprimer l'overlay
if (_progressOverlay != null) {
_progressOverlay!.remove();
_progressOverlay = null;
}
DataLoadingService.instance.setProgressCallback(null);
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
return false;
}
}