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

View File

@ -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/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<AdminManagementWidget> {
bool _isLoading = false;
String? _error;
List<AppUser> _admins = [];
String? _currentUserRole;
@override
void initState() {
super.initState();
_loadCurrentUserRole();
_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 {
final canEdit = _canEditAdmin(user);
final changed = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -61,10 +88,11 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
initialUser: user,
adminMode: true,
withRelais: false,
readOnly: !canEdit,
);
},
);
if (changed == true) {
if (changed == true && canEdit) {
await _loadAdmins();
}
}
@ -86,6 +114,8 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
itemCount: filteredAdmins.length,
itemBuilder: (context, index) {
final user = filteredAdmins[index];
final isSuperAdmin = _isSuperAdmin(user);
final canEdit = _canEditAdmin(user);
return AdminUserCard(
title: user.fullName,
subtitleLines: [
@ -93,10 +123,22 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
'Rôle : ${user.role}',
],
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);
},

View File

@ -6,6 +6,10 @@ class AdminUserCard extends StatefulWidget {
final String? avatarUrl;
final IconData fallbackIcon;
final List<Widget> 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<AdminUserCard> {
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<AdminUserCard> {
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<AdminUserCard> {
style: const TextStyle(
color: Colors.black54,
fontSize: 12,
),
).copyWith(color: widget.infoColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),