From b2d6414fab92a1f8d75f8f5bcda48edb17f8b42a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 18 Feb 2026 11:07:49 +0100 Subject: [PATCH] =?UTF-8?q?refactor(#93):=20homog=C3=A9n=C3=A9iser=20la=20?= =?UTF-8?q?pr=C3=A9sentation=20des=20onglets=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uniformise les 4 onglets de gestion admin avec des composants UI partagés (header, états de liste, carte utilisateur) pour garantir une expérience cohérente sans changement backend. Co-authored-by: Cursor --- .../admin/admin_management_widget.dart | 109 ++++------ ...sistante_maternelle_management_widget.dart | 115 +++++----- .../admin/common/admin_list_header.dart | 58 +++++ .../admin/common/admin_list_state.dart | 49 +++++ .../widgets/admin/common/admin_user_card.dart | 40 ++++ .../lib/widgets/admin/gestionnaire_card.dart | 75 ------- .../admin/parent_managmant_widget.dart | 198 ++++++++---------- 7 files changed, 334 insertions(+), 310 deletions(-) create mode 100644 frontend/lib/widgets/admin/common/admin_list_header.dart create mode 100644 frontend/lib/widgets/admin/common/admin_list_state.dart create mode 100644 frontend/lib/widgets/admin/common/admin_user_card.dart delete mode 100644 frontend/lib/widgets/admin/gestionnaire_card.dart diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart index 58e5eeb..2b1efd4 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,6 +1,9 @@ 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_header.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'; class AdminManagementWidget extends StatefulWidget { const AdminManagementWidget({super.key}); @@ -69,71 +72,51 @@ class _AdminManagementWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - Expanded( - child: TextField( - controller: _searchController, - decoration: const InputDecoration( - hintText: "Rechercher un administrateur...", - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 16), - ElevatedButton.icon( - onPressed: () { - // TODO: Créer admin - }, - icon: const Icon(Icons.add), - label: const Text("Créer un admin"), - ), - ], + AdminListHeader( + searchController: _searchController, + searchHint: 'Rechercher un administrateur...', + actionLabel: 'Créer un admin', + onActionPressed: () { + // TODO: Créer admin + }, ), - 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 (_filteredAdmins.isEmpty) - const Center(child: Text("Aucun administrateur trouvé.")) - else - Expanded( - child: ListView.builder( - itemCount: _filteredAdmins.length, - itemBuilder: (context, index) { - final user = _filteredAdmins[index]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: CircleAvatar( - child: Text(user.fullName.isNotEmpty - ? user.fullName[0].toUpperCase() - : 'A'), - ), - title: Text(user.fullName.isNotEmpty - ? user.fullName - : 'Sans nom'), - subtitle: Text(user.email), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () {}, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () {}, - ), - ], - ), + const SizedBox(height: 16), + 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 + }, ), - ); - }, - ), - ) + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Supprimer', + onPressed: () { + // TODO: Supprimer admin + }, + ), + ], + ); + }, + ), + ), ], ), ); diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart index c8e1a19..c9aa010 100644 --- a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -1,6 +1,9 @@ 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_list_header.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'; class AssistanteMaternelleManagementWidget extends StatefulWidget { const AssistanteMaternelleManagementWidget({super.key}); @@ -79,90 +82,68 @@ class _AssistanteMaternelleManagementWidgetState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 🔎 Zone de filtre - _buildFilterSection(), - + AdminListHeader( + searchController: _zoneController, + searchHint: 'Rechercher une zone géographique...', + filters: _buildFilters(), + ), const SizedBox(height: 16), - - // 📋 Liste des assistantes - if (_isLoading) - const Center(child: CircularProgressIndicator()) - else if (_error != null) - Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) - else if (_filteredAssistantes.isEmpty) - const Center(child: Text("Aucune assistante maternelle trouvée.")) - else - Expanded( - child: ListView.builder( - itemCount: _filteredAssistantes.length, - itemBuilder: (context, index) { - final assistante = _filteredAssistantes[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: CircleAvatar( - backgroundImage: assistante.user.photoUrl != null - ? NetworkImage(assistante.user.photoUrl!) - : null, - child: assistante.user.photoUrl == null - ? const Icon(Icons.face) - : null, - ), - title: Text(assistante.user.fullName.isNotEmpty - ? assistante.user.fullName - : 'Sans nom'), - subtitle: Text( - "N° Agrément : ${assistante.approvalNumber ?? 'N/A'}\nZone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}"), - 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 - }, - ), - ], - ), + 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, + 'N° Agrément : ${assistante.approvalNumber ?? 'N/A'}', + 'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}', + ], + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + // TODO: Ajouter modification + }, ), - ); - }, - ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Supprimer', + onPressed: () { + // TODO: Ajouter suppression + }, + ), + ], + ); + }, ), + ), ], ), ); } - Widget _buildFilterSection() { + Widget _buildFilters() { return Wrap( spacing: 16, runSpacing: 8, children: [ SizedBox( - width: 200, - child: TextField( - controller: _zoneController, - decoration: const InputDecoration( - labelText: "Zone géographique", - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.location_on), - ), - ), - ), - SizedBox( - width: 200, + width: 240, child: TextField( controller: _capacityController, decoration: const InputDecoration( - labelText: "Capacité minimum", + labelText: 'Capacité minimum', border: OutlineInputBorder(), + isDense: true, ), keyboardType: TextInputType.number, ), diff --git a/frontend/lib/widgets/admin/common/admin_list_header.dart b/frontend/lib/widgets/admin/common/admin_list_header.dart new file mode 100644 index 0000000..185c9e2 --- /dev/null +++ b/frontend/lib/widgets/admin/common/admin_list_header.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class AdminListHeader extends StatelessWidget { + final TextEditingController searchController; + final String searchHint; + final String? actionLabel; + final VoidCallback? onActionPressed; + final Widget? filters; + + const AdminListHeader({ + super.key, + required this.searchController, + required this.searchHint, + this.actionLabel, + this.onActionPressed, + this.filters, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: searchHint, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + ), + ), + ), + if (actionLabel != null && onActionPressed != null) ...[ + const SizedBox(width: 16), + ElevatedButton.icon( + onPressed: onActionPressed, + icon: const Icon(Icons.add), + label: Text(actionLabel!), + ), + ], + ], + ), + if (filters != null) ...[ + const SizedBox(height: 12), + filters!, + ], + ], + ); + } +} diff --git a/frontend/lib/widgets/admin/common/admin_list_state.dart b/frontend/lib/widgets/admin/common/admin_list_state.dart new file mode 100644 index 0000000..41b497c --- /dev/null +++ b/frontend/lib/widgets/admin/common/admin_list_state.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class AdminListState extends StatelessWidget { + final bool isLoading; + final String? error; + final bool isEmpty; + final String emptyMessage; + final Widget list; + + const AdminListState({ + super.key, + required this.isLoading, + required this.error, + required this.isEmpty, + required this.emptyMessage, + required this.list, + }); + + @override + Widget build(BuildContext context) { + if (isLoading) { + return const Expanded( + child: Center(child: CircularProgressIndicator()), + ); + } + + if (error != null) { + return Expanded( + child: Center( + child: Text( + 'Erreur: $error', + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ), + ); + } + + if (isEmpty) { + return Expanded( + child: Center( + child: Text(emptyMessage), + ), + ); + } + + return Expanded(child: list); + } +} diff --git a/frontend/lib/widgets/admin/common/admin_user_card.dart b/frontend/lib/widgets/admin/common/admin_user_card.dart new file mode 100644 index 0000000..d9fe1bc --- /dev/null +++ b/frontend/lib/widgets/admin/common/admin_user_card.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class AdminUserCard extends StatelessWidget { + final String title; + final List subtitleLines; + final String? avatarUrl; + final IconData fallbackIcon; + final List actions; + + const AdminUserCard({ + super.key, + required this.title, + required this.subtitleLines, + this.avatarUrl, + this.fallbackIcon = Icons.person, + this.actions = const [], + }); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, + child: avatarUrl == null ? Icon(fallbackIcon) : null, + ), + title: Text(title.isNotEmpty ? title : 'Sans nom'), + subtitle: Text(subtitleLines.join('\n')), + isThreeLine: subtitleLines.length > 1, + trailing: actions.isEmpty + ? null + : Row( + mainAxisSize: MainAxisSize.min, + children: actions, + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/gestionnaire_card.dart b/frontend/lib/widgets/admin/gestionnaire_card.dart deleted file mode 100644 index 5d80255..0000000 --- a/frontend/lib/widgets/admin/gestionnaire_card.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -class GestionnaireCard extends StatelessWidget { - final String name; - final String email; - - const GestionnaireCard({ - Key? key, - required this.name, - required this.email, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 🔹 Infos principales - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), - Text(email, style: const TextStyle(color: Colors.grey)), - ], - ), - const SizedBox(height: 12), - - // 🔹 Attribution à des RPE (dropdown fictif ici) - Row( - children: [ - const Text("RPE attribué : "), - const SizedBox(width: 8), - DropdownButton( - value: "RPE 1", - items: const [ - DropdownMenuItem(value: "RPE 1", child: Text("RPE 1")), - DropdownMenuItem(value: "RPE 2", child: Text("RPE 2")), - DropdownMenuItem(value: "RPE 3", child: Text("RPE 3")), - ], - onChanged: (value) {}, - ), - ], - ), - const SizedBox(height: 12), - - // 🔹 Boutons d'action - Row( - children: [ - TextButton.icon( - onPressed: () { - // Réinitialisation mot de passe - }, - icon: const Icon(Icons.lock_reset), - label: const Text("Réinitialiser MDP"), - ), - const SizedBox(width: 12), - TextButton.icon( - onPressed: () { - // Suppression du compte - }, - icon: const Icon(Icons.delete, color: Colors.red), - label: const Text("Supprimer", style: TextStyle(color: Colors.red)), - ), - ], - ) - ], - ), - ), - ); - } -} diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index 1764056..cac73c8 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -1,6 +1,9 @@ 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_list_header.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'; class ParentManagementWidget extends StatefulWidget { const ParentManagementWidget({super.key}); @@ -59,14 +62,9 @@ class _ParentManagementWidgetState extends State { _filteredParents = _parents.where((p) { final matchesName = p.user.fullName.toLowerCase().contains(query) || p.user.email.toLowerCase().contains(query); - final matchesStatus = _selectedStatus == null || - _selectedStatus == 'Tous' || - (p.user.statut?.toLowerCase() == _selectedStatus?.toLowerCase()); - - // Mapping simple pour le statut affiché vs backend - // Backend: en_attente, actif, suspendu - // Dropdown: En attente, Actif, Suspendu - + final matchesStatus = + _selectedStatus == null || p.user.statut == _selectedStatus; + return matchesName && matchesStatus; }).toList(); }); @@ -79,113 +77,103 @@ class _ParentManagementWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSearchSection(), + AdminListHeader( + searchController: _searchController, + searchHint: 'Rechercher un parent...', + filters: _buildFilters(), + ), const SizedBox(height: 16), - if (_isLoading) - const Center(child: CircularProgressIndicator()) - else if (_error != null) - Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) - else if (_filteredParents.isEmpty) - const Center(child: Text("Aucun parent trouvé.")) - else - Expanded( - child: ListView.builder( - itemCount: _filteredParents.length, - itemBuilder: (context, index) { - final parent = _filteredParents[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: CircleAvatar( - backgroundImage: parent.user.photoUrl != null - ? NetworkImage(parent.user.photoUrl!) - : null, - child: parent.user.photoUrl == null - ? const Icon(Icons.person) - : null, - ), - title: Text(parent.user.fullName.isNotEmpty - ? parent.user.fullName - : 'Sans nom'), - subtitle: Text( - "${parent.user.email}\nStatut : ${parent.user.statut ?? 'Inconnu'} | Enfants : ${parent.childrenCount}", - ), - isThreeLine: true, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.visibility), - tooltip: "Voir dossier", - onPressed: () { - // TODO: Voir le statut du dossier - }, - ), - IconButton( - icon: const Icon(Icons.edit), - tooltip: "Modifier", - onPressed: () { - // TODO: Modifier parent - }, - ), - IconButton( - icon: const Icon(Icons.delete), - tooltip: "Supprimer", - onPressed: () { - // TODO: Supprimer compte - }, - ), - ], - ), + 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.visibility), + tooltip: 'Voir dossier', + onPressed: () { + // TODO: Voir le statut du dossier + }, ), - ); - }, - ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Modifier', + onPressed: () { + // TODO: Modifier parent + }, + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Supprimer', + onPressed: () { + // TODO: Supprimer compte + }, + ), + ], + ); + }, ), + ), ], ), ); } - Widget _buildSearchSection() { - return Wrap( - spacing: 16, - runSpacing: 8, - children: [ - SizedBox( - width: 220, - child: TextField( - controller: _searchController, - decoration: const InputDecoration( - labelText: "Nom du parent", - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.search), - ), - ), + Widget _buildFilters() { + return SizedBox( + width: 240, + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Statut', + border: OutlineInputBorder(), + isDense: true, ), - SizedBox( - width: 220, - child: DropdownButtonFormField( - decoration: const InputDecoration( - labelText: "Statut", - border: OutlineInputBorder(), - ), - value: _selectedStatus, - items: const [ - DropdownMenuItem(value: null, child: Text("Tous")), - DropdownMenuItem(value: "actif", child: Text("Actif")), - DropdownMenuItem(value: "en_attente", child: Text("En attente")), - DropdownMenuItem(value: "suspendu", child: Text("Suspendu")), - ], - onChanged: (value) { - setState(() { - _selectedStatus = value; - _filter(); - }); - }, + value: _selectedStatus, + items: const [ + DropdownMenuItem(value: null, child: Text('Tous')), + DropdownMenuItem(value: 'actif', child: Text('Actif')), + DropdownMenuItem( + value: 'en_attente', + child: Text('En attente'), ), - ), - ], + DropdownMenuItem( + value: 'suspendu', + child: Text('Suspendu'), + ), + ], + onChanged: (value) { + setState(() { + _selectedStatus = value; + }); + _filter(); + }, + ), ); } + + String _displayStatus(String? status) { + switch (status) { + case 'actif': + return 'Actif'; + case 'en_attente': + return 'En attente'; + case 'suspendu': + return 'Suspendu'; + default: + return 'Inconnu'; + } + } }