membre add

This commit is contained in:
d6soft
2025-06-11 09:27:25 +02:00
parent f3f1a9c5e8
commit 4244b961fd
40 changed files with 144003 additions and 143144 deletions

View File

@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
class AppKeys {
// Noms des boîtes Hive
static const String userBoxName = 'user';
static const String usersBoxNameOld = 'users';
static const String amicaleBoxName = 'amicale';
static const String clientsBoxName = 'clients';
static const String operationsBoxName = 'operations';

View File

@@ -1,4 +1,5 @@
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
part 'membre_model.g.dart';
@@ -169,4 +170,46 @@ class MembreModel extends HiveObject {
isActive: isActive ?? this.isActive,
);
}
// Convertir un MembreModel vers UserModel pour l'édition
UserModel toUserModel() {
return UserModel(
id: id,
email: email,
name: name,
username: username,
firstName: firstName,
role: role,
createdAt: createdAt,
lastSyncedAt: DateTime.now(),
isActive: isActive,
isSynced: false,
fkEntite: fkEntite,
fkTitre: fkTitre,
phone: phone,
mobile: mobile,
dateNaissance: dateNaissance,
dateEmbauche: dateEmbauche,
sectName: sectName,
);
}
// Créer un MembreModel depuis un UserModel mis à jour
static MembreModel fromUserModel(UserModel user, MembreModel originalMembre) {
return originalMembre.copyWith(
name: user.name,
firstName: user.firstName,
username: user.username,
email: user.email,
fkEntite: user.fkEntite,
role: user.role,
sectName: user.sectName,
fkTitre: user.fkTitre,
phone: user.phone,
mobile: user.mobile,
dateNaissance: user.dateNaissance,
dateEmbauche: user.dateEmbauche,
isActive: user.isActive,
);
}
}

View File

