Implémente la création de gestionnaire directement depuis le dashboard admin avec formulaire validé, appel API dédié et rattachement optionnel à un relais depuis une combobox. Co-authored-by: Cursor <cursoragent@cursor.com>
252 lines
8.0 KiB
Dart
252 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:p_tits_pas/models/relais_model.dart';
|
|
import 'package:p_tits_pas/services/relais_service.dart';
|
|
import 'package:p_tits_pas/services/user_service.dart';
|
|
|
|
class GestionnaireCreateDialog extends StatefulWidget {
|
|
const GestionnaireCreateDialog({super.key});
|
|
|
|
@override
|
|
State<GestionnaireCreateDialog> createState() =>
|
|
_GestionnaireCreateDialogState();
|
|
}
|
|
|
|
class _GestionnaireCreateDialogState extends State<GestionnaireCreateDialog> {
|
|
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 _isLoadingRelais = true;
|
|
List<RelaisModel> _relais = [];
|
|
String? _selectedRelaisId;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadRelais();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nomController.dispose();
|
|
_prenomController.dispose();
|
|
_emailController.dispose();
|
|
_passwordController.dispose();
|
|
_telephoneController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadRelais() async {
|
|
try {
|
|
final list = await RelaisService.getRelais();
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_relais = list.where((r) => r.actif).toList();
|
|
_isLoadingRelais = false;
|
|
});
|
|
} catch (_) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_relais = [];
|
|
_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;
|
|
final email = value!.trim();
|
|
final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
|
|
if (!ok) return 'Format email invalide';
|
|
return null;
|
|
}
|
|
|
|
String? _validatePassword(String? value) {
|
|
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 {
|
|
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 (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('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;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Créer un gestionnaire'),
|
|
content: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 640),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
controller: _nomController,
|
|
textCapitalization: TextCapitalization.words,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Nom',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Nom'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _prenomController,
|
|
textCapitalization: TextCapitalization.words,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Prénom',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Prénom'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Email',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: _validateEmail,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _passwordController,
|
|
obscureText: _obscurePassword,
|
|
decoration: InputDecoration(
|
|
labelText: 'Mot de passe',
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_obscurePassword = !_obscurePassword;
|
|
});
|
|
},
|
|
icon: Icon(
|
|
_obscurePassword
|
|
? Icons.visibility_off
|
|
: Icons.visibility,
|
|
),
|
|
),
|
|
),
|
|
validator: _validatePassword,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: _telephoneController,
|
|
keyboardType: TextInputType.phone,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Téléphone',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (v) => _required(v, 'Téléphone'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<String?>(
|
|
value: _selectedRelaisId,
|
|
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
|
|
? null
|
|
: (value) {
|
|
setState(() {
|
|
_selectedRelaisId = value;
|
|
});
|
|
},
|
|
),
|
|
if (_isLoadingRelais) ...[
|
|
const SizedBox(height: 8),
|
|
const LinearProgressIndicator(minHeight: 2),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |