diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index e658063..dc223f7 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -1,23 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:p_tits_pas/models/relais_model.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/relais_service.dart'; import 'package:p_tits_pas/services/user_service.dart'; -class GestionnaireCreateDialog extends StatefulWidget { +class AdminUserFormDialog extends StatefulWidget { final AppUser? initialUser; + final bool withRelais; + final bool adminMode; - const GestionnaireCreateDialog({ + const AdminUserFormDialog({ super.key, this.initialUser, + this.withRelais = true, + this.adminMode = false, }); @override - State createState() => - _GestionnaireCreateDialogState(); + State createState() => _AdminUserFormDialogState(); } -class _GestionnaireCreateDialogState extends State { +class _AdminUserFormDialogState extends State { final _formKey = GlobalKey(); final _nomController = TextEditingController(); final _prenomController = TextEditingController(); @@ -49,7 +53,11 @@ class _GestionnaireCreateDialogState extends State { ? null : initialRelaisId; } - _loadRelais(); + if (widget.withRelais) { + _loadRelais(); + } else { + _isLoadingRelais = false; + } } @override @@ -122,6 +130,23 @@ class _GestionnaireCreateDialogState extends State { return null; } + String? _validatePhone(String? value) { + final base = _required(value, 'Téléphone'); + if (base != null) return base; + final digits = _normalizePhone(value!); + if (digits.length != 10) { + return 'Le téléphone doit contenir 10 chiffres'; + } + if (!digits.startsWith('0')) { + return 'Le téléphone doit commencer par 0'; + } + return null; + } + + String _normalizePhone(String raw) { + return raw.replaceAll(RegExp(r'\D'), ''); + } + Future _submit() async { if (_isSubmitting) return; if (!_formKey.currentState!.validate()) return; @@ -132,34 +157,61 @@ class _GestionnaireCreateDialogState extends State { try { if (_isEditMode) { - await UserService.updateGestionnaire( - gestionnaireId: widget.initialUser!.id, - nom: _nomController.text.trim(), - prenom: _prenomController.text.trim(), - email: _emailController.text.trim(), - telephone: _telephoneController.text.trim(), - relaisId: _selectedRelaisId, - password: _passwordController.text.trim().isEmpty - ? null - : _passwordController.text, - ); + if (widget.adminMode) { + await UserService.updateAdministrateur( + adminId: widget.initialUser!.id, + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + telephone: _normalizePhone(_telephoneController.text), + password: _passwordController.text.trim().isEmpty + ? null + : _passwordController.text, + ); + } else { + await UserService.updateGestionnaire( + gestionnaireId: widget.initialUser!.id, + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + telephone: _normalizePhone(_telephoneController.text), + relaisId: _selectedRelaisId, + password: _passwordController.text.trim().isEmpty + ? null + : _passwordController.text, + ); + } } else { - await UserService.createGestionnaire( - nom: _nomController.text.trim(), - prenom: _prenomController.text.trim(), - email: _emailController.text.trim(), - password: _passwordController.text, - telephone: _telephoneController.text.trim(), - relaisId: _selectedRelaisId, - ); + if (widget.adminMode) { + await UserService.createAdministrateur( + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text, + telephone: _normalizePhone(_telephoneController.text), + ); + } else { + await UserService.createGestionnaire( + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text, + telephone: _normalizePhone(_telephoneController.text), + relaisId: _selectedRelaisId, + ); + } } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( _isEditMode - ? 'Gestionnaire modifié avec succès.' - : 'Gestionnaire créé avec succès.', + ? (widget.adminMode + ? 'Administrateur modifié avec succès.' + : 'Gestionnaire modifié avec succès.') + : (widget.adminMode + ? 'Administrateur créé avec succès.' + : 'Gestionnaire créé avec succès.'), ), ), ); @@ -242,8 +294,12 @@ class _GestionnaireCreateDialogState extends State { Expanded( child: Text( _isEditMode - ? 'Modifier un gestionnaire' - : 'Créer un gestionnaire', + ? (widget.adminMode + ? 'Modifier un administrateur' + : 'Modifier un gestionnaire') + : (widget.adminMode + ? 'Créer un administrateur' + : 'Créer un gestionnaire'), ), ), if (_isEditMode) @@ -266,9 +322,9 @@ class _GestionnaireCreateDialogState extends State { children: [ Row( children: [ - Expanded(child: _buildNomField()), - const SizedBox(width: 12), Expanded(child: _buildPrenomField()), + const SizedBox(width: 12), + Expanded(child: _buildNomField()), ], ), const SizedBox(height: 12), @@ -281,8 +337,10 @@ class _GestionnaireCreateDialogState extends State { Expanded(child: _buildTelephoneField()), ], ), - const SizedBox(height: 12), - _buildRelaisField(), + if (widget.withRelais) ...[ + const SizedBox(height: 12), + _buildRelaisField(), + ], ], ), ), @@ -378,14 +436,18 @@ class _GestionnaireCreateDialogState extends State { ? 'Nouveau mot de passe' : 'Mot de passe', border: const OutlineInputBorder(), - suffixIcon: IconButton( - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - icon: Icon( - _obscurePassword ? Icons.visibility_off : Icons.visibility, + suffixIcon: Focus( + skipTraversal: true, + canRequestFocus: false, + child: IconButton( + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + ), ), ), ), @@ -397,11 +459,16 @@ class _GestionnaireCreateDialogState extends State { return TextFormField( controller: _telephoneController, keyboardType: TextInputType.phone, + inputFormatters: const [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + _FrenchPhoneNumberFormatter(), + ], decoration: const InputDecoration( - labelText: 'Téléphone', + labelText: 'Téléphone (ex: 06 12 34 56 78)', border: OutlineInputBorder(), ), - validator: (v) => _required(v, 'Téléphone'), + validator: _validatePhone, ); } @@ -448,4 +515,28 @@ class _GestionnaireCreateDialogState extends State { ], ); } +} + +class _FrenchPhoneNumberFormatter extends TextInputFormatter { + const _FrenchPhoneNumberFormatter(); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); + final normalized = digits.length > 10 ? digits.substring(0, 10) : digits; + final buffer = StringBuffer(); + for (var i = 0; i < normalized.length; i++) { + if (i > 0 && i.isEven) buffer.write(' '); + buffer.write(normalized[i]); + } + final formatted = buffer.toString(); + + return TextEditingValue( + text: formatted, + selection: TextSelection.collapsed(offset: formatted.length), + ); + } } \ No newline at end of file diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index d069bca..67e3bf9 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -75,6 +75,41 @@ class UserService { return AppUser.fromJson(data); } + static Future createAdministrateur({ + required String nom, + required String prenom, + required String email, + required String password, + required String telephone, + }) async { + final response = await http.post( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/admin'), + headers: await _headers(), + body: jsonEncode({ + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'password': password, + 'telephone': telephone, + }), + ); + + if (response.statusCode != 200 && response.statusCode != 201) { + final decoded = jsonDecode(response.body); + if (decoded is Map) { + final message = decoded['message']; + if (message is List && message.isNotEmpty) { + throw Exception(message.join(' - ')); + } + throw Exception(_toStr(message) ?? 'Erreur création administrateur'); + } + throw Exception('Erreur création administrateur'); + } + + final data = jsonDecode(response.body) as Map; + return AppUser.fromJson(data); + } + // Récupérer la liste des parents static Future> getParents() async { final response = await http.get( @@ -194,6 +229,47 @@ class UserService { return AppUser.fromJson(data); } + static Future updateAdministrateur({ + required String adminId, + required String nom, + required String prenom, + required String email, + required String telephone, + String? password, + }) async { + final body = { + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'telephone': telephone, + }; + + if (password != null && password.trim().isNotEmpty) { + body['password'] = password.trim(); + } + + final response = await http.patch( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$adminId'), + headers: await _headers(), + body: jsonEncode(body), + ); + + if (response.statusCode != 200) { + final decoded = jsonDecode(response.body); + if (decoded is Map) { + final message = decoded['message']; + if (message is List && message.isNotEmpty) { + throw Exception(message.join(' - ')); + } + throw Exception(_toStr(message) ?? 'Erreur modification administrateur'); + } + throw Exception('Erreur modification administrateur'); + } + + final data = jsonDecode(response.body) as Map; + return AppUser.fromJson(data); + } + static Future deleteUser(String userId) async { final response = await http.delete( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId'), diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart index 666a64e..a8e2db5 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; @@ -51,6 +52,23 @@ class _AdminManagementWidgetState extends State { } } + Future _openAdminEditDialog(AppUser user) async { + final changed = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return AdminUserFormDialog( + initialUser: user, + adminMode: true, + withRelais: false, + ); + }, + ); + if (changed == true) { + await _loadAdmins(); + } + } + @override Widget build(BuildContext context) { final query = widget.searchQuery.toLowerCase(); @@ -80,7 +98,7 @@ class _AdminManagementWidgetState extends State { icon: const Icon(Icons.edit), tooltip: 'Modifier', onPressed: () { - // TODO: Modifier admin + _openAdminEditDialog(user); }, ), ], diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart index 58b78e6..4ea3ead 100644 --- a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -59,7 +59,7 @@ class _GestionnaireManagementWidgetState context: context, barrierDismissible: false, builder: (dialogContext) { - return GestionnaireCreateDialog(initialUser: user); + return AdminUserFormDialog(initialUser: user); }, ); if (changed == true) { diff --git a/frontend/lib/widgets/admin/user_management_panel.dart b/frontend/lib/widgets/admin/user_management_panel.dart index aa408c4..63bc921 100644 --- a/frontend/lib/widgets/admin/user_management_panel.dart +++ b/frontend/lib/widgets/admin/user_management_panel.dart @@ -17,6 +17,7 @@ class AdminUserManagementPanel extends StatefulWidget { class _AdminUserManagementPanelState extends State { int _subIndex = 0; int _gestionnaireRefreshTick = 0; + int _adminRefreshTick = 0; final TextEditingController _searchController = TextEditingController(); final TextEditingController _amCapacityController = TextEditingController(); String? _parentStatus; @@ -150,6 +151,7 @@ class _AdminUserManagementPanelState extends State { ); case 3: return AdminManagementWidget( + key: ValueKey('admins-$_adminRefreshTick'), searchQuery: _searchController.text, ); default: @@ -176,31 +178,52 @@ class _AdminUserManagementPanelState extends State { } Future _handleAddPressed() async { - if (_subIndex != 0) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'La création est disponible uniquement pour les gestionnaires.', - ), - ), + if (_subIndex == 0) { + final created = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return const AdminUserFormDialog(); + }, ); + + if (!mounted) return; + if (created == true) { + setState(() { + _gestionnaireRefreshTick++; + }); + } return; } - final created = await showDialog( - context: context, - barrierDismissible: false, - builder: (dialogContext) { - return const GestionnaireCreateDialog(); - }, - ); + if (_subIndex == 3) { + final created = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return const AdminUserFormDialog( + adminMode: true, + withRelais: false, + ); + }, + ); + + if (!mounted) return; + if (created == true) { + setState(() { + _adminRefreshTick++; + }); + } + return; + } if (!mounted) return; - if (created == true) { - setState(() { - _gestionnaireRefreshTick++; - }); - } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'La création est disponible pour les gestionnaires et administrateurs.', + ), + ), + ); } }