diff --git a/backend/src/routes/user/dto/update_gestionnaire.dto.ts b/backend/src/routes/user/dto/update_gestionnaire.dto.ts index 4aef50e..c6956bc 100644 --- a/backend/src/routes/user/dto/update_gestionnaire.dto.ts +++ b/backend/src/routes/user/dto/update_gestionnaire.dto.ts @@ -1,10 +1,4 @@ -import { PartialType, ApiProperty } from "@nestjs/swagger"; -import { CreateUserDto } from "./create_user.dto"; -import { IsOptional, IsUUID } from "class-validator"; +import { PartialType } from "@nestjs/swagger"; +import { CreateGestionnaireDto } from "./create_gestionnaire.dto"; -export class UpdateGestionnaireDto extends PartialType(CreateUserDto) { - @ApiProperty({ required: false, description: 'ID du relais de rattachement' }) - @IsOptional() - @IsUUID() - relaisId?: string; -} +export class UpdateGestionnaireDto extends PartialType(CreateGestionnaireDto) {} diff --git a/backend/src/routes/user/user.service.ts b/backend/src/routes/user/user.service.ts index 6b2581e..b8a88e0 100644 --- a/backend/src/routes/user/user.service.ts +++ b/backend/src/routes/user/user.service.ts @@ -155,6 +155,16 @@ 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'); @@ -256,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/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 c80dee4..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( @@ -232,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 { @@ -240,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(); } @@ -270,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 37040b3..be51365 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/user.dart'; -import 'package:p_tits_pas/screens/administrateurs/creation/admin_create.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'; @@ -21,10 +22,12 @@ class _AdminManagementWidgetState extends State { bool _isLoading = false; String? _error; List _admins = []; + String? _currentUserRole; @override void initState() { super.initState(); + _loadCurrentUserRole(); _loadAdmins(); } @@ -52,15 +55,44 @@ 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 AdminCreateDialog(initialUser: user); + return AdminUserFormDialog( + initialUser: user, + adminMode: true, + withRelais: false, + readOnly: !canEdit, + ); }, ); - if (changed == true) { + if (changed == true && canEdit) { await _loadAdmins(); } } @@ -82,17 +114,34 @@ 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: () { _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 5b14762..63bc921 100644 --- a/frontend/lib/widgets/admin/user_management_panel.dart +++ b/frontend/lib/widgets/admin/user_management_panel.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:p_tits_pas/screens/administrateurs/creation/admin_create.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'; @@ -184,7 +183,7 @@ class _AdminUserManagementPanelState extends State { context: context, barrierDismissible: false, builder: (dialogContext) { - return const GestionnaireCreateDialog(); + return const AdminUserFormDialog(); }, ); @@ -202,7 +201,10 @@ class _AdminUserManagementPanelState extends State { context: context, barrierDismissible: false, builder: (dialogContext) { - return const AdminCreateDialog(); + return const AdminUserFormDialog( + adminMode: true, + withRelais: false, + ); }, ); @@ -219,7 +221,7 @@ class _AdminUserManagementPanelState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'La création est disponible uniquement pour les gestionnaires et les administrateurs.', + 'La création est disponible pour les gestionnaires et administrateurs.', ), ), );