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:
2026-06-24 23:06:38 +02:00
co-authored by Cursor
parent 8494341b56
commit 966f4a9c9c
6 changed files with 435 additions and 129 deletions
@@ -0,0 +1,153 @@
/**
* Met à jour l'issue Gitea #140 — epic dashboard admin ch.6 famille.
* Usage: node backend/scripts/update-gitea-issue-140-ch6-famille.js
* Token : .gitea-token (racine), GITEA_TOKEN, ou docs/27_BRIEFING-FRONTEND.md
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const repoRoot = path.join(__dirname, '../..');
let token = process.env.GITEA_TOKEN;
if (!token) {
try {
const tokenFile = path.join(repoRoot, '.gitea-token');
if (fs.existsSync(tokenFile)) token = fs.readFileSync(tokenFile, 'utf8').trim();
} catch (_) {}
}
if (!token) {
try {
const briefing = fs.readFileSync(
path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'),
'utf8',
);
const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/);
if (m) token = m[1].trim();
} catch (_) {}
}
if (!token) {
console.error('Token non trouvé : .gitea-token ou GITEA_TOKEN (voir docs/26_GITEA-API.md)');
process.exit(1);
}
const body = `## Rôle de ce ticket
**#140 est un ticket epic / livraison** : il regroupe la mise en œuvre du **chapitre 6** du doc [28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md](../docs/28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md) (§6.1 et §6.2) sur la branche \`feature/140-dashboard-admin-ch6-famille\`.
Il **ne remplace pas** les tickets détaillés ci-dessous : il sert de **fil de livraison** (PR, recette, fermeture coordonnée). Chaque sous-ticket garde son périmètre propre ; #140 est clos quand l'ensemble est **fonctionnel et homogène en UI**.
---
## Tickets couverts (périmètres embarqués)
| Ticket | Sujet | Rôle dans #140 |
|--------|--------|----------------|
| **#115** | Rattachement parent — **backend** | API \`POST/DELETE …/enfants/:enfantId\` |
| **#116** | Rattachement parent — **frontend** | UI rattacher / détacher depuis la fiche parent |
| **#130** | \`UserService\` — APIs admin | Appels \`getParents\`, \`getParent\`, \`getEnfants\`, \`updateParentFiche\`, etc. |
| **#131** | Fiche parent éditable | Modale parent (dashboard) — *hors fiche AM* |
| **#137** | Onglet **Enfants** | Liste globale admin + accès fiche enfant |
| **#138** | Fiche enfant + liste dans fiche parent | \`AdminChildDetailModal\` + liste enfants en bas de fiche parent |
> **Note :** fermer #140 peut entraîner la fermeture **partielle ou totale** de ces tickets selon ce qui est réellement livré et recetté dans la PR.
---
## Hors scope #140
- **§6.3** — Création dossier admin sans numéro → **#129**
- Fiche **AM** éditable (dashboard) → reste **#131** (partie AM)
- Parcours gestionnaire « famille complexe » → **#139**
- Qualification responsable légal (combobox sur lien) → ticket à créer
---
## Backend (attendu / livré)
- [x] \`PATCH /parents/:id/fiche\` — édition fiche parent (admin/gestionnaire)
- [x] \`POST /parents/:id/enfants/:enfantId\` — rattacher un enfant existant
- [x] \`DELETE /parents/:id/enfants/:enfantId\` — détacher (garde-fou : ≥1 responsable / enfant)
- [x] \`GET /enfants\` enrichi ; \`PATCH /enfants/:id\` pour gestionnaire
---
## Frontend — fait
- [x] Modale **fiche parent** éditable (shell aligné validation, statut gélule, téléphone formaté, \`IdentityBlock\`)
- [x] Onglet **Enfants** — liste globale (\`EnfantManagementWidget\`)
- [x] Liste enfants dans fiche parent — cartes (photo, nom, âge ans/mois, statut), cadre blanc, scroll 2,5 lignes
- [x] Rattacher / détacher un enfant **existant** (API branchée)
- [x] Parsing robuste \`parentChildren\` + URLs médias \`/uploads\` en Flutter web
- [x] \`UserService\` — APIs parents / enfants / affiliation
---
## Frontend — reste à faire (bloquant clôture)
### Fiche enfant — #138 (UI)
- [ ] Reprendre \`AdminChildDetailModal\` : même **look & feel** que fiche parent / modales validation (largeur ~930 px, grille champs, photo enfant)
- [ ] Aligner dates, genre, statut sur les wizards validation famille
### Modale **rattacher** un enfant — #116 (UI)
- [ ] Remplacer le \`SimpleDialog\` actuel par une modale cohérente : liste type \`AdminEnfantUserCard\` (photo, nom, âge), recherche éventuelle
### Création d'un **nouvel** enfant depuis la fiche parent — doc §6.2
- [ ] **Non implémenté** aujourd'hui (seul le rattachement d'un enfant déjà en base existe)
- [ ] À trancher : inclus dans #140 si le back expose un \`POST\` admin depuis le contexte parent, sinon ticket dédié / extension #129
---
## Recette avant merge
1. Parent avec 0 / 1 / 3+ enfants — liste, scroll, compteur
2. Rattacher puis détacher (message si dernier responsable)
3. Clic enfant → fiche enfant (après refonte UI)
4. Onglet Enfants — même rendu cartes
5. Avatars photos (URLs absolues web)
---
## Branche
\`feature/140-dashboard-admin-ch6-famille\`
## Références
- Doc produit : \`docs/28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md\` §6.1, §6.2`;
const payload = JSON.stringify({ body });
const req = https.request(
{
hostname: 'git.ptits-pas.fr',
path: '/api/v1/repos/jmartin/petitspas/issues/140',
method: 'PATCH',
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
(res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
const json = JSON.parse(data);
console.log('Issue #140 mise à jour :', json.html_url);
console.log('updated_at:', json.updated_at);
} else {
console.error('Erreur', res.statusCode, data);
process.exit(1);
}
});
},
);
req.on('error', (err) => {
console.error(err);
process.exit(1);
});
req.write(payload);
req.end();
@@ -1,15 +1,23 @@
import 'package:p_tits_pas/models/enfant_admin_model.dart';
/// Résumé enfant affiché dans la fiche parent (dashboard admin). /// Résumé enfant affiché dans la fiche parent (dashboard admin).
class ParentChildSummary { class ParentChildSummary {
final String id; final String id;
final String? firstName; final String? firstName;
final String? lastName; final String? lastName;
final String status; final String status;
final String? photoUrl;
final String? birthDate;
final String? dueDate;
ParentChildSummary({ ParentChildSummary({
required this.id, required this.id,
this.firstName, this.firstName,
this.lastName, this.lastName,
required this.status, required this.status,
this.photoUrl,
this.birthDate,
this.dueDate,
}); });
String get fullName { String get fullName {
@@ -26,9 +34,30 @@ class ParentChildSummary {
firstName: (json['first_name'] ?? json['prenom'])?.toString(), firstName: (json['first_name'] ?? json['prenom'])?.toString(),
lastName: (json['last_name'] ?? json['nom'])?.toString(), lastName: (json['last_name'] ?? json['nom'])?.toString(),
status: (json['status'] ?? json['statut'] ?? '').toString(), status: (json['status'] ?? json['statut'] ?? '').toString(),
photoUrl: json['photo_url']?.toString(),
birthDate: _dateString(json['birth_date']),
dueDate: _dateString(json['due_date']),
); );
} }
factory ParentChildSummary.fromEnfant(EnfantAdminModel enfant) {
return ParentChildSummary(
id: enfant.id,
firstName: enfant.firstName,
lastName: enfant.lastName,
status: enfant.status,
photoUrl: enfant.photoUrl,
birthDate: enfant.birthDate,
dueDate: enfant.dueDate,
);
}
static String? _dateString(dynamic v) {
if (v == null) return null;
if (v is String) return v.split('T').first;
return v.toString().split('T').first;
}
/// Parse un lien `parentChildren` (objet enfant imbriqué ou id seul). /// Parse un lien `parentChildren` (objet enfant imbriqué ou id seul).
static ParentChildSummary? fromParentChildLink(Map<String, dynamic> link) { static ParentChildSummary? fromParentChildLink(Map<String, dynamic> link) {
final childRaw = link['child'] ?? link['enfant']; final childRaw = link['child'] ?? link['enfant'];
@@ -9,3 +9,71 @@ String formatIsoDateFr(String? s, {String ifEmpty = ''}) {
return s.trim(); return s.trim();
} }
} }
/// Parties d'âge calendaire entre [birth] et [reference] (date-only).
({int years, int months, int days}) computeChildAgeParts(
DateTime birth,
DateTime reference,
) {
final birthDay = DateTime(birth.year, birth.month, birth.day);
final refDay = DateTime(reference.year, reference.month, reference.day);
var years = refDay.year - birthDay.year;
var months = refDay.month - birthDay.month;
var days = refDay.day - birthDay.day;
if (days < 0) {
months--;
final prevMonth = DateTime(refDay.year, refDay.month, 0);
days += prevMonth.day;
}
if (months < 0) {
years--;
months += 12;
}
return (years: years, months: months, days: days);
}
String _yearLabel(int years) => years == 1 ? '1 an' : '$years ans';
String _monthLabel(int months) => months == 1 ? '1 mois' : '$months mois';
/// Libellé d'âge pour un enfant (liste admin, fiche parent, etc.).
String formatChildAgeLabel({
String? birthDate,
String? dueDate,
String? status,
}) {
if (status == 'a_naitre') {
final due = formatIsoDateFr(dueDate, ifEmpty: '');
if (due.isNotEmpty) return 'Naissance prévue : $due';
return 'À naître';
}
if (birthDate == null || birthDate.trim().isEmpty) return '';
try {
final birth = DateTime.parse(birthDate.trim().split('T').first);
final now = DateTime.now();
final parts = computeChildAgeParts(birth, now);
if (parts.years >= 1) {
if (parts.months > 0) {
return 'Âge : ${_yearLabel(parts.years)} ${parts.months} mois';
}
return 'Âge : ${_yearLabel(parts.years)}';
}
if (parts.months >= 1) {
return 'Âge : ${_monthLabel(parts.months)}';
}
final totalDays = DateTime(now.year, now.month, now.day)
.difference(DateTime(birth.year, birth.month, birth.day))
.inDays;
if (totalDays <= 0) return 'Âge : né récemment';
return 'Âge : $totalDays jour${totalDays > 1 ? 's' : ''}';
} catch (_) {
return 'Né le ${formatIsoDateFr(birthDate)}';
}
}
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/models/parent_child_summary.dart';
import 'package:p_tits_pas/utils/date_display_utils.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
String enfantStatusLabel(String status) {
switch (status) {
case 'a_naitre':
return 'À naître';
case 'actif':
return 'Actif';
case 'scolarise':
return 'Scolarisé';
default:
return status;
}
}
List<String> enfantAdminSubtitleLines({
required String status,
String? birthDate,
String? dueDate,
List<String> extra = const [],
}) {
final lines = <String>[];
final age = formatChildAgeLabel(
birthDate: birthDate,
dueDate: dueDate,
status: status,
);
if (age.isNotEmpty) lines.add(age);
if (status.isNotEmpty) {
lines.add('Statut : ${enfantStatusLabel(status)}');
}
lines.addAll(extra);
return lines;
}
/// Carte enfant admin (photo, nom, âge) — même rendu onglet Enfants / fiche parent.
class AdminEnfantUserCard extends StatelessWidget {
final String title;
final String? photoUrl;
final List<String> subtitleLines;
final List<Widget> actions;
const AdminEnfantUserCard({
super.key,
required this.title,
this.photoUrl,
required this.subtitleLines,
this.actions = const [],
});
factory AdminEnfantUserCard.fromEnfant(
EnfantAdminModel enfant, {
List<String> extraSubtitleLines = const [],
List<Widget> actions = const [],
}) {
final parents = enfant.parentLinks
.map((l) => l.parentName ?? 'Parent')
.join(', ');
return AdminEnfantUserCard(
title: enfant.fullName,
photoUrl: enfant.photoUrl,
subtitleLines: enfantAdminSubtitleLines(
status: enfant.status,
birthDate: enfant.birthDate,
dueDate: enfant.dueDate,
extra: [
if (parents.isNotEmpty) 'Responsables : $parents',
...extraSubtitleLines,
],
),
actions: actions,
);
}
factory AdminEnfantUserCard.fromSummary(
ParentChildSummary child, {
List<String> extraSubtitleLines = const [],
List<Widget> actions = const [],
}) {
return AdminEnfantUserCard(
title: child.fullName,
photoUrl: child.photoUrl,
subtitleLines: enfantAdminSubtitleLines(
status: child.status,
birthDate: child.birthDate,
dueDate: child.dueDate,
extra: extraSubtitleLines,
),
actions: actions,
);
}
@override
Widget build(BuildContext context) {
return AdminUserCard(
title: title,
fallbackIcon: Icons.child_care_outlined,
avatarUrl: photoUrl,
subtitleLines: subtitleLines,
actions: actions,
);
}
}
@@ -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/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_child_detail_modal.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/common/identity_block.dart';
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.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 String _statut;
late List<ParentChildSummary> _children; late List<ParentChildSummary> _children;
late final ScrollController _childrenScrollCtrl;
bool _saving = false; bool _saving = false;
bool _dirty = false; bool _dirty = false;
static const _statuts = ['actif', 'en_attente', 'suspendu', 'refuse']; static const _statuts = ['actif', 'en_attente', 'suspendu', 'refuse'];
static const double _modalWidth = 930; static const double _modalWidth = 930;
/// Corps plus haut que les modales validation pour agrandir la liste enfants. /// Hauteur d'une ligne enfant (carte + marge) — viewport = 2,5 lignes visibles.
static const double _bodyHeight = 580; static const double _childListItemHeight = 58;
static const double _childrenListViewportHeight =
_childListItemHeight * 2.5 + 8;
@override @override
void initState() { void initState() {
@@ -60,6 +63,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
_cpCtrl = TextEditingController(text: u.codePostal ?? ''); _cpCtrl = TextEditingController(text: u.codePostal ?? '');
_statut = u.statut ?? 'en_attente'; _statut = u.statut ?? 'en_attente';
_children = List.of(widget.parent.children); _children = List.of(widget.parent.children);
_childrenScrollCtrl = ScrollController();
for (final c in [ for (final c in [
_nomCtrl, _nomCtrl,
_prenomCtrl, _prenomCtrl,
@@ -87,6 +91,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
]) { ]) {
c.dispose(); c.dispose();
} }
_childrenScrollCtrl.dispose();
super.dispose(); super.dispose();
} }
@@ -187,24 +192,15 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
Future<void> _reloadChildren() async { Future<void> _reloadChildren() async {
try { try {
final refreshed = await UserService.getParent(widget.parent.user.id); final refreshed = await UserService.getParent(widget.parent.user.id);
var kids = List.of(refreshed.children); final all = await UserService.getEnfants();
if (kids.any((c) => c.fullName == 'Enfant')) { final byId = {for (final e in all) e.id: e};
final all = await UserService.getEnfants();
final byId = {for (final e in all) e.id: e}; final kids = refreshed.children.map((c) {
kids = kids final full = byId[c.id];
.map((c) { if (full != null) return ParentChildSummary.fromEnfant(full);
if (c.fullName != 'Enfant') return c; return c;
final e = byId[c.id]; }).toList();
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();
}
if (!mounted) return; if (!mounted) return;
setState(() => _children = kids); setState(() => _children = kids);
} catch (_) {} } catch (_) {}
@@ -237,12 +233,13 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
if (confirmed != true || !mounted) return; if (confirmed != true || !mounted) return;
try { try {
final updated = await UserService.detachEnfantFromParent( await UserService.detachEnfantFromParent(
parentUserId: widget.parent.user.id, parentUserId: widget.parent.user.id,
enfantId: child.id, enfantId: child.id,
); );
if (!mounted) return; if (!mounted) return;
setState(() => _children = List.of(updated.children)); await _reloadChildren();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant détaché')), const SnackBar(content: Text('Enfant détaché')),
); );
@@ -294,12 +291,13 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
if (selected == null || !mounted) return; if (selected == null || !mounted) return;
try { try {
final updated = await UserService.attachEnfantToParent( await UserService.attachEnfantToParent(
parentUserId: widget.parent.user.id, parentUserId: widget.parent.user.id,
enfantId: selected.id, enfantId: selected.id,
); );
if (!mounted) return; if (!mounted) return;
setState(() => _children = List.of(updated.children)); await _reloadChildren();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant rattaché')), const SnackBar(content: Text('Enfant rattaché')),
); );
@@ -361,49 +359,41 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
Widget _childrenPanel() { Widget _childrenPanel() {
return Container( return Container(
width: double.infinity, height: _childrenListViewportHeight,
decoration: ValidationFieldDecoration.container(), decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
clipBehavior: Clip.antiAlias,
child: _children.isEmpty child: _children.isEmpty
? const Center( ? const Center(
child: Padding( child: Text(
padding: EdgeInsets.all(20), 'Aucun enfant rattaché',
child: Text( style: TextStyle(fontSize: 14, color: Colors.black54),
'Aucun enfant rattaché',
style: TextStyle(fontSize: 14, color: Colors.black54),
),
), ),
) )
: ListView.separated( : ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 4), controller: _childrenScrollCtrl,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
itemExtent: _childListItemHeight,
itemCount: _children.length, itemCount: _children.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: Colors.grey.shade300,
),
itemBuilder: (_, i) { itemBuilder: (_, i) {
final c = _children[i]; final c = _children[i];
return ListTile( return AdminEnfantUserCard.fromSummary(
dense: true, c,
title: InkWell( actions: [
onTap: () => _openChild(c), IconButton(
child: Text( icon: const Icon(Icons.visibility_outlined),
c.fullName, tooltip: 'Voir / modifier',
style: const TextStyle( onPressed: () => _openChild(c),
fontSize: 14,
color: Colors.black87,
decoration: TextDecoration.underline,
),
), ),
), IconButton(
subtitle: Text( tooltip: 'Détacher',
_childStatusLabel(c.status), icon: Icon(Icons.link_off, color: Colors.orange.shade800),
style: const TextStyle(fontSize: 12, color: Colors.black54), onPressed: () => _detachChild(c),
), ),
trailing: 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final maxH = MediaQuery.of(context).size.height * 0.85;
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: _modalWidth, maxHeight: maxH), constraints: BoxConstraints(
maxWidth: _modalWidth,
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Row(
children: [ children: [
const Padding( const Padding(
padding: EdgeInsets.fromLTRB(18, 18, 0, 12), padding: EdgeInsets.fromLTRB(18, 16, 0, 10),
child: Text( child: Text(
'Fiche parent', 'Fiche parent',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
@@ -473,32 +464,27 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
], ],
), ),
const Divider(height: 1), const Divider(height: 1),
SizedBox( Padding(
height: _bodyHeight, padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: Padding( child: Column(
padding: const EdgeInsets.all(20), mainAxisSize: MainAxisSize.min,
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ _identityFields(),
_identityFields(), const SizedBox(height: 12),
Row( Text(
children: [ 'Nombre d\'enfants : ${_children.length}',
Text( style: const TextStyle(
'Nombre d\'enfants : ${_children.length}', fontSize: 16,
style: const TextStyle( fontWeight: FontWeight.w600,
fontSize: 16, color: Colors.black87,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
), ),
const SizedBox(height: 8), ),
Expanded(child: _childrenPanel()), const SizedBox(height: 8),
const SizedBox(height: 24), _childrenPanel(),
_buildFooter(), 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;
}
}
} }
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart'; import 'package:p_tits_pas/models/enfant_admin_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_child_detail_modal.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137). /// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
@@ -52,19 +52,6 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
} }
} }
String _statusLabel(String status) {
switch (status) {
case 'a_naitre':
return 'À naître';
case 'actif':
return 'Actif';
case 'scolarise':
return 'Scolarisé';
default:
return status;
}
}
Future<void> _openEnfant(EnfantAdminModel enfant) async { Future<void> _openEnfant(EnfantAdminModel enfant) async {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
@@ -93,19 +80,8 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
itemCount: filtered.length, itemCount: filtered.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final enfant = filtered[index]; final enfant = filtered[index];
final parents = enfant.parentLinks return AdminEnfantUserCard.fromEnfant(
.map((l) => l.parentName ?? 'Parent') enfant,
.join(', ');
return AdminUserCard(
title: enfant.fullName,
fallbackIcon: Icons.child_care_outlined,
avatarUrl: enfant.photoUrl,
subtitleLines: [
'Statut : ${_statusLabel(enfant.status)}',
if (enfant.birthDate != null && enfant.birthDate!.isNotEmpty)
'Naissance : ${enfant.birthDate}',
if (parents.isNotEmpty) 'Responsables : $parents',
],
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.visibility_outlined), icon: const Icon(Icons.visibility_outlined),