feat(#158): affiliation enfant au foyer — attach/detach pivot + co-parent.
Squash depuis develop : un POST/DELETE propage les liens à tout le foyer ; front un seul appel API ; doc empreinte. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
99a6c17c23
commit
846afed86c
@ -114,36 +114,86 @@ export class ParentsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rattacher un enfant existant à un parent (enfants_parents). Ticket #115 / doc 28 §6.2.
|
||||
* Membres du foyer (user ids) pour affiliation enfant.
|
||||
* Pivot + co-parent (A→B et B→A) + même numero_dossier. Ticket #158.
|
||||
*/
|
||||
async attachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
|
||||
await this.findOne(parentUserId);
|
||||
private async resolveFoyerParentUserIds(parent: Parents): Promise<string[]> {
|
||||
const ids = new Set<string>([parent.user_id]);
|
||||
|
||||
const existing = await this.parentsChildrenRepository.findOne({
|
||||
where: { parentId: parentUserId, enfantId },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('Cet enfant est déjà rattaché à ce parent');
|
||||
if (parent.co_parent?.id) {
|
||||
ids.add(parent.co_parent.id);
|
||||
}
|
||||
|
||||
const child = await this.parentsRepository.manager.findOne(Children, { where: { id: enfantId } });
|
||||
// Sens inverse : parents qui déclarent ce user comme co-parent
|
||||
const reverseLinks = await this.parentsRepository.find({
|
||||
where: { co_parent: { id: parent.user_id } },
|
||||
relations: ['co_parent'],
|
||||
});
|
||||
for (const p of reverseLinks) {
|
||||
ids.add(p.user_id);
|
||||
if (p.co_parent?.id) ids.add(p.co_parent.id);
|
||||
}
|
||||
|
||||
const dossier = parent.numero_dossier?.trim();
|
||||
if (dossier) {
|
||||
const sameDossier = await this.parentsRepository.find({
|
||||
where: { numero_dossier: dossier },
|
||||
relations: ['co_parent'],
|
||||
});
|
||||
for (const p of sameDossier) {
|
||||
ids.add(p.user_id);
|
||||
if (p.co_parent?.id) ids.add(p.co_parent.id);
|
||||
}
|
||||
}
|
||||
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rattacher un enfant au foyer du parent (tous les responsables). Ticket #158.
|
||||
* Un seul POST suffit : liens créés pour pivot + co-parent / même dossier.
|
||||
*/
|
||||
async attachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
|
||||
const parent = await this.findOne(parentUserId);
|
||||
|
||||
const child = await this.parentsRepository.manager.findOne(Children, {
|
||||
where: { id: enfantId },
|
||||
});
|
||||
if (!child) {
|
||||
throw new NotFoundException('Enfant introuvable');
|
||||
}
|
||||
|
||||
const foyerIds = await this.resolveFoyerParentUserIds(parent);
|
||||
let created = 0;
|
||||
|
||||
for (const memberId of foyerIds) {
|
||||
const existing = await this.parentsChildrenRepository.findOne({
|
||||
where: { parentId: memberId, enfantId },
|
||||
});
|
||||
if (existing) continue;
|
||||
|
||||
await this.parentsChildrenRepository.save(
|
||||
this.parentsChildrenRepository.create({ parentId: parentUserId, enfantId }),
|
||||
this.parentsChildrenRepository.create({
|
||||
parentId: memberId,
|
||||
enfantId,
|
||||
}),
|
||||
);
|
||||
created += 1;
|
||||
}
|
||||
|
||||
if (created === 0) {
|
||||
throw new ConflictException('Cet enfant est déjà rattaché à ce foyer');
|
||||
}
|
||||
|
||||
return this.findOne(parentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détacher un enfant d'un parent sans supprimer l'enfant.
|
||||
* Autorise le détachement du dernier responsable (#157) — l'enfant reste listé
|
||||
* via GET /enfants avec parentLinks vides (alerte front).
|
||||
* Détacher un enfant du foyer du parent (tous les responsables). Ticket #158.
|
||||
* Si plus aucun lien ensuite → enfant orphelin (#157).
|
||||
*/
|
||||
async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
|
||||
await this.findOne(parentUserId);
|
||||
const parent = await this.findOne(parentUserId);
|
||||
|
||||
const link = await this.parentsChildrenRepository.findOne({
|
||||
where: { parentId: parentUserId, enfantId },
|
||||
@ -152,7 +202,12 @@ export class ParentsService {
|
||||
throw new NotFoundException('Lien parent-enfant introuvable');
|
||||
}
|
||||
|
||||
await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId });
|
||||
const foyerIds = await this.resolveFoyerParentUserIds(parent);
|
||||
await this.parentsChildrenRepository.delete({
|
||||
parentId: In(foyerIds),
|
||||
enfantId,
|
||||
});
|
||||
|
||||
return this.findOne(parentUserId);
|
||||
}
|
||||
|
||||
|
||||
104
docs/métriques/2026-07-22_analyse-empreinte.md
Normal file
104
docs/métriques/2026-07-22_analyse-empreinte.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Analyse d’empreinte — P'titsPas
|
||||
|
||||
**Date :** 2026-07-22
|
||||
**Contexte :** snapshot pour mémoire (après alerte disque plein + purge cache Gitea)
|
||||
**Périmètre :** application `jmartin/petitspas` déployée sur le VPS (`/home/deploy/dev/ptitspas-app`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Lignes de code
|
||||
|
||||
Comptage brut (`wc -l`), hors `node_modules` / builds / `.dart_tool`.
|
||||
|
||||
| Zone | Lignes | Fichiers |
|
||||
|------|--------|----------|
|
||||
| **Frontend** (Dart `frontend/lib/`) | ~29 800 | 141 |
|
||||
| **Backend** (TypeScript `backend/src/`) | ~10 000 | 141 |
|
||||
| **BDD** (SQL sous `database/`) | ~1 250 | ~15 |
|
||||
| **Total** | **~41 000** | |
|
||||
|
||||
### Frontend (détail)
|
||||
|
||||
| Dossier | Lignes |
|
||||
|---------|--------|
|
||||
| `widgets/` | ~18 400 |
|
||||
| `screens/` | ~5 000 |
|
||||
| `services/` | ~2 300 |
|
||||
| `models/` | ~2 200 |
|
||||
| `utils/` | ~1 400 |
|
||||
| reste | ~500 |
|
||||
|
||||
### Backend (détail)
|
||||
|
||||
| Dossier | Lignes |
|
||||
|---------|--------|
|
||||
| `routes/` | ~6 500 |
|
||||
| `modules/` | ~1 700 |
|
||||
| `entities/` | ~1 000 |
|
||||
| `common/` + `config/` | ~400 |
|
||||
|
||||
### BDD (détail)
|
||||
|
||||
| Élément | Lignes |
|
||||
|---------|--------|
|
||||
| `BDD.sql` (schéma) | ~470 |
|
||||
| seeds | ~300 |
|
||||
| migrations / patches | ~280 |
|
||||
| tests SQL | ~205 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Empreinte disque — runtime (Docker)
|
||||
|
||||
| Composant | Taille | Notes |
|
||||
|-----------|--------|--------|
|
||||
| Image `ptitspas-app-backend` | ~300 Mo | NestJS |
|
||||
| Image `ptitspas-app-frontend` | ~122 Mo | Flutter web + Nginx |
|
||||
| Image `postgres:17` | ~454 Mo | |
|
||||
| Image `dpage/pgadmin4` | ~534 Mo | optionnel |
|
||||
| Volume `postgres_data` | ~49 Mo | données BDD live |
|
||||
| Volume `backend_uploads` | ~20 Mo | photos |
|
||||
| Volume `backend_documents_legaux` | ~0 | |
|
||||
| **Stack complète (avec pgAdmin)** | **~1,5 Go** | |
|
||||
| **Stack prod sans pgAdmin** | **~945 Mo** | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Empreinte disque — code source
|
||||
|
||||
| Élément | Taille |
|
||||
|---------|--------|
|
||||
| Clone `/home/deploy/dev/ptitspas-app` | ~701 Mo |
|
||||
| dont `backend/node_modules` | ~354 Mo |
|
||||
| dont `frontend` (+ `.dart_tool`) | ~114 Mo |
|
||||
| **Code utile** (hors deps / `.git` / builds) | **~51 Mo** |
|
||||
| Repo Gitea `petitspas.git` | ~109 Mo |
|
||||
| Dump `database/BDD.sql` | ~19 Ko |
|
||||
|
||||
---
|
||||
|
||||
## 4. Pour (re)déployer
|
||||
|
||||
Minimum requis :
|
||||
|
||||
1. Images custom backend + frontend (~422 Mo), ou code + build Docker
|
||||
2. Image `postgres:17` (~454 Mo)
|
||||
3. Volumes persistants (~70 Mo au snapshot)
|
||||
4. Optionnel : pgAdmin (~534 Mo)
|
||||
|
||||
**Ordre de grandeur :** ~1 Go pour faire tourner l’app en prod (sans pgAdmin).
|
||||
|
||||
---
|
||||
|
||||
## 5. Notes infra du jour (lié)
|
||||
|
||||
- Disque VPS passé de **100 %** à ~**44 %** après :
|
||||
- purge cache Docker / journals
|
||||
- purge officielle Gitea `delete_repo_archives` (~24 Go de zip/bundle `petitspas`)
|
||||
- Crons Gitea activés :
|
||||
- `archive_cleanup` @midnight (`OLDER_THAN = 24h`)
|
||||
- `delete_repo_archives` @weekly
|
||||
|
||||
---
|
||||
|
||||
*Fichier généré pour historique projet — ne pas considérer comme métrique CI automatisée.*
|
||||
@ -1277,18 +1277,16 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Édition orphelin (#157) : rattache immédiatement au foyer.
|
||||
// Édition orphelin (#157/#158) : un seul attach — le back propage au foyer.
|
||||
final enfantId = widget.enfant?.id;
|
||||
if (enfantId == null || enfantId.isEmpty) return;
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
for (final parentId in selected.parentUserIds) {
|
||||
await UserService.attachEnfantToParent(
|
||||
parentUserId: parentId,
|
||||
parentUserId: selected.pivotParentUserId,
|
||||
enfantId: enfantId,
|
||||
);
|
||||
}
|
||||
final refreshed = await UserService.getEnfant(enfantId);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
||||
@ -283,7 +283,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Détacher l\'enfant'),
|
||||
content: Text(
|
||||
'Retirer ${child.fullName} de la fiche de ce parent ?',
|
||||
'Retirer ${child.fullName} du foyer (tous les responsables) ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -308,8 +308,9 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
if (!mounted) return;
|
||||
await _reloadChildren();
|
||||
if (!mounted) return;
|
||||
widget.onSaved?.call();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enfant détaché')),
|
||||
const SnackBar(content: Text('Enfant détaché du foyer')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@ -336,8 +337,9 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
||||
if (!mounted) return;
|
||||
await _reloadChildren();
|
||||
if (!mounted) return;
|
||||
widget.onSaved?.call();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enfant rattaché')),
|
||||
const SnackBar(content: Text('Enfant rattaché au foyer')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user