diff --git a/docs/tmp/112-back-reprise-alignement-front.md b/docs/archive/temporaires/TEMP_112-back-reprise-alignement-front.md similarity index 100% rename from docs/tmp/112-back-reprise-alignement-front.md rename to docs/archive/temporaires/TEMP_112-back-reprise-alignement-front.md diff --git a/docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md b/docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md new file mode 100644 index 0000000..43ac761 --- /dev/null +++ b/docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md @@ -0,0 +1,124 @@ +# #131 — En-tête fiche parent : co-parent (note front → back) + +**Ticket :** #131 (fiche parent dashboard, doc `28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1) +**Date :** 2026-06-01 +**Statut front :** livré (en-tête dynamique) +**Modif backend demandée :** **aucune** — ce document fixe le contrat attendu et invite à valider que l’existant le couvre. + +--- + +## 1. Comportement UI (front) + +Dans la modale **fiche parent** (`AdminParentEditModal`) : + +| Zone | Contenu | +|------|---------| +| **Titre** | `prenom` + `nom` du parent affiché (plus le libellé fixe « Fiche parent ») | +| **Sous-titre** | `Co-parent : {prenom} {nom}` — affiché **uniquement** si un co-parent est connu | + +Le titre se met à jour en direct pendant l’édition des champs nom/prénom. +Le sous-titre provient du co-parent **chargé depuis l’API** (pas saisi à la main dans la modale). + +--- + +## 2. Endpoints consommés + +| Méthode | Route | Usage front | +|---------|-------|-------------| +| `GET` | `/api/v1/parents` | Liste parents (onglet Parents) | +| `GET` | `/api/v1/parents/:userId` | Rechargement fiche après rattachement/détachement enfant | +| `PATCH` | `/api/v1/parents/:userId/fiche` | Sauvegarde identité + statut (inchangé) | + +Rôles : `super_admin`, `gestionnaire`, `administrateur` (selon route). + +--- + +## 3. Contrat JSON attendu pour `co_parent` + +Le front parse `ParentModel.fromJson` avec la clé **`co_parent`** (snake_case), objet utilisateur imbriqué. + +### Champs minimum utilisés pour le sous-titre + +| Clé JSON | Usage | +|----------|--------| +| `co_parent` | Objet ou absent/`null` | +| `co_parent.id` | Identifiant (futur lien cliquable éventuel) | +| `co_parent.prenom` | Affichage | +| `co_parent.nom` | Affichage | + +Affichage front : `'{prenom} {nom}'.trim()` → libellé `Co-parent : …`. + +### Exemple de fragment de réponse (`GET /parents/:id`) + +```json +{ + "user_id": "33333333-3333-3333-3333-333333333333", + "numero_dossier": "2026-000042", + "user": { + "id": "33333333-3333-3333-3333-333333333333", + "email": "parent1@example.com", + "prenom": "Paul", + "nom": "PARENT", + "statut": "actif", + "telephone": "0601020304" + }, + "co_parent": { + "id": "44444444-4444-4444-4444-444444444444", + "email": "coparent1@example.com", + "prenom": "Clara", + "nom": "COPARENT", + "role": "parent", + "statut": "actif" + }, + "parentChildren": [] +} +``` + +> **Note :** le front lit `user` (pas `utilisateur`). La doc `11_API.md` § Parents mentionne encore `utilisateur` / `id_co_parent` seul — le contrat **effectif** côté Nest/TypeORM est l’entité `Parents` sérialisée (`user`, `co_parent`, `parentChildren`, …). + +--- + +## 4. État backend (à valider, pas à refaire) + +D’après le code actuel (`parents.service.ts`) : + +- `findAll()` et `findOne(user_id)` chargent déjà la relation **`co_parent`** ; +- la FK métier est `parents.id_co_parent` → `utilisateurs.id` ; +- à l’inscription couple, les deux sens sont en principe renseignés (`auth.service.ts`). + +**Checklist validation back :** + +- [ ] `GET /parents/:id` renvoie bien `co_parent` peuplé quand `id_co_parent` est non null +- [ ] `GET /parents` (liste) inclut aussi `co_parent` (sous-titre disponible dès l’ouverture sans re-fetch) +- [ ] Les champs `prenom` / `nom` du co-parent sont présents dans la réponse JSON + +Si ces trois points passent en recette, **aucun changement backend n’est nécessaire** pour cette fonctionnalité. + +--- + +## 5. Points d’attention (hors périmètre immédiat) + +| Sujet | Détail | +|-------|--------| +| **Lien inverse** | Si le parent B est le co-parent de A (`A.id_co_parent = B`) mais que `B.id_co_parent` est `null`, le sous-titre **ne s’affichera pas** sur la fiche de B. Le front ne fait pas de résolution inverse. À traiter côté back **seulement si** des données legacy ont un lien à sens unique. | +| **Familles > 2 adultes** | Le sous-titre n’affiche que le co-parent direct (`id_co_parent`). Les autres responsables liés uniquement via `enfants_parents` ne sont pas listés ici (cf. doc 28 §6). | +| **Données sensibles** | Vérifier que la sérialisation de `co_parent` n’expose pas `password` / tokens (même remarque que pour `user`). | + +--- + +## 6. Fichiers front concernés + +| Fichier | Rôle | +|---------|------| +| `frontend/lib/models/parent_model.dart` | Parse `co_parent` → `AppUser? coParent` | +| `frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart` | Titre + sous-titre | +| `frontend/lib/services/user_service.dart` | `getParents()` / `getParent()` | + +--- + +## 7. Références + +- `docs/28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1 +- `backend/src/routes/parents/parents.service.ts` — `findOne`, `findAll` +- `backend/src/entities/parents.entity.ts` — relation `co_parent` +- Ticket Gitea **#131** diff --git a/frontend/lib/models/parent_model.dart b/frontend/lib/models/parent_model.dart index 1e52b8d..3a0cd5e 100644 --- a/frontend/lib/models/parent_model.dart +++ b/frontend/lib/models/parent_model.dart @@ -3,11 +3,13 @@ import 'package:p_tits_pas/models/user.dart'; class ParentModel { final AppUser user; + final AppUser? coParent; final int childrenCount; final List children; ParentModel({ required this.user, + this.coParent, this.childrenCount = 0, this.children = const [], }); @@ -20,12 +22,19 @@ class ParentModel { } final user = AppUser.fromJson(userJson); + AppUser? coParent; + final coParentRaw = root['co_parent']; + if (coParentRaw is Map) { + coParent = AppUser.fromJson(Map.from(coParentRaw)); + } + final children = _parseChildren(root); final links = root['parentChildren'] ?? root['parent_children']; final linkCount = links is List ? links.length : 0; return ParentModel( user: user, + coParent: coParent, childrenCount: children.isNotEmpty ? children.length : linkCount, children: children, ); 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 a12ffe5..41ceaa0 100644 --- a/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart +++ b/frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart @@ -3,6 +3,7 @@ import 'package:p_tits_pas/models/enfant_admin_model.dart'; import 'package:p_tits_pas/utils/phone_utils.dart'; 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/user.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_enfant_user_card.dart'; @@ -36,6 +37,7 @@ class _AdminParentEditModalState extends State { late String _statut; late List _children; + AppUser? _coParent; late final ScrollController _childrenScrollCtrl; bool _saving = false; bool _dirty = false; @@ -62,6 +64,7 @@ class _AdminParentEditModalState extends State { _villeCtrl = TextEditingController(text: u.ville ?? ''); _cpCtrl = TextEditingController(text: u.codePostal ?? ''); _statut = u.statut ?? 'en_attente'; + _coParent = widget.parent.coParent; _children = List.of(widget.parent.children); _childrenScrollCtrl = ScrollController(); for (final c in [ @@ -75,9 +78,15 @@ class _AdminParentEditModalState extends State { ]) { c.addListener(_markDirty); } + _nomCtrl.addListener(_onNameFieldChanged); + _prenomCtrl.addListener(_onNameFieldChanged); _reloadChildren(); } + void _onNameFieldChanged() { + setState(() {}); + } + @override void dispose() { for (final c in [ @@ -99,6 +108,22 @@ class _AdminParentEditModalState extends State { if (!_dirty) setState(() => _dirty = true); } + String _headerTitle() { + final fn = _prenomCtrl.text.trim(); + final ln = _nomCtrl.text.trim(); + if (fn.isEmpty && ln.isEmpty) { + final fallback = widget.parent.user.fullName.trim(); + return fallback.isNotEmpty ? fallback : 'Parent'; + } + return '$fn $ln'.trim(); + } + + String? _coParentSubtitle() { + final name = _coParent?.fullName.trim() ?? ''; + if (name.isEmpty) return null; + return 'Co-parent : $name'; + } + String _displayStatus(String status) { switch (status) { case 'actif': @@ -202,7 +227,10 @@ class _AdminParentEditModalState extends State { }).toList(); if (!mounted) return; - setState(() => _children = kids); + setState(() { + _children = kids; + _coParent = refreshed.coParent; + }); } catch (_) {} } @@ -445,15 +473,35 @@ class _AdminParentEditModalState extends State { mainAxisSize: MainAxisSize.min, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.fromLTRB(18, 16, 0, 10), - child: Text( - 'Fiche parent', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(18, 16, 12, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _headerTitle(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + if (_coParentSubtitle() != null) ...[ + const SizedBox(height: 4), + Text( + _coParentSubtitle()!, + style: const TextStyle( + fontSize: 13, + color: Colors.black54, + ), + ), + ], + ], + ), ), ), - const Spacer(), SizedBox(width: 160, child: _statusDropdown()), const SizedBox(width: 4), IconButton(