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:
@@ -70,6 +70,15 @@ class AmicaleModel extends HiveObject {
|
||||
@HiveField(21)
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@HiveField(22)
|
||||
final bool chkMdpManuel;
|
||||
|
||||
@HiveField(23)
|
||||
final bool chkUsernameManuel;
|
||||
|
||||
@HiveField(24)
|
||||
final String? logoBase64; // Logo en base64 (data:image/png;base64,...)
|
||||
|
||||
AmicaleModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -93,6 +102,9 @@ class AmicaleModel extends HiveObject {
|
||||
this.chkStripe = false,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.chkMdpManuel = false,
|
||||
this.chkUsernameManuel = false,
|
||||
this.logoBase64,
|
||||
});
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
@@ -123,6 +135,17 @@ class AmicaleModel extends HiveObject {
|
||||
json['chk_active'] == 1 || json['chk_active'] == true;
|
||||
final bool chkStripe =
|
||||
json['chk_stripe'] == 1 || json['chk_stripe'] == true;
|
||||
final bool chkMdpManuel =
|
||||
json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true;
|
||||
final bool chkUsernameManuel =
|
||||
json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true;
|
||||
|
||||
// Traiter le logo si présent
|
||||
String? logoBase64;
|
||||
if (json['logo'] != null && json['logo'] is Map) {
|
||||
final logoData = json['logo'] as Map<String, dynamic>;
|
||||
logoBase64 = logoData['data_url'] as String?;
|
||||
}
|
||||
|
||||
// Traiter les dates si présentes
|
||||
DateTime? createdAt;
|
||||
@@ -166,6 +189,9 @@ class AmicaleModel extends HiveObject {
|
||||
chkStripe: chkStripe,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
chkMdpManuel: chkMdpManuel,
|
||||
chkUsernameManuel: chkUsernameManuel,
|
||||
logoBase64: logoBase64,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,6 +220,9 @@ class AmicaleModel extends HiveObject {
|
||||
'chk_stripe': chkStripe ? 1 : 0,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'chk_mdp_manuel': chkMdpManuel ? 1 : 0,
|
||||
'chk_username_manuel': chkUsernameManuel ? 1 : 0,
|
||||
// Note: logoBase64 n'est pas envoyé via toJson (lecture seule depuis l'API)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,6 +249,9 @@ class AmicaleModel extends HiveObject {
|
||||
bool? chkStripe,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
bool? chkMdpManuel,
|
||||
bool? chkUsernameManuel,
|
||||
String? logoBase64,
|
||||
}) {
|
||||
return AmicaleModel(
|
||||
id: id,
|
||||
@@ -244,6 +276,9 @@ class AmicaleModel extends HiveObject {
|
||||
chkStripe: chkStripe ?? this.chkStripe,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
|
||||
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
|
||||
logoBase64: logoBase64 ?? this.logoBase64,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,16 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
||||
chkStripe: fields[19] as bool,
|
||||
createdAt: fields[20] as DateTime?,
|
||||
updatedAt: fields[21] as DateTime?,
|
||||
chkMdpManuel: fields[22] as bool,
|
||||
chkUsernameManuel: fields[23] as bool,
|
||||
logoBase64: fields[24] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AmicaleModel obj) {
|
||||
writer
|
||||
..writeByte(22)
|
||||
..writeByte(25)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -89,7 +92,13 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
||||
..writeByte(20)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(21)
|
||||
..write(obj.updatedAt);
|
||||
..write(obj.updatedAt)
|
||||
..writeByte(22)
|
||||
..write(obj.chkMdpManuel)
|
||||
..writeByte(23)
|
||||
..write(obj.chkUsernameManuel)
|
||||
..writeByte(24)
|
||||
..write(obj.logoBase64);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -70,6 +70,12 @@ class ClientModel extends HiveObject {
|
||||
@HiveField(21)
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@HiveField(22)
|
||||
final bool? chkMdpManuel;
|
||||
|
||||
@HiveField(23)
|
||||
final bool? chkUsernameManuel;
|
||||
|
||||
ClientModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -93,6 +99,8 @@ class ClientModel extends HiveObject {
|
||||
this.chkStripe,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.chkMdpManuel,
|
||||
this.chkUsernameManuel,
|
||||
});
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
@@ -138,6 +146,8 @@ class ClientModel extends HiveObject {
|
||||
chkStripe: json['chk_stripe'] == 1 || json['chk_stripe'] == true,
|
||||
createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null,
|
||||
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null,
|
||||
chkMdpManuel: json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true,
|
||||
chkUsernameManuel: json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,6 +176,8 @@ class ClientModel extends HiveObject {
|
||||
'chk_stripe': chkStripe,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'chk_mdp_manuel': chkMdpManuel,
|
||||
'chk_username_manuel': chkUsernameManuel,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,6 +204,8 @@ class ClientModel extends HiveObject {
|
||||
bool? chkStripe,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
bool? chkMdpManuel,
|
||||
bool? chkUsernameManuel,
|
||||
}) {
|
||||
return ClientModel(
|
||||
id: id,
|
||||
@@ -216,6 +230,8 @@ class ClientModel extends HiveObject {
|
||||
chkStripe: chkStripe ?? this.chkStripe,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
|
||||
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,15 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
|
||||
chkStripe: fields[19] as bool?,
|
||||
createdAt: fields[20] as DateTime?,
|
||||
updatedAt: fields[21] as DateTime?,
|
||||
chkMdpManuel: fields[22] as bool?,
|
||||
chkUsernameManuel: fields[23] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ClientModel obj) {
|
||||
writer
|
||||
..writeByte(22)
|
||||
..writeByte(24)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -89,7 +91,11 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
|
||||
..writeByte(20)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(21)
|
||||
..write(obj.updatedAt);
|
||||
..write(obj.updatedAt)
|
||||
..writeByte(22)
|
||||
..write(obj.chkMdpManuel)
|
||||
..writeByte(23)
|
||||
..write(obj.chkUsernameManuel);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ class ApiService {
|
||||
late final String _baseUrl;
|
||||
late final String _appIdentifier;
|
||||
String? _sessionId;
|
||||
|
||||
// Getters pour les propriétés (lecture seule)
|
||||
String? get sessionId => _sessionId;
|
||||
String get baseUrl => _baseUrl;
|
||||
|
||||
// Singleton thread-safe
|
||||
static ApiService get instance {
|
||||
@@ -142,8 +146,11 @@ class ApiService {
|
||||
Future<Response> post(String path, {dynamic data}) async {
|
||||
try {
|
||||
return await _dio.post(path, data: data);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la requête POST', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +158,11 @@ class ApiService {
|
||||
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
|
||||
try {
|
||||
return await _dio.get(path, queryParameters: queryParameters);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la requête GET', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +170,11 @@ class ApiService {
|
||||
Future<Response> put(String path, {dynamic data}) async {
|
||||
try {
|
||||
return await _dio.put(path, data: data);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la requête PUT', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +182,81 @@ class ApiService {
|
||||
Future<Response> delete(String path) async {
|
||||
try {
|
||||
return await _dio.delete(path);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la requête DELETE', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour uploader un logo d'amicale
|
||||
Future<Map<String, dynamic>> uploadLogo(int entiteId, dynamic imageFile) async {
|
||||
try {
|
||||
FormData formData;
|
||||
|
||||
// Gestion différente selon la plateforme (Web ou Mobile)
|
||||
if (kIsWeb) {
|
||||
// Pour le web, imageFile est un XFile
|
||||
final bytes = await imageFile.readAsBytes();
|
||||
|
||||
// Vérification de la taille (5 Mo max)
|
||||
const int maxSize = 5 * 1024 * 1024;
|
||||
if (bytes.length > maxSize) {
|
||||
throw ApiException(
|
||||
'Le fichier est trop volumineux. Taille maximale: 5 Mo',
|
||||
statusCode: 413
|
||||
);
|
||||
}
|
||||
|
||||
formData = FormData.fromMap({
|
||||
'logo': MultipartFile.fromBytes(
|
||||
bytes,
|
||||
filename: imageFile.name,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
// Pour mobile, imageFile est un File
|
||||
final fileLength = await imageFile.length();
|
||||
|
||||
// Vérification de la taille (5 Mo max)
|
||||
const int maxSize = 5 * 1024 * 1024;
|
||||
if (fileLength > maxSize) {
|
||||
throw ApiException(
|
||||
'Le fichier est trop volumineux. Taille maximale: 5 Mo',
|
||||
statusCode: 413
|
||||
);
|
||||
}
|
||||
|
||||
formData = FormData.fromMap({
|
||||
'logo': await MultipartFile.fromFile(
|
||||
imageFile.path,
|
||||
filename: imageFile.path.split('/').last,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
'/entites/$entiteId/logo',
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw ApiException('Erreur lors de l\'upload du logo',
|
||||
statusCode: response.statusCode);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de l\'upload du logo', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
163
app/lib/core/services/logger_service.dart
Normal file
163
app/lib/core/services/logger_service.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
|
||||
/// Service de logging centralisé qui désactive automatiquement les logs en production
|
||||
class LoggerService {
|
||||
static LoggerService? _instance;
|
||||
static bool? _isProduction;
|
||||
|
||||
// Singleton
|
||||
static LoggerService get instance {
|
||||
_instance ??= LoggerService._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
LoggerService._internal();
|
||||
|
||||
/// Détermine si on est en production
|
||||
static bool get isProduction {
|
||||
if (_isProduction != null) return _isProduction!;
|
||||
|
||||
try {
|
||||
// Vérifier si ApiService est initialisé
|
||||
final env = ApiService.instance.getCurrentEnvironment();
|
||||
_isProduction = env == 'PROD';
|
||||
} catch (e) {
|
||||
// Si ApiService n'est pas encore initialisé, utiliser kReleaseMode
|
||||
_isProduction = kReleaseMode;
|
||||
}
|
||||
|
||||
return _isProduction!;
|
||||
}
|
||||
|
||||
/// Réinitialiser le cache de l'environnement (utile pour les tests)
|
||||
static void resetEnvironmentCache() {
|
||||
_isProduction = null;
|
||||
}
|
||||
|
||||
/// Log simple (remplace debugPrint)
|
||||
static void log(String message, {String? prefix}) {
|
||||
if (!isProduction) {
|
||||
if (prefix != null) {
|
||||
debugPrint('$prefix $message');
|
||||
} else {
|
||||
debugPrint(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'information
|
||||
static void info(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('ℹ️ $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de succès
|
||||
static void success(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('✅ $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'avertissement
|
||||
static void warning(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('⚠️ $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'erreur (toujours affiché même en production pour le debugging)
|
||||
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
|
||||
// Les erreurs sont toujours loggées, même en production
|
||||
debugPrint('❌ $message');
|
||||
if (error != null) {
|
||||
debugPrint(' Error: $error');
|
||||
}
|
||||
if (stackTrace != null && !isProduction) {
|
||||
// La stack trace n'est affichée qu'en DEV/REC
|
||||
debugPrint(' Stack trace:\n$stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de debug (avec emoji personnalisé)
|
||||
static void debug(String message, {String emoji = '🔧'}) {
|
||||
if (!isProduction) {
|
||||
debugPrint('$emoji $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de requête API
|
||||
static void api(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('🔗 $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de base de données
|
||||
static void database(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('💾 $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de navigation
|
||||
static void navigation(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('🧭 $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de performance/timing
|
||||
static void performance(String message) {
|
||||
if (!isProduction) {
|
||||
debugPrint('⏱️ $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Log conditionnel basé sur un flag custom
|
||||
static void conditional(String message, {required bool condition}) {
|
||||
if (!isProduction && condition) {
|
||||
debugPrint(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Groupe de logs (pour regrouper visuellement des logs liés)
|
||||
static void group(String title, List<String> messages) {
|
||||
if (!isProduction) {
|
||||
debugPrint('┌─ $title');
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
if (i == messages.length - 1) {
|
||||
debugPrint('└─ ${messages[i]}');
|
||||
} else {
|
||||
debugPrint('├─ ${messages[i]}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'objet JSON (formaté)
|
||||
static void json(String label, Map<String, dynamic> data) {
|
||||
if (!isProduction) {
|
||||
debugPrint('📋 $label:');
|
||||
data.forEach((key, value) {
|
||||
debugPrint(' $key: $value');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour faciliter l'utilisation
|
||||
extension LoggerExtension on String {
|
||||
void log({String? prefix}) => LoggerService.log(this, prefix: prefix);
|
||||
void logInfo() => LoggerService.info(this);
|
||||
void logSuccess() => LoggerService.success(this);
|
||||
void logWarning() => LoggerService.warning(this);
|
||||
void logError([dynamic error, StackTrace? stackTrace]) =>
|
||||
LoggerService.error(this, error, stackTrace);
|
||||
void logDebug({String emoji = '🔧'}) => LoggerService.debug(this, emoji: emoji);
|
||||
void logApi() => LoggerService.api(this);
|
||||
void logDatabase() => LoggerService.database(this);
|
||||
void logNavigation() => LoggerService.navigation(this);
|
||||
void logPerformance() => LoggerService.performance(this);
|
||||
}
|
||||
Reference in New Issue
Block a user