membre add
This commit is contained in:
File diff suppressed because one or more lines are too long
31
app/.dart_tool/extension_discovery/README.md
Normal file
31
app/.dart_tool/extension_discovery/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
Extension Discovery Cache
|
||||
=========================
|
||||
|
||||
This folder is used by `package:extension_discovery` to cache lists of
|
||||
packages that contains extensions for other packages.
|
||||
|
||||
DO NOT USE THIS FOLDER
|
||||
----------------------
|
||||
|
||||
* Do not read (or rely) the contents of this folder.
|
||||
* Do write to this folder.
|
||||
|
||||
If you're interested in the lists of extensions stored in this folder use the
|
||||
API offered by package `extension_discovery` to get this information.
|
||||
|
||||
If this package doesn't work for your use-case, then don't try to read the
|
||||
contents of this folder. It may change, and will not remain stable.
|
||||
|
||||
Use package `extension_discovery`
|
||||
---------------------------------
|
||||
|
||||
If you want to access information from this folder.
|
||||
|
||||
Feel free to delete this folder
|
||||
-------------------------------
|
||||
|
||||
Files in this folder act as a cache, and the cache is discarded if the files
|
||||
are older than the modification time of `.dart_tool/package_config.json`.
|
||||
|
||||
Hence, it should never be necessary to clear this cache manually, if you find a
|
||||
need to do please file a bug.
|
||||
1
app/.dart_tool/extension_discovery/vs_code.json
Normal file
1
app/.dart_tool/extension_discovery/vs_code.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1789,6 +1789,7 @@ file:///Users/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/services/location_service.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/main.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
|
||||
@@ -1833,10 +1834,10 @@ file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widge
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/passages/passage_form.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/profile_dialog.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/user_form.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/user_form_dialog.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/crypto.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/embedder_config.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/http.dart
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1788,6 +1788,7 @@ file:///Users/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/services/location_service.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/main.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
|
||||
@@ -1832,10 +1833,10 @@ file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widge
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/passages/passage_form.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/profile_dialog.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/user_form.dart
|
||||
file:///Users/pierre/dev/geosector/app/lib/presentation/widgets/user_form_dialog.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/crypto.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/embedder_config.dart
|
||||
org-dartlang-sdk:///dart-sdk/lib/_http/http.dart
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -535,12 +535,6 @@
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.0"
|
||||
},
|
||||
{
|
||||
"name": "nested",
|
||||
"rootUri": "file:///Users/pierre/.pub-cache/hosted/pub.dev/nested-1.0.0",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "nm",
|
||||
"rootUri": "file:///Users/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0",
|
||||
@@ -655,12 +649,6 @@
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "provider",
|
||||
"rootUri": "file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "pub_semver",
|
||||
"rootUri": "file:///Users/pierre/.pub-cache/hosted/pub.dev/pub_semver-2.2.0",
|
||||
|
||||
@@ -338,10 +338,6 @@ mqtt5_client
|
||||
3.0
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/mqtt5_client-4.11.0/
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/mqtt5_client-4.11.0/lib/
|
||||
nested
|
||||
2.12
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/nested-1.0.0/
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/nested-1.0.0/lib/
|
||||
nm
|
||||
2.12
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0/
|
||||
@@ -418,10 +414,6 @@ proj4dart
|
||||
2.12
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/
|
||||
provider
|
||||
2.12
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/
|
||||
pub_semver
|
||||
3.4
|
||||
file:///Users/pierre/.pub-cache/hosted/pub.dev/pub_semver-2.2.0/
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "geosector_app",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"dependencies": [
|
||||
"connectivity_plus",
|
||||
"cupertino_icons",
|
||||
@@ -24,8 +24,6 @@
|
||||
"latlong2",
|
||||
"mqtt5_client",
|
||||
"package_info_plus",
|
||||
"path_provider",
|
||||
"provider",
|
||||
"retry",
|
||||
"syncfusion_flutter_charts",
|
||||
"universal_html",
|
||||
@@ -336,27 +334,6 @@
|
||||
"path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "provider",
|
||||
"version": "6.1.5",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"flutter",
|
||||
"nested"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider",
|
||||
"version": "2.1.5",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_android",
|
||||
"path_provider_foundation",
|
||||
"path_provider_linux",
|
||||
"path_provider_platform_interface",
|
||||
"path_provider_windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hive_flutter",
|
||||
"version": "1.1.0",
|
||||
@@ -1228,6 +1205,18 @@
|
||||
"vector_graphics_codec"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider",
|
||||
"version": "2.1.5",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_android",
|
||||
"path_provider_foundation",
|
||||
"path_provider_linux",
|
||||
"path_provider_platform_interface",
|
||||
"path_provider_windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nm",
|
||||
"version": "0.5.0",
|
||||
@@ -1264,59 +1253,6 @@
|
||||
"typed_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nested",
|
||||
"version": "1.0.0",
|
||||
"dependencies": [
|
||||
"flutter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_windows",
|
||||
"version": "2.3.0",
|
||||
"dependencies": [
|
||||
"ffi",
|
||||
"flutter",
|
||||
"path",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_platform_interface",
|
||||
"version": "2.1.2",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"platform",
|
||||
"plugin_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_linux",
|
||||
"version": "2.2.1",
|
||||
"dependencies": [
|
||||
"ffi",
|
||||
"flutter",
|
||||
"path",
|
||||
"path_provider_platform_interface",
|
||||
"xdg_directories"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_foundation",
|
||||
"version": "2.4.1",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_android",
|
||||
"version": "2.2.16",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sky_engine",
|
||||
"version": "0.0.0",
|
||||
@@ -1437,9 +1373,50 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"version": "3.1.6",
|
||||
"dependencies": []
|
||||
"name": "path_provider_windows",
|
||||
"version": "2.3.0",
|
||||
"dependencies": [
|
||||
"ffi",
|
||||
"flutter",
|
||||
"path",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_platform_interface",
|
||||
"version": "2.1.2",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"platform",
|
||||
"plugin_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_linux",
|
||||
"version": "2.2.1",
|
||||
"dependencies": [
|
||||
"ffi",
|
||||
"flutter",
|
||||
"path",
|
||||
"path_provider_platform_interface",
|
||||
"xdg_directories"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_foundation",
|
||||
"version": "2.4.1",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "path_provider_android",
|
||||
"version": "2.2.16",
|
||||
"dependencies": [
|
||||
"flutter",
|
||||
"path_provider_platform_interface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "petitparser",
|
||||
@@ -1470,6 +1447,11 @@
|
||||
"lists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"version": "3.1.6",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "lists",
|
||||
"version": "1.0.1",
|
||||
|
||||
File diff suppressed because one or more lines are too long
2815
app/README2-APP.md
2815
app/README2-APP.md
File diff suppressed because it is too large
Load Diff
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -701,14 +701,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -758,7 +750,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -861,14 +853,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: geosector_app
|
||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||
publish_to: 'none'
|
||||
version: 0.3.3
|
||||
version: 0.3.4
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@@ -17,8 +17,6 @@ dependencies:
|
||||
# État et gestion des données
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
path_provider: ^2.1.1
|
||||
provider: ^6.1.2
|
||||
|
||||
# API & Réseau
|
||||
dio: ^5.3.3
|
||||
|
||||
Reference in New Issue
Block a user