@@ -99,13 +99,13 @@ class MembreRepository extends ChangeNotifier {
}
// Sauvegarder un membre
Future<void> saveMembre(MembreModel membre) async {
Future<void> saveMembreBox(MembreModel membre) async {
await _membreBox.put(membre.id, membre);
notifyListeners();
}
// Supprimer un membre
Future<void> deleteMembre(int id) async {
Future<void> deleteMembreBox(int id) async {
await _membreBox.delete(id);
notifyListeners();
}
@@ -113,48 +113,33 @@ class MembreRepository extends ChangeNotifier {
// === MÉTHODES API ===
// Créer un membre via l'API
Future<bool> createMembre(MembreModel membre) async {
Future<MembreModel?> createMembre(MembreModel membre) async {
_isLoading = true;
notifyListeners();
try {
// Préparer les données pour l'API - exclure l'id pour la création
final data = membre.toJson();
// Convertir en UserModel pour l'API
final userModel = membre.toUserModel();
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
// Appeler l'API pour créer le membre
final response = await ApiService.instance.post('/membres', data: data);
// Appeler l'API users
final response = await ApiService.instance.post('/users', data: data);
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID du nouveau membre
final membreId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
// Créer le membre avec les données retournées par l'API
final createdMember = MembreModel.fromJson(response.data);
// Créer le membre localement avec l'ID retourné par l'API
final newMembre = MembreModel(
id: membreId,
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
await saveMembreBox(createdMember);
await saveMembre(newMembre);
return true;
return createdMember; // Retourner le membre créé
}
return false;
return null;
} catch (e) {
debugPrint('Erreur lors de la création du membre: $e');
return false;
rethrow; // Propager l'exception pour la gestion d'erreurs
} finally {
_isLoading = false;
notifyListeners();
@@ -167,15 +152,15 @@ class MembreRepository extends ChangeNotifier {
notifyListeners();
try {
// Préparer les données pour l'API
final data = membre.toJson();
// Convertir en UserModel pour l'API
final userModel = membre.toUserModel();
// Appeler l'API pour mettre à jour le membre
final response = await ApiService.instance.put('/membres/${membre.id}', data: data);
// Appeler l'API users au lieu de membres
final response = await ApiService.instance.put('/users/${membre.id}', data: userModel.toJson());
if (response.statusCode == 200) {
// Sauvegarder le membre mis à jour localement
await saveMembre(membre);
await saveMembreBox(membre);
return true;
}
@@ -190,17 +175,17 @@ class MembreRepository extends ChangeNotifier {
}
// Supprimer un membre via l'API
Future<bool> deleteMembreViaApi(int id) async {
Future<bool> deleteMembre(int id) async {
_isLoading = true;
notifyListeners();
try {
// Appeler l'API pour supprimer le membre
final response = await ApiService.instance.delete('/membres/$id');
// Appeler l'API users au lieu de membres (correction ici)
final response = await ApiService.instance.delete('/users/$id');
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer le membre localement
await deleteMembre(id);
await deleteMembreBox(id);
return true;
}
@@ -259,12 +244,12 @@ class MembreRepository extends ChangeNotifier {
}
// Récupérer les membres depuis l'API
Future<List<MembreModel>> fetchMembresFromApi() async {
Future<List<MembreModel>> fetchMembres() async {
_isLoading = true;
notifyListeners();
try {
final response = await ApiService.instance.get('/membres');
final response = await ApiService.instance.get('/users');
if (response.statusCode == 200) {
final membresData = response.data;

View File

@@ -423,6 +423,49 @@ class UserRepository extends ChangeNotifier {
await _userBox.delete(id);
}
/// Mettre à jour un utilisateur (pour le profil personnel et la gestion des membres)
Future<UserModel> updateUser(UserModel updatedUser) async {
try {
debugPrint('🔄 Mise à jour utilisateur: ${updatedUser.email}');
// D'ABORD essayer de synchroniser avec l'API
try {
final hasConnection = await ApiService.instance.hasInternetConnection();
if (hasConnection) {
// Tentative de mise à jour sur l'API
await ApiService.instance.updateUser(updatedUser);
debugPrint('✅ Utilisateur mis à jour sur l\'API');
// Si succès API, sauvegarder localement avec sync = true
final syncedUser = updatedUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
);
await _userBox.put(syncedUser.id, syncedUser);
// Si c'est l'utilisateur connecté, mettre à jour le service
if (currentUser?.id == syncedUser.id) {
await CurrentUserService.instance.setUser(syncedUser);
}
notifyListeners();
return syncedUser;
} else {
debugPrint('⚠️ Pas de connexion internet');
throw Exception('Pas de connexion internet');
}
} catch (apiError) {
debugPrint('❌ Erreur API lors de la mise à jour: $apiError');
// Relancer l'erreur pour qu'elle soit gérée par l'appelant
rethrow;
}
} catch (e) {
debugPrint('❌ Erreur mise à jour utilisateur: $e');
rethrow;
}
}
// === MÉTHODES UTILITAIRES POUR LES DONNÉES ===
/// Récupérer la dernière opération active

View File

@@ -7,6 +7,7 @@ import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:retry/retry.dart';
import 'package:universal_html/html.dart' as html;
import 'package:geosector_app/core/utils/api_exception.dart';
class ApiService {
static ApiService? _instance;
@@ -179,31 +180,32 @@ class ApiService {
final response = await _dio.post(AppKeys.loginEndpoint, data: {
'username': username,
'password': password,
'type': type, // Ajouter le type de connexion (user ou admin)
'type': type,
});
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
final status = data['status'] as String?;
// Afficher le message en cas d'erreur
// Si le statut n'est pas 'success', créer une exception avec le message de l'API
if (status != 'success') {
final message = data['message'] as String?;
debugPrint('Erreur d\'authentification: $message');
final message = data['message'] as String? ?? 'Erreur de connexion';
throw ApiException(message);
}
// Si le statut est 'success', récupérer le session_id
if (status == 'success' && data.containsKey('session_id')) {
// Si succès, configurer la session
if (data.containsKey('session_id')) {
final sessionId = data['session_id'];
// Définir la session pour les futures requêtes
if (sessionId != null) {
setSessionId(sessionId);
}
}
return data;
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la connexion', originalError: e);
}
}
@@ -245,21 +247,39 @@ class ApiService {
}
}
Future<UserModel> updateUser(UserModel user) async {
try {
final response = await _dio.put('/users/${user.id}', data: user.toJson());
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
// Si l'API retourne {status: "success", message: "..."}
if (data.containsKey('status') && data['status'] == 'success') {
// L'API confirme le succès mais ne retourne pas l'objet user
// On retourne l'utilisateur original qui a été envoyé
debugPrint('✅ API updateUser success: ${data['message']}');
return user;
}
// Si l'API retourne directement un UserModel (fallback)
return UserModel.fromJson(data);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
throw ApiException('Erreur inattendue lors de la mise à jour', originalError: e);
}
}
// Appliquer la même logique aux autres méthodes
Future<UserModel> createUser(UserModel user) async {
try {
final response = await _dio.post('/users', data: user.toJson());
return UserModel.fromJson(response.data);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
}
}
Future<UserModel> updateUser(UserModel user) async {
try {
final response = await _dio.put('/users/${user.id}', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
throw ApiException('Erreur inattendue lors de la création', originalError: e);
}
}

View File

@@ -0,0 +1,273 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:async';
/// Exception personnalisée pour les erreurs API avec méthodes d'affichage intégrées
class ApiException implements Exception {
final String message;
final int? statusCode;
final String? errorCode;
final Map<String, dynamic>? details;
final Object? originalError;
const ApiException(
this.message, {
this.statusCode,
this.errorCode,
this.details,
this.originalError,
});
/// Créer une ApiException depuis une DioException
factory ApiException.fromDioException(DioException dioException) {
final response = dioException.response;
final statusCode = response?.statusCode;
// Essayer d'extraire le message de la réponse API
String message = 'Erreur de communication avec le serveur';
String? errorCode;
Map<String, dynamic>? details;
if (response?.data != null) {
try {
final data = response!.data as Map<String, dynamic>;
// Message spécifique de l'API
if (data.containsKey('message')) {
message = data['message'] as String;
}
// Code d'erreur spécifique
if (data.containsKey('error_code')) {
errorCode = data['error_code'] as String;
}
// Détails supplémentaires
if (data.containsKey('errors')) {
details = data['errors'] as Map<String, dynamic>?;
}
} catch (e) {
// Si on ne peut pas parser la réponse, utiliser le message par défaut
}
}
// Messages par défaut selon le code de statut
if (response?.data == null || message == 'Erreur de communication avec le serveur') {
switch (statusCode) {
case 400:
message = 'Données invalides';
break;
case 401:
message = 'Non autorisé : veuillez vous reconnecter';
break;
case 403:
message = 'Accès interdit';
break;
case 404:
message = 'Ressource non trouvée';
break;
case 409:
message = 'Conflit : données déjà existantes';
break;
case 422:
message = 'Données de validation incorrectes';
break;
case 500:
message = 'Erreur serveur interne';
break;
case 502:
case 503:
case 504:
message = 'Service temporairement indisponible';
break;
default:
switch (dioException.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
message = 'Délai d\'attente dépassé';
break;
case DioExceptionType.connectionError:
message = 'Problème de connexion réseau';
break;
case DioExceptionType.cancel:
message = 'Requête annulée';
break;
default:
message = 'Erreur de communication avec le serveur';
}
}
}
return ApiException(
message,
statusCode: statusCode,
errorCode: errorCode,
details: details,
originalError: dioException,
);
}
/// Créer une ApiException depuis n'importe quelle erreur
factory ApiException.fromError(Object error) {
if (error is ApiException) {
return error;
}
final errorString = error.toString();
if (errorString.contains('SocketException') || errorString.contains('NetworkException')) {
return const ApiException('Problème de connexion réseau');
}
if (errorString.contains('TimeoutException')) {
return const ApiException('Délai d\'attente dépassé');
}
return ApiException('Erreur inattendue', originalError: error);
}
@override
String toString() => message;
/// Obtenir un message d'erreur formaté pour l'affichage
String get displayMessage => message;
/// Vérifier si c'est une erreur de validation
bool get isValidationError => statusCode == 422 || statusCode == 400;
/// Vérifier si c'est une erreur d'authentification
bool get isAuthError => statusCode == 401 || statusCode == 403;
/// Vérifier si c'est une erreur de conflit (données déjà existantes)
bool get isConflictError => statusCode == 409;
/// Vérifier si c'est une erreur réseau
bool get isNetworkError =>
statusCode == null ||
statusCode == 502 ||
statusCode == 503 ||
statusCode == 504 ||
originalError is DioException && (originalError as DioException).type == DioExceptionType.connectionError;
// === MÉTHODES D'AFFICHAGE INTÉGRÉES ===
/// Afficher cette erreur (méthode d'instance)
void show(BuildContext context, {Duration? duration}) {
if (context.mounted) {
final isInDialog = _isInDialog(context);
if (isInDialog) {
_showOverlaySnackBar(context, displayMessage, Colors.red, duration);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(displayMessage),
backgroundColor: Colors.red,
duration: duration ?? const Duration(seconds: 5),
action: SnackBarAction(
label: 'Fermer',
textColor: Colors.white,
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
),
),
);
}
}
}
/// Méthode statique pour afficher une erreur depuis n'importe quel objet
static void showError(BuildContext context, Object error, {Duration? duration}) {
final apiException = ApiException.fromError(error);
apiException.show(context, duration: duration);
}
/// Méthode statique pour afficher un message de succès (compatible avec les Dialogs)
static void showSuccess(BuildContext context, String message, {Duration? duration}) {
if (context.mounted) {
final isInDialog = _isInDialog(context);
if (isInDialog) {
_showOverlaySnackBar(context, message, Colors.green, duration);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: duration ?? const Duration(seconds: 3),
),
);
}
}
}
/// Vérifier si le contexte est dans un Dialog
static bool _isInDialog(BuildContext context) {
return context.findAncestorWidgetOfExactType<Dialog>() != null;
}
/// Afficher un SnackBar en overlay au-dessus de tout
static void _showOverlaySnackBar(BuildContext context, String message, Color backgroundColor, Duration? duration) {
final overlay = Overlay.of(context);
late OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: MediaQuery.of(context).padding.top + 20, // En haut de l'écran
left: 16,
right: 16,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Icon(
backgroundColor == Colors.red ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 18),
onPressed: () => overlayEntry.remove(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
),
],
),
),
),
),
);
overlay.insert(overlayEntry);
// Auto-suppression après la durée spécifiée
Timer(duration ?? const Duration(seconds: 5), () {
if (overlayEntry.mounted) {
overlayEntry.remove();
}
});
}
}