feat(#131): fiches parent/AM éditable, placement AM↔enfant, statuts garde/sans_garde

Squash merge develop → master.

- Fiche parent éditable (co-parent, PATCH fiche, GET /parents)
- Fiche AM 3 onglets (PATCH fiche, rattacher/détacher enfants)
- Table enfants_assistantes_maternelles + enum garde/sans_garde
- Migration SQL + BDD.sql canonique
- Correctifs recette : @Get() parents, DTO fiche AM, fix NIR

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 22:49:57 +02:00
co-authored by Cursor
parent b99745e0fe
commit 003fe6b762
69 changed files with 6290 additions and 417 deletions
+123
View File
@@ -0,0 +1,123 @@
/**
* Commentaire de clôture + fermeture issue Gitea #131.
* Usage: node backend/scripts/close-gitea-issue-131.js
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const repoRoot = path.join(__dirname, '../..');
const ISSUE = 131;
const REPO = 'jmartin/petitspas';
const body = `## Fermeture ticket #131 — livré sur \`develop\` et \`master\`
Branche **\`feature/131\`** mergée dans **\`develop\`**, puis squash merge **\`develop\`\`master\`** (déploiement production).
### Fiche parent (dashboard admin)
- Modale **\`AdminParentEditModal\`** éditable dès l'ouverture
- En-tête dynamique : **nom/prénom** + sous-titre **co-parent** (si connu)
- Gélule **statut** modifiable
- **\`PATCH /api/v1/parents/:id/fiche\`** — identité + statut
- **\`GET /api/v1/parents\`** — liste parents (fix décorateur \`@Get()\` manquant)
- Réponses API : \`co_parent\` peuplé, secrets user masqués (\`sanitizeUserForApi\`)
- Liste enfants en bas de fiche + rattachement/détachement
### Fiche AM (dashboard admin)
- Modale **\`AdminAmEditModal\`** — 3 onglets : Identité | Fiche pro | Enfants accueillis
- **\`PATCH /api/v1/assistantes-maternelles/:id/fiche\`** — identité + champs pro (NIR, date/lieu naissance, date agrément, places, disponibilité)
- **\`POST/DELETE …/enfants/:enfantId\`** — rattacher / détacher un enfant
- Grille capacité **\`AdminAmChildrenCapacityGrid\`** (2×2)
- Rattachement enfants **différé jusqu'à Sauvegarder** (pas d'appel API immédiat)
### Back — placement AM ↔ enfant
- Table **\`enfants_assistantes_maternelles\`** (placement temporel, 1 garde active/enfant)
- Enum enfant : \`a_naitre\`, \`garde\`, \`sans_garde\`, \`scolarise\` — **plus \`actif\`**
- Rattachement → statut enfant \`garde\` ; détachement → \`sans_garde\`
- Migration : \`database/migrations/2026_enfants_assistantes_maternelles.sql\`
- Schéma canonique : \`database/BDD.sql\`
### Front — statuts & polish
- **\`enfant_status_utils.dart\`** — libellés/couleurs \`garde\`/\`sans_garde\`
- Mise à jour cartes enfants, modale détail, filtres dashboard
### Correctifs recette
- \`GET /parents\` 404 → ajout \`@Get()\` sur \`getAll()\`
- Sauvegarde AM 400 → alignement DTO \`UpdateAmFicheAdminDto\` sur payload front
- Crash NIR → fix \`toISOString\` + pas d'effacement NIR (colonne NOT NULL)
### Hors périmètre #131 (tickets voisins)
- **#137** onglet Enfants global · **#138** fiche enfant complète · **#115/#116** affiliation avancée
---
*Issue fermée après merge sur \`develop\` + \`master\` et déploiement production.*`;
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');
process.exit(1);
}
function request(method, apiPath, payloadObj) {
const payload = payloadObj ? JSON.stringify(payloadObj) : null;
return new Promise((resolve, reject) => {
const opts = {
hostname: 'git.ptits-pas.fr',
path: `/api/v1/repos/${REPO}${apiPath}`,
method,
headers: {
Authorization: 'token ' + token,
'Content-Type': 'application/json',
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
},
};
const req = https.request(opts, (res) => {
let d = '';
res.on('data', (c) => (d += c));
res.on('end', () => {
if (res.statusCode !== 200 && res.statusCode !== 201) {
reject(new Error(`HTTP ${res.statusCode}: ${d}`));
return;
}
try {
resolve(d ? JSON.parse(d) : {});
} catch (_) {
resolve({});
}
});
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
(async () => {
try {
console.log(`POST commentaire issue #${ISSUE}...`);
await request('POST', `/issues/${ISSUE}/comments`, { body });
console.log('Commentaire publié.');
console.log(`PATCH fermeture issue #${ISSUE}...`);
await request('PATCH', `/issues/${ISSUE}`, { state: 'closed' });
console.log(`Issue #${ISSUE} fermée.`);
} catch (e) {
console.error(e.message || e);
process.exit(1);
}
})();
@@ -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();