Compare commits

..

57 Commits

Author SHA1 Message Date
7a3d997ff9 fix(#132): UX création enfant — champs, genre et validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:51:00 +02:00
6b89d7b405 fix(#132): envoyer birth_date à la création (enfant né).
Le if imbriqué n’incluait birth_date que pour le cas à naître.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:46:48 +02:00
f7ac628445 feat(#132): photo à la création enfant staff (multipart + bools).
Transform "true"/"false" pour class-validator ; multipart staff
avec FileInterceptor('photo') inchangé côté stockage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:08:52 +02:00
26e9738ec7 feat(#132): upload photo à la création d'enfant (admin).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:07:47 +02:00
0ec3410457 feat(#132): POST /enfants staff — parent_user_id + liens foyer.
Autorise GESTIONNAIRE/ADMIN/SUPER_ADMIN à créer un enfant rattaché
à un foyer existant (JSON + parent_user_id). Parent multipart inchangé.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:03:41 +02:00
c3acf1970d feat(#132): création enfant admin — modale + sélection famille.
Onglet Enfants « Ajouter » ouvre la fiche en mode création ; bloc Famille/dossier ; client POST /enfants avec parent_user_id (contrat back à livrer).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:00:41 +02:00
6fe2b89a61 fix(#151): autoriser GET /relais pour gestionnaire + robustesse combo
- Back: RoleType.GESTIONNAIRE sur GET list/get (CRUD write admin-only)
- Front: ne plus effacer la sélection si le chargement échoue
- Fallback affichage depuis relaisNom de l'utilisateur

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:52:46 +02:00
d6a9b3fd66 Merge branch 'feature/149-clic-case-libre-rattacher-enfant' into develop
Clic case libre → rattacher enfant (#149).
2026-07-17 16:19:14 +02:00
134b9781c8 feat(#149): clic sur case libre pour rattacher un enfant (fiche AM).
Les emplacements vides de la grille ouvrent le même flux que le lien footer, avec hover et respect de la capacité max.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:19:12 +02:00
b936d27445 Merge branch 'feature/148-rattacher-enfant-capacite-max' into develop
Fix capacité max — lien rattacher enfant (#148).
2026-07-17 16:12:54 +02:00
18c1d1eba7 fix(#148): désactiver « Rattacher un enfant » si capacité AM pleine.
Le lien du footer est grisé avec tooltip quand enfants ≥ capacité max ; il se réactive après détachement.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:12:53 +02:00
f74b14c203 Merge branch 'feature/147-modale-selection-am-enfant' into develop
Modale de sélection d'AM (#147) depuis la fiche enfant.
2026-07-17 16:12:23 +02:00
f194b5f9e8 feat(#147): modale de sélection d'AM depuis la fiche enfant.
Réutilise AdminSelectListModal avec filtre Libre, cartes saturées en rouge, avertissement + lien fiche AM, et recalcul des places à chaque rattachement.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:11:55 +02:00
5ab8ae3423 Merge branch 'feature/146-modale-selection-enfant-am' into develop
Modale de sélection d'enfant (#146) pour rattachement AM/parent.
2026-07-17 15:43:10 +02:00
3277f77846 fix(#146): densifier la liste et corriger l'affichage des responsables.
Les cartes compactes montrent plus d'enfants d'un coup ; le nom ne monopolise plus la largeur au détriment des infos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 15:39:59 +02:00
2d80ad0d7e feat(#146): modale de sélection d'enfant pour rattachement AM/parent.
Recherche dynamique, filtre Sans garde côté AM, confirmation de transfert et détachement préalable pour laisser le point de vigilance places sur l'AM d'origine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 15:31:47 +02:00
09386f8aa6 feat(#145): lien co-parent cliquable dans la fiche parent.
Même UX que les responsables de la fiche enfant : ouverture de
AdminParentEditModal au clic sur le nom du co-parent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 14:56:32 +02:00
faa50f637c fix(#138): enrichir les noms des responsables sur GET enfant.
GET /enfants/:id ne joint pas parent.user ; on complète les noms via
GET parent pour l'affichage des liens (ex. ouverture depuis la fiche AM).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:19:09 +02:00
267fe63aec fix(#138): différer le rattachement AM de la fiche enfant au Sauvegarder.
Attach/détach AM restent locaux comme sur la fiche AM ; la sync API
n'a lieu qu'à l'enregistrement, avec rechargement du statut au retour
de la fiche AM.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:12:46 +02:00
90b185740c feat(#138): ouvrir la fiche parent depuis les responsables de la modale enfant.
Les noms du sous-titre deviennent des liens cliquables vers
AdminParentEditModal, comme l'ouverture de fiche AM.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:54:32 +02:00
b903dbf60b fix(#138): libellé garde accordé au genre (Gardé / Gardée).
Remplace « En garde » par Gardé/Gardée selon le genre, comme
Scolarisé/Scolarisée.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:51:07 +02:00
291ea26b34 fix(#138): lire le consentement photo sans heuristique photo.
Admin et reprise utilisentent uniquement la valeur API ; plus de
pré-cochage implicite si une photo est présente.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:46:54 +02:00
59919834b9 fix(#138): carte AM dédiée, titre placement et consent_photo au payload.
Améliore la zone AM/scolarisation (carte dédiée + titre), envoie
consent_photo à l'inscription parent, et parse le consentement enfant
de façon plus robuste (heuristique photo tant que le back ne persiste pas).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:45 +02:00
cf6acbae7c fix(#138): cadre vide en sans garde et passage auto en garde à l'attach AM.
En statut sans_garde, la zone placement affiche toujours le cadre vide ;
sélectionner une AM passe le statut à en garde, et passer à sans_garde
détache l'AM rattachée.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:45 +02:00
7951c38d4d feat(#138): refonte modale enfant admin en format paysage.
Alignement sur les fiches AM/parent : photo identité, champs en grille,
zone AM rattachable (y compris à naître) ou carte scolarisation,
suppression réservée au super_admin et ouverture au clic sur la carte
depuis la fiche parent.

Refs: #138
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:45 +02:00
77d952a6f7 fix(#144): persister le consentement photo des enfants
- DTO inscription enfant : champ consent_photo
- Inscription + reprise parent : sauvegarde bool + date
- Front payload : envoi consent_photo
- PATCH /enfants : horodatage du consentement

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:36:05 +02:00
b4b546044d merge fix(#143): masquer Supprimer gestionnaire sur sa propre fiche 2026-07-12 12:10:11 +02:00
6788351070 fix(#143): masquer Supprimer quand un gestionnaire édite sa propre fiche
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 12:10:06 +02:00
7b69f27ca5 feat(#142): ouvrir la modale au clic sur les cartes du dashboard admin.
Unifie le comportement des listes Parents, AM, Enfants, Gestionnaires, Admins et À valider avec le bouton d'action au survol.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 23:18:40 +02:00
8ed68797aa fix(#131): PATCH fiche AM — NIR sans crash date ni effacement NOT NULL
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:41:45 +02:00
9ad371a342 fix(#131): aligner PATCH fiche AM back sur le payload front
Étend UpdateAmFicheAdminDto (nir, date/lieu naissance, agreement_date)
et persiste ces champs dans updateFicheAdmin avec validation NIR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:37:42 +02:00
18718670e9 fix(#131): différer rattachement enfants AM jusqu'à Sauvegarder.
Rattachement et détachement restent locaux, recalculent places_available et sont persistés avec le PATCH fiche.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:36:06 +02:00
fbf22f2540 Revert "fix(#131): aligner PATCH fiche AM sur le DTO back actuel."
This reverts commit 43a2cd213b7b861ff239f5cfd756a47561b692f0.
2026-07-07 23:13:58 +02:00
43a2cd213b fix(#131): aligner PATCH fiche AM sur le DTO back actuel.
Retire nir, dates et lieux de naissance du body tant que UpdateAmFicheAdminDto ne les accepte pas.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:12:38 +02:00
c865d11dc3 Revert "fix(#131): activer Sauvegarder sur statut, enfants et champs AM."
This reverts commit d03a8e6c8b4363e9443e4a64711a7c68a21c5700.
2026-07-07 23:12:09 +02:00
d03a8e6c8b fix(#131): activer Sauvegarder sur statut, enfants et champs AM.
Compare l'état de la fiche à un snapshot initial (liste enfants, statut, places, formulaire) et corrige la gélule statut pour les valeurs hors liste.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:11:24 +02:00
66c7f22280 feat(#131): grille enfants AM, statuts garde et polish champs admin.
Aligne le front sur les statuts enfant back (garde/sans_garde), refond l’onglet Enfants accueillis en grille 2×2 avec correction des places, et unifie les champs validation éditables/lecture seule.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:01:41 +02:00
53721ffbb3 fix(#131): ajouter @Get() manquant sur liste parents
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 22:07:24 +02:00
52e40d0001 feat(#131): back placement AM↔enfant + BDD garde/sans_garde
- API PATCH …/fiche, POST/DELETE …/enfants/:enfantId, GET avec amChildren
- Table enfants_assistantes_maternelles (option D, 1 garde active/enfant)
- Statuts enfant: garde/sans_garde remplacent actif (inscription → sans_garde)
- BDD.sql canonique réécrit; migration pour BDD existantes
- Seed test: hash bcrypt corrigé pour mot de passe « password »

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 14:05:39 +02:00
4985726bc6 fix(#131): polish fiche AM — champs pro, places, vigilance et photo.
Fiche pro entièrement éditable, places déclarées en lecture seule avec alerte rouge, icône vigilance sur la liste AM, et cadrage photo aligné sur le wizard validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:37:21 +02:00
479a32b4bf feat(#131): fiche AM éditable en 3 onglets et widgets admin partagés.
Modale admin AM (identité, dossier pro, enfants), API front avec repli, note back affiliation, et extraction gélule statut / liste enfants pour la fiche parent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:33:41 +02:00
2fd97ddecb fix(#131): aligner la gélule de statut sur l'en-tête parent.
Le titre, le sélecteur de statut et le bouton fermer partagent le même
padding : la gélule n'est plus collée au bord supérieur de la modale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:34:12 +02:00
ce474797c4 fix(#131): co_parent en réponse parents + masquage secrets user
Contrat API fiche parent : co_parent peuplé (déjà chargé), sans password
ni tokens sur user/co_parent. Doc tmp front→back.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 23:39:30 +02:00
ebf794e1ac 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>
2026-06-24 23:37:04 +02:00
d55f240f56 Merge branch 'feature/140-dashboard-admin-ch6-famille' into develop
Livraison epic #140 — dashboard admin ch.6 famille (doc 28 §6.1–§6.2).

Inclus :
- Backend : PATCH fiche parent, rattachement/détachement enfant (#115)
- Frontend : fiche parent éditable (#131), onglet Enfants (#137), fiche/liste enfant (#138), affiliation UI (#116), UserService (#130)
- IdentityBlock partagé, parsing parentChildren, avatars web /uploads

Reste ouvert sur tickets dédiés : fiche enfant UI (#138), modale rattacher (#116), fiche AM (#131), création enfant (#129).
2026-06-24 23:22:39 +02:00
966f4a9c9c feat(#140): liste enfants unifiée — cartes, âge précis et cadre scrollable
Partage AdminEnfantUserCard entre fiche parent et onglet Enfants, enrichit ParentChildSummary (photo, dates) et compacte la modale parent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 23:06:38 +02:00
8494341b56 feat(#140): fiches admin famille — IdentityBlock, parsing enfants et avatars web
Extrait IdentityBlock réutilisable, aligne les modales parent/enfant sur le style validation, corrige le parsing parentChildren et les URLs /uploads en Flutter web.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 17:42:00 +02:00
1dddc67933 feat(#140): dashboard admin ch.6 — fiche parent, enfants, affiliation
Back: PATCH /parents/:id/fiche, attach/detach enfant, GET /enfants enrichi.
Front: modale parent éditable, onglet Enfants, fiche enfant, UserService.

Couvre doc 28 §6.1–6.2 ; tickets liés #115 #116 #130 #131 #137 #138.
Hors scope: fiche AM (#131), création admin (#129).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 00:35:00 +02:00
df776d8200 fix(#112): dates et places AM en reprise (resetForReprise)
Corrige l’ombre des paramètres dateOfBirth, agreementDate et placesAvailable
dans resetForReprise ; renforce le parsing reprise-dossier et ajoute un test.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 19:12:14 +02:00
c26ed00374 fix(#112): préremplissage AM complet en reprise + lien login repositionné
Parse dates/places/consentement de façon robuste, rafraîchit l'étape 2 AM
et place « J'ai un numéro de dossier » sous « Créer un compte ».

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:29:31 +02:00
9d54d9b19b feat(#112): email accusé resoumission aux parents
Après PATCH reprise-resoumettre (parent), envoi à chaque parent du
dossier : confirmation resoumission + rappel numero_dossier.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:26:23 +02:00
c438009286 feat(#112): lien login « J'ai un numéro de dossier » + reprise-identify
Modale numéro + e-mail, obtention du token puis redirection vers /reprise.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:01:18 +02:00
9b7231f1da fix(#112): afficher photos enfants en reprise + consentement cohérent
Charge existingPhotoUrl dans les cartes enfant (étapes 3 et 5) et pré-coche
le consentement photo lorsqu'une photo est déjà en base.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:00:15 +02:00
f300505225 feat(#112): aligner reprise front sur dossier complet GET/PATCH
Préremplit parents, enfants, motivation et fiche AM depuis reprise-dossier
et envoie le body PATCH complet (co-parent, enfants par id, champs pro AM).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:46:51 +02:00
25c10c885a docs(#112): note alignement front reprise dossier complet
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:42:35 +02:00
d70577b1c3 feat(#112): reprise après refus — dossier complet GET/PATCH
- GET reprise-dossier : parents[], enfants[], texte_motivation (famille) ou fiche AM (#119)
- PATCH reprise-resoumettre : co-parent, enfants (update par id), motivation, fiche AM
- Resoumission famille : tous les parents en en_attente + invalidation token groupée
- DTOs étendus + est_multiple sur enfants dossier famille
- Tests unitaires getRepriseDossier parent/AM

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:39:53 +02:00
671da71752 feat(#112): reprise après refus via lien e-mail (/reprise?token=)
Branche le flux front : chargement du dossier, session reprise, wizards
parent/AM préremplis et resoumission via reprise-resoumettre.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:28:41 +02:00
12 changed files with 25 additions and 209 deletions

View File

@ -146,27 +146,16 @@ export class EnfantsService {
throw new ForbiddenException('Accès interdit');
}
/** Flag API #157 — true si aucun lien enfants_parents. */
private withSansResponsable(child: Children): Children & { sans_responsable: boolean } {
return Object.assign(child, {
sans_responsable: !child.parentLinks || child.parentLinks.length === 0,
});
}
// Liste des enfants (admin/gestionnaire) — inclut les orphelins (parentLinks: [])
async findAll(): Promise<Array<Children & { sans_responsable: boolean }>> {
const children = await this.childrenRepository.find({
// Liste des enfants (admin/gestionnaire)
async findAll(): Promise<Children[]> {
return this.childrenRepository.find({
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'],
order: { last_name: 'ASC', first_name: 'ASC' },
});
return children.map((c) => this.withSansResponsable(c));
}
// Récupérer un enfant par id
async findOne(
id: string,
currentUser: Users,
): Promise<Children & { sans_responsable: boolean }> {
async findOne(id: string, currentUser: Users): Promise<Children> {
const child = await this.childrenRepository.findOne({
where: { id },
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'],
@ -183,14 +172,14 @@ export class EnfantsService {
case RoleType.ADMINISTRATEUR:
case RoleType.SUPER_ADMIN:
case RoleType.GESTIONNAIRE:
// accès complet (y compris orphelins)
// accès complet
break;
default:
throw new ForbiddenException('Accès interdit');
}
return this.withSansResponsable(child);
return child;
}

View File

@ -138,9 +138,7 @@ export class ParentsService {
}
/**
* 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 d'un parent sans supprimer l'enfant. Ticket #115 / doc 28 §6.2.
*/
async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
await this.findOne(parentUserId);
@ -152,6 +150,11 @@ export class ParentsService {
throw new NotFoundException('Lien parent-enfant introuvable');
}
const totalLinks = await this.parentsChildrenRepository.count({ where: { enfantId } });
if (totalLinks <= 1) {
throw new BadRequestException('Un enfant doit rester rattaché à au moins un responsable');
}
await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId });
return this.findOne(parentUserId);
}

View File

@ -14,8 +14,6 @@ class EnfantAdminModel {
final bool consentPhoto;
final bool isMultiple;
final List<EnfantParentLink> parentLinks;
/// Flag API #157 (sinon déduit de [parentLinks]).
final bool? sansResponsable;
EnfantAdminModel({
required this.id,
@ -29,7 +27,6 @@ class EnfantAdminModel {
this.consentPhoto = false,
this.isMultiple = false,
this.parentLinks = const [],
this.sansResponsable,
});
String get fullName {
@ -40,12 +37,6 @@ class EnfantAdminModel {
return '$fn $ln';
}
/// Aucun lien parent valide ticket #157.
bool get hasNoResponsable {
if (sansResponsable != null) return sansResponsable!;
return !parentLinks.any((l) => l.parentId.trim().isNotEmpty);
}
EnfantAdminModel copyWith({
String? id,
String? firstName,
@ -58,7 +49,6 @@ class EnfantAdminModel {
bool? consentPhoto,
bool? isMultiple,
List<EnfantParentLink>? parentLinks,
bool? sansResponsable,
}) {
return EnfantAdminModel(
id: id ?? this.id,
@ -72,7 +62,6 @@ class EnfantAdminModel {
consentPhoto: consentPhoto ?? this.consentPhoto,
isMultiple: isMultiple ?? this.isMultiple,
parentLinks: parentLinks ?? this.parentLinks,
sansResponsable: sansResponsable ?? this.sansResponsable,
);
}
@ -92,13 +81,6 @@ class EnfantAdminModel {
_parseBool(json['consentement_photo']) ||
_parseBool(json['consentPhoto']);
bool? sansResponsable;
if (json.containsKey('sans_responsable') ||
json.containsKey('sansResponsable')) {
sansResponsable = _parseBool(json['sans_responsable']) ||
_parseBool(json['sansResponsable']);
}
return EnfantAdminModel(
id: (json['id'] ?? '').toString(),
firstName: json['first_name'] as String? ?? json['prenom'] as String?,
@ -114,7 +96,6 @@ class EnfantAdminModel {
isMultiple: _parseBool(json['is_multiple']) ||
_parseBool(json['est_multiple']),
parentLinks: links,
sansResponsable: sansResponsable,
);
}

View File

@ -62,56 +62,4 @@ class ParentModel {
return children;
}
/// Nombre denfants distincts du foyer (ce parent + co-parent / même dossier).
/// Évite Claire=7 / Thomas=6 quand un lien nest que sur un des deux (#157).
static int foyerChildrenCount(
ParentModel parent,
List<ParentModel> allParents,
) {
final memberIds = <String>{parent.user.id};
final coId = parent.coParent?.id.trim();
if (coId != null && coId.isNotEmpty) memberIds.add(coId);
final dossier = (parent.user.numeroDossier ?? '').trim();
for (final other in allParents) {
if (memberIds.contains(other.user.id)) continue;
final otherCo = other.coParent?.id.trim();
if (otherCo != null && memberIds.contains(otherCo)) {
memberIds.add(other.user.id);
continue;
}
if (dossier.isNotEmpty &&
(other.user.numeroDossier ?? '').trim() == dossier) {
memberIds.add(other.user.id);
final oc = other.coParent?.id.trim();
if (oc != null && oc.isNotEmpty) memberIds.add(oc);
}
}
final childIds = <String>{};
for (final p in allParents) {
if (!memberIds.contains(p.user.id)) continue;
for (final c in p.children) {
final id = c.id.trim();
if (id.isNotEmpty) childIds.add(id);
}
// Repli si la liste enfants nest pas hydratée.
if (p.children.isEmpty && p.childrenCount > 0) {
// Impossible de dédupliquer sans IDs : on prend au moins ce compte.
// (évite dafficher 0 si lAPI nenvoie que childrenCount)
}
}
if (childIds.isNotEmpty) return childIds.length;
var maxCount = 0;
for (final p in allParents) {
if (!memberIds.contains(p.user.id)) continue;
final n = p.children.isNotEmpty ? p.children.length : p.childrenCount;
if (n > maxCount) maxCount = n;
}
return maxCount;
}
}

View File

@ -1,10 +0,0 @@
import 'package:p_tits_pas/models/enfant_admin_model.dart';
/// Enfant sans lien parent (orphelins daffiliation) ticket #157.
bool enfantHasNoResponsable(EnfantAdminModel enfant) => enfant.hasNoResponsable;
/// Message vigilance liste Enfants (même usage que [amPlacesVigilanceMessage]).
String? enfantSansResponsableVigilanceMessage(EnfantAdminModel enfant) {
if (!enfantHasNoResponsable(enfant)) return null;
return 'Aucun responsable rattaché — à rattacher à un foyer';
}

View File

@ -446,7 +446,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
builder: (ctx) => AlertDialog(
title: const Text('Détacher l\'enfant'),
content: Text(
'Retirer ${child.fullName} de la fiche de cette assistante ?',
'Retirer ${child.fullName} de la fiche de cette assistante ?\n'
'(L\'enfant ne sera pas supprimé.)',
),
actions: [
TextButton(

View File

@ -65,9 +65,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
/// Famille choisie en mode création (#132).
AdminFamilleFoyer? _selectedFamily;
/// Liens parents locaux (édition) mis à jour après rattachement foyer (#157).
List<EnfantParentLink>? _localParentLinks;
/// Photo locale (création) upload multipart `photo`.
Uint8List? _photoBytes;
String? _photoFilename;
@ -265,14 +262,10 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
}
List<EnfantParentLink> get _parentLinks =>
(_localParentLinks ??
widget.enfant?.parentLinks ??
const <EnfantParentLink>[])
(widget.enfant?.parentLinks ?? const <EnfantParentLink>[])
.where((l) => l.parentId.trim().isNotEmpty)
.toList();
bool get _isOrphan => !widget.isCreating && _parentLinks.isEmpty;
Future<void> _openParent(EnfantParentLink link) async {
if (_busy) return;
final id = link.parentId.trim();
@ -587,7 +580,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
if (!enfantStatusValues.contains(_status)) {
_status = 'sans_garde';
}
_localParentLinks = enfant.parentLinks;
_coerceGenderForStatus();
});
} catch (_) {
@ -968,9 +960,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
padding: const EdgeInsets.only(top: 16),
child: Align(
alignment: Alignment.bottomCenter,
child: SingleChildScrollView(
child: _placementBlock(),
),
child: _placementBlock(),
),
),
),
@ -1229,24 +1219,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isOrphan) ...[
Text(
'Sélection du dossier de la famille',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.red.shade800,
),
),
const SizedBox(height: 4),
Text(
'Aucun responsable rattaché — choisissez un foyer',
style: TextStyle(fontSize: 12, color: Colors.red.shade700),
),
const SizedBox(height: 8),
_familyPlacementSection(),
const SizedBox(height: 16),
],
Text(
_placementTitle,
style: TextStyle(
@ -1268,45 +1240,10 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
title: 'Choisir une famille',
);
if (selected == null || !mounted) return;
if (widget.isCreating) {
setState(() {
_selectedFamily = selected;
_dirty = true;
});
return;
}
// Édition orphelin (#157) : rattache immédiatement 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,
enfantId: enfantId,
);
}
final refreshed = await UserService.getEnfant(enfantId);
if (!mounted) return;
setState(() {
_localParentLinks = refreshed.parentLinks;
_selectedFamily = selected;
_saving = false;
});
widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant rattaché au foyer')),
);
} catch (e) {
if (!mounted) return;
setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
setState(() {
_selectedFamily = selected;
_dirty = true;
});
}
Widget _familyPlacementSection() {

View File

@ -3,7 +3,6 @@ import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/models/parent_child_summary.dart';
import 'package:p_tits_pas/utils/date_display_utils.dart';
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
import 'package:p_tits_pas/utils/enfant_vigilance.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
List<String> enfantAdminSubtitleLines({
@ -37,8 +36,6 @@ class AdminEnfantUserCard extends StatelessWidget {
final VoidCallback? onCardTap;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? contentPadding;
final Color? borderColor;
final String? vigilanceTooltip;
const AdminEnfantUserCard({
super.key,
@ -49,8 +46,6 @@ class AdminEnfantUserCard extends StatelessWidget {
this.onCardTap,
this.margin,
this.contentPadding,
this.borderColor,
this.vigilanceTooltip,
});
factory AdminEnfantUserCard.fromEnfant(
@ -60,13 +55,10 @@ class AdminEnfantUserCard extends StatelessWidget {
VoidCallback? onCardTap,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? contentPadding,
Color? borderColor,
String? vigilanceTooltip,
}) {
final parents = enfant.parentLinks
.map((l) => l.parentName ?? 'Parent')
.join(', ');
final orphan = enfantHasNoResponsable(enfant);
return AdminEnfantUserCard(
title: enfant.fullName,
photoUrl: enfant.photoUrl,
@ -77,7 +69,6 @@ class AdminEnfantUserCard extends StatelessWidget {
gender: enfant.gender,
extra: [
if (parents.isNotEmpty) 'Responsables : $parents',
if (orphan) 'Aucun responsable rattaché',
...extraSubtitleLines,
],
),
@ -85,10 +76,6 @@ class AdminEnfantUserCard extends StatelessWidget {
onCardTap: onCardTap,
margin: margin,
contentPadding: contentPadding,
borderColor: borderColor ??
(orphan ? Colors.red.shade300 : null),
vigilanceTooltip: vigilanceTooltip ??
enfantSansResponsableVigilanceMessage(enfant),
);
}
@ -127,8 +114,6 @@ class AdminEnfantUserCard extends StatelessWidget {
onCardTap: onCardTap,
margin: margin,
contentPadding: contentPadding,
borderColor: borderColor,
vigilanceTooltip: vigilanceTooltip,
);
}
}

View File

@ -283,7 +283,8 @@ 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} de la fiche de ce parent ?\n'
'(L\'enfant ne sera pas supprimé.)',
),
actions: [
TextButton(

View File

@ -4,12 +4,10 @@ import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132 / #157).
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132).
class AdminFamilleFoyer {
/// Parent pivot pour `POST /enfants` (`parent_user_id`).
final String pivotParentUserId;
/// Co-parent éventuel (rattachement foyer #157).
final String? coParentUserId;
final String? numeroDossier;
final String displayTitle;
final List<String> parentNames;
@ -18,18 +16,9 @@ class AdminFamilleFoyer {
required this.pivotParentUserId,
required this.displayTitle,
required this.parentNames,
this.coParentUserId,
this.numeroDossier,
});
/// Parents du foyer à lier à lenfant (pivot puis co-parent).
List<String> get parentUserIds {
final ids = <String>[pivotParentUserId];
final co = (coParentUserId ?? '').trim();
if (co.isNotEmpty && co != pivotParentUserId) ids.add(co);
return ids;
}
String get subtitle {
final parts = <String>[];
final dossier = (numeroDossier ?? '').trim();
@ -72,7 +61,6 @@ List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
foyers.add(
AdminFamilleFoyer(
pivotParentUserId: p.user.id,
coParentUserId: co?.id,
numeroDossier: dossier.isNotEmpty ? dossier : null,
displayTitle: title,
parentNames: names,

View File

@ -72,14 +72,7 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
normalizeEnfantStatus(e.status) ==
normalizeEnfantStatus(widget.statusFilter);
return matchesName && matchesStatus;
}).toList()
..sort((a, b) {
// Orphelins (#157) en tête, puis ordre alphabétique.
final ao = a.hasNoResponsable ? 0 : 1;
final bo = b.hasNoResponsable ? 0 : 1;
if (ao != bo) return ao.compareTo(bo);
return a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase());
});
}).toList();
return UserList(
isLoading: _isLoading,

View File

@ -80,7 +80,7 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
onCardTap: () => _openParentDetails(parent),
subtitleLines: [
parent.user.email,
'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${ParentModel.foyerChildrenCount(parent, _parents)}',
'Statut : ${_displayStatus(parent.user.statut)} | Enfants : ${parent.children.isNotEmpty ? parent.children.length : parent.childrenCount}',
],
actions: [
IconButton(