Compare commits

..

6 Commits

Author SHA1 Message Date
99a6c17c23 feat(#157): enfants sans responsable — alerte liste et détach dernier parent.
Squash depuis develop : détachement dernier parent autorisé, flag
sans_responsable, vigilance liste Enfants, rattachement foyer depuis
la fiche, polish détach et compteur foyer interim (#158 à suivre).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 19:43:58 +02:00
04f49cb62f feat(#132): création enfant staff (onglet Enfants) — front + back.
Squash depuis develop : POST /enfants staff + parent_user_id, photo
multipart, modale création + sélection famille, fixes UX/birth_date.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 19:00:12 +02:00
3c7f4f6e16 fix(#151): autoriser GET /relais pour gestionnaire + robustesse combo
Permet au gestionnaire de peupler la liste Relais (création/édition).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:54:17 +02:00
dcd407a3da feat(#146–#149): sélection enfant/AM, capacité max et case libre.
Modales de rattachement partagées, filtre Libre/Sans garde, recalcul des places à l'attach, désactivation si capacité pleine, clic sur case libre.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:25:42 +02:00
fde63f8e72 feat(#145): lien co-parent cliquable dans la fiche parent.
Squash merge develop → master.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 14:56:45 +02:00
1f8f1b9507 feat(#138,#143,#144): fiche enfant admin, consentement photo et fix gestionnaire.
Squash merge develop → master.
- #138 : modale enfant paysage, zone AM/scolarisation, liens responsables
- #144 : persistance consent_photo à l'inscription enfant
- #143 : masquer Supprimer sur la propre fiche gestionnaire

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:21:43 +02:00
3 changed files with 27 additions and 82 deletions

View File

@ -114,86 +114,36 @@ export class ParentsService {
} }
/** /**
* Membres du foyer (user ids) pour affiliation enfant. * Rattacher un enfant existant à un parent (enfants_parents). Ticket #115 / doc 28 §6.2.
* Pivot + co-parent (AB et BA) + même numero_dossier. Ticket #158.
*/
private async resolveFoyerParentUserIds(parent: Parents): Promise<string[]> {
const ids = new Set<string>([parent.user_id]);
if (parent.co_parent?.id) {
ids.add(parent.co_parent.id);
}
// 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> { async attachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
const parent = await this.findOne(parentUserId); await this.findOne(parentUserId);
const child = await this.parentsRepository.manager.findOne(Children, { const existing = await this.parentsChildrenRepository.findOne({
where: { id: enfantId }, where: { parentId: parentUserId, enfantId },
}); });
if (existing) {
throw new ConflictException('Cet enfant est déjà rattaché à ce parent');
}
const child = await this.parentsRepository.manager.findOne(Children, { where: { id: enfantId } });
if (!child) { if (!child) {
throw new NotFoundException('Enfant introuvable'); 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( await this.parentsChildrenRepository.save(
this.parentsChildrenRepository.create({ this.parentsChildrenRepository.create({ parentId: parentUserId, enfantId }),
parentId: memberId,
enfantId,
}),
); );
created += 1;
}
if (created === 0) {
throw new ConflictException('Cet enfant est déjà rattaché à ce foyer');
}
return this.findOne(parentUserId); return this.findOne(parentUserId);
} }
/** /**
* Détacher un enfant du foyer du parent (tous les responsables). Ticket #158. * Détacher un enfant d'un parent sans supprimer l'enfant.
* Si plus aucun lien ensuite enfant orphelin (#157). * Autorise le détachement du dernier responsable (#157) l'enfant reste listé
* via GET /enfants avec parentLinks vides (alerte front).
*/ */
async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> { async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
const parent = await this.findOne(parentUserId); await this.findOne(parentUserId);
const link = await this.parentsChildrenRepository.findOne({ const link = await this.parentsChildrenRepository.findOne({
where: { parentId: parentUserId, enfantId }, where: { parentId: parentUserId, enfantId },
@ -202,12 +152,7 @@ export class ParentsService {
throw new NotFoundException('Lien parent-enfant introuvable'); throw new NotFoundException('Lien parent-enfant introuvable');
} }
const foyerIds = await this.resolveFoyerParentUserIds(parent); await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId });
await this.parentsChildrenRepository.delete({
parentId: In(foyerIds),
enfantId,
});
return this.findOne(parentUserId); return this.findOne(parentUserId);
} }

View File

@ -1277,16 +1277,18 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
return; return;
} }
// Édition orphelin (#157/#158) : un seul attach le back propage au foyer. // Édition orphelin (#157) : rattache immédiatement au foyer.
final enfantId = widget.enfant?.id; final enfantId = widget.enfant?.id;
if (enfantId == null || enfantId.isEmpty) return; if (enfantId == null || enfantId.isEmpty) return;
setState(() => _saving = true); setState(() => _saving = true);
try { try {
for (final parentId in selected.parentUserIds) {
await UserService.attachEnfantToParent( await UserService.attachEnfantToParent(
parentUserId: selected.pivotParentUserId, parentUserId: parentId,
enfantId: enfantId, enfantId: enfantId,
); );
}
final refreshed = await UserService.getEnfant(enfantId); final refreshed = await UserService.getEnfant(enfantId);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {

View File

@ -283,7 +283,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Détacher l\'enfant'), title: const Text('Détacher l\'enfant'),
content: Text( content: Text(
'Retirer ${child.fullName} du foyer (tous les responsables) ?', 'Retirer ${child.fullName} de la fiche de ce parent ?',
), ),
actions: [ actions: [
TextButton( TextButton(
@ -308,9 +308,8 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
if (!mounted) return; if (!mounted) return;
await _reloadChildren(); await _reloadChildren();
if (!mounted) return; if (!mounted) return;
widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant détaché du foyer')), const SnackBar(content: Text('Enfant détaché')),
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
@ -337,9 +336,8 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
if (!mounted) return; if (!mounted) return;
await _reloadChildren(); await _reloadChildren();
if (!mounted) return; if (!mounted) return;
widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant rattaché au foyer')), const SnackBar(content: Text('Enfant rattaché')),
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;