feat(#140): liste enfants unifiée — cartes, âge précis et cadre scrollable
Partage AdminEnfantUserCard entre fiche parent et onglet Enfants, enrichit ParentChildSummary (photo, dates) et compacte la modale parent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import 'package:p_tits_pas/models/parent_child_summary.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_child_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
||||
|
||||
@@ -36,14 +36,17 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
|
||||
late String _statut;
|
||||
late List<ParentChildSummary> _children;
|
||||
late final ScrollController _childrenScrollCtrl;
|
||||
bool _saving = false;
|
||||
bool _dirty = false;
|
||||
|
||||
static const _statuts = ['actif', 'en_attente', 'suspendu', 'refuse'];
|
||||
|
||||
static const double _modalWidth = 930;
|
||||
/// Corps plus haut que les modales validation pour agrandir la liste enfants.
|
||||
static const double _bodyHeight = 580;
|
||||
/// Hauteur d'une ligne enfant (carte + marge) — viewport = 2,5 lignes visibles.
|
||||
static const double _childListItemHeight = 58;
|
||||
static const double _childrenListViewportHeight =
|
||||
_childListItemHeight * 2.5 + 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -60,6 +63,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
||||
_statut = u.statut ?? 'en_attente';
|
||||
_children = List.of(widget.parent.children);
|
||||
_childrenScrollCtrl = ScrollController();
|
||||
for (final c in [
|
||||
_nomCtrl,
|
||||
_prenomCtrl,
|
||||
@@ -87,6 +91,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
]) {
|
||||
c.dispose();
|
||||
}
|
||||
_childrenScrollCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -187,24 +192,15 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
Future<void> _reloadChildren() async {
|
||||
try {
|
||||
final refreshed = await UserService.getParent(widget.parent.user.id);
|
||||
var kids = List.of(refreshed.children);
|
||||
if (kids.any((c) => c.fullName == 'Enfant')) {
|
||||
final all = await UserService.getEnfants();
|
||||
final byId = {for (final e in all) e.id: e};
|
||||
kids = kids
|
||||
.map((c) {
|
||||
if (c.fullName != 'Enfant') return c;
|
||||
final e = byId[c.id];
|
||||
if (e == null) return c;
|
||||
return ParentChildSummary(
|
||||
id: c.id,
|
||||
firstName: e.firstName,
|
||||
lastName: e.lastName,
|
||||
status: c.status.isNotEmpty ? c.status : e.status,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
final all = await UserService.getEnfants();
|
||||
final byId = {for (final e in all) e.id: e};
|
||||
|
||||
final kids = refreshed.children.map((c) {
|
||||
final full = byId[c.id];
|
||||
if (full != null) return ParentChildSummary.fromEnfant(full);
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _children = kids);
|
||||
} catch (_) {}
|
||||
@@ -237,12 +233,13 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
try {
|
||||
final updated = await UserService.detachEnfantFromParent(
|
||||
await UserService.detachEnfantFromParent(
|
||||
parentUserId: widget.parent.user.id,
|
||||
enfantId: child.id,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _children = List.of(updated.children));
|
||||
await _reloadChildren();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enfant détaché')),
|
||||
);
|
||||
@@ -294,12 +291,13 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
if (selected == null || !mounted) return;
|
||||
|
||||
try {
|
||||
final updated = await UserService.attachEnfantToParent(
|
||||
await UserService.attachEnfantToParent(
|
||||
parentUserId: widget.parent.user.id,
|
||||
enfantId: selected.id,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _children = List.of(updated.children));
|
||||
await _reloadChildren();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enfant rattaché')),
|
||||
);
|
||||
@@ -361,49 +359,41 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
|
||||
Widget _childrenPanel() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: ValidationFieldDecoration.container(),
|
||||
height: _childrenListViewportHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _children.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Aucun enfant rattaché',
|
||||
style: TextStyle(fontSize: 14, color: Colors.black54),
|
||||
),
|
||||
child: Text(
|
||||
'Aucun enfant rattaché',
|
||||
style: TextStyle(fontSize: 14, color: Colors.black54),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
: ListView.builder(
|
||||
controller: _childrenScrollCtrl,
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
|
||||
itemExtent: _childListItemHeight,
|
||||
itemCount: _children.length,
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
final c = _children[i];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: InkWell(
|
||||
onTap: () => _openChild(c),
|
||||
child: Text(
|
||||
c.fullName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
return AdminEnfantUserCard.fromSummary(
|
||||
c,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility_outlined),
|
||||
tooltip: 'Voir / modifier',
|
||||
onPressed: () => _openChild(c),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_childStatusLabel(c.status),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: 'Détacher',
|
||||
icon: Icon(Icons.link_off, color: Colors.orange.shade800),
|
||||
onPressed: () => _detachChild(c),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Détacher',
|
||||
icon: Icon(Icons.link_off, color: Colors.orange.shade800),
|
||||
onPressed: () => _detachChild(c),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -444,19 +434,20 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxH = MediaQuery.of(context).size.height * 0.85;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _modalWidth, maxHeight: maxH),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _modalWidth,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(18, 18, 0, 12),
|
||||
padding: EdgeInsets.fromLTRB(18, 16, 0, 10),
|
||||
child: Text(
|
||||
'Fiche parent',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
||||
@@ -473,32 +464,27 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SizedBox(
|
||||
height: _bodyHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_identityFields(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Nombre d\'enfants : ${_children.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_identityFields(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Nombre d\'enfants : ${_children.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _childrenPanel()),
|
||||
const SizedBox(height: 24),
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_childrenPanel(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -506,17 +492,4 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _childStatusLabel(String status) {
|
||||
switch (status) {
|
||||
case 'a_naitre':
|
||||
return 'À naître';
|
||||
case 'actif':
|
||||
return 'Actif';
|
||||
case 'scolarise':
|
||||
return 'Scolarisé';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user