feat(#93): optimiser l’affichage Parents/AM avec modale de détails

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 <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-20 11:35:42 +01:00
parent b2d6414fab
commit 5da2ab9005
7 changed files with 429 additions and 265 deletions

View File

@ -1,12 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/models/user.dart';
import 'package:p_tits_pas/services/user_service.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_list_state.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
class AdminManagementWidget extends StatefulWidget { class AdminManagementWidget extends StatefulWidget {
const AdminManagementWidget({super.key}); final String searchQuery;
const AdminManagementWidget({
super.key,
required this.searchQuery,
});
@override @override
State<AdminManagementWidget> createState() => _AdminManagementWidgetState(); State<AdminManagementWidget> createState() => _AdminManagementWidgetState();
@ -16,21 +20,15 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
List<AppUser> _admins = []; List<AppUser> _admins = [];
List<AppUser> _filteredAdmins = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAdmins(); _loadAdmins();
_searchController.addListener(_onSearchChanged);
} }
@override @override
void dispose() { void dispose() => super.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _loadAdmins() async { Future<void> _loadAdmins() async {
setState(() { setState(() {
@ -42,7 +40,6 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_admins = list; _admins = list;
_filteredAdmins = list;
_isLoading = false; _isLoading = false;
}); });
} catch (e) { } catch (e) {
@ -54,42 +51,29 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
} }
} }
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 @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
AdminListHeader(
searchController: _searchController,
searchHint: 'Rechercher un administrateur...',
actionLabel: 'Créer un admin',
onActionPressed: () {
// TODO: Créer admin
},
),
const SizedBox(height: 16),
AdminListState( AdminListState(
isLoading: _isLoading, isLoading: _isLoading,
error: _error, error: _error,
isEmpty: _filteredAdmins.isEmpty, isEmpty: filteredAdmins.isEmpty,
emptyMessage: 'Aucun administrateur trouvé.', emptyMessage: 'Aucun administrateur trouvé.',
list: ListView.builder( list: ListView.builder(
itemCount: _filteredAdmins.length, itemCount: filteredAdmins.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = _filteredAdmins[index]; final user = filteredAdmins[index];
return AdminUserCard( return AdminUserCard(
title: user.fullName, title: user.fullName,
subtitleLines: [ subtitleLines: [

View File

@ -1,12 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/assistante_maternelle_model.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/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_list_state.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
class AssistanteMaternelleManagementWidget extends StatefulWidget { class AssistanteMaternelleManagementWidget extends StatefulWidget {
const AssistanteMaternelleManagementWidget({super.key}); final String searchQuery;
final int? capacityMin;
const AssistanteMaternelleManagementWidget({
super.key,
required this.searchQuery,
this.capacityMin,
});
@override @override
State<AssistanteMaternelleManagementWidget> createState() => State<AssistanteMaternelleManagementWidget> createState() =>
@ -18,25 +25,15 @@ class _AssistanteMaternelleManagementWidgetState
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
List<AssistanteMaternelleModel> _assistantes = []; List<AssistanteMaternelleModel> _assistantes = [];
List<AssistanteMaternelleModel> _filteredAssistantes = [];
final TextEditingController _zoneController = TextEditingController();
final TextEditingController _capacityController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAssistantes(); _loadAssistantes();
_zoneController.addListener(_filter);
_capacityController.addListener(_filter);
} }
@override @override
void dispose() { void dispose() => super.dispose();
_zoneController.dispose();
_capacityController.dispose();
super.dispose();
}
Future<void> _loadAssistantes() async { Future<void> _loadAssistantes() async {
setState(() { setState(() {
@ -48,7 +45,6 @@ class _AssistanteMaternelleManagementWidgetState
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_assistantes = list; _assistantes = list;
_filter();
_isLoading = false; _isLoading = false;
}); });
} catch (e) { } 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 @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AdminListHeader(
searchController: _zoneController,
searchHint: 'Rechercher une zone géographique...',
filters: _buildFilters(),
),
const SizedBox(height: 16),
AdminListState( AdminListState(
isLoading: _isLoading, isLoading: _isLoading,
error: _error, error: _error,
isEmpty: _filteredAssistantes.isEmpty, isEmpty: filteredAssistantes.isEmpty,
emptyMessage: 'Aucune assistante maternelle trouvée.', emptyMessage: 'Aucune assistante maternelle trouvée.',
list: ListView.builder( list: ListView.builder(
itemCount: _filteredAssistantes.length, itemCount: filteredAssistantes.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final assistante = _filteredAssistantes[index]; final assistante = filteredAssistantes[index];
return AdminUserCard( return AdminUserCard(
title: assistante.user.fullName, title: assistante.user.fullName,
avatarUrl: assistante.user.photoUrl, avatarUrl: assistante.user.photoUrl,
fallbackIcon: Icons.face, fallbackIcon: Icons.face,
subtitleLines: [ subtitleLines: [
assistante.user.email, assistante.user.email,
'N° Agrément : ${assistante.approvalNumber ?? 'N/A'}',
'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}', 'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}',
], ],
actions: [ actions: [
@ -111,14 +95,7 @@ class _AssistanteMaternelleManagementWidgetState
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
tooltip: 'Modifier', tooltip: 'Modifier',
onPressed: () { onPressed: () {
// TODO: Ajouter modification _openAssistanteDetails(assistante);
},
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Supprimer',
onPressed: () {
// TODO: Ajouter suppression
}, },
), ),
], ],
@ -131,24 +108,58 @@ class _AssistanteMaternelleManagementWidgetState
); );
} }
Widget _buildFilters() { void _openAssistanteDetails(AssistanteMaternelleModel assistante) {
return Wrap( showDialog<void>(
spacing: 16, context: context,
runSpacing: 8, builder: (context) => AdminDetailModal(
children: [ title: assistante.user.fullName.isEmpty
SizedBox( ? 'Assistante maternelle'
width: 240, : assistante.user.fullName,
child: TextField( subtitle: assistante.user.email,
controller: _capacityController, fields: [
decoration: const InputDecoration( AdminDetailField(label: 'ID', value: _v(assistante.user.id)),
labelText: 'Capacité minimum', AdminDetailField(
border: OutlineInputBorder(), label: 'Numero agrement',
isDense: true, value: _v(assistante.approvalNumber),
),
keyboardType: TextInputType.number,
), ),
), 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;
} }

View File

@ -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<AdminDetailField> 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'),
),
],
),
),
],
),
),
),
);
}
}

View File

@ -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!,
],
],
);
}
}

