diff --git a/backend/src/routes/user/dto/create_user.dto.ts b/backend/src/routes/user/dto/create_user.dto.ts index cae620d..ee2f702 100644 --- a/backend/src/routes/user/dto/create_user.dto.ts +++ b/backend/src/routes/user/dto/create_user.dto.ts @@ -36,10 +36,10 @@ export class CreateUserDto { @MaxLength(100) nom: string; - @ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE }) + @ApiProperty({ enum: GenreType, required: false }) @IsOptional() @IsEnum(GenreType) - genre?: GenreType = GenreType.AUTRE; + genre?: GenreType; @ApiProperty({ enum: RoleType }) @IsEnum(RoleType) @@ -86,7 +86,7 @@ export class CreateUserDto { @ApiProperty({ default: false }) @IsOptional() @IsBoolean() - consentement_photo?: boolean = false; + consentement_photo?: boolean; @ApiProperty({ required: false }) @IsOptional() @@ -96,7 +96,7 @@ export class CreateUserDto { @ApiProperty({ default: false }) @IsOptional() @IsBoolean() - changement_mdp_obligatoire?: boolean = false; + changement_mdp_obligatoire?: boolean; @ApiProperty({ example: true }) @IsBoolean() diff --git a/backend/src/routes/user/dto/update_gestionnaire.dto.ts b/backend/src/routes/user/dto/update_gestionnaire.dto.ts index f9a2360..c6956bc 100644 --- a/backend/src/routes/user/dto/update_gestionnaire.dto.ts +++ b/backend/src/routes/user/dto/update_gestionnaire.dto.ts @@ -1,4 +1,4 @@ import { PartialType } from "@nestjs/swagger"; -import { CreateUserDto } from "./create_user.dto"; +import { CreateGestionnaireDto } from "./create_gestionnaire.dto"; -export class UpdateGestionnaireDto extends PartialType(CreateUserDto) {} \ No newline at end of file +export class UpdateGestionnaireDto extends PartialType(CreateGestionnaireDto) {} diff --git a/backend/src/routes/user/user.controller.ts b/backend/src/routes/user/user.controller.ts index af65ff1..f5fc93f 100644 --- a/backend/src/routes/user/user.controller.ts +++ b/backend/src/routes/user/user.controller.ts @@ -55,9 +55,9 @@ export class UserController { return this.userService.findOne(id); } - // Modifier un utilisateur (réservé super_admin) + // Modifier un utilisateur (réservé super_admin et admin) @Patch(':id') - @Roles(RoleType.SUPER_ADMIN) + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) @ApiOperation({ summary: 'Mettre à jour un utilisateur' }) @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) updateUser( diff --git a/backend/src/routes/user/user.service.ts b/backend/src/routes/user/user.service.ts index 08017f7..b8a88e0 100644 --- a/backend/src/routes/user/user.service.ts +++ b/backend/src/routes/user/user.service.ts @@ -155,11 +155,26 @@ export class UserService { async updateUser(id: string, dto: UpdateUserDto, currentUser: Users): Promise { const user = await this.findOne(id); + // Le super administrateur conserve une identité figée. + if ( + user.role === RoleType.SUPER_ADMIN && + (dto.nom !== undefined || dto.prenom !== undefined) + ) { + throw new ForbiddenException( + 'Le nom et le prénom du super administrateur ne peuvent pas être modifiés', + ); + } + // Interdire changement de rôle si pas super admin if (dto.role && currentUser.role !== RoleType.SUPER_ADMIN) { throw new ForbiddenException('Accès réservé aux super admins'); } + // Un admin ne peut pas modifier un super admin + if (currentUser.role === RoleType.ADMINISTRATEUR && user.role === RoleType.SUPER_ADMIN) { + throw new ForbiddenException('Vous ne pouvez pas modifier un super administrateur'); + } + // Empêcher de modifier le flag changement_mdp_obligatoire pour admin/gestionnaire if ( (user.role === RoleType.ADMINISTRATEUR || user.role === RoleType.GESTIONNAIRE) && @@ -251,6 +266,12 @@ export class UserService { if (currentUser.role !== RoleType.SUPER_ADMIN) { throw new ForbiddenException('Accès réservé aux super admins'); } + const user = await this.findOne(id); + if (user.role === RoleType.SUPER_ADMIN) { + throw new ForbiddenException( + 'Le super administrateur ne peut pas être supprimé', + ); + } const result = await this.usersRepository.delete(id); if (result.affected === 0) { throw new NotFoundException('Utilisateur introuvable'); diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 16edd1e..80dee9f 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -30,10 +30,10 @@ | 17 | [Backend] API Création gestionnaire | ✅ Terminé | | 91 | [Frontend] Inscription AM – Branchement soumission formulaire à l'API | Ouvert | | 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | ✅ Terminé | -| 93 | [Frontend] Panneau Admin - Homogeneiser la presentation des onglets | Ouvert | +| 93 | [Frontend] Panneau Admin - Homogeneiser la presentation des onglets | ✅ Fermé | | 94 | [Backend] Relais - modele, API CRUD et liaison gestionnaire | ✅ Terminé | -| 95 | [Frontend] Admin - gestion des relais et rattachement gestionnaire | Ouvert | -| 96 | [Frontend] Admin - Création administrateur via modale (sans relais) | Ouvert | +| 95 | [Frontend] Admin - gestion des relais et rattachement gestionnaire | ✅ Fermé | +| 96 | [Frontend] Admin - Création administrateur via modale (sans relais) | ✅ Terminé | | 97 | [Backend] Harmoniser API création administrateur avec le contrat frontend | ✅ Terminé | | 89 | Log des appels API en mode debug | Ouvert | @@ -665,9 +665,10 @@ Le back-office admin doit gérer des Relais avec des données réelles en base, --- -### Ticket #97 : [Backend] Harmoniser API création administrateur avec le contrat frontend +### Ticket #97 : [Backend] Harmoniser API création administrateur avec le contrat frontend ✅ **Estimation** : 3h **Labels** : `backend`, `p2`, `auth`, `admin` +**Statut** : ✅ TERMINÉ (Fermé le 2026-02-24) **Description** : Rendre l'API de création administrateur cohérente et stable avec le besoin frontend (modale simplifiée), en définissant un contrat clair et minimal. @@ -680,6 +681,7 @@ Rendre l'API de création administrateur cohérente et stable avec le besoin fro - [ ] Validation stricte --- + ## 🟢 PRIORITÉ 3 : Frontend - Interfaces ### Ticket #35 : [Frontend] Écran Création Gestionnaire @@ -1073,9 +1075,10 @@ Branchement du formulaire d'inscription AM (étape 4) à l'endpoint d'inscriptio --- -### Ticket #93 : [Frontend] Panneau Admin - Homogénéisation des onglets +### Ticket #93 : [Frontend] Panneau Admin - Homogénéisation des onglets ✅ **Estimation** : 4h **Labels** : `frontend`, `p3`, `admin`, `ux` +**Statut** : ✅ TERMINÉ (Fermé le 2026-02-24) **Description** : Uniformiser l'UI/UX des 4 onglets du dashboard admin (Gestionnaires, Parents, AM, Admins). @@ -1088,9 +1091,10 @@ Uniformiser l'UI/UX des 4 onglets du dashboard admin (Gestionnaires, Parents, AM --- -### Ticket #95 : [Frontend] Admin - Gestion des Relais et rattachement gestionnaire +### Ticket #95 : [Frontend] Admin - Gestion des Relais et rattachement gestionnaire ✅ **Estimation** : 5h **Labels** : `frontend`, `p3`, `admin` +**Statut** : ✅ TERMINÉ (Fermé le 2026-02-24) **Description** : Interface de gestion des Relais dans le dashboard admin et rattachement des gestionnaires. @@ -1103,24 +1107,22 @@ Interface de gestion des Relais dans le dashboard admin et rattachement des gest --- -<<<<<<< HEAD -======= -### Ticket #96 : [Frontend] Admin - Création administrateur via modale (sans relais) +### Ticket #96 : [Frontend] Admin - Création administrateur via modale (sans relais) ✅ **Estimation** : 3h **Labels** : `frontend`, `p3`, `admin` +**Statut** : ✅ TERMINÉ (Fermé le 2026-02-24) **Description** : Permettre la création d'un administrateur via une modale simple depuis le dashboard admin. **Tâches** : -- [ ] Bouton "Créer administrateur" dans l'onglet Administrateurs -- [ ] Modale avec formulaire simplifié (Nom, Prénom, Email, MDP, Téléphone) -- [ ] Appel API `POST /users` (ou endpoint dédié si #97 implémenté) -- [ ] Gestion succès/erreur et rafraîchissement liste +- [x] Bouton "Créer administrateur" dans l'onglet Administrateurs +- [x] Modale avec formulaire simplifié (Nom, Prénom, Email, MDP, Téléphone) +- [x] Appel API `POST /users` (ou endpoint dédié si #97 implémenté) +- [x] Gestion succès/erreur et rafraîchissement liste --- ->>>>>>> develop ## 🔵 PRIORITÉ 4 : Tests & Documentation ### Ticket #52 : [Tests] Tests unitaires Backend diff --git a/docs/SuperNounou_SSS-001.md b/docs/SuperNounou_SSS-001.md index 68553e3..2cf8699 100644 --- a/docs/SuperNounou_SSS-001.md +++ b/docs/SuperNounou_SSS-001.md @@ -1,6 +1,6 @@ # SuperNounou – SSS-001 ## Spécification technique & opérationnelle unifiée -_Version 0.2 – 24 avril 2025_ +_Version 0.3 – 27 janvier 2026_ --- @@ -62,6 +62,13 @@ Collection Postman, scripts cURL, guide « Appeler l’API ». ### B.4 Intégrations futures SSO LDAP/SAML, webhook `contract.validated`, export statistiques CSV. +### B.5 Contrat de gestion des comptes d'administration +- Création d'un administrateur avec un contrat minimal stable : `nom`, `prenom`, `email`, `password`, `telephone`. +- Le rôle n'est jamais fourni par le frontend pour ce flux ; le backend impose `ADMINISTRATEUR`. +- Les champs hors périmètre (adresse complète, photo, métadonnées métier non nécessaires) ne sont pas requis. +- Les protections d'autorisation restent actives : un `SUPER_ADMIN` n'est pas supprimable et son identité (`nom`, `prenom`) est non modifiable. +- Côté interface d'administration, les actions d'édition sont conditionnées aux droits ; les entrées non éditables restent consultables en lecture seule. + --- # C – Déploiement, CI/CD et Observabilité *(nouveau)* @@ -106,3 +113,4 @@ AES-256, JWT, KMS, OpenAPI, RPO, RTO, rate-limit, HMAC, Compose, CI/CD… |---------|------------|------------------|---------------------------------| | 0.1-draft | 2025-04-24 | Équipe projet | Création du SSS unifié | | 0.2 | 2025-04-24 | ChatGPT & Julien | Ajout déploiement / CI/CD / logs | +| 0.3 | 2026-01-27 | Équipe projet | Contrat admin harmonisé et règles d'autorisation | diff --git a/frontend/lib/screens/administrateurs/creation/admin_create.dart b/frontend/lib/screens/administrateurs/creation/admin_create.dart new file mode 100644 index 0000000..e57ac71 --- /dev/null +++ b/frontend/lib/screens/administrateurs/creation/admin_create.dart @@ -0,0 +1,357 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/services/user_service.dart'; + +class AdminCreateDialog extends StatefulWidget { + final AppUser? initialUser; + + const AdminCreateDialog({ + super.key, + this.initialUser, + }); + + @override + State createState() => _AdminCreateDialogState(); +} + +class _AdminCreateDialogState 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 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(); + } + } + + @override + void dispose() { + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _telephoneController.dispose(); + super.dispose(); + } + + 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.updateAdmin( + adminId: widget.initialUser!.id, + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + telephone: _telephoneController.text.trim(), + password: _passwordController.text.trim().isEmpty + ? null + : _passwordController.text, + ); + } else { + await UserService.createAdmin( + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text, + telephone: _telephoneController.text.trim(), + ); + } + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + _isEditMode + ? 'Administrateur modifié avec succès.' + : 'Administrateur 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('Administrateur 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: Row( + children: [ + Expanded( + child: Text( + _isEditMode + ? 'Modifier un administrateur' + : 'Créer un administrateur', + ), + ), + 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: [ + 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()), + ], + ), + ], + ), + ), + ), + ), + 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'), + ); + } +} diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index e658063..3a2ce05 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -1,29 +1,37 @@ 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; + final bool readOnly; - const GestionnaireCreateDialog({ + const AdminUserFormDialog({ super.key, this.initialUser, + this.withRelais = true, + this.adminMode = false, + this.readOnly = 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(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _telephoneController = TextEditingController(); + final _passwordToggleFocusNode = + FocusNode(skipTraversal: true, canRequestFocus: false); bool _isSubmitting = false; bool _obscurePassword = true; @@ -31,6 +39,50 @@ class _GestionnaireCreateDialogState extends State { List _relais = []; String? _selectedRelaisId; bool get _isEditMode => widget.initialUser != null; + bool get _isSuperAdminTarget => + widget.initialUser?.role.toLowerCase() == 'super_admin'; + bool get _isLockedAdminIdentity => + _isEditMode && widget.adminMode && _isSuperAdminTarget; + String get _targetRoleKey { + if (widget.initialUser != null) { + return widget.initialUser!.role.toLowerCase(); + } + return widget.adminMode ? 'administrateur' : 'gestionnaire'; + } + + String get _targetRoleLabel { + switch (_targetRoleKey) { + case 'super_admin': + return 'Super administrateur'; + case 'administrateur': + return 'Administrateur'; + case 'gestionnaire': + return 'Gestionnaire'; + case 'assistante_maternelle': + return 'Assistante maternelle'; + case 'parent': + return 'Parent'; + default: + return 'Utilisateur'; + } + } + + IconData get _targetRoleIcon { + switch (_targetRoleKey) { + case 'super_admin': + return Icons.verified_user_outlined; + case 'administrateur': + return Icons.admin_panel_settings_outlined; + case 'gestionnaire': + return Icons.assignment_ind_outlined; + case 'assistante_maternelle': + return Icons.child_care_outlined; + case 'parent': + return Icons.supervisor_account_outlined; + default: + return Icons.person_outline; + } + } @override void initState() { @@ -40,7 +92,7 @@ class _GestionnaireCreateDialogState extends State { _nomController.text = user.nom ?? ''; _prenomController.text = user.prenom ?? ''; _emailController.text = user.email; - _telephoneController.text = user.telephone ?? ''; + _telephoneController.text = _formatPhoneForDisplay(user.telephone ?? ''); // En édition, on ne préremplit jamais le mot de passe. _passwordController.clear(); final initialRelaisId = user.relaisId?.trim(); @@ -49,7 +101,11 @@ class _GestionnaireCreateDialogState extends State { ? null : initialRelaisId; } - _loadRelais(); + if (widget.withRelais) { + _loadRelais(); + } else { + _isLoadingRelais = false; + } } @override @@ -59,6 +115,7 @@ class _GestionnaireCreateDialogState extends State { _emailController.dispose(); _passwordController.dispose(); _telephoneController.dispose(); + _passwordToggleFocusNode.dispose(); super.dispose(); } @@ -122,7 +179,68 @@ class _GestionnaireCreateDialogState extends State { return null; } + String? _validatePhone(String? value) { + if (_isEditMode && (value == null || value.trim().isEmpty)) { + return null; + } + 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'), ''); + } + + String _formatPhoneForDisplay(String raw) { + final normalized = _normalizePhone(raw); + final digits = + normalized.length > 10 ? normalized.substring(0, 10) : normalized; + final buffer = StringBuffer(); + for (var i = 0; i < digits.length; i++) { + if (i > 0 && i.isEven) buffer.write(' '); + buffer.write(digits[i]); + } + return buffer.toString(); + } + + String _toTitleCase(String raw) { + final trimmed = raw.trim(); + if (trimmed.isEmpty) return trimmed; + final words = trimmed.split(RegExp(r'\s+')); + final normalizedWords = words.map(_capitalizeComposedWord).toList(); + return normalizedWords.join(' '); + } + + String _capitalizeComposedWord(String word) { + if (word.isEmpty) return word; + final lower = word.toLowerCase(); + final separators = {"-", "'", "’"}; + final buffer = StringBuffer(); + var capitalizeNext = true; + + for (var i = 0; i < lower.length; i++) { + final char = lower[i]; + if (capitalizeNext && RegExp(r'[a-zà-öø-ÿ]').hasMatch(char)) { + buffer.write(char.toUpperCase()); + capitalizeNext = false; + } else { + buffer.write(char); + capitalizeNext = separators.contains(char); + } + } + return buffer.toString(); + } + Future _submit() async { + if (widget.readOnly) return; if (_isSubmitting) return; if (!_formKey.currentState!.validate()) return; @@ -131,35 +249,87 @@ class _GestionnaireCreateDialogState extends State { }); try { + final normalizedNom = _toTitleCase(_nomController.text); + final normalizedPrenom = _toTitleCase(_prenomController.text); + final normalizedPhone = _normalizePhone(_telephoneController.text); + final passwordProvided = _passwordController.text.trim().isNotEmpty; + 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) { + final lockedNom = _toTitleCase(widget.initialUser!.nom ?? ''); + final lockedPrenom = _toTitleCase(widget.initialUser!.prenom ?? ''); + await UserService.updateAdministrateur( + adminId: widget.initialUser!.id, + nom: _isLockedAdminIdentity ? lockedNom : normalizedNom, + prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom, + email: _emailController.text.trim(), + telephone: normalizedPhone.isEmpty + ? _normalizePhone(widget.initialUser!.telephone ?? '') + : normalizedPhone, + password: passwordProvided ? _passwordController.text : null, + ); + } else { + final currentUser = widget.initialUser!; + final initialNom = _toTitleCase(currentUser.nom ?? ''); + final initialPrenom = _toTitleCase(currentUser.prenom ?? ''); + final initialEmail = currentUser.email.trim(); + final initialPhone = _normalizePhone(currentUser.telephone ?? ''); + + final onlyRelaisChanged = + normalizedNom == initialNom && + normalizedPrenom == initialPrenom && + _emailController.text.trim() == initialEmail && + normalizedPhone == initialPhone && + !passwordProvided; + + if (onlyRelaisChanged) { + await UserService.updateGestionnaireRelais( + gestionnaireId: currentUser.id, + relaisId: _selectedRelaisId, + ); + } else { + await UserService.updateGestionnaire( + gestionnaireId: currentUser.id, + nom: normalizedNom, + prenom: normalizedPrenom, + email: _emailController.text.trim(), + telephone: normalizedPhone.isEmpty ? initialPhone : normalizedPhone, + relaisId: _selectedRelaisId, + password: passwordProvided ? _passwordController.text : null, + ); + } + } } 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: normalizedNom, + prenom: normalizedPrenom, + email: _emailController.text.trim(), + password: _passwordController.text, + telephone: _normalizePhone(_telephoneController.text), + ); + } else { + await UserService.createGestionnaire( + nom: normalizedNom, + prenom: normalizedPrenom, + 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.'), ), ), ); @@ -183,6 +353,8 @@ class _GestionnaireCreateDialogState extends State { } Future _delete() async { + if (widget.readOnly) return; + if (_isSuperAdminTarget) return; if (!_isEditMode || _isSubmitting) return; final confirmed = await showDialog( @@ -239,14 +411,26 @@ class _GestionnaireCreateDialogState extends State { return AlertDialog( title: Row( children: [ + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xFFEDE5FA), + child: Icon( + _targetRoleIcon, + size: 20, + color: const Color(0xFF6B3FA0), + ), + ), + const SizedBox(width: 8), Expanded( child: Text( _isEditMode - ? 'Modifier un gestionnaire' - : 'Créer un gestionnaire', + ? (widget.readOnly + ? 'Consulter un "$_targetRoleLabel"' + : 'Modifier un "$_targetRoleLabel"') + : 'Créer un "$_targetRoleLabel"', ), ), - if (_isEditMode) + if (_isEditMode && !widget.readOnly) IconButton( icon: const Icon(Icons.close), tooltip: 'Fermer', @@ -266,9 +450,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,20 +465,28 @@ class _GestionnaireCreateDialogState extends State { Expanded(child: _buildTelephoneField()), ], ), - const SizedBox(height: 12), - _buildRelaisField(), + if (widget.withRelais) ...[ + const SizedBox(height: 12), + _buildRelaisField(), + ], ], ), ), ), ), actions: [ - if (_isEditMode) ...[ - OutlinedButton( - onPressed: _isSubmitting ? null : _delete, - style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700), - child: const Text('Supprimer'), + if (widget.readOnly) ...[ + FilledButton( + onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), + child: const Text('Fermer'), ), + ] else if (_isEditMode) ...[ + if (!_isSuperAdminTarget) + OutlinedButton( + onPressed: _isSubmitting ? null : _delete, + style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700), + child: const Text('Supprimer'), + ), FilledButton.icon( onPressed: _isSubmitting ? null : _submit, icon: _isSubmitting @@ -331,42 +523,50 @@ class _GestionnaireCreateDialogState extends State { Widget _buildNomField() { return TextFormField( controller: _nomController, + readOnly: widget.readOnly || _isLockedAdminIdentity, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Nom', border: OutlineInputBorder(), ), - validator: (v) => _required(v, 'Nom'), + validator: (widget.readOnly || _isLockedAdminIdentity) + ? null + : (v) => _required(v, 'Nom'), ); } Widget _buildPrenomField() { return TextFormField( controller: _prenomController, + readOnly: widget.readOnly || _isLockedAdminIdentity, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Prénom', border: OutlineInputBorder(), ), - validator: (v) => _required(v, 'Prénom'), + validator: (widget.readOnly || _isLockedAdminIdentity) + ? null + : (v) => _required(v, 'Prénom'), ); } Widget _buildEmailField() { return TextFormField( controller: _emailController, + readOnly: widget.readOnly, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), - validator: _validateEmail, + validator: widget.readOnly ? null : _validateEmail, ); } Widget _buildPasswordField() { return TextFormField( controller: _passwordController, + readOnly: widget.readOnly, obscureText: _obscurePassword, enableSuggestions: false, autocorrect: false, @@ -378,30 +578,43 @@ 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: widget.readOnly + ? null + : ExcludeFocus( + child: IconButton( + focusNode: _passwordToggleFocusNode, + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + ), + ), + ), ), - validator: _validatePassword, + validator: widget.readOnly ? null : _validatePassword, ); } Widget _buildTelephoneField() { return TextFormField( controller: _telephoneController, + readOnly: widget.readOnly, keyboardType: TextInputType.phone, + inputFormatters: widget.readOnly + ? null + : [ + 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: widget.readOnly ? null : _validatePhone, ); } @@ -433,7 +646,7 @@ class _GestionnaireCreateDialogState extends State { ), ), ], - onChanged: _isLoadingRelais + onChanged: (_isLoadingRelais || widget.readOnly) ? null : (value) { setState(() { @@ -448,4 +661,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..ab8e5a7 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( @@ -132,6 +167,82 @@ class UserService { return []; } + static Future createAdmin({ + 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); + } + + static Future updateAdmin({ + 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 updateGestionnaireRelais({ required String gestionnaireId, required String? relaisId, @@ -156,7 +267,7 @@ class UserService { required String nom, required String prenom, required String email, - required String telephone, + String? telephone, required String? relaisId, String? password, }) async { @@ -164,10 +275,13 @@ class UserService { 'nom': nom, 'prenom': prenom, 'email': email, - 'telephone': telephone, 'relaisId': relaisId, }; + if (telephone != null && telephone.trim().isNotEmpty) { + body['telephone'] = telephone.trim(); + } + if (password != null && password.trim().isNotEmpty) { body['password'] = password.trim(); } @@ -194,6 +308,50 @@ class UserService { return AppUser.fromJson(data); } + static Future updateAdministrateur({ + required String adminId, + required String nom, + required String prenom, + required String email, + String? telephone, + String? password, + }) async { + final body = { + 'nom': nom, + 'prenom': prenom, + 'email': email, + }; + + if (telephone != null && telephone.trim().isNotEmpty) { + body['telephone'] = telephone.trim(); + } + + 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..be51365 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,5 +1,7 @@ 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/auth_service.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'; @@ -20,10 +22,12 @@ class _AdminManagementWidgetState extends State { bool _isLoading = false; String? _error; List _admins = []; + String? _currentUserRole; @override void initState() { super.initState(); + _loadCurrentUserRole(); _loadAdmins(); } @@ -51,6 +55,48 @@ class _AdminManagementWidgetState extends State { } } + Future _loadCurrentUserRole() async { + final cached = await AuthService.getCurrentUser(); + if (!mounted) return; + if (cached != null) { + setState(() { + _currentUserRole = cached.role.toLowerCase(); + }); + return; + } + final refreshed = await AuthService.refreshCurrentUser(); + if (!mounted || refreshed == null) return; + setState(() { + _currentUserRole = refreshed.role.toLowerCase(); + }); + } + + bool _isSuperAdmin(AppUser user) => user.role.toLowerCase() == 'super_admin'; + + bool _canEditAdmin(AppUser target) { + if (!_isSuperAdmin(target)) return true; + return _currentUserRole == 'super_admin'; + } + + Future _openAdminEditDialog(AppUser user) async { + final canEdit = _canEditAdmin(user); + final changed = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return AdminUserFormDialog( + initialUser: user, + adminMode: true, + withRelais: false, + readOnly: !canEdit, + ); + }, + ); + if (changed == true && canEdit) { + await _loadAdmins(); + } + } + @override Widget build(BuildContext context) { final query = widget.searchQuery.toLowerCase(); @@ -68,19 +114,36 @@ class _AdminManagementWidgetState extends State { itemCount: filteredAdmins.length, itemBuilder: (context, index) { final user = filteredAdmins[index]; + final isSuperAdmin = _isSuperAdmin(user); + final canEdit = _canEditAdmin(user); return AdminUserCard( title: user.fullName, + fallbackIcon: isSuperAdmin + ? Icons.verified_user_outlined + : Icons.manage_accounts_outlined, subtitleLines: [ user.email, - 'Rôle : ${user.role}', + 'Téléphone : ${user.telephone?.trim().isNotEmpty == true ? user.telephone : 'Non renseigné'}', ], avatarUrl: user.photoUrl, + borderColor: isSuperAdmin + ? const Color(0xFF8E6AC8) + : Colors.grey.shade300, + backgroundColor: isSuperAdmin + ? const Color(0xFFF4EEFF) + : Colors.white, + titleColor: isSuperAdmin ? const Color(0xFF5D2F99) : null, + infoColor: isSuperAdmin + ? const Color(0xFF6D4EA1) + : Colors.black54, actions: [ IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Modifier', + icon: Icon( + canEdit ? Icons.edit_outlined : Icons.visibility_outlined, + ), + tooltip: canEdit ? 'Modifier' : 'Consulter', onPressed: () { - // TODO: Modifier admin + _openAdminEditDialog(user); }, ), ], diff --git a/frontend/lib/widgets/admin/common/admin_user_card.dart b/frontend/lib/widgets/admin/common/admin_user_card.dart index 914e218..92d4237 100644 --- a/frontend/lib/widgets/admin/common/admin_user_card.dart +++ b/frontend/lib/widgets/admin/common/admin_user_card.dart @@ -6,6 +6,10 @@ class AdminUserCard extends StatefulWidget { final String? avatarUrl; final IconData fallbackIcon; final List actions; + final Color? borderColor; + final Color? backgroundColor; + final Color? titleColor; + final Color? infoColor; const AdminUserCard({ super.key, @@ -14,6 +18,10 @@ class AdminUserCard extends StatefulWidget { this.avatarUrl, this.fallbackIcon = Icons.person, this.actions = const [], + this.borderColor, + this.backgroundColor, + this.titleColor, + this.infoColor, }); @override @@ -43,9 +51,10 @@ class _AdminUserCardState extends State { child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 0, + color: widget.backgroundColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), - side: BorderSide(color: Colors.grey.shade300), + side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), @@ -76,7 +85,7 @@ class _AdminUserCardState extends State { style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, - ), + ).copyWith(color: widget.titleColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -88,7 +97,7 @@ class _AdminUserCardState extends State { style: const TextStyle( color: Colors.black54, fontSize: 12, - ), + ).copyWith(color: widget.infoColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart index 58b78e6..2692831 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) { @@ -86,6 +86,7 @@ class _GestionnaireManagementWidgetState final user = filteredGestionnaires[index]; return AdminUserCard( title: user.fullName, + fallbackIcon: Icons.assignment_ind_outlined, avatarUrl: user.photoUrl, subtitleLines: [ user.email, diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index cfa8637..5b8e20a 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -75,6 +75,7 @@ class _ParentManagementWidgetState extends State { final parent = filteredParents[index]; return AdminUserCard( title: parent.user.fullName, + fallbackIcon: Icons.supervisor_account_outlined, avatarUrl: parent.user.photoUrl, subtitleLines: [ parent.user.email, 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.', + ), + ), + ); } }