From 1e85819fea519400eef07c9e8be83c60ab6e3ba2 Mon Sep 17 00:00:00 2001 From: Hanim Date: Mon, 15 Sep 2025 15:21:12 +0200 Subject: [PATCH] feat: Ajout des requet sur le widget AssistanteMaternelleManagementWidget --- ...sistante_maternelle_management_widget.dart | 352 +++++++++++---- .../widgets/admin/base_user_management.dart | 424 ++++++++++++++++++ 2 files changed, 686 insertions(+), 90 deletions(-) create mode 100644 frontend/lib/widgets/admin/base_user_management.dart diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart index 220f946..b452f2b 100644 --- a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -1,106 +1,278 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/services/user_service.dart'; +import 'package:p_tits_pas/widgets/admin/base_user_management.dart'; class AssistanteMaternelleManagementWidget extends StatelessWidget { const AssistanteMaternelleManagementWidget({super.key}); @override Widget build(BuildContext context) { - final assistantes = [ - { - "nom": "Marie Dupont", - "numeroAgrement": "AG123456", - "zone": "Paris 14", - "capacite": 3, - }, - { - "nom": "Claire Martin", - "numeroAgrement": "AG654321", - "zone": "Lyon 7", - "capacite": 2, - }, - ]; - - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 🔎 Zone de filtre - _buildFilterSection(), - - const SizedBox(height: 16), - - // 📋 Liste des assistantes - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: assistantes.length, - itemBuilder: (context, index) { - final assistante = assistantes[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: const Icon(Icons.face), - title: Text(assistante['nom'].toString()), - subtitle: Text( - "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () { - // TODO: Ajouter modification - }, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - // TODO: Ajouter suppression - }, - ), - ], - ), - ), - ); - }, - ), - ], - ), + return BaseUserManagementWidget( + config: UserDisplayConfig( + title: 'Assistantes Maternelles', + role: 'assistante_maternelle', + defaultIcon: Icons.face, + filterFields: [ + FilterField( + label: 'Rechercher', + hint: 'Nom ou email', + type: FilterType.text, + filter: (user, query) { + final fullName = '${user['prenom'] ?? ''} ${user['nom'] ?? ''}'.toLowerCase(); + final email = (user['email'] ?? '').toLowerCase(); + return fullName.contains(query.toLowerCase()) || + email.contains(query.toLowerCase()); + }, + ), + FilterField( + label: 'Zone géographique', + hint: 'Ville ou département', + type: FilterType.text, + filter: (user, query) { + final zone = (user['zone'] ?? user['ville'] ?? user['code_postal'] ?? '').toLowerCase(); + return zone.contains(query.toLowerCase()); + }, + ), + FilterField( + label: 'Capacité minimum', + hint: 'Nombre d\'enfants', + type: FilterType.number, + filter: (user, query) { + final capacite = int.tryParse(user['capacite']?.toString() ?? '0') ?? 0; + final minCapacite = int.tryParse(query) ?? 0; + return capacite >= minCapacite; + }, + ), + FilterField( + label: 'Statut', + hint: 'Tous', + type: FilterType.dropdown, + options: ['actif', 'en attente', 'inactif'], + filter: (user, status) { + if (status.isEmpty) return true; + return user['statut']?.toString().toLowerCase() == status.toLowerCase(); + }, + ), + ], + actions: [ + UserAction( + icon: Icons.edit, + color: Colors.orange, + tooltip: 'Modifier', + onPressed: _editAssistante, + ), + UserAction( + icon: Icons.delete, + color: Colors.red, + tooltip: 'Supprimer', + onPressed: _deleteAssistante, + ), + UserAction( + icon: Icons.location_on, + color: Colors.green, + tooltip: 'Voir zone', + onPressed: _showZone, + ), + ], + getSubtitle: (user) { + final email = user['email'] ?? ''; + final numeroAgrement = user['numeroAgrement'] ?? user['agrement'] ?? 'N/A'; + final zone = user['code_postal'] ?? user['ville'] ?? 'Non spécifiée'; + final capacite = user['capacite'] ?? user['capaciteAccueil'] ?? 'N/A'; + return '$email\nN° Agrément: $numeroAgrement\nZone: $zone | Capacité: $capacite'; + }, + getDisplayName: (user) => '${user['prenom'] ?? ''} ${user['nom'] ?? ''}', + ), ); } - Widget _buildFilterSection() { - return Wrap( - spacing: 16, - runSpacing: 8, - children: [ - SizedBox( - width: 200, - child: TextField( - decoration: const InputDecoration( - labelText: "Zone géographique", - border: OutlineInputBorder(), - ), - onChanged: (value) { - // TODO: Ajouter logique de filtrage par zone - }, - ), + static Future _editAssistante(BuildContext context, Map assistante) async { + // TODO: Implémenter l'édition + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Fonctionnalité de modification à implémenter'), + backgroundColor: Colors.orange, + ), + ); + } + + static Future _deleteAssistante(BuildContext context, Map assistante) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text( + 'Êtes-vous sûr de vouloir supprimer le compte de ${assistante['firstName']} ${assistante['lastName']} ?\n\n' + 'Cette action supprimera également tous les contrats et données associés.' ), - SizedBox( - width: 200, - child: TextField( - decoration: const InputDecoration( - labelText: "Capacité minimum", - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - onChanged: (value) { - // TODO: Ajouter logique de filtrage par capacité - }, + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Annuler'), ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Supprimer'), + ), + ], + ), + ); + + if (confirmed == true) { + try { + final userService = UserService(); + final success = await userService.deleteUser(assistante['id']); + + if (success && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${assistante['firstName']} ${assistante['lastName']} supprimé avec succès'), + backgroundColor: Colors.green, + ), + ); + // Le widget se rechargera automatiquement via le système de state + } else { + throw Exception('Erreur lors de la suppression'); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + } + + static Future _showZone(BuildContext context, Map assistante) async { + final zone = assistante['zone'] ?? assistante['ville'] ?? 'Non spécifiée'; + final adresse = assistante['adresse'] ?? assistante['address'] ?? ''; + final codePostal = assistante['codePostal'] ?? assistante['zipCode'] ?? ''; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Zone d\'intervention - ${assistante['firstName']} ${assistante['lastName']}'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (zone.isNotEmpty) Text('Zone: $zone', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + if (adresse.isNotEmpty) Text('Adresse: $adresse'), + if (codePostal.isNotEmpty) Text('Code postal: $codePostal'), + const SizedBox(height: 16), + Text('Capacité d\'accueil: ${assistante['capacite'] ?? assistante['capaciteAccueil'] ?? 'N/A'} enfants'), + Text('N° Agrément: ${assistante['numeroAgrement'] ?? assistante['agrement'] ?? 'N/A'}'), + ], ), - ], + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + TextButton( + onPressed: () { + // TODO: Ouvrir dans Maps + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Intégration Maps à implémenter'), + ), + ); + }, + child: const Text('Voir sur la carte'), + ), + ], + ), ); } } + +// return Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // 🔎 Zone de filtre +// _buildFilterSection(), + +// const SizedBox(height: 16), + +// // 📋 Liste des assistantes +// ListView.builder( +// shrinkWrap: true, +// physics: const NeverScrollableScrollPhysics(), +// itemCount: assistantes.length, +// itemBuilder: (context, index) { +// final assistante = assistantes[index]; +// return Card( +// margin: const EdgeInsets.symmetric(vertical: 8), +// child: ListTile( +// leading: const Icon(Icons.face), +// title: Text(assistante['nom'].toString()), +// subtitle: Text( +// "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"), +// trailing: Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// IconButton( +// icon: const Icon(Icons.edit), +// onPressed: () { +// // TODO: Ajouter modification +// }, +// ), +// IconButton( +// icon: const Icon(Icons.delete), +// onPressed: () { +// // TODO: Ajouter suppression +// }, +// ), +// ], +// ), +// ), +// ); +// }, +// ), +// ], +// ), +// ); +// } + +// Widget _buildFilterSection() { +// return Wrap( +// spacing: 16, +// runSpacing: 8, +// children: [ +// SizedBox( +// width: 200, +// child: TextField( +// decoration: const InputDecoration( +// labelText: "Zone géographique", +// border: OutlineInputBorder(), +// ), +// onChanged: (value) { +// // TODO: Ajouter logique de filtrage par zone +// }, +// ), +// ), +// SizedBox( +// width: 200, +// child: TextField( +// decoration: const InputDecoration( +// labelText: "Capacité minimum", +// border: OutlineInputBorder(), +// ), +// keyboardType: TextInputType.number, +// onChanged: (value) { +// // TODO: Ajouter logique de filtrage par capacité +// }, +// ), +// ), +// ], +// ); +// } +// } diff --git a/frontend/lib/widgets/admin/base_user_management.dart b/frontend/lib/widgets/admin/base_user_management.dart new file mode 100644 index 0000000..3567dbf --- /dev/null +++ b/frontend/lib/widgets/admin/base_user_management.dart @@ -0,0 +1,424 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/services/user_service.dart'; + +/// Configuration pour personnaliser l'affichage des utilisateurs +class UserDisplayConfig { + final String title; + final String role; + final IconData defaultIcon; + final List filterFields; + final List actions; + final String Function(Map) getSubtitle; + final String Function(Map) getDisplayName; + + const UserDisplayConfig({ + required this.title, + required this.role, + required this.defaultIcon, + required this.filterFields, + required this.actions, + required this.getSubtitle, + required this.getDisplayName, + }); +} + +/// Configuration d'un champ de filtre +class FilterField { + final String label; + final String hint; + final FilterType type; + final List? options; + final bool Function(Map, String) filter; + + const FilterField({ + required this.label, + required this.hint, + required this.type, + required this.filter, + this.options, + }); +} + +enum FilterType { text, dropdown, number } + +/// Configuration d'une action sur un utilisateur +class UserAction { + final IconData icon; + final Color color; + final String tooltip; + final Future Function(BuildContext, Map) onPressed; + + const UserAction({ + required this.icon, + required this.color, + required this.tooltip, + required this.onPressed, + }); +} + +/// Widget de gestion d'utilisateurs réutilisable +class BaseUserManagementWidget extends StatefulWidget { + final UserDisplayConfig config; + + const BaseUserManagementWidget({ + super.key, + required this.config, + }); + + @override + State createState() => + _BaseUserManagementWidgetState(); +} + +class _BaseUserManagementWidgetState extends State { + final UserService _userService = UserService(); + final Map _filterControllers = {}; + + List> _allUsers = []; + List> _filteredUsers = []; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _initializeFilters(); + _loadUsers(); + } + + void _initializeFilters() { + for (final field in widget.config.filterFields) { + _filterControllers[field.label] = TextEditingController(); + } + } + + @override + void dispose() { + for (final controller in _filterControllers.values) { + controller.dispose(); + } + super.dispose(); + } + + Future _loadUsers() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final users = await _userService.getUsersByRole(widget.config.role); + setState(() { + _allUsers = users; + _filteredUsers = users; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _applyFilters() { + setState(() { + _filteredUsers = _allUsers.where((user) { + return widget.config.filterFields.every((field) { + final controller = _filterControllers[field.label]; + if (controller == null || controller.text.isEmpty) return true; + return field.filter(user, controller.text); + }); + }).toList(); + }); + } + + String _getStatusDisplay(Map user) { + final status = user['statut']; + if (status == null) return 'Non défini'; + + switch (status.toString().toLowerCase()) { + case 'actif': + return 'Actif'; + case 'en attente': + return 'En attente'; + case 'inactif': + return 'Inactif'; + case 'deleted': + return 'Supprimé'; + default: + return status.toString(); + } + } + + Color _getStatusColor(Map user) { + final status = user['statut']?.toString().toLowerCase(); + switch (status) { + case 'actif': + return Colors.green; + case 'en attente': + return Colors.orange; + case 'inactif': + return Colors.grey; + case 'supprimé': + return Colors.red; + default: + return Colors.grey; + } + } + + void _showUserDetails(Map user) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(widget.config.getDisplayName(user)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Email: ${user['email']}'), + Text('Rôle: ${user['role']}'), + Text('Statut: ${_getStatusDisplay(user)}'), + Text('ID: ${user['id']}'), + if (user['createdAt'] != null) + Text( + 'Créé le: ${DateTime.parse(user['createdAt']).toLocal().toString().split(' ')[0]}'), + // Affichage des champs spécifiques selon le type d'utilisateur + ...user.entries + .where((e) => ![ + 'id', + 'email', + 'role', + 'status', + 'createdAt', + 'updatedAt', + 'firstName', + 'lastName' + ].contains(e.key)) + .map((e) => Text('${e.key}: ${e.value}')) + .toList(), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${widget.config.title} (${_filteredUsers.length})', + style: Theme.of(context).textTheme.headlineSmall, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadUsers, + tooltip: 'Actualiser', + ), + ], + ), + const SizedBox(height: 16), + _buildFilterSection(), + const SizedBox(height: 16), + Expanded( + child: _buildUsersList(), + ), + ], + ), + ); + } + + Widget _buildFilterSection() { + return Wrap( + spacing: 16, + runSpacing: 8, + children: widget.config.filterFields.map((field) { + final controller = _filterControllers[field.label]!; + + switch (field.type) { + case FilterType.text: + case FilterType.number: + return SizedBox( + width: 250, + child: TextField( + controller: controller, + keyboardType: field.type == FilterType.number + ? TextInputType.number + : TextInputType.text, + decoration: InputDecoration( + labelText: field.label, + hintText: field.hint, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.search), + ), + onChanged: (value) => _applyFilters(), + ), + ); + + case FilterType.dropdown: + return SizedBox( + width: 200, + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem(value: '', child: Text("Tous")), + ...?field.options?.map((option) => + DropdownMenuItem(value: option, child: Text(option))), + ], + onChanged: (value) { + controller.text = value ?? ''; + _applyFilters(); + }, + ), + ); + } + }).toList(), + ); + } + + Widget _buildUsersList() { + if (_isLoading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement...'), + ], + ), + ); + } + + if (_error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red[300]), + const SizedBox(height: 16), + Text( + 'Erreur de chargement', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + _error!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadUsers, + child: const Text('Réessayer'), + ), + ], + ), + ); + } + + if (_filteredUsers.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.people_outline, size: 48, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + _allUsers.isEmpty ? 'Aucun utilisateur trouvé' : 'Aucun résultat', + style: Theme.of(context).textTheme.titleLarge, + ), + if (_allUsers.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text('Essayez de modifier vos critères de recherche'), + ], + ], + ), + ); + } + + return ListView.builder( + itemCount: _filteredUsers.length, + itemBuilder: (context, index) { + final user = _filteredUsers[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getStatusColor(user).withOpacity(0.2), + child: Icon( + widget.config.defaultIcon, + color: _getStatusColor(user), + ), + ), + title: Text(widget.config.getDisplayName(user)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.config.getSubtitle(user)), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _getStatusColor(user).withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _getStatusColor(user)), + ), + child: Text( + _getStatusDisplay(user), + style: TextStyle( + color: _getStatusColor(user), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility, color: Colors.blue), + tooltip: "Voir détails", + onPressed: () => _showUserDetails(user), + ), + ...widget.config.actions + .map( + (action) => IconButton( + icon: Icon(action.icon, color: action.color), + tooltip: action.tooltip, + onPressed: () => action.onPressed(context, user), + ), + ) + .toList(), + ], + ), + ), + ); + }, + ); + } +}