feat(#96): différencier la consultation admin et le mode édition

Affiche une identité visuelle dédiée pour les super admins et adapte l’action par ligne (oeil en lecture seule, crayon en édition) avec modale strictement read-only quand l’utilisateur n’a pas les droits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-24 21:48:11 +01:00
parent d8572e7fd6
commit e2ebc6a0a1
3 changed files with 108 additions and 35 deletions

View File

@ -9,12 +9,14 @@ class AdminUserFormDialog extends StatefulWidget {
final AppUser? initialUser; final AppUser? initialUser;
final bool withRelais; final bool withRelais;
final bool adminMode; final bool adminMode;
final bool readOnly;
const AdminUserFormDialog({ const AdminUserFormDialog({
super.key, super.key,
this.initialUser, this.initialUser,
this.withRelais = true, this.withRelais = true,
this.adminMode = false, this.adminMode = false,
this.readOnly = false,
}); });
@override @override
@ -194,6 +196,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
} }
Future<void> _submit() async { Future<void> _submit() async {
if (widget.readOnly) return;
if (_isSubmitting) return; if (_isSubmitting) return;
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
@ -304,6 +307,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
} }
Future<void> _delete() async { Future<void> _delete() async {
if (widget.readOnly) return;
if (!_isEditMode || _isSubmitting) return; if (!_isEditMode || _isSubmitting) return;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
@ -363,15 +367,19 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
Expanded( Expanded(
child: Text( child: Text(
_isEditMode _isEditMode
? (widget.adminMode ? (widget.readOnly
? 'Modifier un administrateur' ? (widget.adminMode
: 'Modifier un gestionnaire') ? 'Consulter un administrateur'
: 'Consulter un gestionnaire')
: (widget.adminMode
? 'Modifier un administrateur'
: 'Modifier un gestionnaire'))
: (widget.adminMode : (widget.adminMode
? 'Créer un administrateur' ? 'Créer un administrateur'
: 'Créer un gestionnaire'), : 'Créer un gestionnaire'),
), ),
), ),
if (_isEditMode) if (_isEditMode && !widget.readOnly)
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
tooltip: 'Fermer', tooltip: 'Fermer',
@ -416,7 +424,12 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
), ),
), ),
actions: [ actions: [
if (_isEditMode) ...[ if (widget.readOnly) ...[
FilledButton(
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false),
child: const Text('Fermer'),
),
] else if (_isEditMode) ...[
OutlinedButton( OutlinedButton(
onPressed: _isSubmitting ? null : _delete, onPressed: _isSubmitting ? null : _delete,
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700), style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
@ -458,42 +471,46 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
Widget _buildNomField() { Widget _buildNomField() {
return TextFormField( return TextFormField(
controller: _nomController, controller: _nomController,
readOnly: widget.readOnly,
textCapitalization: TextCapitalization.words, textCapitalization: TextCapitalization.words,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Nom', labelText: 'Nom',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: (v) => _required(v, 'Nom'), validator: widget.readOnly ? null : (v) => _required(v, 'Nom'),
); );
} }
Widget _buildPrenomField() { Widget _buildPrenomField() {
return TextFormField( return TextFormField(
controller: _prenomController, controller: _prenomController,
readOnly: widget.readOnly,
textCapitalization: TextCapitalization.words, textCapitalization: TextCapitalization.words,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Prénom', labelText: 'Prénom',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: (v) => _required(v, 'Prénom'), validator: widget.readOnly ? null : (v) => _required(v, 'Prénom'),
); );
} }
Widget _buildEmailField() { Widget _buildEmailField() {
return TextFormField( return TextFormField(
controller: _emailController, controller: _emailController,
readOnly: widget.readOnly,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Email', labelText: 'Email',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: _validateEmail, validator: widget.readOnly ? null : _validateEmail,
); );
} }
Widget _buildPasswordField() { Widget _buildPasswordField() {
return TextFormField( return TextFormField(
controller: _passwordController, controller: _passwordController,
readOnly: widget.readOnly,
obscureText: _obscurePassword, obscureText: _obscurePassword,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
@ -505,38 +522,43 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
? 'Nouveau mot de passe' ? 'Nouveau mot de passe'
: 'Mot de passe', : 'Mot de passe',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: ExcludeFocus( suffixIcon: widget.readOnly
child: IconButton( ? null
focusNode: _passwordToggleFocusNode, : ExcludeFocus(
onPressed: () { child: IconButton(
setState(() { focusNode: _passwordToggleFocusNode,
_obscurePassword = !_obscurePassword; onPressed: () {
}); setState(() {
}, _obscurePassword = !_obscurePassword;
icon: Icon( });
_obscurePassword ? Icons.visibility_off : Icons.visibility, },
), icon: Icon(
), _obscurePassword ? Icons.visibility_off : Icons.visibility,
), ),
),
),
), ),
validator: _validatePassword, validator: widget.readOnly ? null : _validatePassword,
); );
} }
Widget _buildTelephoneField() { Widget _buildTelephoneField() {
return TextFormField( return TextFormField(
controller: _telephoneController, controller: _telephoneController,
readOnly: widget.readOnly,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
inputFormatters: [ inputFormatters: widget.readOnly
FilteringTextInputFormatter.digitsOnly, ? null
LengthLimitingTextInputFormatter(10), : [
_FrenchPhoneNumberFormatter(), FilteringTextInputFormatter.digitsOnly,
], LengthLimitingTextInputFormatter(10),
_FrenchPhoneNumberFormatter(),
],
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Téléphone (ex: 06 12 34 56 78)', labelText: 'Téléphone (ex: 06 12 34 56 78)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: _validatePhone, validator: widget.readOnly ? null : _validatePhone,
); );
} }
@ -568,7 +590,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
), ),
), ),
], ],
onChanged: _isLoadingRelais onChanged: (_isLoadingRelais || widget.readOnly)
? null ? null
: (value) { : (value) {
setState(() { setState(() {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/user.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/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/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
@ -21,10 +22,12 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
List<AppUser> _admins = []; List<AppUser> _admins = [];
String? _currentUserRole;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadCurrentUserRole();
_loadAdmins(); _loadAdmins();
} }
@ -52,7 +55,31 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
} }
} }
Future<void> _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<void> _openAdminEditDialog(AppUser user) async { Future<void> _openAdminEditDialog(AppUser user) async {
final canEdit = _canEditAdmin(user);
final changed = await showDialog<bool>( final changed = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -61,10 +88,11 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
initialUser: user, initialUser: user,
adminMode: true, adminMode: true,
withRelais: false, withRelais: false,
readOnly: !canEdit,
); );
}, },
); );
if (changed == true) { if (changed == true && canEdit) {
await _loadAdmins(); await _loadAdmins();
} }
} }
@ -86,6 +114,8 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
itemCount: filteredAdmins.length, itemCount: filteredAdmins.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = filteredAdmins[index]; final user = filteredAdmins[index];
final isSuperAdmin = _isSuperAdmin(user);
final canEdit = _canEditAdmin(user);
return AdminUserCard( return AdminUserCard(
title: user.fullName, title: user.fullName,
subtitleLines: [ subtitleLines: [
@ -93,10 +123,22 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
'Rôle : ${user.role}', 'Rôle : ${user.role}',
], ],
avatarUrl: user.photoUrl, 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: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: Icon(
tooltip: 'Modifier', canEdit ? Icons.edit_outlined : Icons.visibility_outlined,
),
tooltip: canEdit ? 'Modifier' : 'Consulter',
onPressed: () { onPressed: () {
_openAdminEditDialog(user); _openAdminEditDialog(user);
}, },

View File

@ -6,6 +6,10 @@ class AdminUserCard extends StatefulWidget {
final String? avatarUrl; final String? avatarUrl;
final IconData fallbackIcon; final IconData fallbackIcon;
final List<Widget> actions; final List<Widget> actions;
final Color? borderColor;
final Color? backgroundColor;
final Color? titleColor;
final Color? infoColor;
const AdminUserCard({ const AdminUserCard({
super.key, super.key,
@ -14,6 +18,10 @@ class AdminUserCard extends StatefulWidget {
this.avatarUrl, this.avatarUrl,
this.fallbackIcon = Icons.person, this.fallbackIcon = Icons.person,
this.actions = const [], this.actions = const [],
this.borderColor,
this.backgroundColor,
this.titleColor,
this.infoColor,
}); });
@override @override
@ -43,9 +51,10 @@ class _AdminUserCardState extends State<AdminUserCard> {
child: Card( child: Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
elevation: 0, elevation: 0,
color: widget.backgroundColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
side: BorderSide(color: Colors.grey.shade300), side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
@ -76,7 +85,7 @@ class _AdminUserCardState extends State<AdminUserCard> {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 14, fontSize: 14,
), ).copyWith(color: widget.titleColor),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -88,7 +97,7 @@ class _AdminUserCardState extends State<AdminUserCard> {
style: const TextStyle( style: const TextStyle(
color: Colors.black54, color: Colors.black54,
fontSize: 12, fontSize: 12,
), ).copyWith(color: widget.infoColor),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),