diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index de00a86..e658063 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -1,17 +1,451 @@ 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 GestionnairesCreate extends StatelessWidget { - const GestionnairesCreate({super.key}); +class GestionnaireCreateDialog extends StatefulWidget { + final AppUser? initialUser; + + const GestionnaireCreateDialog({ + super.key, + this.initialUser, + }); + + @override + State createState() => + _GestionnaireCreateDialogState(); +} + +class _GestionnaireCreateDialogState extends State { + final _formKey = GlobalKey(); + final _nomController = TextEditingController(); + final _prenomController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _telephoneController = TextEditingController(); + + bool _isSubmitting = false; + bool _obscurePassword = true; + 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(); + } + + @override + void dispose() { + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _telephoneController.dispose(); + super.dispose(); + } + + Future _loadRelais() async { + 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 = filtered; + _isLoadingRelais = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _selectedRelaisId = null; + _relais = []; + _isLoadingRelais = false; + }); + } + } + + String? _required(String? value, String field) { + if (value == null || value.trim().isEmpty) { + return '$field est requis'; + } + return null; + } + + String? _validateEmail(String? value) { + final base = _required(value, 'Email'); + if (base != null) return base; + final email = value!.trim(); + final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email); + if (!ok) return 'Format email invalide'; + return null; + } + + 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'; + return null; + } + + Future _submit() async { + if (_isSubmitting) return; + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isSubmitting = true; + }); + + 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, + ); + } 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( + SnackBar( + content: Text( + _isEditMode + ? 'Gestionnaire modifié avec succès.' + : 'Gestionnaire créé avec succès.', + ), + ), + ); + 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, + ), + ); + } finally { + if (!mounted) return; + setState(() { + _isSubmitting = false; + }); + } + } + + 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 Scaffold( - appBar: AppBar( - title: const Text('Créer un gestionnaire'), + return AlertDialog( + 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), + ), + ], ), - body: const Center( - child: Text('Formulaire de création de gestionnaire'), + content: SizedBox( + width: 620, + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded(child: _buildNomField()), + const SizedBox(width: 12), + Expanded(child: _buildPrenomField()), + ], + ), + 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: [ + 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, + ), + ), + ), + 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), + ], + ], ); } } \ No newline at end of file diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index da4f64e..d069bca 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -37,6 +37,44 @@ class UserService { return data.map((e) => AppUser.fromJson(e)).toList(); } + static Future createGestionnaire({ + required String nom, + required String prenom, + required String email, + required String password, + required String telephone, + String? relaisId, + }) async { + final response = await http.post( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'), + headers: await _headers(), + body: jsonEncode({ + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'password': password, + 'telephone': telephone, + 'cguAccepted': true, + 'relaisId': relaisId, + }), + ); + + 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 gestionnaire'); + } + throw Exception('Erreur création gestionnaire'); + } + + 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( @@ -112,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); }, ), ], diff --git a/frontend/lib/widgets/admin/user_management_panel.dart b/frontend/lib/widgets/admin/user_management_panel.dart index 7f1e698..aa408c4 100644 --- a/frontend/lib/widgets/admin/user_management_panel.dart +++ b/frontend/lib/widgets/admin/user_management_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart'; import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; @@ -15,6 +16,7 @@ class AdminUserManagementPanel extends StatefulWidget { class _AdminUserManagementPanelState extends State { int _subIndex = 0; + int _gestionnaireRefreshTick = 0; final TextEditingController _searchController = TextEditingController(); final TextEditingController _amCapacityController = TextEditingController(); String? _parentStatus; @@ -133,6 +135,7 @@ class _AdminUserManagementPanelState extends State { switch (_subIndex) { case 0: return GestionnaireManagementWidget( + key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'), searchQuery: _searchController.text, ); case 1: @@ -164,13 +167,40 @@ class _AdminUserManagementPanelState extends State { searchController: _searchController, searchHint: _searchHintForTab(), filterControl: _subBarFilterControl(), - onAddPressed: () { - // TODO: brancher création selon onglet actif - }, + onAddPressed: _handleAddPressed, addLabel: 'Ajouter', ), Expanded(child: _buildBody()), ], ); } + + 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.', + ), + ), + ); + return; + } + + final created = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return const GestionnaireCreateDialog(); + }, + ); + + if (!mounted) return; + if (created == true) { + setState(() { + _gestionnaireRefreshTick++; + }); + } + } }