feat(admin): onglet À valider, dossier unifié et modales de validation
- Onglet « À valider » (AM + familles), pending-families et détail dossier par numéro. - Wizards validation AM et famille, modale commune, chargement via GET /dossiers/:num. - UI : cartes enfants, photo AM (cadre uniforme, ratio), NIR affiché formaté (espaces autour du tiret). - Backend : DTO / routes parents pending ; scripts de test et mise à jour issue Gitea. Tickets #107, #119. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test API GET /dossiers/:numeroDossier (dossier unifié AM ou famille).
|
||||
*
|
||||
* Prérequis : backend démarré (npm run start:dev dans backend/).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/test-api-dossiers.js
|
||||
* NUMERO_DOSSIER=2026-000001 node scripts/test-api-dossiers.js
|
||||
* BASE_URL=https://app.ptits-pas.fr/api/v1 TEST_EMAIL=xxx TEST_PASSWORD=yyy NUMERO_DOSSIER=2026-000001 node scripts/test-api-dossiers.js
|
||||
*
|
||||
* Sans TEST_EMAIL/TEST_PASSWORD : 401 sur les routes protégées.
|
||||
* NUMERO_DOSSIER : optionnel ; si absent, utilise le premier numero_dossier de pending-families (avec token).
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000/api/v1';
|
||||
const TEST_EMAIL = process.env.TEST_EMAIL;
|
||||
const TEST_PASSWORD = process.env.TEST_PASSWORD;
|
||||
const NUMERO_DOSSIER = process.env.NUMERO_DOSSIER;
|
||||
|
||||
async function request(method, path, body = null, token = null) {
|
||||
const url = path.startsWith('http') ? path : `${BASE_URL}${path}`;
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
};
|
||||
if (token) opts.headers.Authorization = `Bearer ${token}`;
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
const res = await fetch(url, opts);
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch (_) {
|
||||
data = text;
|
||||
}
|
||||
return { status: res.status, data };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Base URL:', BASE_URL);
|
||||
console.log('Numéro dossier (env):', NUMERO_DOSSIER ?? '(sera déduit si token fourni)');
|
||||
console.log('');
|
||||
|
||||
let token = null;
|
||||
if (TEST_EMAIL && TEST_PASSWORD) {
|
||||
console.log('1. Login...');
|
||||
const loginRes = await request('POST', '/auth/login', {
|
||||
email: TEST_EMAIL,
|
||||
password: TEST_PASSWORD,
|
||||
});
|
||||
if (loginRes.status !== 200 && loginRes.status !== 201) {
|
||||
console.log(' Échec login:', loginRes.status, loginRes.data);
|
||||
process.exit(1);
|
||||
}
|
||||
token = loginRes.data?.access_token ?? loginRes.data?.accessToken ?? null;
|
||||
if (!token) {
|
||||
console.log(' Réponse login sans token:', JSON.stringify(loginRes.data, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' OK, token reçu.');
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('TEST_EMAIL / TEST_PASSWORD non définis : GET /dossiers/:numero nécessite un token (401 attendu).');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
let numeroDossier = NUMERO_DOSSIER;
|
||||
if (!numeroDossier && token) {
|
||||
console.log('2. Récupération d\'un numéro de dossier (GET /parents/pending-families)...');
|
||||
const pendingRes = await request('GET', '/parents/pending-families', null, token);
|
||||
if (pendingRes.status === 200 && Array.isArray(pendingRes.data) && pendingRes.data.length > 0) {
|
||||
numeroDossier = pendingRes.data[0].numero_dossier || null;
|
||||
console.log(' Premier numero_dossier:', numeroDossier);
|
||||
} else {
|
||||
console.log(' Aucune famille en attente ou erreur. Utilisez NUMERO_DOSSIER=2026-000001');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (!numeroDossier) {
|
||||
numeroDossier = '2026-000001';
|
||||
console.log('2. Pas de numéro fourni, test avec numéro par défaut:', numeroDossier);
|
||||
} else {
|
||||
console.log('2. GET /dossiers/' + encodeURIComponent(numeroDossier));
|
||||
}
|
||||
|
||||
const dossierRes = await request(
|
||||
'GET',
|
||||
'/dossiers/' + encodeURIComponent(numeroDossier),
|
||||
null,
|
||||
token
|
||||
);
|
||||
|
||||
console.log(' Status:', dossierRes.status);
|
||||
if (dossierRes.status === 200 && dossierRes.data) {
|
||||
const d = dossierRes.data;
|
||||
console.log(' type:', d.type);
|
||||
console.log(' dossier (clés):', d.dossier ? Object.keys(d.dossier) : '-');
|
||||
if (d.dossier && Array.isArray(d.dossier.enfants)) {
|
||||
console.log(' enfants:', d.dossier.enfants.length);
|
||||
d.dossier.enfants.forEach((e, i) => {
|
||||
console.log(
|
||||
` [${i + 1}] id=${e.id} first_name=${e.first_name} last_name=${e.last_name} birth_date=${e.birth_date} gender=${e.gender} genre=${e.genre} status=${e.status}`
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
console.log('Réponse brute (dossier):');
|
||||
console.log(JSON.stringify(d.dossier, null, 2));
|
||||
} else {
|
||||
console.log(' Réponse:', JSON.stringify(dossierRes.data, null, 2));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Fin du test.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Erreur:', err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test des endpoints "comptes en attente" (ticket #107).
|
||||
*
|
||||
* Prérequis : backend démarré (npm run start:dev dans backend/).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/test-pending-api.js
|
||||
* TEST_EMAIL=xxx TEST_PASSWORD=yyy node scripts/test-pending-api.js
|
||||
* BASE_URL=https://app.ptits-pas.fr/api/v1 TEST_EMAIL=xxx TEST_PASSWORD=yyy node scripts/test-pending-api.js
|
||||
*
|
||||
* Sans TEST_EMAIL/TEST_PASSWORD : les GET protégés renverront 401 (normal).
|
||||
* Avec un compte gestionnaire ou admin : affiche les listes en attente.
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000/api/v1';
|
||||
const TEST_EMAIL = process.env.TEST_EMAIL;
|
||||
const TEST_PASSWORD = process.env.TEST_PASSWORD;
|
||||
|
||||
async function request(method, path, body = null, token = null) {
|
||||
const url = path.startsWith('http') ? path : `${BASE_URL}${path}`;
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
};
|
||||
if (token) opts.headers.Authorization = `Bearer ${token}`;
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
const res = await fetch(url, opts);
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch (_) {
|
||||
data = text;
|
||||
}
|
||||
return { status: res.status, data };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Base URL:', BASE_URL);
|
||||
console.log('');
|
||||
|
||||
let token = null;
|
||||
if (TEST_EMAIL && TEST_PASSWORD) {
|
||||
console.log('1. Login...');
|
||||
const loginRes = await request('POST', '/auth/login', {
|
||||
email: TEST_EMAIL,
|
||||
password: TEST_PASSWORD,
|
||||
});
|
||||
if (loginRes.status !== 200 && loginRes.status !== 201) {
|
||||
console.log(' Échec login:', loginRes.status, loginRes.data);
|
||||
process.exit(1);
|
||||
}
|
||||
token = loginRes.data?.access_token ?? loginRes.data?.accessToken ?? null;
|
||||
if (!token) {
|
||||
console.log(' Réponse login sans token:', JSON.stringify(loginRes.data, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' OK, token reçu.');
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('TEST_EMAIL / TEST_PASSWORD non définis : les appels protégés vont renvoyer 401.');
|
||||
console.log('Exemple: TEST_EMAIL=admin@example.com TEST_PASSWORD=xxx node scripts/test-pending-api.js');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('2. GET /users/pending?role=assistante_maternelle');
|
||||
const pendingUsersRes = await request(
|
||||
'GET',
|
||||
'/users/pending?role=assistante_maternelle',
|
||||
null,
|
||||
token
|
||||
);
|
||||
console.log(' Status:', pendingUsersRes.status);
|
||||
if (pendingUsersRes.status === 200) {
|
||||
const list = Array.isArray(pendingUsersRes.data) ? pendingUsersRes.data : [];
|
||||
console.log(' Nombre d\'utilisateurs en attente (AM):', list.length);
|
||||
list.forEach((u, i) => {
|
||||
console.log(
|
||||
` [${i + 1}] id=${u.id} email=${u.email} role=${u.role} statut=${u.statut} numero_dossier=${u.numero_dossier ?? '-'}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.log(' Réponse:', JSON.stringify(pendingUsersRes.data, null, 2));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
console.log('3. GET /parents/pending-families');
|
||||
const pendingFamiliesRes = await request('GET', '/parents/pending-families', null, token);
|
||||
console.log(' Status:', pendingFamiliesRes.status);
|
||||
if (pendingFamiliesRes.status === 200) {
|
||||
const list = Array.isArray(pendingFamiliesRes.data) ? pendingFamiliesRes.data : [];
|
||||
console.log(' Nombre de familles en attente:', list.length);
|
||||
list.forEach((f, i) => {
|
||||
console.log(
|
||||
` [${i + 1}] libelle=${f.libelle} parentIds=${JSON.stringify(f.parentIds)} numero_dossier=${f.numero_dossier ?? '-'}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.log(' Réponse:', JSON.stringify(pendingFamiliesRes.data, null, 2));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Fin du test.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Erreur:', err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Met à jour l'issue Gitea #119 : endpoint unifié GET /dossiers/:numeroDossier (option A)
|
||||
* Usage: node backend/scripts/update-gitea-issue-119-dossiers.js
|
||||
* Token : .gitea-token (racine), GITEA_TOKEN, ou docs/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/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é');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const body = `## Besoin
|
||||
|
||||
Un **seul** endpoint **GET par numéro de dossier** qui renvoie le dossier complet, **AM ou famille** selon le numéro. Clé unique = numéro de dossier (usage : modale de validation, consultation gestionnaire, reprise, etc.).
|
||||
|
||||
**Option A – Endpoint unifié**
|
||||
|
||||
- **Route** : \`GET /api/v1/dossiers/:numeroDossier\` (ou \`GET /dossiers/:numeroDossier\` selon préfixe API).
|
||||
- Le backend détermine si le numéro appartient à une **AM** ou à une **famille** (ex. lookup \`users\` / \`parents\` / \`assistantes_maternelles\`).
|
||||
- **Réponse** avec discriminent :
|
||||
- \`{ type: 'family', dossier: { numero_dossier, parents, enfants, presentation } }\`
|
||||
- \`{ type: 'am', dossier: { numero_dossier, user, ... } }\` (fiche AM complète, champs utiles sans secrets)
|
||||
- **Rôles** : SUPER_ADMIN, ADMINISTRATEUR, GESTIONNAIRE.
|
||||
- **Réponses** : 200 (dossier), 403, 404 (numéro inconnu).
|
||||
|
||||
Aucun filtre par statut : on renvoie le dossier s'il existe ; le front affiche Valider/Refuser selon le statut.
|
||||
|
||||
**Labels suggérés** : backend, api, dossiers, gestionnaire
|
||||
|
||||
---
|
||||
|
||||
## Implémentation
|
||||
|
||||
- **Nouveau module ou route** : \`GET /dossiers/:numeroDossier\`.
|
||||
- **Service** : trouver qui possède ce \`numero_dossier\` (famille → \`parents\`, AM → \`users\` + \`assistantes_maternelles\`). Appeler la logique existante dossier-famille ou construire le payload AM, puis retourner \`{ type, dossier }\`.
|
||||
- **Réutiliser** : la logique actuelle \`GET /parents/dossier-famille/:numeroDossier\` peut être appelée en interne pour \`type: 'family'\` ; ajouter une branche \`type: 'am'\` avec un DTO « dossier AM complet ».
|
||||
- DTO(s) : garder \`DossierFamilleCompletDto\` pour la famille ; ajouter un DTO pour le dossier AM (user sans secrets + infos AM). Réponse unifiée : \`{ type: 'am' | 'family', dossier: ... }\`.`;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title: 'Endpoint unifié GET /dossiers/:numeroDossier (AM ou famille)',
|
||||
body,
|
||||
});
|
||||
|
||||
const opts = {
|
||||
hostname: 'git.ptits-pas.fr',
|
||||
path: '/api/v1/repos/jmartin/petitspas/issues/119',
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: 'token ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(opts, (res) => {
|
||||
let d = '';
|
||||
res.on('data', (c) => (d += c));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const o = JSON.parse(d);
|
||||
if (o.number || o.id) {
|
||||
console.log('Issue #119 mise à jour.');
|
||||
console.log('URL:', o.html_url || 'https://git.ptits-pas.fr/jmartin/petitspas/issues/119');
|
||||
} else {
|
||||
console.error('Erreur API:', o.message || d);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Réponse:', d);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
req.write(payload);
|
||||
req.end();
|
||||
Reference in New Issue
Block a user