diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index b6d8ab6..e658063 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.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 { - const GestionnaireCreateDialog({super.key}); + final AppUser? initialUser; + + const GestionnaireCreateDialog({ + super.key, + this.initialUser, + }); @override State createState() => @@ -24,10 +30,25 @@ class _GestionnaireCreateDialogState extends State { bool _isLoadingRelais = true; List _relais = []; String? _selectedRelaisId; + bool get _isEditMode => widget.initialUser != null; @override void initState() { super.initState(); + final user = widget.initialUser; + if (user != null) { + _nomController.text = user.nom ?? ''; + _prenomController.text = user.prenom ?? ''; + _emailController.text = user.email; + _telephoneController.text = user.telephone ?? ''; + // En édition, on ne préremplit jamais le mot de passe. + _passwordController.clear(); + final initialRelaisId = user.relaisId?.trim(); + _selectedRelaisId = + (initialRelaisId == null || initialRelaisId.isEmpty) + ? null + : initialRelaisId; + } _loadRelais(); } @@ -45,13 +66,30 @@ class _GestionnaireCreateDialogState extends State { try { final list = await RelaisService.getRelais(); if (!mounted) return; + final uniqueById = {}; + for (final relais in list) { + uniqueById[relais.id] = relais; + } + + final filtered = uniqueById.values.where((r) => r.actif).toList(); + if (_selectedRelaisId != null && + !filtered.any((r) => r.id == _selectedRelaisId)) { + final selected = uniqueById[_selectedRelaisId!]; + if (selected != null) { + filtered.add(selected); + } else { + _selectedRelaisId = null; + } + } + setState(() { - _relais = list.where((r) => r.actif).toList(); + _relais = filtered; _isLoadingRelais = false; }); } catch (_) { if (!mounted) return; setState(() { + _selectedRelaisId = null; _relais = []; _isLoadingRelais = false; }); @@ -75,6 +113,9 @@ class _GestionnaireCreateDialogState extends State { } String? _validatePassword(String? value) { + if (_isEditMode && (value == null || value.trim().isEmpty)) { + return null; + } final base = _required(value, 'Mot de passe'); if (base != null) return base; if (value!.trim().length < 6) return 'Minimum 6 caractères'; @@ -90,17 +131,37 @@ class _GestionnaireCreateDialogState extends State { }); try { - 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 (_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, + ); + } 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 (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Gestionnaire créé avec succès.')), + SnackBar( + content: Text( + _isEditMode + ? 'Gestionnaire modifié avec succès.' + : 'Gestionnaire créé avec succès.', + ), + ), ); Navigator.of(context).pop(true); } catch (e) { @@ -121,131 +182,269 @@ class _GestionnaireCreateDialogState extends State { } } + Future _delete() async { + if (!_isEditMode || _isSubmitting) return; + + final confirmed = await showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text( + 'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Annuler'), + ), + FilledButton( + onPressed: () => Navigator.of(ctx).pop(true), + style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700), + child: const Text('Supprimer'), + ), + ], + ); + }, + ); + + if (confirmed != true) return; + + setState(() { + _isSubmitting = true; + }); + try { + await UserService.deleteUser(widget.initialUser!.id); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Gestionnaire supprimé.')), + ); + Navigator.of(context).pop(true); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString().replaceFirst('Exception: ', '')), + backgroundColor: Colors.red.shade700, + ), + ); + setState(() { + _isSubmitting = false; + }); + } + } + @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Créer un gestionnaire'), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 640), + title: Row( + children: [ + Expanded( + child: Text( + _isEditMode + ? 'Modifier un gestionnaire' + : 'Créer un gestionnaire', + ), + ), + if (_isEditMode) + IconButton( + icon: const Icon(Icons.close), + tooltip: 'Fermer', + onPressed: _isSubmitting + ? null + : () => Navigator.of(context).pop(false), + ), + ], + ), + content: SizedBox( + width: 620, child: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - controller: _nomController, - textCapitalization: TextCapitalization.words, - decoration: const InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder(), - ), - validator: (v) => _required(v, 'Nom'), - ), - const SizedBox(height: 12), - TextFormField( - controller: _prenomController, - textCapitalization: TextCapitalization.words, - decoration: const InputDecoration( - labelText: 'Prénom', - border: OutlineInputBorder(), - ), - validator: (v) => _required(v, 'Prénom'), - ), - const SizedBox(height: 12), - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - labelText: 'Email', - border: OutlineInputBorder(), - ), - validator: _validateEmail, - ), - const SizedBox(height: 12), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Mot de passe', - border: const OutlineInputBorder(), - suffixIcon: IconButton( - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - icon: Icon( - _obscurePassword - ? Icons.visibility_off - : Icons.visibility, - ), - ), - ), - validator: _validatePassword, - ), - const SizedBox(height: 12), - TextFormField( - controller: _telephoneController, - keyboardType: TextInputType.phone, - decoration: const InputDecoration( - labelText: 'Téléphone', - border: OutlineInputBorder(), - ), - validator: (v) => _required(v, 'Téléphone'), - ), - const SizedBox(height: 12), - DropdownButtonFormField( - value: _selectedRelaisId, - decoration: const InputDecoration( - labelText: 'Relais principal', - border: OutlineInputBorder(), - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Aucun relais'), - ), - ..._relais.map( - (relais) => DropdownMenuItem( - value: relais.id, - child: Text(relais.nom), - ), - ), + Row( + children: [ + Expanded(child: _buildNomField()), + const SizedBox(width: 12), + Expanded(child: _buildPrenomField()), ], - onChanged: _isLoadingRelais - ? null - : (value) { - setState(() { - _selectedRelaisId = value; - }); - }, ), - if (_isLoadingRelais) ...[ - const SizedBox(height: 8), - const LinearProgressIndicator(minHeight: 2), - ], + const SizedBox(height: 12), + _buildEmailField(), + const SizedBox(height: 12), + Row( + children: [ + Expanded(child: _buildPasswordField()), + const SizedBox(width: 12), + Expanded(child: _buildTelephoneField()), + ], + ), + const SizedBox(height: 12), + _buildRelaisField(), ], ), ), ), ), actions: [ - OutlinedButton( - onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), - child: const Text('Annuler'), + if (_isEditMode) ...[ + OutlinedButton( + onPressed: _isSubmitting ? null : _delete, + style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700), + child: const Text('Supprimer'), + ), + FilledButton.icon( + onPressed: _isSubmitting ? null : _submit, + icon: _isSubmitting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.edit), + label: Text(_isSubmitting ? 'Modification...' : 'Modifier'), + ), + ] else ...[ + OutlinedButton( + onPressed: + _isSubmitting ? null : () => Navigator.of(context).pop(false), + child: const Text('Annuler'), + ), + FilledButton.icon( + onPressed: _isSubmitting ? null : _submit, + icon: _isSubmitting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.person_add_alt_1), + label: Text(_isSubmitting ? 'Création...' : 'Créer'), + ), + ], + ], + ); + } + + Widget _buildNomField() { + return TextFormField( + controller: _nomController, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder(), + ), + validator: (v) => _required(v, 'Nom'), + ); + } + + Widget _buildPrenomField() { + return TextFormField( + controller: _prenomController, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + labelText: 'Prénom', + border: OutlineInputBorder(), + ), + validator: (v) => _required(v, 'Prénom'), + ); + } + + Widget _buildEmailField() { + return TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + validator: _validateEmail, + ); + } + + Widget _buildPasswordField() { + return TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + enableSuggestions: false, + autocorrect: false, + autofillHints: _isEditMode + ? const [] + : const [AutofillHints.newPassword], + decoration: InputDecoration( + labelText: _isEditMode + ? 'Nouveau mot de passe' + : 'Mot de passe', + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + ), ), - FilledButton.icon( - onPressed: _isSubmitting ? null : _submit, - icon: _isSubmitting - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.person_add_alt_1), - label: Text(_isSubmitting ? 'Création...' : 'Créer'), + ), + validator: _validatePassword, + ); + } + + Widget _buildTelephoneField() { + return TextFormField( + controller: _telephoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: 'Téléphone', + border: OutlineInputBorder(), + ), + validator: (v) => _required(v, 'Téléphone'), + ); + } + + Widget _buildRelaisField() { + final selectedValue = _selectedRelaisId != null && + _relais.any((relais) => relais.id == _selectedRelaisId) + ? _selectedRelaisId + : null; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownButtonFormField( + isExpanded: true, + value: selectedValue, + decoration: const InputDecoration( + labelText: 'Relais principal', + border: OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Aucun relais'), + ), + ..._relais.map( + (relais) => DropdownMenuItem( + value: relais.id, + child: Text(relais.nom), + ), + ), + ], + onChanged: _isLoadingRelais + ? null + : (value) { + setState(() { + _selectedRelaisId = value; + }); + }, ), + if (_isLoadingRelais) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(minHeight: 2), + ], ], ); } diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index 32d581d..d069bca 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -150,4 +150,66 @@ class UserService { ); } } + + static Future updateGestionnaire({ + required String gestionnaireId, + required String nom, + required String prenom, + required String email, + required String telephone, + required String? relaisId, + String? password, + }) async { + final body = { + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'telephone': telephone, + 'relaisId': relaisId, + }; + + if (password != null && password.trim().isNotEmpty) { + body['password'] = password.trim(); + } + + final response = await http.patch( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}/$gestionnaireId'), + 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 gestionnaire'); + } + throw Exception('Erreur modification gestionnaire'); + } + + 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'), + headers: await _headers(), + ); + + if (response.statusCode != 200 && response.statusCode != 204) { + 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 suppression utilisateur'); + } + throw Exception('Erreur suppression utilisateur'); + } + } } diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart index 80d5d91..58b78e6 100644 --- a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.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/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'; @@ -24,7 +23,6 @@ class _GestionnaireManagementWidgetState bool _isLoading = false; String? _error; List _gestionnaires = []; - List _relais = []; @override void initState() { @@ -42,17 +40,9 @@ class _GestionnaireManagementWidgetState }); try { final gestionnaires = await UserService.getGestionnaires(); - List relais = []; - try { - relais = await RelaisService.getRelais(); - } catch (_) { - // L'ecran reste utilisable meme si la route Relais n'est pas disponible. - } - if (!mounted) return; setState(() { _gestionnaires = gestionnaires; - _relais = relais; _isLoading = false; }); } catch (e) { @@ -64,81 +54,16 @@ class _GestionnaireManagementWidgetState } } - Future _openRelaisAssignmentDialog(AppUser user) async { - String? selectedRelaisId = user.relaisId; - final saved = await showDialog( + Future _openGestionnaireEditDialog(AppUser user) async { + final changed = await showDialog( context: context, - builder: (ctx) { - return StatefulBuilder( - builder: (context, setStateDialog) { - return AlertDialog( - title: Text( - 'Rattacher ${user.fullName.isEmpty ? user.email : user.fullName}', - ), - content: DropdownButtonFormField( - value: selectedRelaisId, - isExpanded: true, - decoration: const InputDecoration( - labelText: 'Relais principal', - border: OutlineInputBorder(), - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Aucun relais'), - ), - ..._relais.map( - (relais) => DropdownMenuItem( - value: relais.id, - child: Text(relais.nom), - ), - ), - ], - onChanged: (value) { - setStateDialog(() { - selectedRelaisId = value; - }); - }, - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(false), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: () => Navigator.of(ctx).pop(true), - child: const Text('Enregistrer'), - ), - ], - ); - }, - ); + barrierDismissible: false, + builder: (dialogContext) { + return GestionnaireCreateDialog(initialUser: user); }, ); - - if (saved != true) return; - - try { - await UserService.updateGestionnaireRelais( - gestionnaireId: user.id, - relaisId: selectedRelaisId, - ); - if (!mounted) return; + if (changed == true) { await _loadGestionnaires(); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Rattachement relais mis a jour.')), - ); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - e.toString().replaceAll('Exception: ', ''), - ), - backgroundColor: Colors.red.shade600, - ), - ); } } @@ -168,16 +93,11 @@ class _GestionnaireManagementWidgetState 'Relais : ${user.relaisNom ?? 'Non rattaché'}', ], actions: [ - IconButton( - icon: const Icon(Icons.location_city_outlined), - tooltip: 'Rattacher un relais', - onPressed: () => _openRelaisAssignmentDialog(user), - ), IconButton( icon: const Icon(Icons.edit), tooltip: 'Modifier', onPressed: () { - // TODO: Modifier gestionnaire. + _openGestionnaireEditDialog(user); }, ), ],