View File

@ -20,20 +20,72 @@ class AdminUserCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
child: ListTile( elevation: 0,
leading: CircleAvatar( shape: RoundedRectangleBorder(
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, borderRadius: BorderRadius.circular(8),
child: avatarUrl == null ? Icon(fallbackIcon) : null, side: BorderSide(color: Colors.grey.shade300),
), ),
title: Text(title.isNotEmpty ? title : 'Sans nom'), child: Padding(
subtitle: Text(subtitleLines.join('\n')), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
isThreeLine: subtitleLines.length > 1, child: Row(
trailing: actions.isEmpty children: [
? null CircleAvatar(
: Row( radius: 14,
mainAxisSize: MainAxisSize.min, backgroundColor: const Color(0xFFEDE5FA),
children: actions, 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,
),
),
),
],
),
), ),
); );
} }

View File

@ -136,39 +136,91 @@ class DashboardAppBarAdmin extends StatelessWidget
class DashboardUserManagementSubBar extends StatelessWidget { class DashboardUserManagementSubBar extends StatelessWidget {
final int selectedSubIndex; final int selectedSubIndex;
final ValueChanged<int> onSubTabChange; final ValueChanged<int> onSubTabChange;
final TextEditingController searchController;
final String searchHint;
final Widget? filterControl;
final VoidCallback? onAddPressed;
final String addLabel;
const DashboardUserManagementSubBar({ const DashboardUserManagementSubBar({
Key? key, Key? key,
required this.selectedSubIndex, required this.selectedSubIndex,
required this.onSubTabChange, required this.onSubTabChange,
required this.searchController,
required this.searchHint,
this.filterControl,
this.onAddPressed,
this.addLabel = '+ Ajouter',
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 48, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade100, color: Colors.grey.shade100,
border: Border(bottom: BorderSide(color: Colors.grey.shade300)), border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
), ),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: Center( child: Row(
child: Row( children: [
mainAxisSize: MainAxisSize.min, _buildSubNavItem(context, 'Gestionnaires', 0),
children: [ const SizedBox(width: 12),
_buildSubNavItem(context, 'Gestionnaires', 0), _buildSubNavItem(context, 'Parents', 1),
const SizedBox(width: 16), const SizedBox(width: 12),
_buildSubNavItem(context, 'Parents', 1), _buildSubNavItem(context, 'Assistantes maternelles', 2),
const SizedBox(width: 16), const SizedBox(width: 12),
_buildSubNavItem(context, 'Assistantes maternelles', 2), _buildSubNavItem(context, 'Administrateurs', 3),
const SizedBox(width: 16), const SizedBox(width: 36),
_buildSubNavItem(context, 'Administrateurs', 3), _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) { Widget _buildSubNavItem(BuildContext context, String title, int index) {
final bool isActive = index == selectedSubIndex; final bool isActive = index == selectedSubIndex;
return InkWell( return InkWell(

View File

@ -1,12 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/parent_model.dart'; import 'package:p_tits_pas/models/parent_model.dart';
import 'package:p_tits_pas/services/user_service.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_list_state.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
class ParentManagementWidget extends StatefulWidget { class ParentManagementWidget extends StatefulWidget {
const ParentManagementWidget({super.key}); final String searchQuery;
final String? statusFilter;
const ParentManagementWidget({
super.key,
required this.searchQuery,
this.statusFilter,
});
@override @override
State<ParentManagementWidget> createState() => _ParentManagementWidgetState(); State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
@ -16,23 +23,15 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
List<ParentModel> _parents = []; List<ParentModel> _parents = [];
List<ParentModel> _filteredParents = [];
final TextEditingController _searchController = TextEditingController();
String? _selectedStatus;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadParents(); _loadParents();
_searchController.addListener(_filter);
} }
@override @override
void dispose() { void dispose() => super.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _loadParents() async { Future<void> _loadParents() async {
setState(() { setState(() {
@ -44,7 +43,6 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_parents = list; _parents = list;
_filter(); // Apply initial filter (if any)
_isLoading = false; _isLoading = false;
}); });
} catch (e) { } catch (e) {
@ -56,70 +54,44 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
} }
} }
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 @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AdminListHeader(
searchController: _searchController,
searchHint: 'Rechercher un parent...',
filters: _buildFilters(),
),
const SizedBox(height: 16),
AdminListState( AdminListState(
isLoading: _isLoading, isLoading: _isLoading,
error: _error, error: _error,
isEmpty: _filteredParents.isEmpty, isEmpty: filteredParents.isEmpty,
emptyMessage: 'Aucun parent trouvé.', emptyMessage: 'Aucun parent trouvé.',
list: ListView.builder( list: ListView.builder(
itemCount: _filteredParents.length, itemCount: filteredParents.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final parent = _filteredParents[index]; final parent = filteredParents[index];
return AdminUserCard( return AdminUserCard(
title: parent.user.fullName, title: parent.user.fullName,
avatarUrl: parent.user.photoUrl, avatarUrl: parent.user.photoUrl,
subtitleLines: [ subtitleLines: [
parent.user.email, parent.user.email,
'Statut : ${_displayStatus(parent.user.statut)}', 'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${parent.childrenCount}',
'Enfants : ${parent.childrenCount}',
], ],
actions: [ actions: [
IconButton(
icon: const Icon(Icons.visibility),
tooltip: 'Voir dossier',
onPressed: () {
// TODO: Voir le statut du dossier
},
),
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
tooltip: 'Modifier', tooltip: 'Modifier',
onPressed: () { onPressed: () {
// TODO: Modifier parent _openParentDetails(parent);
},
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Supprimer',
onPressed: () {
// TODO: Supprimer compte
}, },
), ),
], ],
@ -132,38 +104,6 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
); );
} }
Widget _buildFilters() {
return SizedBox(
width: 240,
child: DropdownButtonFormField<String?>(
decoration: const InputDecoration(
labelText: 'Statut',
border: OutlineInputBorder(),
isDense: true,
),
value: _selectedStatus,
items: const [
DropdownMenuItem<String?>(value: null, child: Text('Tous')),
DropdownMenuItem<String?>(value: 'actif', child: Text('Actif')),
DropdownMenuItem<String?>(
value: 'en_attente',
child: Text('En attente'),
),
DropdownMenuItem<String?>(
value: 'suspendu',
child: Text('Suspendu'),
),
],
onChanged: (value) {
setState(() {
_selectedStatus = value;
});
_filter();
},
),
);
}
String _displayStatus(String? status) { String _displayStatus(String? status) {
switch (status) { switch (status) {
case 'actif': case 'actif':
@ -176,4 +116,49 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
return 'Inconnu'; return 'Inconnu';
} }
} }
void _openParentDetails(ParentModel parent) {
showDialog<void>(
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;
} }