Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6261b312 | ||
|
|
9557cf9947 |
@@ -0,0 +1,14 @@
|
||||
# Mini-spec — Uniformisation modale staff (Gestionnaire / Administrateur)
|
||||
|
||||
**Ticket** : **#164** — https://git.ptits-pas.fr/jmartin/petitspas/issues/164
|
||||
**Branche** : `feature/164-staff-modal-uniformisation` (depuis `develop`)
|
||||
**Périmètre** : **front only** — pas d’API / BDD
|
||||
**Milestone** : **0.1.0**
|
||||
|
||||
Voir le corps du ticket #164 pour la spec complète.
|
||||
|
||||
## Livré
|
||||
|
||||
- `frontend/lib/widgets/dashboard/staff_user_form_modal.dart` → `StaffUserFormModal`
|
||||
- Ancien `AdminUserFormDialog` / `gestionnaires_create.dart` retiré
|
||||
- Imports : `user_management_panel`, `gestionnaire_management_widget`, `admin_management_widget`
|
||||
@@ -1,675 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/relais_model.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/relais_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
|
||||
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
|
||||
State<AdminUserFormDialog> createState() => _AdminUserFormDialogState();
|
||||
}
|
||||
|
||||
class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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;
|
||||
bool _isLoadingRelais = true;
|
||||
List<RelaisModel> _relais = [];
|
||||
String? _selectedRelaisId;
|
||||
String? _currentUserId;
|
||||
String? _currentUserRole;
|
||||
bool get _isEditMode => widget.initialUser != null;
|
||||
bool get _isSuperAdminTarget =>
|
||||
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
||||
bool get _isSelfTarget =>
|
||||
_isEditMode &&
|
||||
_currentUserId != null &&
|
||||
widget.initialUser!.id == _currentUserId;
|
||||
/// Gestionnaire : pas de delete staff. Admin+ seulement (#154 / #160).
|
||||
bool get _canDeleteTarget {
|
||||
if (!_isEditMode || widget.readOnly) return false;
|
||||
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
||||
return canDeleteGestionnaire(_currentUserRole);
|
||||
}
|
||||
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() {
|
||||
super.initState();
|
||||
final user = widget.initialUser;
|
||||
if (user != null) {
|
||||
_nomController.text = user.nom ?? '';
|
||||
_prenomController.text = user.prenom ?? '';
|
||||
_emailController.text = user.email;
|
||||
_telephoneController.text = formatPhoneForDisplay(user.telephone ?? '');
|
||||
// En édition, on ne préremplit jamais le mot de passe.
|
||||
_passwordController.clear();
|
||||
final initialRelaisId = user.relaisId?.trim();
|
||||
_selectedRelaisId =
|
||||
(initialRelaisId == null || initialRelaisId.isEmpty)
|
||||
? null
|
||||
: initialRelaisId;
|
||||
}
|
||||
if (widget.withRelais) {
|
||||
_loadRelais();
|
||||
} else {
|
||||
_isLoadingRelais = false;
|
||||
}
|
||||
_loadCurrentUserId();
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentUserId() async {
|
||||
final cached = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
if (cached != null) {
|
||||
setState(() {
|
||||
_currentUserId = cached.id;
|
||||
_currentUserRole = cached.role;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final refreshed = await AuthService.refreshCurrentUser();
|
||||
if (!mounted || refreshed == null) return;
|
||||
setState(() {
|
||||
_currentUserId = refreshed.id;
|
||||
_currentUserRole = refreshed.role;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomController.dispose();
|
||||
_prenomController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_passwordToggleFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Fallback si GET /relais échoue : conserve le relais déjà connu sur l'utilisateur.
|
||||
List<RelaisModel> _fallbackRelaisFromUser() {
|
||||
final id = _selectedRelaisId?.trim();
|
||||
if (id == null || id.isEmpty) return const [];
|
||||
final nom = (widget.initialUser?.relaisNom ?? '').trim();
|
||||
return [
|
||||
RelaisModel(
|
||||
id: id,
|
||||
nom: nom.isNotEmpty ? nom : 'Relais actuel',
|
||||
adresse: '',
|
||||
actif: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _loadRelais() async {
|
||||
try {
|
||||
final list = await RelaisService.getRelais();
|
||||
if (!mounted) return;
|
||||
final uniqueById = <String, RelaisModel>{};
|
||||
for (final relais in list) {
|
||||
uniqueById[relais.id] = relais;
|
||||
}
|
||||
|
||||
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
||||
if (_selectedRelaisId != null &&
|
||||
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
||||
final selected = uniqueById[_selectedRelaisId!];
|
||||
if (selected != null) {
|
||||
filtered.add(selected);
|
||||
} else {
|
||||
// Garder l'id sélectionné et afficher un item de secours (nom carte).
|
||||
filtered.addAll(_fallbackRelaisFromUser());
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_relais = filtered;
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
// Ne pas nullifier _selectedRelaisId (#151) — la carte a déjà le bon libellé.
|
||||
setState(() {
|
||||
_relais = _fallbackRelaisFromUser();
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _required(String? value, String field) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '$field est requis';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
final base = _required(value, 'Email');
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateEmail(value, allowEmpty: true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
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 = <String>{"-", "'", "’"};
|
||||
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<void> _submit() async {
|
||||
if (widget.readOnly) return;
|
||||
if (_isSubmitting) return;
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final normalizedNom = _toTitleCase(_nomController.text);
|
||||
final normalizedPrenom = _toTitleCase(_prenomController.text);
|
||||
final normalizedPhone = normalizePhone(_telephoneController.text);
|
||||
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||
|
||||
if (_isEditMode) {
|
||||
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 {
|
||||
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
|
||||
? (widget.adminMode
|
||||
? 'Administrateur modifié avec succès.'
|
||||
: 'Gestionnaire modifié avec succès.')
|
||||
: (widget.adminMode
|
||||
? 'Administrateur créé avec succès.'
|
||||
: 'Gestionnaire créé avec succès.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e.toString().replaceFirst('Exception: ', ''),
|
||||
),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
if (widget.readOnly) return;
|
||||
if (!_canDeleteTarget) return;
|
||||
if (!_isEditMode || _isSubmitting) return;
|
||||
|
||||
final name = widget.initialUser!.fullName.isEmpty
|
||||
? widget.initialUser!.email
|
||||
: widget.initialUser!.fullName;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: widget.adminMode
|
||||
? 'Supprimer l\'administrateur'
|
||||
: 'Supprimer le gestionnaire',
|
||||
people: [
|
||||
widget.adminMode
|
||||
? SuppressionPersonLine.administrateur(name)
|
||||
: SuppressionPersonLine.gestionnaire(name),
|
||||
],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
try {
|
||||
await UserService.deleteUser(widget.initialUser!.id);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Gestionnaire 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: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: const Color(0xFFEDE5FA),
|
||||
child: Icon(
|
||||
_targetRoleIcon,
|
||||
size: 20,
|
||||
color: const Color(0xFF6B3FA0),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isEditMode
|
||||
? (widget.readOnly
|
||||
? 'Consulter un "$_targetRoleLabel"'
|
||||
: 'Modifier un "$_targetRoleLabel"')
|
||||
: 'Créer un "$_targetRoleLabel"',
|
||||
),
|
||||
),
|
||||
if (_isEditMode && !widget.readOnly)
|
||||
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: _buildPrenomField()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildNomField()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildPasswordField()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildTelephoneField()),
|
||||
],
|
||||
),
|
||||
if (widget.withRelais) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildRelaisField(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (widget.readOnly) ...[
|
||||
FilledButton(
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
] else if (_isEditMode) ...[
|
||||
if (_canDeleteTarget)
|
||||
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,
|
||||
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
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: (widget.readOnly || _isLockedAdminIdentity)
|
||||
? null
|
||||
: (v) => _required(v, 'Prénom'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return EmailTextFormField(
|
||||
controller: _emailController,
|
||||
readOnly: widget.readOnly,
|
||||
label: 'Email',
|
||||
validator: widget.readOnly ? null : _validateEmail,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField() {
|
||||
return TextFormField(
|
||||
controller: _passwordController,
|
||||
readOnly: widget.readOnly,
|
||||
obscureText: _obscurePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
autofillHints: _isEditMode
|
||||
? const <String>[]
|
||||
: const [AutofillHints.newPassword],
|
||||
decoration: InputDecoration(
|
||||
labelText: _isEditMode
|
||||
? 'Nouveau mot de passe'
|
||||
: 'Mot de passe',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: widget.readOnly
|
||||
? null
|
||||
: ExcludeFocus(
|
||||
child: IconButton(
|
||||
focusNode: _passwordToggleFocusNode,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: widget.readOnly ? null : _validatePassword,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTelephoneField() {
|
||||
return FrenchPhoneTextFormField(
|
||||
controller: _telephoneController,
|
||||
readOnly: widget.readOnly,
|
||||
label: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
validator: widget.readOnly ? null : _validatePhone,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRelaisField() {
|
||||
final selectedValue = _selectedRelaisId != null &&
|
||||
_relais.any((relais) => relais.id == _selectedRelaisId)
|
||||
? _selectedRelaisId
|
||||
: null;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
value: selectedValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Relais principal',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('Aucun relais'),
|
||||
),
|
||||
..._relais.map(
|
||||
(relais) => DropdownMenuItem<String?>(
|
||||
value: relais.id,
|
||||
child: Text(relais.nom),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (_isLoadingRelais || widget.readOnly)
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_selectedRelaisId = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isLoadingRelais) ...[
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/staff_user_form_modal.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||
@@ -100,7 +100,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return AdminUserFormDialog(
|
||||
return StaffUserFormModal(
|
||||
initialUser: user,
|
||||
adminMode: true,
|
||||
withRelais: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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/widgets/dashboard/staff_user_form_modal.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||
@@ -74,7 +74,7 @@ class _GestionnaireManagementWidgetState
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return AdminUserFormDialog(initialUser: user);
|
||||
return StaffUserFormModal(initialUser: user);
|
||||
},
|
||||
);
|
||||
if (changed == true) {
|
||||
|
||||
@@ -0,0 +1,794 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/relais_model.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/relais_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||
|
||||
/// Modale création / édition / consultation staff (gestionnaire / admin) — #164.
|
||||
class StaffUserFormModal extends StatefulWidget {
|
||||
final AppUser? initialUser;
|
||||
final bool withRelais;
|
||||
final bool adminMode;
|
||||
final bool readOnly;
|
||||
|
||||
const StaffUserFormModal({
|
||||
super.key,
|
||||
this.initialUser,
|
||||
this.withRelais = true,
|
||||
this.adminMode = false,
|
||||
this.readOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StaffUserFormModal> createState() => _StaffUserFormModalState();
|
||||
}
|
||||
|
||||
class _StaffUserFormModalState extends State<StaffUserFormModal> {
|
||||
static const double _modalWidth = 930;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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;
|
||||
bool _isLoadingRelais = true;
|
||||
bool _dirty = false;
|
||||
List<RelaisModel> _relais = [];
|
||||
String? _selectedRelaisId;
|
||||
String? _currentUserId;
|
||||
String? _currentUserRole;
|
||||
|
||||
String _baselineNom = '';
|
||||
String _baselinePrenom = '';
|
||||
String _baselineEmail = '';
|
||||
String _baselinePhone = '';
|
||||
String? _baselineRelaisId;
|
||||
|
||||
bool get _isEditMode => widget.initialUser != null;
|
||||
bool get _isSuperAdminTarget =>
|
||||
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
||||
bool get _isSelfTarget =>
|
||||
_isEditMode &&
|
||||
_currentUserId != null &&
|
||||
widget.initialUser!.id == _currentUserId;
|
||||
bool get _canDeleteTarget {
|
||||
if (!_isEditMode || widget.readOnly) return false;
|
||||
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
||||
return canDeleteGestionnaire(_currentUserRole);
|
||||
}
|
||||
|
||||
bool get _isLockedAdminIdentity =>
|
||||
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
||||
|
||||
bool get _fieldsEnabled => !widget.readOnly && !_isSubmitting;
|
||||
|
||||
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';
|
||||
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;
|
||||
default:
|
||||
return Icons.person_outline;
|
||||
}
|
||||
}
|
||||
|
||||
@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 = formatPhoneForDisplay(user.telephone ?? '');
|
||||
_passwordController.clear();
|
||||
final initialRelaisId = user.relaisId?.trim();
|
||||
_selectedRelaisId =
|
||||
(initialRelaisId == null || initialRelaisId.isEmpty)
|
||||
? null
|
||||
: initialRelaisId;
|
||||
_captureBaseline();
|
||||
}
|
||||
for (final c in [
|
||||
_nomController,
|
||||
_prenomController,
|
||||
_emailController,
|
||||
_passwordController,
|
||||
_telephoneController,
|
||||
]) {
|
||||
c.addListener(_onFieldChanged);
|
||||
}
|
||||
if (widget.withRelais) {
|
||||
_loadRelais();
|
||||
} else {
|
||||
_isLoadingRelais = false;
|
||||
}
|
||||
_loadCurrentUserId();
|
||||
}
|
||||
|
||||
void _captureBaseline() {
|
||||
_baselineNom = formatPersonNameCase(_nomController.text);
|
||||
_baselinePrenom = formatPersonNameCase(_prenomController.text);
|
||||
_baselineEmail = normalizeEmailText(_emailController.text);
|
||||
_baselinePhone = normalizePhone(_telephoneController.text);
|
||||
_baselineRelaisId = _selectedRelaisId;
|
||||
}
|
||||
|
||||
void _onFieldChanged() {
|
||||
if (widget.readOnly || !_isEditMode) return;
|
||||
final dirty = _computeDirty();
|
||||
if (dirty != _dirty) setState(() => _dirty = dirty);
|
||||
}
|
||||
|
||||
bool _computeDirty() {
|
||||
if (!_isEditMode) return true;
|
||||
final nom = formatPersonNameCase(_nomController.text);
|
||||
final prenom = formatPersonNameCase(_prenomController.text);
|
||||
final email = normalizeEmailText(_emailController.text);
|
||||
final phone = normalizePhone(_telephoneController.text);
|
||||
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||
return nom != _baselineNom ||
|
||||
prenom != _baselinePrenom ||
|
||||
email != _baselineEmail ||
|
||||
phone != _baselinePhone ||
|
||||
passwordProvided ||
|
||||
_selectedRelaisId != _baselineRelaisId;
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentUserId() async {
|
||||
final cached = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
if (cached != null) {
|
||||
setState(() {
|
||||
_currentUserId = cached.id;
|
||||
_currentUserRole = cached.role;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final refreshed = await AuthService.refreshCurrentUser();
|
||||
if (!mounted || refreshed == null) return;
|
||||
setState(() {
|
||||
_currentUserId = refreshed.id;
|
||||
_currentUserRole = refreshed.role;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in [
|
||||
_nomController,
|
||||
_prenomController,
|
||||
_emailController,
|
||||
_passwordController,
|
||||
_telephoneController,
|
||||
]) {
|
||||
c.removeListener(_onFieldChanged);
|
||||
c.dispose();
|
||||
}
|
||||
_passwordToggleFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<RelaisModel> _fallbackRelaisFromUser() {
|
||||
final id = _selectedRelaisId?.trim();
|
||||
if (id == null || id.isEmpty) return const [];
|
||||
final nom = (widget.initialUser?.relaisNom ?? '').trim();
|
||||
return [
|
||||
RelaisModel(
|
||||
id: id,
|
||||
nom: nom.isNotEmpty ? nom : 'Relais actuel',
|
||||
adresse: '',
|
||||
actif: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _loadRelais() async {
|
||||
try {
|
||||
final list = await RelaisService.getRelais();
|
||||
if (!mounted) return;
|
||||
final uniqueById = <String, RelaisModel>{};
|
||||
for (final relais in list) {
|
||||
uniqueById[relais.id] = relais;
|
||||
}
|
||||
|
||||
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
||||
if (_selectedRelaisId != null &&
|
||||
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
||||
final selected = uniqueById[_selectedRelaisId!];
|
||||
if (selected != null) {
|
||||
filtered.add(selected);
|
||||
} else {
|
||||
filtered.addAll(_fallbackRelaisFromUser());
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_relais = filtered;
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_relais = _fallbackRelaisFromUser();
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _required(String? value, String field) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '$field est requis';
|
||||
}
|
||||
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<void> _submit() async {
|
||||
if (widget.readOnly) return;
|
||||
if (_isSubmitting) return;
|
||||
if (_isEditMode && !_dirty) return;
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
try {
|
||||
final normalizedNom = formatPersonNameCase(_nomController.text);
|
||||
final normalizedPrenom = formatPersonNameCase(_prenomController.text);
|
||||
final normalizedPhone = normalizePhone(_telephoneController.text);
|
||||
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||
|
||||
if (_isEditMode) {
|
||||
if (widget.adminMode) {
|
||||
final lockedNom = formatPersonNameCase(widget.initialUser!.nom ?? '');
|
||||
final lockedPrenom =
|
||||
formatPersonNameCase(widget.initialUser!.prenom ?? '');
|
||||
await UserService.updateAdministrateur(
|
||||
adminId: widget.initialUser!.id,
|
||||
nom: _isLockedAdminIdentity ? lockedNom : normalizedNom,
|
||||
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom,
|
||||
email: normalizeEmailText(_emailController.text),
|
||||
telephone: normalizedPhone.isEmpty
|
||||
? normalizePhone(widget.initialUser!.telephone ?? '')
|
||||
: normalizedPhone,
|
||||
password: passwordProvided ? _passwordController.text : null,
|
||||
);
|
||||
} else {
|
||||
final currentUser = widget.initialUser!;
|
||||
final initialNom = formatPersonNameCase(currentUser.nom ?? '');
|
||||
final initialPrenom = formatPersonNameCase(currentUser.prenom ?? '');
|
||||
final initialEmail = normalizeEmailText(currentUser.email);
|
||||
final initialPhone = normalizePhone(currentUser.telephone ?? '');
|
||||
|
||||
final onlyRelaisChanged = normalizedNom == initialNom &&
|
||||
normalizedPrenom == initialPrenom &&
|
||||
normalizeEmailText(_emailController.text) == 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: normalizeEmailText(_emailController.text),
|
||||
telephone:
|
||||
normalizedPhone.isEmpty ? initialPhone : normalizedPhone,
|
||||
relaisId: _selectedRelaisId,
|
||||
password: passwordProvided ? _passwordController.text : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (widget.adminMode) {
|
||||
await UserService.createAdministrateur(
|
||||
nom: normalizedNom,
|
||||
prenom: normalizedPrenom,
|
||||
email: normalizeEmailText(_emailController.text),
|
||||
password: _passwordController.text,
|
||||
telephone: normalizePhone(_telephoneController.text),
|
||||
);
|
||||
} else {
|
||||
await UserService.createGestionnaire(
|
||||
nom: normalizedNom,
|
||||
prenom: normalizedPrenom,
|
||||
email: normalizeEmailText(_emailController.text),
|
||||
password: _passwordController.text,
|
||||
telephone: normalizePhone(_telephoneController.text),
|
||||
relaisId: _selectedRelaisId,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_isEditMode
|
||||
? (widget.adminMode
|
||||
? 'Administrateur modifié avec succès.'
|
||||
: 'Gestionnaire modifié avec succès.')
|
||||
: (widget.adminMode
|
||||
? 'Administrateur créé avec succès.'
|
||||
: 'Gestionnaire créé avec succès.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
if (widget.readOnly) return;
|
||||
if (!_canDeleteTarget) return;
|
||||
if (!_isEditMode || _isSubmitting) return;
|
||||
|
||||
final name = widget.initialUser!.fullName.isEmpty
|
||||
? widget.initialUser!.email
|
||||
: widget.initialUser!.fullName;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: widget.adminMode
|
||||
? 'Supprimer l\'administrateur'
|
||||
: 'Supprimer le gestionnaire',
|
||||
people: [
|
||||
widget.adminMode
|
||||
? SuppressionPersonLine.administrateur(name)
|
||||
: SuppressionPersonLine.gestionnaire(name),
|
||||
],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
try {
|
||||
await UserService.deleteUser(widget.initialUser!.id);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
widget.adminMode
|
||||
? 'Administrateur supprimé.'
|
||||
: 'Gestionnaire 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);
|
||||
}
|
||||
}
|
||||
|
||||
String _headerTitle() {
|
||||
if (!_isEditMode) {
|
||||
return widget.adminMode
|
||||
? 'Créer un administrateur'
|
||||
: 'Créer un gestionnaire';
|
||||
}
|
||||
final prenom = (_prenomController.text.trim().isNotEmpty
|
||||
? _prenomController.text
|
||||
: (widget.initialUser?.prenom ?? ''))
|
||||
.trim();
|
||||
final nom = (_nomController.text.trim().isNotEmpty
|
||||
? _nomController.text
|
||||
: (widget.initialUser?.nom ?? ''))
|
||||
.trim();
|
||||
final full = '$prenom $nom'.trim();
|
||||
if (full.isNotEmpty) return full;
|
||||
return widget.initialUser?.email ?? _targetRoleLabel;
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
if (widget.readOnly) {
|
||||
return Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (!_isEditMode) {
|
||||
return Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
style: ValidationModalTheme.primaryElevatedStyle,
|
||||
onPressed: _isSubmitting ? null : _submit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('Créer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (_canDeleteTarget)
|
||||
OutlinedButton(
|
||||
onPressed: _isSubmitting ? null : _delete,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red.shade700,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
if (_canDeleteTarget) const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
style: ValidationModalTheme.primaryElevatedStyle,
|
||||
onPressed: !_dirty || _isSubmitting ? null : _submit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _namedField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required String requiredLabel,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
return ValidationLabeledField(
|
||||
label: label,
|
||||
field: SizedBox(
|
||||
height: ValidationFormMetrics.fieldHeight,
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
style: ValidationFormMetrics.fieldTextStyle,
|
||||
decoration: ValidationFieldDecoration.input(),
|
||||
validator: (!enabled || widget.readOnly)
|
||||
? null
|
||||
: (v) => _required(v, requiredLabel),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _passwordField() {
|
||||
return ValidationLabeledField(
|
||||
label: _isEditMode ? 'Nouveau mot de passe' : 'Mot de passe',
|
||||
field: SizedBox(
|
||||
height: ValidationFormMetrics.fieldHeight,
|
||||
child: TextFormField(
|
||||
controller: _passwordController,
|
||||
enabled: _fieldsEnabled,
|
||||
obscureText: _obscurePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
autofillHints: _isEditMode
|
||||
? const <String>[]
|
||||
: const [AutofillHints.newPassword],
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
style: ValidationFormMetrics.fieldTextStyle,
|
||||
decoration: ValidationFieldDecoration.input().copyWith(
|
||||
suffixIcon: widget.readOnly
|
||||
? null
|
||||
: ExcludeFocus(
|
||||
child: IconButton(
|
||||
focusNode: _passwordToggleFocusNode,
|
||||
onPressed: !_fieldsEnabled
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||
errorMaxLines: 2,
|
||||
),
|
||||
validator: widget.readOnly ? null : _validatePassword,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _relaisField() {
|
||||
final selectedValue = _selectedRelaisId != null &&
|
||||
_relais.any((relais) => relais.id == _selectedRelaisId)
|
||||
? _selectedRelaisId
|
||||
: null;
|
||||
|
||||
return ValidationLabeledField(
|
||||
label: 'Relais principal',
|
||||
field: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: ValidationFormMetrics.fieldHeight,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
value: selectedValue,
|
||||
decoration: ValidationFieldDecoration.input(),
|
||||
style: ValidationFormMetrics.fieldTextStyle,
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('Aucun relais'),
|
||||
),
|
||||
..._relais.map(
|
||||
(relais) => DropdownMenuItem<String?>(
|
||||
value: relais.id,
|
||||
child: Text(relais.nom),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (_isLoadingRelais || !_fieldsEnabled)
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_selectedRelaisId = value;
|
||||
if (_isEditMode) {
|
||||
_dirty = _computeDirty();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isLoadingRelais) ...[
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nameEnabled =
|
||||
_fieldsEnabled && !_isLockedAdminIdentity;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: _modalWidth),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 16, 4, 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2, right: 10),
|
||||
child: Icon(
|
||||
_targetRoleIcon,
|
||||
size: 22,
|
||||
color: ValidationModalTheme.primaryActionBackground,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_headerTitle(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (_isEditMode) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_targetRoleLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 40,
|
||||
minHeight: 40,
|
||||
),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => Navigator.of(context).pop(false),
|
||||
tooltip: 'Fermer',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _namedField(
|
||||
label: 'Prénom',
|
||||
controller: _prenomController,
|
||||
requiredLabel: 'Prénom',
|
||||
enabled: nameEnabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _namedField(
|
||||
label: 'Nom',
|
||||
controller: _nomController,
|
||||
requiredLabel: 'Nom',
|
||||
enabled: nameEnabled,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||
ValidationLabeledField(
|
||||
label: 'Email',
|
||||
field: IgnorePointer(
|
||||
ignoring: !_fieldsEnabled,
|
||||
child: ValidationEmailField(
|
||||
controller: _emailController,
|
||||
hintText: 'ex. nom@domaine.fr',
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _passwordField()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ValidationLabeledField(
|
||||
label: 'Téléphone',
|
||||
field: IgnorePointer(
|
||||
ignoring: !_fieldsEnabled,
|
||||
child: ValidationPhoneField(
|
||||
controller: _telephoneController,
|
||||
hintText: '06 12 34 56 78',
|
||||
allowEmpty: _isEditMode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.withRelais) ...[
|
||||
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||
_relaisField(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 16, 14),
|
||||
child: _buildFooter(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/staff_user_form_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/am_dossier_create_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/assistante_maternelle_management_widget.dart';
|
||||
@@ -385,7 +385,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return const AdminUserFormDialog();
|
||||
return const StaffUserFormModal();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -403,7 +403,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return const AdminUserFormDialog(
|
||||
return const StaffUserFormModal(
|
||||
adminMode: true,
|
||||
withRelais: false,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user