From 966f4a9c9c5b858a4a5c156b034b0c778a739c1a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Wed, 24 Jun 2026 23:06:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(#140):=20liste=20enfants=20unifi=C3=A9e=20?= =?UTF-8?q?=E2=80=94=20cartes,=20=C3=A2ge=20pr=C3=A9cis=20et=20cadre=20scr?= =?UTF-8?q?ollable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partage AdminEnfantUserCard entre fiche parent et onglet Enfants, enrichit ParentChildSummary (photo, dates) et compacte la modale parent. Co-authored-by: Cursor --- .../update-gitea-issue-140-ch6-famille.js | 153 +++++++++++++++ frontend/lib/models/parent_child_summary.dart | 29 +++ frontend/lib/utils/date_display_utils.dart | 68 +++++++ .../admin/common/admin_enfant_user_card.dart | 107 +++++++++++ .../admin/common/admin_parent_edit_modal.dart | 177 ++++++++---------- .../admin/enfant_management_widget.dart | 30 +-- 6 files changed, 435 insertions(+), 129 deletions(-) create mode 100644 backend/scripts/update-gitea-issue-140-ch6-famille.js create mode 100644 frontend/lib/widgets/admin/common/admin_enfant_user_card.dart diff --git a/backend/scripts/update-gitea-issue-140-ch6-famille.js b/backend/scripts/update-gitea-issue-140-ch6-famille.js new file mode 100644 index 0000000..d2f0992 --- /dev/null +++ b/backend/scripts/update-gitea-issue-140-ch6-famille.js @@ -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(); diff --git a/frontend/lib/models/parent_child_summary.dart b/frontend/lib/models/parent_child_summary.dart index e40ccd7..837ff4b 100644 --- a/frontend/lib/models/parent_child_summary.dart +++ b/frontend/lib/models/parent_child_summary.dart @@ -1,15 +1,23 @@ +import 'package:p_tits_pas/models/enfant_admin_model.dart'; + /// Résumé enfant affiché dans la fiche parent (dashboard admin). class ParentChildSummary { final String id; final String? firstName; final String? lastName; final String status; + final String? photoUrl; + final String? birthDate; + final String? dueDate; ParentChildSummary({ required this.id, this.firstName, this.lastName, required this.status, + this.photoUrl, + this.birthDate, + this.dueDate, }); String get fullName { @@ -26,9 +34,30 @@ class ParentChildSummary { firstName: (json['first_name'] ?? json['prenom'])?.toString(), lastName: (json['last_name'] ?? json['nom'])?.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). static ParentChildSummary? fromParentChildLink(Map link) { final childRaw = link['child'] ?? link['enfant']; diff --git a/frontend/lib/utils/date_display_utils.dart b/frontend/lib/utils/date_display_utils.dart index 075f2d2..81318e3 100644 --- a/frontend/lib/utils/date_display_utils.dart +++ b/frontend/lib/utils/date_display_utils.dart @@ -9,3 +9,71 @@ String formatIsoDateFr(String? s, {String ifEmpty = '–'}) { 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)}'; + } +} diff --git a/frontend/lib/widgets/admin/common/admin_enfant_user_card.dart b/frontend/lib/widgets/admin/common/admin_enfant_user_card.dart new file mode 100644 index 0000000..f45f059 --- /dev/null +++ b/frontend/lib/widgets/admin/common/admin_enfant_user_card.dart @@ -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 enfantAdminSubtitleLines({ + required String status, + String? birthDate, + String? dueDate, + List extra = const [], +}) { + final lines = []; + 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 subtitleLines; + final List actions; + + const AdminEnfantUserCard({ + super.key, + required this.title, + this.photoUrl, + required this.subtitleLines, + this.actions = const [], + }); + + factory AdminEnfantUserCard.fromEnfant( + EnfantAdminModel enfant, { + List extraSubtitleLines = const [], + List 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 extraSubtitleLines = const [], + List 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, + ); + } +} diff --git a/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart b/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart index 44c1585..a12ffe5 100644 --- a/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart +++ b/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart @@ -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 { late String _statut; late List _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 { _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 { ]) { c.dispose(); } + _childrenScrollCtrl.dispose(); super.dispose(); } @@ -187,24 +192,15 @@ class _AdminParentEditModalState extends State { Future _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 { 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 { 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 { 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 { @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 { ], ), 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 { ), ); } - - String _childStatusLabel(String status) { - switch (status) { - case 'a_naitre': - return 'À naître'; - case 'actif': - return 'Actif'; - case 'scolarise': - return 'Scolarisé'; - default: - return status; - } - } } diff --git a/frontend/lib/widgets/admin/enfant_management_widget.dart b/frontend/lib/widgets/admin/enfant_management_widget.dart index 3353897..cb11674 100644 --- a/frontend/lib/widgets/admin/enfant_management_widget.dart +++ b/frontend/lib/widgets/admin/enfant_management_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.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/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'; /// Onglet liste globale des enfants (doc 28 §6.2, ticket #137). @@ -52,19 +52,6 @@ class _EnfantManagementWidgetState extends State { } } - String _statusLabel(String status) { - switch (status) { - case 'a_naitre': - return 'À naître'; - case 'actif': - return 'Actif'; - case 'scolarise': - return 'Scolarisé'; - default: - return status; - } - } - Future _openEnfant(EnfantAdminModel enfant) async { await showDialog( context: context, @@ -93,19 +80,8 @@ class _EnfantManagementWidgetState extends State { itemCount: filtered.length, itemBuilder: (context, index) { final enfant = filtered[index]; - final parents = enfant.parentLinks - .map((l) => l.parentName ?? 'Parent') - .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', - ], + return AdminEnfantUserCard.fromEnfant( + enfant, actions: [ IconButton( icon: const Icon(Icons.visibility_outlined),