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:
parent
b2d6414fab
commit
5da2ab9005
@ -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: [
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
138
frontend/lib/widgets/admin/common/admin_detail_modal.dart
Normal file
138
frontend/lib/widgets/admin/common/admin_detail_modal.dart
Normal 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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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!,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user