feat(#35): créer un gestionnaire via modale avec sélection de relais
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>
This commit is contained in:
parent
fac3ae9baa
commit
42bb872c41
@ -1,17 +1,252 @@
|
|||||||
import 'package:flutter/material.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 GestionnairesCreate extends StatelessWidget {
|
class GestionnaireCreateDialog extends StatefulWidget {
|
||||||
const GestionnairesCreate({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AlertDialog(
|
||||||
appBar: AppBar(
|
title: const Text('Créer un gestionnaire'),
|
||||||
title: const Text('Créer un gestionnaire'),
|
content: ConstrainedBox(
|
||||||
),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
body: const Center(
|
child: Form(
|
||||||
child: Text('Formulaire de création de gestionnaire'),
|
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,6 +37,44 @@ class UserService {
|
|||||||
return data.map((e) => AppUser.fromJson(e)).toList();
|
return data.map((e) => AppUser.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<AppUser> createGestionnaire({
|
||||||
|
required String nom,
|
||||||
|
required String prenom,
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String telephone,
|
||||||
|
String? relaisId,
|
||||||
|
}) async {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
body: jsonEncode(<String, dynamic>{
|
||||||
|
'nom': nom,
|
||||||
|
'prenom': prenom,
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
'telephone': telephone,
|
||||||
|
'cguAccepted': true,
|
||||||
|
'relaisId': relaisId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final message = decoded['message'];
|
||||||
|
if (message is List && message.isNotEmpty) {
|
||||||
|
throw Exception(message.join(' - '));
|
||||||
|
}
|
||||||
|
throw Exception(_toStr(message) ?? 'Erreur création gestionnaire');
|
||||||
|
}
|
||||||
|
throw Exception('Erreur création gestionnaire');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AppUser.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
// Récupérer la liste des parents
|
// Récupérer la liste des parents
|
||||||
static Future<List<ParentModel>> getParents() async {
|
static Future<List<ParentModel>> getParents() async {
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||||
@ -15,6 +16,7 @@ class AdminUserManagementPanel extends StatefulWidget {
|
|||||||
|
|
||||||
class _AdminUserManagementPanelState extends State<AdminUserManagementPanel> {
|
class _AdminUserManagementPanelState extends State<AdminUserManagementPanel> {
|
||||||
int _subIndex = 0;
|
int _subIndex = 0;
|
||||||
|
int _gestionnaireRefreshTick = 0;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final TextEditingController _amCapacityController = TextEditingController();
|
final TextEditingController _amCapacityController = TextEditingController();
|
||||||
String? _parentStatus;
|
String? _parentStatus;
|
||||||
@ -133,6 +135,7 @@ class _AdminUserManagementPanelState extends State<AdminUserManagementPanel> {
|
|||||||
switch (_subIndex) {
|
switch (_subIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
return GestionnaireManagementWidget(
|
return GestionnaireManagementWidget(
|
||||||
|
key: ValueKey('gestionnaires-$_gestionnaireRefreshTick'),
|
||||||
searchQuery: _searchController.text,
|
searchQuery: _searchController.text,
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
@ -164,13 +167,40 @@ class _AdminUserManagementPanelState extends State<AdminUserManagementPanel> {
|
|||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
searchHint: _searchHintForTab(),
|
searchHint: _searchHintForTab(),
|
||||||
filterControl: _subBarFilterControl(),
|
filterControl: _subBarFilterControl(),
|
||||||
onAddPressed: () {
|
onAddPressed: _handleAddPressed,
|
||||||
// TODO: brancher création selon onglet actif
|
|
||||||
},
|
|
||||||
addLabel: 'Ajouter',
|
addLabel: 'Ajouter',
|
||||||
),
|
),
|
||||||
Expanded(child: _buildBody()),
|
Expanded(child: _buildBody()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAddPressed() async {
|
||||||
|
if (_subIndex != 0) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'La création est disponible uniquement pour les gestionnaires.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final created = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return const GestionnaireCreateDialog();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
if (created == true) {
|
||||||
|
setState(() {
|
||||||
|
_gestionnaireRefreshTick++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user