From bc8362bdb7ab3e22e13f33e2c25a0cd3e5819a9d Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Mon, 23 Feb 2026 17:59:03 +0100 Subject: [PATCH] =?UTF-8?q?refactor(#93):=20extraire=20un=20widget=20UserL?= =?UTF-8?q?ist=20r=C3=A9utilisable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralise le pattern d'affichage des listes utilisateurs pour garantir une UI homogène entre gestionnaires, parents, assistantes maternelles et administrateurs. Co-authored-by: Cursor --- .../admin_dashboardScreen.dart | 35 +--- .../admin/admin_management_widget.dart | 60 +++--- ...sistante_maternelle_management_widget.dart | 62 +++--- .../lib/widgets/admin/common/user_list.dart | 45 +++++ .../admin/gestionnaire_management_widget.dart | 152 +++++---------- .../admin/parent_managmant_widget.dart | 60 +++--- .../widgets/admin/user_management_panel.dart | 176 ++++++++++++++++++ 7 files changed, 352 insertions(+), 238 deletions(-) create mode 100644 frontend/lib/widgets/admin/common/user_list.dart create mode 100644 frontend/lib/widgets/admin/user_management_panel.dart diff --git a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart index 12395f3..892eb8b 100644 --- a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/services/configuration_service.dart'; -import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; -import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; -import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; -import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/parametres_panel.dart'; +import 'package:p_tits_pas/widgets/admin/user_management_panel.dart'; import 'package:p_tits_pas/widgets/app_footer.dart'; import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; @@ -18,7 +15,6 @@ class AdminDashboardScreen extends StatefulWidget { class _AdminDashboardScreenState extends State { bool? _setupCompleted; int mainTabIndex = 0; - int subIndex = 0; int settingsSubIndex = 0; @override @@ -27,6 +23,11 @@ class _AdminDashboardScreenState extends State { _loadSetupStatus(); } + @override + void dispose() { + super.dispose(); + } + Future _loadSetupStatus() async { try { final completed = await ConfigurationService.getSetupStatus(); @@ -51,12 +52,6 @@ class _AdminDashboardScreenState extends State { }); } - void onSubTabChange(int index) { - setState(() { - subIndex = index; - }); - } - void onSettingsSubTabChange(int index) { setState(() { settingsSubIndex = index; @@ -89,10 +84,7 @@ class _AdminDashboardScreenState extends State { body: Column( children: [ if (mainTabIndex == 0) - DashboardUserManagementSubBar( - selectedSubIndex: subIndex, - onSubTabChange: onSubTabChange, - ) + const SizedBox.shrink() else DashboardSettingsSubBar( selectedSubIndex: settingsSubIndex, @@ -114,17 +106,6 @@ class _AdminDashboardScreenState extends State { selectedSettingsTabIndex: settingsSubIndex, ); } - switch (subIndex) { - case 0: - return const GestionnaireManagementWidget(); - case 1: - return const ParentManagementWidget(); - case 2: - return const AssistanteMaternelleManagementWidget(); - case 3: - return const AdminManagementWidget(); - default: - return const Center(child: Text('Page non trouvée')); - } + return const AdminUserManagementPanel(); } } diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart index 6a69695..666a64e 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/user_service.dart'; -import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; +import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; class AdminManagementWidget extends StatefulWidget { final String searchQuery; @@ -60,42 +60,32 @@ class _AdminManagementWidgetState extends State { return name.contains(query) || email.contains(query); }).toList(); - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AdminListState( - isLoading: _isLoading, - error: _error, - isEmpty: filteredAdmins.isEmpty, - emptyMessage: 'Aucun administrateur trouvé.', - list: ListView.builder( - itemCount: filteredAdmins.length, - itemBuilder: (context, index) { - final user = filteredAdmins[index]; - return AdminUserCard( - title: user.fullName, - subtitleLines: [ - user.email, - 'Rôle : ${user.role}', - ], - avatarUrl: user.photoUrl, - actions: [ - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Modifier', - onPressed: () { - // TODO: Modifier admin - }, - ), - ], - ); + return UserList( + isLoading: _isLoading, + error: _error, + isEmpty: filteredAdmins.isEmpty, + emptyMessage: 'Aucun administrateur trouvé.', + itemCount: filteredAdmins.length, + itemBuilder: (context, index) { + final user = filteredAdmins[index]; + return AdminUserCard( + title: user.fullName, + subtitleLines: [ + user.email, + 'Rôle : ${user.role}', + ], + avatarUrl: user.photoUrl, + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + // TODO: Modifier admin }, ), - ), - ], - ), + ], + ); + }, ); } } diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart index e91d570..d655055 100644 --- a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/assistante_maternelle_model.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart'; -import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; +import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; class AssistanteMaternelleManagementWidget extends StatefulWidget { final String searchQuery; @@ -68,43 +68,33 @@ class _AssistanteMaternelleManagementWidgetState return matchesName && matchesCapacity; }).toList(); - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AdminListState( - isLoading: _isLoading, - error: _error, - isEmpty: filteredAssistantes.isEmpty, - emptyMessage: 'Aucune assistante maternelle trouvée.', - list: ListView.builder( - itemCount: filteredAssistantes.length, - itemBuilder: (context, index) { - final assistante = filteredAssistantes[index]; - return AdminUserCard( - title: assistante.user.fullName, - avatarUrl: assistante.user.photoUrl, - fallbackIcon: Icons.face, - subtitleLines: [ - assistante.user.email, - 'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}', - ], - actions: [ - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Modifier', - onPressed: () { - _openAssistanteDetails(assistante); - }, - ), - ], - ); + return UserList( + isLoading: _isLoading, + error: _error, + isEmpty: filteredAssistantes.isEmpty, + emptyMessage: 'Aucune assistante maternelle trouvée.', + itemCount: filteredAssistantes.length, + itemBuilder: (context, index) { + final assistante = filteredAssistantes[index]; + return AdminUserCard( + title: assistante.user.fullName, + avatarUrl: assistante.user.photoUrl, + fallbackIcon: Icons.face, + subtitleLines: [ + assistante.user.email, + 'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}', + ], + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + _openAssistanteDetails(assistante); }, ), - ), - ], - ), + ], + ); + }, ); } diff --git a/frontend/lib/widgets/admin/common/user_list.dart b/frontend/lib/widgets/admin/common/user_list.dart new file mode 100644 index 0000000..1df400c --- /dev/null +++ b/frontend/lib/widgets/admin/common/user_list.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart'; + +class UserList extends StatelessWidget { + final bool isLoading; + final String? error; + final bool isEmpty; + final String emptyMessage; + final int itemCount; + final Widget Function(BuildContext context, int index) itemBuilder; + final EdgeInsetsGeometry padding; + + const UserList({ + super.key, + required this.isLoading, + required this.error, + required this.isEmpty, + required this.emptyMessage, + required this.itemCount, + required this.itemBuilder, + this.padding = const EdgeInsets.all(16), + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AdminListState( + isLoading: isLoading, + error: error, + isEmpty: isEmpty, + emptyMessage: emptyMessage, + list: ListView.builder( + itemCount: itemCount, + itemBuilder: itemBuilder, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart index b9c9651..80d5d91 100644 --- a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -3,9 +3,16 @@ import 'package:p_tits_pas/models/relais_model.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/relais_service.dart'; import 'package:p_tits_pas/services/user_service.dart'; +import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; +import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; class GestionnaireManagementWidget extends StatefulWidget { - const GestionnaireManagementWidget({Key? key}) : super(key: key); + final String searchQuery; + + const GestionnaireManagementWidget({ + Key? key, + required this.searchQuery, + }) : super(key: key); @override State createState() => @@ -18,21 +25,15 @@ class _GestionnaireManagementWidgetState String? _error; List _gestionnaires = []; List _relais = []; - List _filteredGestionnaires = []; - final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _loadGestionnaires(); - _searchController.addListener(_onSearchChanged); } @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } + void dispose() => super.dispose(); Future _loadGestionnaires() async { setState(() { @@ -52,7 +53,6 @@ class _GestionnaireManagementWidgetState setState(() { _gestionnaires = gestionnaires; _relais = relais; - _filteredGestionnaires = gestionnaires; _isLoading = false; }); } catch (e) { @@ -64,17 +64,6 @@ class _GestionnaireManagementWidgetState } } - void _onSearchChanged() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredGestionnaires = _gestionnaires.where((u) { - final name = u.fullName.toLowerCase(); - final email = u.email.toLowerCase(); - return name.contains(query) || email.contains(query); - }).toList(); - }); - } - Future _openRelaisAssignmentDialog(AppUser user) async { String? selectedRelaisId = user.relaisId; final saved = await showDialog( @@ -155,92 +144,45 @@ class _GestionnaireManagementWidgetState @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: TextField( - controller: _searchController, - decoration: const InputDecoration( - hintText: 'Rechercher un gestionnaire...', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 16), - ElevatedButton.icon( - onPressed: () { - // TODO: Rediriger vers la page de creation. - }, - icon: const Icon(Icons.add), - label: const Text('Creer un gestionnaire'), - ), - ], - ), - const SizedBox(height: 24), - if (_isLoading) - const Center(child: CircularProgressIndicator()) - else if (_error != null) - Center( - child: Text( - 'Erreur: $_error', - style: const TextStyle(color: Colors.red), - ), - ) - else if (_filteredGestionnaires.isEmpty) - const Center(child: Text('Aucun gestionnaire trouve.')) - else - Expanded( - child: ListView.builder( - itemCount: _filteredGestionnaires.length, - itemBuilder: (context, index) { - final user = _filteredGestionnaires[index]; - return Card( - margin: const EdgeInsets.only(bottom: 10), - child: ListTile( - leading: CircleAvatar( - child: Text( - (user.prenom?.isNotEmpty == true - ? user.prenom!.substring(0, 1) - : user.email.substring(0, 1)) - .toUpperCase(), - ), - ), - title: Text( - user.fullName.isNotEmpty ? user.fullName : 'Sans nom', - ), - subtitle: Text( - '${user.email} • Relais: ${user.relaisNom ?? 'Non rattache'}', - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.location_city_outlined), - tooltip: 'Rattacher un relais', - onPressed: () => _openRelaisAssignmentDialog(user), - ), - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Modifier', - onPressed: () { - // TODO: Modifier gestionnaire. - }, - ), - ], - ), - ), - ); - }, - ), + final query = widget.searchQuery.toLowerCase(); + final filteredGestionnaires = _gestionnaires.where((u) { + final name = u.fullName.toLowerCase(); + final email = u.email.toLowerCase(); + return name.contains(query) || email.contains(query); + }).toList(); + + return UserList( + isLoading: _isLoading, + error: _error, + isEmpty: filteredGestionnaires.isEmpty, + emptyMessage: 'Aucun gestionnaire trouvé.', + itemCount: filteredGestionnaires.length, + itemBuilder: (context, index) { + final user = filteredGestionnaires[index]; + return AdminUserCard( + title: user.fullName, + avatarUrl: user.photoUrl, + subtitleLines: [ + user.email, + 'Statut : ${user.statut ?? 'Inconnu'}', + 'Relais : ${user.relaisNom ?? 'Non rattaché'}', + ], + actions: [ + IconButton( + icon: const Icon(Icons.location_city_outlined), + tooltip: 'Rattacher un relais', + onPressed: () => _openRelaisAssignmentDialog(user), ), - ], - ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + // TODO: Modifier gestionnaire. + }, + ), + ], + ); + }, ); } } diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index c6d302f..cfa8637 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/parent_model.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart'; -import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; +import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; class ParentManagementWidget extends StatefulWidget { final String searchQuery; @@ -65,42 +65,32 @@ class _ParentManagementWidgetState extends State { return matchesName && matchesStatus; }).toList(); - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AdminListState( - isLoading: _isLoading, - error: _error, - isEmpty: filteredParents.isEmpty, - emptyMessage: 'Aucun parent trouvé.', - list: ListView.builder( - itemCount: filteredParents.length, - itemBuilder: (context, index) { - final parent = filteredParents[index]; - return AdminUserCard( - title: parent.user.fullName, - avatarUrl: parent.user.photoUrl, - subtitleLines: [ - parent.user.email, - 'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${parent.childrenCount}', - ], - actions: [ - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Modifier', - onPressed: () { - _openParentDetails(parent); - }, - ), - ], - ); + return UserList( + isLoading: _isLoading, + error: _error, + isEmpty: filteredParents.isEmpty, + emptyMessage: 'Aucun parent trouvé.', + itemCount: filteredParents.length, + itemBuilder: (context, index) { + final parent = filteredParents[index]; + return AdminUserCard( + title: parent.user.fullName, + avatarUrl: parent.user.photoUrl, + subtitleLines: [ + parent.user.email, + 'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${parent.childrenCount}', + ], + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + _openParentDetails(parent); }, ), - ), - ], - ), + ], + ); + }, ); } diff --git a/frontend/lib/widgets/admin/user_management_panel.dart b/frontend/lib/widgets/admin/user_management_panel.dart new file mode 100644 index 0000000..7f1e698 --- /dev/null +++ b/frontend/lib/widgets/admin/user_management_panel.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.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/dashboard_admin.dart'; +import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; +import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; + +class AdminUserManagementPanel extends StatefulWidget { + const AdminUserManagementPanel({super.key}); + + @override + State createState() => + _AdminUserManagementPanelState(); +} + +class _AdminUserManagementPanelState extends State { + int _subIndex = 0; + final TextEditingController _searchController = TextEditingController(); + final TextEditingController _amCapacityController = TextEditingController(); + String? _parentStatus; + + @override + void initState() { + super.initState(); + _searchController.addListener(_onFilterChanged); + _amCapacityController.addListener(_onFilterChanged); + } + + @override + void dispose() { + _searchController.removeListener(_onFilterChanged); + _amCapacityController.removeListener(_onFilterChanged); + _searchController.dispose(); + _amCapacityController.dispose(); + super.dispose(); + } + + void _onFilterChanged() { + if (!mounted) return; + setState(() {}); + } + + void _onSubTabChange(int index) { + setState(() { + _subIndex = index; + _searchController.clear(); + _parentStatus = null; + _amCapacityController.clear(); + }); + } + + String _searchHintForTab() { + switch (_subIndex) { + case 0: + return 'Rechercher un gestionnaire...'; + case 1: + return 'Rechercher un parent...'; + case 2: + return 'Rechercher une assistante...'; + case 3: + return 'Rechercher un administrateur...'; + default: + return 'Rechercher...'; + } + } + + Widget? _subBarFilterControl() { + if (_subIndex == 1) { + return DropdownButtonHideUnderline( + child: DropdownButton( + value: _parentStatus, + isExpanded: true, + hint: const Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Statut', style: TextStyle(fontSize: 12)), + ), + items: const [ + DropdownMenuItem( + value: null, + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Tous', style: TextStyle(fontSize: 12)), + ), + ), + DropdownMenuItem( + value: 'actif', + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Actif', style: TextStyle(fontSize: 12)), + ), + ), + DropdownMenuItem( + value: 'en_attente', + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text('En attente', style: TextStyle(fontSize: 12)), + ), + ), + DropdownMenuItem( + value: 'suspendu', + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Suspendu', style: TextStyle(fontSize: 12)), + ), + ), + ], + onChanged: (value) { + setState(() { + _parentStatus = value; + }); + }, + ), + ); + } + + if (_subIndex == 2) { + return TextField( + controller: _amCapacityController, + decoration: const InputDecoration( + hintText: 'Capacité min', + hintStyle: TextStyle(fontSize: 12), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + keyboardType: TextInputType.number, + ); + } + return null; + } + + Widget _buildBody() { + switch (_subIndex) { + case 0: + return GestionnaireManagementWidget( + searchQuery: _searchController.text, + ); + case 1: + return ParentManagementWidget( + searchQuery: _searchController.text, + statusFilter: _parentStatus, + ); + case 2: + return AssistanteMaternelleManagementWidget( + searchQuery: _searchController.text, + capacityMin: int.tryParse(_amCapacityController.text), + ); + case 3: + return AdminManagementWidget( + searchQuery: _searchController.text, + ); + default: + return const Center(child: Text('Page non trouvée')); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + DashboardUserManagementSubBar( + selectedSubIndex: _subIndex, + onSubTabChange: _onSubTabChange, + searchController: _searchController, + searchHint: _searchHintForTab(), + filterControl: _subBarFilterControl(), + onAddPressed: () { + // TODO: brancher création selon onglet actif + }, + addLabel: 'Ajouter', + ), + Expanded(child: _buildBody()), + ], + ); + } +}