From b2d6414fab92a1f8d75f8f5bcda48edb17f8b42a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 18 Feb 2026 11:07:49 +0100 Subject: [PATCH 1/5] =?UTF-8?q?refactor(#93):=20homog=C3=A9n=C3=A9iser=20l?= =?UTF-8?q?a=20pr=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'; + } + } } From 5da2ab900597d222bb88210f551365956f3a3af1 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 20 Feb 2026 11:35:42 +0100 Subject: [PATCH 2/5] =?UTF-8?q?feat(#93):=20optimiser=20l=E2=80=99affichag?= =?UTF-8?q?e=20Parents/AM=20avec=20modale=20de=20d=C3=A9tails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intègre un bandeau unique (onglets à gauche, recherche/filtre en pilule, bouton Ajouter à droite) et compacte les cartes Parents/AM avec ouverture d’une modale complète sur Modifier (croix, actions Modifier/Supprimer). Co-authored-by: Cursor --- .../admin/admin_management_widget.dart | 50 ++---- ...sistante_maternelle_management_widget.dart | 139 ++++++++-------- .../admin/common/admin_detail_modal.dart | 138 ++++++++++++++++ .../admin/common/admin_list_header.dart | 58 ------- .../widgets/admin/common/admin_user_card.dart | 78 +++++++-- .../lib/widgets/admin/dashboard_admin.dart | 78 +++++++-- .../admin/parent_managmant_widget.dart | 153 ++++++++---------- 7 files changed, 429 insertions(+), 265 deletions(-) create mode 100644 frontend/lib/widgets/admin/common/admin_detail_modal.dart delete mode 100644 frontend/lib/widgets/admin/common/admin_list_header.dart diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart index 2b1efd4..14ef51c 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -1,12 +1,16 @@ 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}); + final String searchQuery; + + const AdminManagementWidget({ + super.key, + required this.searchQuery, + }); @override State createState() => _AdminManagementWidgetState(); @@ -16,21 +20,15 @@ class _AdminManagementWidgetState extends State { bool _isLoading = false; String? _error; List _admins = []; - List _filteredAdmins = []; - final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _loadAdmins(); - _searchController.addListener(_onSearchChanged); } @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } + void dispose() => super.dispose(); Future _loadAdmins() async { setState(() { @@ -42,7 +40,6 @@ class _AdminManagementWidgetState extends State { if (!mounted) return; setState(() { _admins = list; - _filteredAdmins = list; _isLoading = false; }); } catch (e) { @@ -54,42 +51,29 @@ class _AdminManagementWidgetState extends State { } } - void _onSearchChanged() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredAdmins = _admins.where((u) { - final name = u.fullName.toLowerCase(); - final email = u.email.toLowerCase(); - return name.contains(query) || email.contains(query); - }).toList(); - }); - } - @override Widget build(BuildContext context) { + final query = widget.searchQuery.toLowerCase(); + final filteredAdmins = _admins.where((u) { + final name = u.fullName.toLowerCase(); + final email = u.email.toLowerCase(); + return name.contains(query) || email.contains(query); + }).toList(); + return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - AdminListHeader( - searchController: _searchController, - searchHint: 'Rechercher un administrateur...', - actionLabel: 'Créer un admin', - onActionPressed: () { - // TODO: Créer admin - }, - ), - const SizedBox(height: 16), AdminListState( isLoading: _isLoading, error: _error, - isEmpty: _filteredAdmins.isEmpty, + isEmpty: filteredAdmins.isEmpty, emptyMessage: 'Aucun administrateur trouvé.', list: ListView.builder( - itemCount: _filteredAdmins.length, + itemCount: filteredAdmins.length, itemBuilder: (context, index) { - final user = _filteredAdmins[index]; + final user = filteredAdmins[index]; return AdminUserCard( title: user.fullName, subtitleLines: [ diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart index c9aa010..e91d570 100644 --- a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -1,12 +1,19 @@ 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_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'; class AssistanteMaternelleManagementWidget extends StatefulWidget { - const AssistanteMaternelleManagementWidget({super.key}); + final String searchQuery; + final int? capacityMin; + + const AssistanteMaternelleManagementWidget({ + super.key, + required this.searchQuery, + this.capacityMin, + }); @override State createState() => @@ -18,25 +25,15 @@ class _AssistanteMaternelleManagementWidgetState bool _isLoading = false; String? _error; List _assistantes = []; - List _filteredAssistantes = []; - - final TextEditingController _zoneController = TextEditingController(); - final TextEditingController _capacityController = TextEditingController(); @override void initState() { super.initState(); _loadAssistantes(); - _zoneController.addListener(_filter); - _capacityController.addListener(_filter); } @override - void dispose() { - _zoneController.dispose(); - _capacityController.dispose(); - super.dispose(); - } + void dispose() => super.dispose(); Future _loadAssistantes() async { setState(() { @@ -48,7 +45,6 @@ class _AssistanteMaternelleManagementWidgetState if (!mounted) return; setState(() { _assistantes = list; - _filter(); _isLoading = false; }); } catch (e) { @@ -60,50 +56,38 @@ class _AssistanteMaternelleManagementWidgetState } } - void _filter() { - final zoneQuery = _zoneController.text.toLowerCase(); - final capacityQuery = int.tryParse(_capacityController.text); - - setState(() { - _filteredAssistantes = _assistantes.where((am) { - final matchesZone = zoneQuery.isEmpty || - (am.residenceCity?.toLowerCase().contains(zoneQuery) ?? false); - final matchesCapacity = capacityQuery == null || - (am.maxChildren != null && am.maxChildren! >= capacityQuery); - return matchesZone && matchesCapacity; - }).toList(); - }); - } - @override Widget build(BuildContext context) { + final query = widget.searchQuery.toLowerCase(); + final filteredAssistantes = _assistantes.where((am) { + final matchesName = am.user.fullName.toLowerCase().contains(query) || + am.user.email.toLowerCase().contains(query) || + (am.residenceCity?.toLowerCase().contains(query) ?? false); + final matchesCapacity = widget.capacityMin == null || + (am.maxChildren != null && am.maxChildren! >= widget.capacityMin!); + return matchesName && matchesCapacity; + }).toList(); + return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AdminListHeader( - searchController: _zoneController, - searchHint: 'Rechercher une zone géographique...', - filters: _buildFilters(), - ), - const SizedBox(height: 16), AdminListState( isLoading: _isLoading, error: _error, - isEmpty: _filteredAssistantes.isEmpty, + isEmpty: filteredAssistantes.isEmpty, emptyMessage: 'Aucune assistante maternelle trouvée.', list: ListView.builder( - itemCount: _filteredAssistantes.length, + itemCount: filteredAssistantes.length, itemBuilder: (context, index) { - final assistante = _filteredAssistantes[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: [ @@ -111,14 +95,7 @@ class _AssistanteMaternelleManagementWidgetState icon: const Icon(Icons.edit), tooltip: 'Modifier', onPressed: () { - // TODO: Ajouter modification - }, - ), - IconButton( - icon: const Icon(Icons.delete), - tooltip: 'Supprimer', - onPressed: () { - // TODO: Ajouter suppression + _openAssistanteDetails(assistante); }, ), ], @@ -131,24 +108,58 @@ class _AssistanteMaternelleManagementWidgetState ); } - Widget _buildFilters() { - return Wrap( - spacing: 16, - runSpacing: 8, - children: [ - SizedBox( - width: 240, - child: TextField( - controller: _capacityController, - decoration: const InputDecoration( - labelText: 'Capacité minimum', - border: OutlineInputBorder(), - isDense: true, - ), - keyboardType: TextInputType.number, + void _openAssistanteDetails(AssistanteMaternelleModel assistante) { + showDialog( + context: context, + builder: (context) => AdminDetailModal( + title: assistante.user.fullName.isEmpty + ? 'Assistante maternelle' + : assistante.user.fullName, + subtitle: assistante.user.email, + fields: [ + AdminDetailField(label: 'ID', value: _v(assistante.user.id)), + AdminDetailField( + label: 'Numero agrement', + value: _v(assistante.approvalNumber), ), - ), - ], + AdminDetailField( + label: 'Ville residence', + value: _v(assistante.residenceCity), + ), + AdminDetailField( + label: 'Capacite max', + value: assistante.maxChildren?.toString() ?? '-', + ), + AdminDetailField( + label: 'Places disponibles', + value: assistante.placesAvailable?.toString() ?? '-', + ), + AdminDetailField( + label: 'Telephone', + value: _v(assistante.user.telephone), + ), + AdminDetailField(label: 'Adresse', value: _v(assistante.user.adresse)), + AdminDetailField(label: 'Ville', value: _v(assistante.user.ville)), + AdminDetailField( + label: 'Code postal', + value: _v(assistante.user.codePostal), + ), + ], + onEdit: () { + Navigator.of(context).pop(); + ScaffoldMessenger.of(this.context).showSnackBar( + const SnackBar(content: Text('Action Modifier a implementer')), + ); + }, + onDelete: () { + Navigator.of(context).pop(); + ScaffoldMessenger.of(this.context).showSnackBar( + const SnackBar(content: Text('Action Supprimer a implementer')), + ); + }, + ), ); } + + String _v(String? value) => (value == null || value.isEmpty) ? '-' : value; } diff --git a/frontend/lib/widgets/admin/common/admin_detail_modal.dart b/frontend/lib/widgets/admin/common/admin_detail_modal.dart new file mode 100644 index 0000000..affc0d8 --- /dev/null +++ b/frontend/lib/widgets/admin/common/admin_detail_modal.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; + +class AdminDetailField { + final String label; + final String value; + + const AdminDetailField({ + required this.label, + required this.value, + }); +} + +class AdminDetailModal extends StatelessWidget { + final String title; + final String? subtitle; + final List fields; + final VoidCallback onEdit; + final VoidCallback onDelete; + + const AdminDetailModal({ + super.key, + required this.title, + this.subtitle, + required this.fields, + required this.onEdit, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 620), + child: Padding( + padding: const EdgeInsets.all(18), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + if (subtitle != null && subtitle!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + style: const TextStyle(color: Colors.black54), + ), + ], + ], + ), + ), + IconButton( + tooltip: 'Fermer', + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close), + ), + ], + ), + const SizedBox(height: 8), + const Divider(height: 1), + const SizedBox(height: 12), + Flexible( + child: SingleChildScrollView( + child: Column( + children: fields + .map( + (field) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 180, + child: Text( + field.label, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ), + Expanded( + child: Text( + field.value, + style: const TextStyle(color: Colors.black87), + ), + ), + ], + ), + ), + ) + .toList(), + ), + ), + ), + const SizedBox(height: 14), + Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton.icon( + onPressed: onDelete, + icon: const Icon(Icons.delete_outline), + label: const Text('Supprimer'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red.shade700, + side: BorderSide(color: Colors.red.shade300), + ), + ), + const SizedBox(width: 10), + ElevatedButton.icon( + onPressed: onEdit, + icon: const Icon(Icons.edit), + label: const Text('Modifier'), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/common/admin_list_header.dart b/frontend/lib/widgets/admin/common/admin_list_header.dart deleted file mode 100644 index 185c9e2..0000000 --- a/frontend/lib/widgets/admin/common/admin_list_header.dart +++ /dev/null @@ -1,58 +0,0 @@ -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_user_card.dart b/frontend/lib/widgets/admin/common/admin_user_card.dart index d9fe1bc..8745d4c 100644 --- a/frontend/lib/widgets/admin/common/admin_user_card.dart +++ b/frontend/lib/widgets/admin/common/admin_user_card.dart @@ -20,20 +20,72 @@ class AdminUserCard extends StatelessWidget { 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, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.grey.shade300), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + CircleAvatar( + radius: 14, + backgroundColor: const Color(0xFFEDE5FA), + backgroundImage: + avatarUrl != null ? NetworkImage(avatarUrl!) : null, + child: avatarUrl == null + ? Icon( + fallbackIcon, + size: 16, + color: const Color(0xFF6B3FA0), + ) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title.isNotEmpty ? title : 'Sans nom', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + ...subtitleLines.map( + (line) => Text( + line, + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + ), + ), + ), + ], ), + ), + if (actions.isNotEmpty) + IconTheme( + data: const IconThemeData(size: 18), + child: IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(4), + minimumSize: const Size(28, 28), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: actions, + ), + ), + ), + ], + ), ), ); } diff --git a/frontend/lib/widgets/admin/dashboard_admin.dart b/frontend/lib/widgets/admin/dashboard_admin.dart index 18d16ea..ff1aa3b 100644 --- a/frontend/lib/widgets/admin/dashboard_admin.dart +++ b/frontend/lib/widgets/admin/dashboard_admin.dart @@ -136,39 +136,91 @@ class DashboardAppBarAdmin extends StatelessWidget class DashboardUserManagementSubBar extends StatelessWidget { final int selectedSubIndex; final ValueChanged onSubTabChange; + final TextEditingController searchController; + final String searchHint; + final Widget? filterControl; + final VoidCallback? onAddPressed; + final String addLabel; const DashboardUserManagementSubBar({ Key? key, required this.selectedSubIndex, required this.onSubTabChange, + required this.searchController, + required this.searchHint, + this.filterControl, + this.onAddPressed, + this.addLabel = '+ Ajouter', }) : super(key: key); @override Widget build(BuildContext context) { return Container( - height: 48, + height: 56, decoration: BoxDecoration( color: Colors.grey.shade100, border: Border(bottom: BorderSide(color: Colors.grey.shade300)), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildSubNavItem(context, 'Gestionnaires', 0), - const SizedBox(width: 16), - _buildSubNavItem(context, 'Parents', 1), - const SizedBox(width: 16), - _buildSubNavItem(context, 'Assistantes maternelles', 2), - const SizedBox(width: 16), - _buildSubNavItem(context, 'Administrateurs', 3), + child: Row( + children: [ + _buildSubNavItem(context, 'Gestionnaires', 0), + const SizedBox(width: 12), + _buildSubNavItem(context, 'Parents', 1), + const SizedBox(width: 12), + _buildSubNavItem(context, 'Assistantes maternelles', 2), + const SizedBox(width: 12), + _buildSubNavItem(context, 'Administrateurs', 3), + const SizedBox(width: 36), + _pillField( + width: 320, + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: searchHint, + prefixIcon: const Icon(Icons.search, size: 18), + border: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + ), + ), + ), + if (filterControl != null) ...[ + const SizedBox(width: 12), + _pillField(width: 150, child: filterControl!), ], - ), + const Spacer(), + _buildAddButton(), + ], ), ); } + Widget _pillField({required double width, required Widget child}) { + return Container( + width: width, + height: 34, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.black26), + ), + alignment: Alignment.centerLeft, + child: child, + ); + } + + Widget _buildAddButton() { + return ElevatedButton.icon( + onPressed: onAddPressed, + icon: const Icon(Icons.add), + label: Text(addLabel), + ); + } + Widget _buildSubNavItem(BuildContext context, String title, int index) { final bool isActive = index == selectedSubIndex; return InkWell( diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index cac73c8..c6d302f 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -1,12 +1,19 @@ 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_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'; class ParentManagementWidget extends StatefulWidget { - const ParentManagementWidget({super.key}); + final String searchQuery; + final String? statusFilter; + + const ParentManagementWidget({ + super.key, + required this.searchQuery, + this.statusFilter, + }); @override State createState() => _ParentManagementWidgetState(); @@ -16,23 +23,15 @@ class _ParentManagementWidgetState extends State { bool _isLoading = false; String? _error; List _parents = []; - List _filteredParents = []; - - final TextEditingController _searchController = TextEditingController(); - String? _selectedStatus; @override void initState() { super.initState(); _loadParents(); - _searchController.addListener(_filter); } @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } + void dispose() => super.dispose(); Future _loadParents() async { setState(() { @@ -44,7 +43,6 @@ class _ParentManagementWidgetState extends State { if (!mounted) return; setState(() { _parents = list; - _filter(); // Apply initial filter (if any) _isLoading = false; }); } catch (e) { @@ -56,70 +54,44 @@ class _ParentManagementWidgetState extends State { } } - void _filter() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredParents = _parents.where((p) { - final matchesName = p.user.fullName.toLowerCase().contains(query) || - p.user.email.toLowerCase().contains(query); - final matchesStatus = - _selectedStatus == null || p.user.statut == _selectedStatus; - - return matchesName && matchesStatus; - }).toList(); - }); - } - @override Widget build(BuildContext context) { + final query = widget.searchQuery.toLowerCase(); + final filteredParents = _parents.where((p) { + final matchesName = p.user.fullName.toLowerCase().contains(query) || + p.user.email.toLowerCase().contains(query); + final matchesStatus = + widget.statusFilter == null || p.user.statut == widget.statusFilter; + return matchesName && matchesStatus; + }).toList(); + return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AdminListHeader( - searchController: _searchController, - searchHint: 'Rechercher un parent...', - filters: _buildFilters(), - ), - const SizedBox(height: 16), AdminListState( isLoading: _isLoading, error: _error, - isEmpty: _filteredParents.isEmpty, + isEmpty: filteredParents.isEmpty, emptyMessage: 'Aucun parent trouvé.', list: ListView.builder( - itemCount: _filteredParents.length, + itemCount: filteredParents.length, itemBuilder: (context, index) { - final parent = _filteredParents[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}', + '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 + _openParentDetails(parent); }, ), ], @@ -132,38 +104,6 @@ class _ParentManagementWidgetState extends State { ); } - Widget _buildFilters() { - return SizedBox( - width: 240, - child: DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Statut', - border: OutlineInputBorder(), - isDense: true, - ), - 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': @@ -176,4 +116,49 @@ class _ParentManagementWidgetState extends State { return 'Inconnu'; } } + + void _openParentDetails(ParentModel parent) { + showDialog( + context: context, + builder: (context) => AdminDetailModal( + title: parent.user.fullName.isEmpty ? 'Parent' : parent.user.fullName, + subtitle: parent.user.email, + fields: [ + AdminDetailField(label: 'ID', value: _v(parent.user.id)), + AdminDetailField( + label: 'Statut', + value: _displayStatus(parent.user.statut), + ), + AdminDetailField( + label: 'Telephone', + value: _v(parent.user.telephone), + ), + AdminDetailField(label: 'Adresse', value: _v(parent.user.adresse)), + AdminDetailField(label: 'Ville', value: _v(parent.user.ville)), + AdminDetailField( + label: 'Code postal', + value: _v(parent.user.codePostal), + ), + AdminDetailField( + label: 'Nombre d\'enfants', + value: parent.childrenCount.toString(), + ), + ], + onEdit: () { + Navigator.of(context).pop(); + ScaffoldMessenger.of(this.context).showSnackBar( + const SnackBar(content: Text('Action Modifier a implementer')), + ); + }, + onDelete: () { + Navigator.of(context).pop(); + ScaffoldMessenger.of(this.context).showSnackBar( + const SnackBar(content: Text('Action Supprimer a implementer')), + ); + }, + ), + ); + } + + String _v(String? value) => (value == null || value.isEmpty) ? '-' : value; } From aec1990ec91c3bc554d90fb88a417b3950270fa7 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 20 Feb 2026 15:10:27 +0100 Subject: [PATCH 3/5] refactor(#93): uniformiser la ligne utilisateur et afficher Modifier au survol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Met le rendu des lignes sur une seule ligne (icone, nom, infos) et n’affiche l’action Modifier qu’au hover pour alléger visuellement les listes. Co-authored-by: Cursor --- .../admin/admin_management_widget.dart | 7 - .../widgets/admin/common/admin_user_card.dart | 160 +++++++++++------- 2 files changed, 101 insertions(+), 66 deletions(-) diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart index 14ef51c..6a69695 100644 --- a/frontend/lib/widgets/admin/admin_management_widget.dart +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -89,13 +89,6 @@ class _AdminManagementWidgetState extends State { // TODO: Modifier admin }, ), - IconButton( - icon: const Icon(Icons.delete), - tooltip: 'Supprimer', - onPressed: () { - // TODO: Supprimer admin - }, - ), ], ); }, diff --git a/frontend/lib/widgets/admin/common/admin_user_card.dart b/frontend/lib/widgets/admin/common/admin_user_card.dart index 8745d4c..914e218 100644 --- a/frontend/lib/widgets/admin/common/admin_user_card.dart +++ b/frontend/lib/widgets/admin/common/admin_user_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -class AdminUserCard extends StatelessWidget { +class AdminUserCard extends StatefulWidget { final String title; final List subtitleLines; final String? avatarUrl; @@ -16,75 +16,117 @@ class AdminUserCard extends StatelessWidget { this.actions = const [], }); + @override + State createState() => _AdminUserCardState(); +} + +class _AdminUserCardState extends State { + bool _isHovered = false; + @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: Colors.grey.shade300), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: [ - CircleAvatar( - radius: 14, - backgroundColor: const Color(0xFFEDE5FA), - backgroundImage: - avatarUrl != null ? NetworkImage(avatarUrl!) : null, - child: avatarUrl == null - ? Icon( - fallbackIcon, - size: 16, - color: const Color(0xFF6B3FA0), - ) - : null, + final infoLine = + widget.subtitleLines.where((e) => e.trim().isNotEmpty).join(' '); + final actionsWidth = + widget.actions.isNotEmpty ? widget.actions.length * 30.0 : 0.0; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + child: InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(10), + hoverColor: const Color(0x149CC5C0), + child: Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: Colors.grey.shade300), ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + child: Row( children: [ - Text( - title.isNotEmpty ? title : 'Sans nom', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, + CircleAvatar( + radius: 14, + backgroundColor: const Color(0xFFEDE5FA), + backgroundImage: widget.avatarUrl != null + ? NetworkImage(widget.avatarUrl!) + : null, + child: widget.avatarUrl == null + ? Icon( + widget.fallbackIcon, + size: 16, + color: const Color(0xFF6B3FA0), + ) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Row( + children: [ + Flexible( + fit: FlexFit.loose, + child: Text( + widget.title.isNotEmpty ? widget.title : 'Sans nom', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + infoLine, + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), - const SizedBox(height: 2), - ...subtitleLines.map( - (line) => Text( - line, - style: const TextStyle( - color: Colors.black54, - fontSize: 12, + if (widget.actions.isNotEmpty) + SizedBox( + width: actionsWidth, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 120), + opacity: _isHovered ? 1 : 0, + child: IgnorePointer( + ignoring: !_isHovered, + child: IconTheme( + data: const IconThemeData(size: 17), + child: IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(4), + minimumSize: const Size(28, 28), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: widget.actions, + ), + ), + ), + ), ), ), - ), ], ), ), - if (actions.isNotEmpty) - IconTheme( - data: const IconThemeData(size: 18), - child: IconButtonTheme( - data: IconButtonThemeData( - style: IconButton.styleFrom( - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(4), - minimumSize: const Size(28, 28), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: actions, - ), - ), - ), - ], + ), ), ), ); From ac3178903d5e24018ec60887cd677324c7920245 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 20 Feb 2026 15:41:42 +0100 Subject: [PATCH 4/5] =?UTF-8?q?docs(#93):=20tracer=20l'=C3=A9volution=20RB?= =?UTF-8?q?AC=20intra-RPE=20dans=20le=20CDC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documente la future gouvernance par rôles au sein d'un même relais pour cadrer les évolutions ultérieures sans l'intégrer au périmètre des tickets backend/frontend actuels. Co-authored-by: Cursor --- docs/EVOLUTIONS_CDC.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/EVOLUTIONS_CDC.md b/docs/EVOLUTIONS_CDC.md index 6496fc6..6ee6d1e 100644 --- a/docs/EVOLUTIONS_CDC.md +++ b/docs/EVOLUTIONS_CDC.md @@ -255,4 +255,24 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante : #### X.1.3 Impact sur l'application - Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`). - Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes. -- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée. \ No newline at end of file +- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée. + +## 8. Évolution future - Gouvernance intra-RPE + +### 8.1 Niveaux d'accès et rôles différenciés dans un même Relais + +#### 8.1.1 Situation actuelle +- Le périmètre actuel prévoit un rattachement simple entre gestionnaire et relais. +- Le rôle "gestionnaire" est traité de manière uniforme dans l'outil. + +#### 8.1.2 Évolution à prévoir +- Introduire un modèle de rôles internes au relais (par exemple : responsable/coordinatrice, animatrice/référente, administratif). +- Permettre des niveaux d'autorité différents selon les actions (pilotage, validation, consultation, administration locale). +- Définir des permissions fines par fonctionnalité (lecture, création, modification, suppression, validation). +- Prévoir une gestion multi-utilisateurs par relais avec traçabilité des décisions. + +#### 8.1.3 Impact attendu +- Évolution du modèle de données vers un RBAC intra-RPE. +- Adaptation des écrans d'administration pour gérer les rôles locaux. +- Renforcement des contrôles d'accès backend et des règles métier. +- Clarification des workflows décisionnels dans l'application. \ No newline at end of file From bc8362bdb7ab3e22e13f33e2c25a0cd3e5819a9d Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Mon, 23 Feb 2026 17:59:03 +0100 Subject: [PATCH 5/5] =?UTF-8?q?refactor(#93):=20extraire=20un=20widget=20U?= =?UTF-8?q?serList=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()), + ], + ); + } +}