membre add
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
273
app/lib/core/utils/api_exception.dart
Normal file
273
app/lib/core/utils/api_exception.dart
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/presentation/widgets/user_form_dialog.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/amicale_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/membre_repository.dart';
|
||||
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
|
||||
/// Class pour dessiner les petits points blancs sur le fond
|
||||
class DotsPainter extends CustomPainter {
|
||||
@@ -93,30 +95,86 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
void _handleEditMembre(MembreModel membre) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => UserFormDialog(
|
||||
title: 'Modifier le membre',
|
||||
user: membre.toUserModel(),
|
||||
showRoleSelector: true,
|
||||
showActiveCheckbox: true, // Activer la checkbox
|
||||
allowUsernameEdit: true, // Permettre l'édition du username
|
||||
// allowSectNameEdit sera automatiquement true via UserForm
|
||||
availableRoles: const [
|
||||
RoleOption(
|
||||
value: 1,
|
||||
label: 'Membre',
|
||||
description: 'Peut consulter et distribuer dans ses secteurs',
|
||||
),
|
||||
RoleOption(
|
||||
value: 2,
|
||||
label: 'Administrateur',
|
||||
description: 'Peut gérer l\'amicale et ses membres',
|
||||
),
|
||||
],
|
||||
onSubmit: (updatedUser) async {
|
||||
try {
|
||||
// Convertir le UserModel mis à jour vers MembreModel
|
||||
final updatedMembre = MembreModel.fromUserModel(updatedUser, membre);
|
||||
|
||||
// Utiliser directement updateMembre qui passe par l'API /users
|
||||
final success = await widget.membreRepository.updateMembre(updatedMembre);
|
||||
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ApiException.showSuccess(context, 'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
|
||||
} else if (!success && mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la mise à jour'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mise à jour membre: $e');
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDeleteMembre(MembreModel membre) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Modifier le membre'),
|
||||
content: Text('Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'),
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\nCette action est irréversible.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
// TODO: Naviguer vers la page de modification
|
||||
// Navigator.of(context).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => EditMembrePage(
|
||||
// membre: membre,
|
||||
// membreRepository: widget.membreRepository,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
try {
|
||||
// Utiliser la méthode qui passe par l'API
|
||||
final success = await widget.membreRepository.deleteMembre(membre.id);
|
||||
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} supprimé avec succès');
|
||||
} else if (!success && mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la suppression'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Modifier'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -126,15 +184,82 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
void _handleAddMembre() {
|
||||
if (_currentUser?.fkEntite == null) return;
|
||||
|
||||
// TODO: Naviguer vers la page d'ajout de membre
|
||||
// Navigator.of(context).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => AddMembrePage(
|
||||
// amicaleId: _currentUser!.fkEntite!,
|
||||
// membreRepository: widget.membreRepository,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// Créer un UserModel vide avec les valeurs par défaut
|
||||
final newUser = UserModel(
|
||||
id: 0, // ID temporaire pour nouveau membre
|
||||
username: '',
|
||||
firstName: '',
|
||||
name: '',
|
||||
sectName: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
fkTitre: 1, // Par défaut M.
|
||||
fkEntite: _currentUser!.fkEntite!, // Association à l'amicale courante
|
||||
role: 1, // Par défaut membre
|
||||
isActive: true, // Par défaut actif
|
||||
createdAt: DateTime.now(),
|
||||
lastSyncedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => UserFormDialog(
|
||||
title: 'Ajouter un nouveau membre',
|
||||
user: newUser,
|
||||
showRoleSelector: true,
|
||||
showActiveCheckbox: true,
|
||||
allowUsernameEdit: true,
|
||||
availableRoles: const [
|
||||
RoleOption(
|
||||
value: 1,
|
||||
label: 'Membre',
|
||||
description: 'Peut consulter et distribuer dans ses secteurs',
|
||||
),
|
||||
RoleOption(
|
||||
value: 2,
|
||||
label: 'Administrateur',
|
||||
description: 'Peut gérer l\'amicale et ses membres',
|
||||
),
|
||||
],
|
||||
onSubmit: (newUserData) async {
|
||||
try {
|
||||
// Créer un nouveau MembreModel directement
|
||||
final newMembre = MembreModel(
|
||||
id: 0, // L'API assignera un vrai ID
|
||||
username: newUserData.username,
|
||||
firstName: newUserData.firstName,
|
||||
name: newUserData.name,
|
||||
sectName: newUserData.sectName,
|
||||
phone: newUserData.phone,
|
||||
mobile: newUserData.mobile,
|
||||
email: newUserData.email,
|
||||
fkTitre: newUserData.fkTitre,
|
||||
fkEntite: newUserData.fkEntite!,
|
||||
role: newUserData.role,
|
||||
isActive: newUserData.isActive,
|
||||
dateNaissance: newUserData.dateNaissance,
|
||||
dateEmbauche: newUserData.dateEmbauche,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final createdMembre = await widget.membreRepository.createMembre(newMembre);
|
||||
|
||||
if (createdMembre != null && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ApiException.showSuccess(context, 'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès');
|
||||
} else if (mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la création du membre'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création membre: $e');
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -328,7 +453,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
child: MembreTableWidget(
|
||||
membres: membres,
|
||||
onEdit: _handleEditMembre,
|
||||
onDelete: null, // Géré par l'admin principal
|
||||
onDelete: _handleDeleteMembre,
|
||||
membreRepository: widget.membreRepository,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
|
||||
import 'package:geosector_app/presentation/widgets/profile_dialog.dart';
|
||||
import 'package:geosector_app/presentation/widgets/user_form_dialog.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// AppBar personnalisée pour les tableaux de bord
|
||||
@@ -117,10 +118,32 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
icon: const Icon(Icons.person),
|
||||
tooltip: 'Mon compte',
|
||||
onPressed: () {
|
||||
// Afficher la boîte de dialogue de profil avec l'utilisateur actuel
|
||||
// Afficher la boîte de dialogue UserForm avec l'utilisateur actuel
|
||||
final user = userRepository.currentUser;
|
||||
if (user != null) {
|
||||
ProfileDialog.show(context, user);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => UserFormDialog(
|
||||
title: 'Mon compte',
|
||||
user: user,
|
||||
readOnly: false,
|
||||
showRoleSelector: false,
|
||||
onSubmit: (updatedUser) async {
|
||||
try {
|
||||
// Sauvegarder les modifications de l'utilisateur
|
||||
await userRepository.updateUser(updatedUser);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ApiException.showSuccess(context, 'Profil mis à jour');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mise à jour de votre profil: $e');
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
|
||||
/// Widget qui affiche les informations sur l'environnement actuel
|
||||
@@ -8,13 +7,13 @@ class EnvironmentInfoWidget extends StatelessWidget {
|
||||
final bool showInDialog;
|
||||
|
||||
const EnvironmentInfoWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.showInDialog = false,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||
final apiService = ApiService.instance;
|
||||
final environment = apiService.getCurrentEnvironment();
|
||||
final apiUrl = apiService.getCurrentApiUrl();
|
||||
final appIdentifier = apiService.getCurrentAppIdentifier();
|
||||
@@ -27,9 +26,7 @@ class EnvironmentInfoWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'🌍 Environnement GeoSector',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getEnvironmentColor(environment)),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: _getEnvironmentColor(environment)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow(context, 'Environnement', environment),
|
||||
@@ -70,10 +67,7 @@ class EnvironmentInfoWidget extends StatelessWidget {
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -6,6 +6,7 @@ class MembreRowWidget extends StatelessWidget {
|
||||
final Function(MembreModel)? onEdit;
|
||||
final Function(MembreModel)? onDelete;
|
||||
final bool isAlternate;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const MembreRowWidget({
|
||||
super.key,
|
||||
@@ -13,6 +14,7 @@ class MembreRowWidget extends StatelessWidget {
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.isAlternate = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -20,43 +22,45 @@ class MembreRowWidget extends StatelessWidget {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Couleur de fond alternée
|
||||
final backgroundColor = isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface;
|
||||
final backgroundColor = isAlternate ? theme.colorScheme.primary.withValues(alpha: 0.05) : Colors.transparent;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _showMembreDetails(context),
|
||||
// Envelopper le contenu dans un InkWell
|
||||
onTap: onTap, // Utiliser le callback onTap
|
||||
hoverColor: theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// ... existing row content ...
|
||||
|
||||
// ID
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
membre.id.toString(),
|
||||
membre.id.toString() ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// Prénom (firstName)
|
||||
// Prénom
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.firstName ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Nom (name)
|
||||
// Nom
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.name ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -64,13 +68,12 @@ class MembreRowWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
membre.email,
|
||||
membre.email ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Rôle (role au lieu de fkRole)
|
||||
// Rôle
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
@@ -79,22 +82,19 @@ class MembreRowWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Statut (isActive au lieu de chkActive)
|
||||
// Statut
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: membre.isActive ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: membre.isActive ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
|
||||
),
|
||||
color: _getStatusColor(membre.isActive),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Text(
|
||||
membre.isActive ? 'Actif' : 'Inactif',
|
||||
membre.isActive == true ? 'Actif' : 'Inactif',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: membre.isActive ? Colors.green[700] : Colors.red[700],
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -109,43 +109,12 @@ class MembreRowWidget extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton Edit
|
||||
if (onEdit != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () => onEdit!(membre),
|
||||
tooltip: 'Modifier',
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
||||
// Espacement entre les boutons
|
||||
if (onEdit != null && onDelete != null) const SizedBox(width: 8),
|
||||
|
||||
// Bouton Delete
|
||||
if (onDelete != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
size: 20,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
icon: const Icon(Icons.delete, size: 22),
|
||||
onPressed: () => onDelete!(membre),
|
||||
tooltip: 'Supprimer',
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -212,14 +181,18 @@ class MembreRowWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(bool? isActive) {
|
||||
return isActive == true ? Colors.green : Colors.red;
|
||||
}
|
||||
|
||||
// Méthode pour convertir l'ID de rôle en nom lisible
|
||||
String _getRoleName(int roleId) {
|
||||
switch (roleId) {
|
||||
case 1:
|
||||
return 'User';
|
||||
return 'Membre';
|
||||
case 2:
|
||||
return 'Admin';
|
||||
case 3:
|
||||
case 9:
|
||||
return 'Super';
|
||||
default:
|
||||
return roleId.toString();
|
||||
|
||||
@@ -48,10 +48,15 @@ class MembreTableWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête du tableau
|
||||
// En-tête du tableau avec fond grisé
|
||||
if (showHeader)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// ID
|
||||
@@ -184,6 +189,7 @@ class MembreTableWidget extends StatelessWidget {
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
isAlternate: index % 2 == 1,
|
||||
onTap: onEdit != null ? () => onEdit!(membre) : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/presentation/widgets/user_form.dart';
|
||||
|
||||
class ProfileDialog extends StatefulWidget {
|
||||
final UserModel user;
|
||||
|
||||
const ProfileDialog({
|
||||
Key? key,
|
||||
required this.user,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Méthode statique pour afficher la boîte de dialogue
|
||||
static Future<bool?> show(BuildContext context, UserModel user) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ProfileDialog(user: user),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ProfileDialog> createState() => _ProfileDialogState();
|
||||
}
|
||||
|
||||
class _ProfileDialogState extends State<ProfileDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late UserModel _user;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_user = widget.user;
|
||||
}
|
||||
|
||||
// Fonction pour capitaliser la première lettre de chaque mot
|
||||
String _capitalizeFirstLetter(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
|
||||
return text.split(' ').map((word) {
|
||||
if (word.isEmpty) return word;
|
||||
return word[0].toUpperCase() + word.substring(1).toLowerCase();
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
// Fonction pour mettre en majuscule
|
||||
String _toUpperCase(String text) {
|
||||
return text.toUpperCase();
|
||||
}
|
||||
|
||||
// Fonction pour valider et soumettre le formulaire
|
||||
Future<void> _saveProfile(UserModel updatedUser) async {
|
||||
// Validation supplémentaire
|
||||
if (!_validateUser(updatedUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Formatage des données
|
||||
final formattedUser = updatedUser.copyWith(
|
||||
name: _toUpperCase(updatedUser.name ?? ''),
|
||||
firstName: _capitalizeFirstLetter(updatedUser.firstName ?? ''),
|
||||
);
|
||||
|
||||
// Sauvegarde de l'utilisateur
|
||||
await userRepository.saveUser(formattedUser);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Profil mis à jour avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true); // Fermer la modale avec succès
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de la mise à jour du profil: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation supplémentaire
|
||||
bool _validateUser(UserModel user) {
|
||||
// Vérifier que l'email est valide
|
||||
if (user.email.isEmpty ||
|
||||
!user.email.contains('@') ||
|
||||
!user.email.contains('.')) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez entrer une adresse email valide'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier que le nom ou le sectName est renseigné
|
||||
if ((user.name == null || user.name!.isEmpty) &&
|
||||
(user.sectName == null || user.sectName!.isEmpty)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Le nom ou le nom du secteur doit être renseigné'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier que le téléphone fixe est valide s'il est renseigné
|
||||
if (user.phone != null &&
|
||||
user.phone!.isNotEmpty &&
|
||||
user.phone!.length != 10) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Le numéro de téléphone fixe doit contenir 10 chiffres'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier que le téléphone mobile est valide s'il est renseigné
|
||||
if (user.mobile != null &&
|
||||
user.mobile!.isNotEmpty &&
|
||||
user.mobile!.length != 10) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Le numéro de téléphone mobile doit contenir 10 chiffres'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: 700,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Mon compte',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
tooltip: 'Fermer',
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Formulaire
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: UserForm(
|
||||
user: _user,
|
||||
onSubmit: _saveProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isLoading ? null : () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
// Appeler directement la méthode onSubmit du UserForm
|
||||
// qui va déclencher la validation et la soumission
|
||||
_saveProfile(_user);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,17 @@ class UserForm extends StatefulWidget {
|
||||
final UserModel? user;
|
||||
final Function(UserModel)? onSubmit;
|
||||
final bool readOnly;
|
||||
final bool allowUsernameEdit;
|
||||
final bool allowSectNameEdit;
|
||||
|
||||
const UserForm({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.user,
|
||||
this.onSubmit,
|
||||
this.readOnly = false,
|
||||
}) : super(key: key);
|
||||
this.allowUsernameEdit = false,
|
||||
this.allowSectNameEdit = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UserForm> createState() => _UserFormState();
|
||||
@@ -27,6 +31,7 @@ class _UserFormState extends State<UserForm> {
|
||||
late final TextEditingController _usernameController;
|
||||
late final TextEditingController _firstNameController;
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _sectNameController;
|
||||
late final TextEditingController _phoneController;
|
||||
late final TextEditingController _mobileController;
|
||||
late final TextEditingController _emailController;
|
||||
@@ -47,6 +52,7 @@ class _UserFormState extends State<UserForm> {
|
||||
_usernameController = TextEditingController(text: user?.username ?? '');
|
||||
_firstNameController = TextEditingController(text: user?.firstName ?? '');
|
||||
_nameController = TextEditingController(text: user?.name ?? '');
|
||||
_sectNameController = TextEditingController(text: user?.sectName ?? '');
|
||||
_phoneController = TextEditingController(text: user?.phone ?? '');
|
||||
_mobileController = TextEditingController(text: user?.mobile ?? '');
|
||||
_emailController = TextEditingController(text: user?.email ?? '');
|
||||
@@ -54,15 +60,9 @@ class _UserFormState extends State<UserForm> {
|
||||
_dateNaissance = user?.dateNaissance;
|
||||
_dateEmbauche = user?.dateEmbauche;
|
||||
|
||||
_dateNaissanceController = TextEditingController(
|
||||
text: _dateNaissance != null
|
||||
? DateFormat('dd/MM/yyyy').format(_dateNaissance!)
|
||||
: '');
|
||||
_dateNaissanceController = TextEditingController(text: _dateNaissance != null ? DateFormat('dd/MM/yyyy').format(_dateNaissance!) : '');
|
||||
|
||||
_dateEmbaucheController = TextEditingController(
|
||||
text: _dateEmbauche != null
|
||||
? DateFormat('dd/MM/yyyy').format(_dateEmbauche!)
|
||||
: '');
|
||||
_dateEmbaucheController = TextEditingController(text: _dateEmbauche != null ? DateFormat('dd/MM/yyyy').format(_dateEmbauche!) : '');
|
||||
|
||||
_fkTitre = user?.fkTitre ?? 1;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ class _UserFormState extends State<UserForm> {
|
||||
_usernameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_nameController.dispose();
|
||||
_sectNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_mobileController.dispose();
|
||||
_emailController.dispose();
|
||||
@@ -80,6 +81,19 @@ class _UserFormState extends State<UserForm> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Validation conditionnelle pour name/sectName
|
||||
String? _validateNameOrSectName(String? value, bool isNameField) {
|
||||
final nameValue = _nameController.text.trim();
|
||||
final sectNameValue = _sectNameController.text.trim();
|
||||
|
||||
// Si les deux sont vides
|
||||
if (nameValue.isEmpty && sectNameValue.isEmpty) {
|
||||
return isNameField ? "Veuillez renseigner soit le nom soit le nom de tournée" : "Veuillez renseigner soit le nom de tournée soit le nom";
|
||||
}
|
||||
|
||||
return null; // Validation OK si au moins un des deux est rempli
|
||||
}
|
||||
|
||||
// Méthode simplifiée pour sélectionner une date
|
||||
void _selectDate(BuildContext context, bool isDateNaissance) {
|
||||
// Utiliser un bloc try-catch pour capturer toutes les erreurs possibles
|
||||
@@ -98,12 +112,10 @@ class _UserFormState extends State<UserForm> {
|
||||
// Mettre à jour la date et le texte du contrôleur
|
||||
if (isDateNaissance) {
|
||||
_dateNaissance = picked;
|
||||
_dateNaissanceController.text =
|
||||
DateFormat('dd/MM/yyyy').format(picked);
|
||||
_dateNaissanceController.text = DateFormat('dd/MM/yyyy').format(picked);
|
||||
} else {
|
||||
_dateEmbauche = picked;
|
||||
_dateEmbaucheController.text =
|
||||
DateFormat('dd/MM/yyyy').format(picked);
|
||||
_dateEmbaucheController.text = DateFormat('dd/MM/yyyy').format(picked);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -111,7 +123,7 @@ class _UserFormState extends State<UserForm> {
|
||||
// Gérer les erreurs spécifiques au sélecteur de date
|
||||
debugPrint('Erreur lors de la sélection de la date: $error');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Erreur lors de la sélection de la date'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
@@ -121,7 +133,7 @@ class _UserFormState extends State<UserForm> {
|
||||
// Gérer toutes les autres erreurs
|
||||
debugPrint('Exception lors de l\'affichage du sélecteur de date: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Impossible d\'afficher le sélecteur de date'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
@@ -129,11 +141,14 @@ class _UserFormState extends State<UserForm> {
|
||||
}
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
// Méthode publique pour valider et récupérer l'utilisateur
|
||||
UserModel? validateAndGetUser() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final user = widget.user?.copyWith(
|
||||
return widget.user?.copyWith(
|
||||
username: _usernameController.text,
|
||||
firstName: _firstNameController.text,
|
||||
name: _nameController.text,
|
||||
sectName: _sectNameController.text,
|
||||
phone: _phoneController.text,
|
||||
mobile: _mobileController.text,
|
||||
email: _emailController.text,
|
||||
@@ -142,42 +157,113 @@ class _UserFormState extends State<UserForm> {
|
||||
dateEmbauche: _dateEmbauche,
|
||||
) ??
|
||||
UserModel(
|
||||
id: 0, // Sera remplacé par l'API
|
||||
id: 0,
|
||||
username: _usernameController.text,
|
||||
firstName: _firstNameController.text,
|
||||
name: _nameController.text,
|
||||
sectName: _sectNameController.text,
|
||||
phone: _phoneController.text,
|
||||
mobile: _mobileController.text,
|
||||
email: _emailController.text,
|
||||
fkTitre: _fkTitre,
|
||||
dateNaissance: _dateNaissance,
|
||||
dateEmbauche: _dateEmbauche,
|
||||
role: 1, // Valeur par défaut
|
||||
role: 1,
|
||||
createdAt: DateTime.now(),
|
||||
lastSyncedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(user);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isWideScreen = MediaQuery.of(context).size.width > 900;
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom d'utilisateur (en lecture seule)
|
||||
CustomTextField(
|
||||
controller: _usernameController,
|
||||
label: "Nom d'utilisateur",
|
||||
readOnly: true, // Toujours en lecture seule
|
||||
prefixIcon: Icons.account_circle,
|
||||
),
|
||||
// Ligne 1: Username et Email (si écran large)
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _usernameController,
|
||||
label: "Nom d'utilisateur",
|
||||
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
|
||||
prefixIcon: Icons.account_circle,
|
||||
isRequired: widget.allowUsernameEdit,
|
||||
validator: widget.allowUsernameEdit
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _emailController,
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true, // Email toujours obligatoire
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer l'adresse email";
|
||||
}
|
||||
if (!value.contains('@') || !value.contains('.')) {
|
||||
return "Veuillez entrer une adresse email valide";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// Version mobile: Username seul
|
||||
CustomTextField(
|
||||
controller: _usernameController,
|
||||
label: "Nom d'utilisateur",
|
||||
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
|
||||
prefixIcon: Icons.account_circle,
|
||||
isRequired: widget.allowUsernameEdit, // Obligatoire si éditable
|
||||
validator: widget.allowUsernameEdit
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Email seul en mobile
|
||||
CustomTextField(
|
||||
controller: _emailController,
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true, // Email toujours obligatoire
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer l'adresse email";
|
||||
}
|
||||
if (!value.contains('@') || !value.contains('.')) {
|
||||
return "Veuillez entrer une adresse email valide";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Titre (M. ou Mme)
|
||||
@@ -188,7 +274,7 @@ class _UserFormState extends State<UserForm> {
|
||||
"Titre",
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -225,115 +311,210 @@ class _UserFormState extends State<UserForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Prénom
|
||||
CustomTextField(
|
||||
controller: _firstNameController,
|
||||
label: "Prénom",
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le prénom";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nom
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
label: "Nom",
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Téléphone fixe
|
||||
CustomTextField(
|
||||
controller: _phoneController,
|
||||
label: "Téléphone fixe",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro de téléphone doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Téléphone mobile
|
||||
CustomTextField(
|
||||
controller: _mobileController,
|
||||
label: "Téléphone mobile",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro de mobile doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email
|
||||
CustomTextField(
|
||||
controller: _emailController,
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer l'adresse email";
|
||||
}
|
||||
if (!value.contains('@') || !value.contains('.')) {
|
||||
return "Veuillez entrer une adresse email valide";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date de naissance
|
||||
CustomTextField(
|
||||
controller: _dateNaissanceController,
|
||||
label: "Date de naissance",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, true),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
// Ligne 2: Prénom et Nom
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _firstNameController,
|
||||
label: "Prénom",
|
||||
readOnly: widget.readOnly,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _nameController,
|
||||
label: "Nom",
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) => _validateNameOrSectName(value, true),
|
||||
onChanged: (value) {
|
||||
// Revalider sectName quand name change
|
||||
if (widget.allowSectNameEdit) {
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// Version mobile: Prénom et nom séparés
|
||||
CustomTextField(
|
||||
controller: _firstNameController,
|
||||
label: "Prénom",
|
||||
readOnly: widget.readOnly,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
label: "Nom",
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) => _validateNameOrSectName(value, true),
|
||||
onChanged: (value) {
|
||||
// Revalider sectName quand name change
|
||||
if (widget.allowSectNameEdit) {
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date d'embauche
|
||||
CustomTextField(
|
||||
controller: _dateEmbaucheController,
|
||||
label: "Date d'embauche",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, false),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
// Ligne 2.5: Nom de tournée (sectName) - uniquement si éditable
|
||||
if (widget.allowSectNameEdit) ...[
|
||||
CustomTextField(
|
||||
controller: _sectNameController,
|
||||
label: "Nom de tournée",
|
||||
readOnly: widget.readOnly,
|
||||
validator: (value) => _validateNameOrSectName(value, false),
|
||||
onChanged: (value) {
|
||||
// Revalider name quand sectName change
|
||||
_formKey.currentState?.validate();
|
||||
},
|
||||
hintText: "Nom utilisé pour identifier la tournée",
|
||||
),
|
||||
),
|
||||
// Espace en bas du formulaire
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Ligne 3: Téléphones (fixe et mobile)
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _phoneController,
|
||||
label: "Téléphone fixe",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _mobileController,
|
||||
label: "Téléphone mobile",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// Version mobile: Téléphones séparés
|
||||
CustomTextField(
|
||||
controller: _phoneController,
|
||||
label: "Téléphone fixe",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CustomTextField(
|
||||
controller: _mobileController,
|
||||
label: "Téléphone mobile",
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: widget.readOnly,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Ligne 4: Dates (naissance et embauche)
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _dateNaissanceController,
|
||||
label: "Date de naissance",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, true),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _dateEmbaucheController,
|
||||
label: "Date d'embauche",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, false),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// Version mobile: Dates séparées
|
||||
CustomTextField(
|
||||
controller: _dateNaissanceController,
|
||||
label: "Date de naissance",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, true),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CustomTextField(
|
||||
controller: _dateEmbaucheController,
|
||||
label: "Date d'embauche",
|
||||
readOnly: true,
|
||||
onTap: widget.readOnly ? null : () => _selectDate(context, false),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
@@ -360,7 +541,7 @@ class _UserFormState extends State<UserForm> {
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -368,3 +549,6 @@ class _UserFormState extends State<UserForm> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Exporter la classe State pour pouvoir l'utiliser avec GlobalKey
|
||||
typedef UserFormState = _UserFormState;
|
||||
|
||||
237
app/lib/presentation/widgets/user_form_dialog.dart
Normal file
237
app/lib/presentation/widgets/user_form_dialog.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/presentation/widgets/user_form.dart';
|
||||
|
||||
class UserFormDialog extends StatefulWidget {
|
||||
final UserModel? user;
|
||||
final String title;
|
||||
final bool readOnly;
|
||||
final Function(UserModel)? onSubmit;
|
||||
final bool showRoleSelector;
|
||||
final List<RoleOption>? availableRoles;
|
||||
final bool showActiveCheckbox;
|
||||
final bool allowUsernameEdit;
|
||||
|
||||
const UserFormDialog({
|
||||
super.key,
|
||||
this.user,
|
||||
required this.title,
|
||||
this.readOnly = false,
|
||||
this.onSubmit,
|
||||
this.showRoleSelector = false,
|
||||
this.availableRoles,
|
||||
this.showActiveCheckbox = false,
|
||||
this.allowUsernameEdit = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UserFormDialog> createState() => _UserFormDialogState();
|
||||
}
|
||||
|
||||
class RoleOption {
|
||||
final int value;
|
||||
final String label;
|
||||
final String description;
|
||||
|
||||
const RoleOption({
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.description,
|
||||
});
|
||||
}
|
||||
|
||||
class _UserFormDialogState extends State<UserFormDialog> {
|
||||
final GlobalKey<UserFormState> _userFormKey = GlobalKey<UserFormState>();
|
||||
int? _selectedRole;
|
||||
bool? _isActive;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedRole = widget.user?.role;
|
||||
_isActive = widget.user?.isActive ?? true; // Initialiser le statut actif
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
// Utiliser la méthode validateAndGetUser du UserForm
|
||||
final userData = _userFormKey.currentState?.validateAndGetUser();
|
||||
|
||||
if (userData != null) {
|
||||
var finalUser = userData;
|
||||
|
||||
// Ajouter le rôle sélectionné si applicable
|
||||
if (widget.showRoleSelector && _selectedRole != null) {
|
||||
finalUser = finalUser.copyWith(role: _selectedRole);
|
||||
}
|
||||
|
||||
// Ajouter le statut actif si applicable
|
||||
if (widget.showActiveCheckbox && _isActive != null) {
|
||||
finalUser = finalUser.copyWith(isActive: _isActive);
|
||||
}
|
||||
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(finalUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: 700,
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Contenu du formulaire
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Sélecteur de rôle (si activé)
|
||||
if (widget.showRoleSelector && widget.availableRoles != null) ...[
|
||||
Text(
|
||||
'Rôle dans l\'amicale',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: widget.availableRoles!.map((role) {
|
||||
return RadioListTile<int>(
|
||||
title: Text(role.label),
|
||||
subtitle: Text(
|
||||
role.description,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
value: role.value,
|
||||
groupValue: _selectedRole,
|
||||
onChanged: widget.readOnly
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_selectedRole = value;
|
||||
});
|
||||
},
|
||||
activeColor: theme.colorScheme.primary,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Checkbox Statut Actif (si activé)
|
||||
if (widget.showActiveCheckbox) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
title: Text(
|
||||
'Compte actif',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_isActive == true ? 'Le membre peut se connecter et utiliser l\'application' : 'Le membre ne peut pas se connecter',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
value: _isActive,
|
||||
onChanged: widget.readOnly
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_isActive = value ?? true;
|
||||
});
|
||||
},
|
||||
activeColor: theme.colorScheme.primary,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Formulaire utilisateur avec la clé
|
||||
UserForm(
|
||||
key: _userFormKey,
|
||||
user: widget.user,
|
||||
readOnly: widget.readOnly,
|
||||
allowUsernameEdit: widget.allowUsernameEdit,
|
||||
allowSectNameEdit: widget.allowUsernameEdit,
|
||||
onSubmit: null, // Pas besoin de callback
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Footer avec boutons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (!widget.readOnly)
|
||||
ElevatedButton(
|
||||
onPressed: _handleSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user