Intègre en un seul commit les évolutions récentes de develop vers master, incluant la modale admin/gestionnaire, les protections super admin, les ajustements API associés et la mise à jour documentaire des tickets/spec. Co-authored-by: Cursor <cursoragent@cursor.com>
358 lines
10 KiB
Dart
358 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:p_tits_pas/models/user.dart';
|
|
import 'package:p_tits_pas/services/user_service.dart';
|
|
|
|
class AdminCreateDialog extends StatefulWidget {
|
|
final AppUser? initialUser;
|
|
|
|
const AdminCreateDialog({
|
|
super.key,
|
|
this.initialUser,
|
|
});
|
|
|
|
@override
|
|
State<AdminCreateDialog> createState() => _AdminCreateDialogState();
|
|
}
|
|
|
|
class _AdminCreateDialogState extends State<AdminCreateDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nomController = TextEditingController();
|
|
final _prenomController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
final _telephoneController = TextEditingController();
|
|
|
|
bool _isSubmitting = false;
|
|
bool _obscurePassword = true;
|
|
bool get _isEditMode => widget.initialUser != null;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final user = widget.initialUser;
|
|
if (user != null) {
|
|
_nomController.text = user.nom ?? '';
|
|
_prenomController.text = user.prenom ?? '';
|
|
_emailController.text = user.email;
|
|
_telephoneController.text = user.telephone ?? '';
|
|
// En édition, on ne préremplit jamais le mot de passe.
|
|
_passwordController.clear();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nomController.dispose();
|
|
_prenomController.dispose();
|
|
_emailController.dispose();
|
|
_passwordController.dispose();
|
|
_telephoneController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String? _required(String? value, String field) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return '$field est requis';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String? _validateEmail(String? value) {
|
|
final base = _required(value, 'Email');
|
|
if (base != null) return base;
|
|
final email = value!.trim();
|
|
final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
|
|
if (!ok) return 'Format email invalide';
|
|
return null;
|
|
}
|
|
|
|
String? _validatePassword(String? value) {
|
|
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
|
return null;
|
|
}
|
|
final base = _required(value, 'Mot de passe');
|
|
if (base != null) return base;
|
|
if (value!.trim().length < 6) return 'Minimum 6 caractères';
|
|
return null;
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (_isSubmitting) return;
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
setState(() {
|
|
_isSubmitting = true;
|
|
});
|
|
|
|
try {
|
|
if (_isEditMode) {
|
|
await UserService.updateAdmin(
|
|
adminId: widget.initialUser!.id,
|
|
nom: _nomController.text.trim(),
|
|
prenom: _prenomController.text.trim(),
|
|
email: _emailController.text.trim(),
|
|
telephone: _telephoneController.text.trim(),
|
|
password: _passwordController.text.trim().isEmpty
|
|
? null
|
|
: _passwordController.text,
|
|
);
|
|
} else {
|
|
await UserService.createAdmin(
|
|
nom: _nomController.text.trim(),
|
|
prenom: _prenomController.text.trim(),
|
|
email: _emailController.text.trim(),
|
|
password: _passwordController.text,
|
|
telephone: _telephoneController.text.trim(),
|
|
);
|
|
}
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
_isEditMode
|
|
? 'Administrateur modifié avec succès.'
|
|
: 'Administrateur créé avec succès.',
|
|
),
|
|
),
|
|
);
|
|
Navigator.of(context).pop(true);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
e.toString().replaceFirst('Exception: ', ''),
|
|
),
|
|
backgroundColor: Colors.red.shade700,
|
|
),
|
|
);
|
|
} finally {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_isSubmitting = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _delete() async {
|
|
if (!_isEditMode || _isSubmitting) return;
|
|
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text(
|
|
'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
if (confirmed != true) return;
|
|
|
|
setState(() {
|
|
_isSubmitting = true;
|
|
});
|
|
try {
|
|
await UserService.deleteUser(widget.initialUser!.id);
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Administrateur supprimé.')),
|
|
);
|
|
Navigator.of(context).pop(true);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
|
backgroundColor: Colors.red.shade700,
|
|
),
|
|
);
|
|
setState(() {
|
|
_isSubmitting = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_isEditMode
|
|
? 'Modifier un administrateur'
|
|
: 'Créer un administrateur',
|
|
),
|
|
),
|
|
if (_isEditMode)
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
tooltip: 'Fermer',
|
|
onPressed: _isSubmitting
|
|
? null
|
|
: () => Navigator.of(context).pop(false),
|
|
),
|
|
],
|
|
),
|
|
content: SizedBox(
|
|
width: 620,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildNomField()),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _buildPrenomField()),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildEmailField(),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildPasswordField()),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _buildTelephoneField()),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
if (_isEditMode) ...[
|
|
OutlinedButton(
|
|
onPressed: _isSubmitting ? null : _delete,
|
|
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
FilledButton.icon(
|
|
onPressed: _isSubmitting ? null : _submit,
|
|
icon: _isSubmitting
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.edit),
|
|
label: Text(_isSubmitting ? 'Modification...' : 'Modifier'),
|
|
),
|
|
] else ...[
|
|
OutlinedButton(
|
|
onPressed:
|
|
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
FilledButton.icon(
|
|
onPressed: _isSubmitting ? null : _submit,
|
|
icon: _isSubmitting
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.person_add_alt_1),
|
|
label: Text(_isSubmitting ? 'Création...' : 'Créer'),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNomField() {
|
|
return TextFormField(
|
|
controller: _nomController,
|
|
textCapitalization: TextCapitalization.words,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Nom',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Nom'),
|
|
);
|
|
}
|
|
|
|
Widget _buildPrenomField() {
|
|
return TextFormField(
|
|
controller: _prenomController,
|
|
textCapitalization: TextCapitalization.words,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Prénom',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Prénom'),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmailField() {
|
|
return TextFormField(
|
|
controller: _emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Email',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: _validateEmail,
|
|
);
|
|
}
|
|
|
|
Widget _buildPasswordField() {
|
|
return TextFormField(
|
|
controller: _passwordController,
|
|
obscureText: _obscurePassword,
|
|
enableSuggestions: false,
|
|
autocorrect: false,
|
|
autofillHints: _isEditMode
|
|
? const <String>[]
|
|
: const [AutofillHints.newPassword],
|
|
decoration: InputDecoration(
|
|
labelText: _isEditMode
|
|
? 'Nouveau mot de passe'
|
|
: 'Mot de passe',
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_obscurePassword = !_obscurePassword;
|
|
});
|
|
},
|
|
icon: Icon(
|
|
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
|
),
|
|
),
|
|
),
|
|
validator: _validatePassword,
|
|
);
|
|
}
|
|
|
|
Widget _buildTelephoneField() {
|
|
return TextFormField(
|
|
controller: _telephoneController,
|
|
keyboardType: TextInputType.phone,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Téléphone',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Téléphone'),
|
|
);
|
|
}
|
|
}
|