feat(#131): en-tête fiche parent avec nom et co-parent.
Affiche le prénom/nom du parent en titre et le co-parent en sous-titre ; note handoff back dans archive/temporaires. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d55f240f56
commit
ebf794e1ac
124
docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md
Normal file
124
docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md
Normal file
@ -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**
|
||||||
@ -3,11 +3,13 @@ import 'package:p_tits_pas/models/user.dart';
|
|||||||
|
|
||||||
class ParentModel {
|
class ParentModel {
|
||||||
final AppUser user;
|
final AppUser user;
|
||||||
|
final AppUser? coParent;
|
||||||
final int childrenCount;
|
final int childrenCount;
|
||||||
final List<ParentChildSummary> children;
|
final List<ParentChildSummary> children;
|
||||||
|
|
||||||
ParentModel({
|
ParentModel({
|
||||||
required this.user,
|
required this.user,
|
||||||
|
this.coParent,
|
||||||
this.childrenCount = 0,
|
this.childrenCount = 0,
|
||||||
this.children = const [],
|
this.children = const [],
|
||||||
});
|
});
|
||||||
@ -20,12 +22,19 @@ class ParentModel {
|
|||||||
}
|
}
|
||||||
final user = AppUser.fromJson(userJson);
|
final user = AppUser.fromJson(userJson);
|
||||||
|
|
||||||
|
AppUser? coParent;
|
||||||
|
final coParentRaw = root['co_parent'];
|
||||||
|
if (coParentRaw is Map) {
|
||||||
|
coParent = AppUser.fromJson(Map<String, dynamic>.from(coParentRaw));
|
||||||
|
}
|
||||||
|
|
||||||
final children = _parseChildren(root);
|
final children = _parseChildren(root);
|
||||||
final links = root['parentChildren'] ?? root['parent_children'];
|
final links = root['parentChildren'] ?? root['parent_children'];
|
||||||
final linkCount = links is List ? links.length : 0;
|
final linkCount = links is List ? links.length : 0;
|
||||||
|
|
||||||
return ParentModel(
|
return ParentModel(
|
||||||
user: user,
|
user: user,
|
||||||
|
coParent: coParent,
|
||||||
childrenCount: children.isNotEmpty ? children.length : linkCount,
|
childrenCount: children.isNotEmpty ? children.length : linkCount,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/models/parent_child_summary.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/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/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_enfant_user_card.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
||||||
@ -36,6 +37,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
|
|
||||||
late String _statut;
|
late String _statut;
|
||||||
late List<ParentChildSummary> _children;
|
late List<ParentChildSummary> _children;
|
||||||
|
AppUser? _coParent;
|
||||||
late final ScrollController _childrenScrollCtrl;
|
late final ScrollController _childrenScrollCtrl;
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
@ -62,6 +64,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
_villeCtrl = TextEditingController(text: u.ville ?? '');
|
_villeCtrl = TextEditingController(text: u.ville ?? '');
|
||||||
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
||||||
_statut = u.statut ?? 'en_attente';
|
_statut = u.statut ?? 'en_attente';
|
||||||
|
_coParent = widget.parent.coParent;
|
||||||
_children = List.of(widget.parent.children);
|
_children = List.of(widget.parent.children);
|
||||||
_childrenScrollCtrl = ScrollController();
|
_childrenScrollCtrl = ScrollController();
|
||||||
for (final c in [
|
for (final c in [
|
||||||
@ -75,9 +78,15 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
]) {
|
]) {
|
||||||
c.addListener(_markDirty);
|
c.addListener(_markDirty);
|
||||||
}
|
}
|
||||||
|
_nomCtrl.addListener(_onNameFieldChanged);
|
||||||
|
_prenomCtrl.addListener(_onNameFieldChanged);
|
||||||
_reloadChildren();
|
_reloadChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNameFieldChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (final c in [
|
for (final c in [
|
||||||
@ -99,6 +108,22 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
if (!_dirty) setState(() => _dirty = true);
|
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) {
|
String _displayStatus(String status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'actif':
|
case 'actif':
|
||||||
@ -202,7 +227,10 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _children = kids);
|
setState(() {
|
||||||
|
_children = kids;
|
||||||
|
_coParent = refreshed.coParent;
|
||||||
|
});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,15 +473,35 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Expanded(
|
||||||
padding: EdgeInsets.fromLTRB(18, 16, 0, 10),
|
child: Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.fromLTRB(18, 16, 12, 10),
|
||||||
'Fiche parent',
|
child: Column(
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
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()),
|
SizedBox(width: 160, child: _statusDropdown()),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user