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:
2026-02-23 17:32:03 +01:00
co-authored by Cursor
parent b2d6414fab
commit 5da2ab9005
7 changed files with 429 additions and 265 deletions
@@ -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) {
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,
),
),
),
],
),
),
);
}