From 42bb872c41c76aac4ddbeb4dabee633c85fbdafe Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Mon, 23 Feb 2026 23:23:13 +0100 Subject: [PATCH] =?UTF-8?q?feat(#35):=20cr=C3=A9er=20un=20gestionnaire=20v?= =?UTF-8?q?ia=20modale=20avec=20s=C3=A9lection=20de=20relais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémente la création de gestionnaire directement depuis le dashboard admin avec formulaire validé, appel API dédié et rattachement optionnel à un relais depuis une combobox. Co-authored-by: Cursor --- .../creation/gestionnaires_create.dart | 251 +++++++++++++++++- frontend/lib/services/user_service.dart | 38 +++ .../widgets/admin/user_management_panel.dart | 36 ++- 3 files changed, 314 insertions(+), 11 deletions(-) diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index de00a86..b6d8ab6 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -1,17 +1,252 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/relais_model.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 { + const GestionnaireCreateDialog({super.key}); + + @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; + + @override + void initState() { + super.initState(); + _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; + setState(() { + _relais = list.where((r) => r.actif).toList(); + _isLoadingRelais = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _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) { + 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 { + 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.')), + ); + 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; + }); + } + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Créer un gestionnaire'), - ), - body: const Center( - child: Text('Formulaire de création de gestionnaire'), + return AlertDialog( + title: const Text('Créer un gestionnaire'), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 640), + 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), + ), + ), + ], + onChanged: _isLoadingRelais + ? null + : (value) { + setState(() { + _selectedRelaisId = value; + }); + }, + ), + if (_isLoadingRelais) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(minHeight: 2), + ], + ], + ), + ), + ), ), + actions: [ + 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'), + ), + ], ); } } \ No newline at end of file diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index da4f64e..32d581d 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( 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++; + }); + } + } }