Compare commits

...

8 Commits

Author SHA1 Message Date
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
8 changed files with 1093 additions and 307 deletions

View File

@ -37,6 +37,34 @@ class EnfantAdminModel {
return '$fn $ln';
}
EnfantAdminModel copyWith({
String? id,
String? firstName,
String? lastName,
String? gender,
String? birthDate,
String? dueDate,
String? status,
String? photoUrl,
bool? consentPhoto,
bool? isMultiple,
List<EnfantParentLink>? parentLinks,
}) {
return EnfantAdminModel(
id: id ?? this.id,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
gender: gender ?? this.gender,
birthDate: birthDate ?? this.birthDate,
dueDate: dueDate ?? this.dueDate,
status: status ?? this.status,
photoUrl: photoUrl ?? this.photoUrl,
consentPhoto: consentPhoto ?? this.consentPhoto,
isMultiple: isMultiple ?? this.isMultiple,
parentLinks: parentLinks ?? this.parentLinks,
);
}
factory EnfantAdminModel.fromJson(Map<String, dynamic> json) {
final linksRaw = json['parentLinks'] as List?;
final links = <EnfantParentLink>[];
@ -48,17 +76,25 @@ class EnfantAdminModel {
}
}
final photoUrl = json['photo_url'] as String? ?? json['photoUrl'] as String?;
final consentPhoto = _parseBool(json['consent_photo']) ||
_parseBool(json['consentement_photo']) ||
_parseBool(json['consentPhoto']);
return EnfantAdminModel(
id: (json['id'] ?? '').toString(),
firstName: json['first_name'] as String?,
lastName: json['last_name'] as String?,
gender: json['gender'] as String?,
birthDate: _dateString(json['birth_date']),
dueDate: _dateString(json['due_date']),
status: normalizeEnfantStatus(json['status']?.toString()),
photoUrl: json['photo_url'] as String?,
consentPhoto: json['consent_photo'] == true,
isMultiple: json['is_multiple'] == true,
firstName: json['first_name'] as String? ?? json['prenom'] as String?,
lastName: json['last_name'] as String? ?? json['nom'] as String?,
gender: json['gender'] as String? ?? json['genre'] as String?,
birthDate: _dateString(json['birth_date'] ?? json['date_naissance']),
dueDate: _dateString(json['due_date'] ?? json['date_prevue_naissance']),
status: normalizeEnfantStatus(
(json['status'] ?? json['statut'])?.toString(),
),
photoUrl: photoUrl,
consentPhoto: consentPhoto,
isMultiple: _parseBool(json['is_multiple']) ||
_parseBool(json['est_multiple']),
parentLinks: links,
);
}
@ -76,6 +112,15 @@ class EnfantAdminModel {
};
}
static bool _parseBool(dynamic value) {
if (value == true || value == 1) return true;
if (value is String) {
final s = value.trim().toLowerCase();
return s == 'true' || s == '1' || s == 't' || s == 'yes';
}
return false;
}
static String? _dateString(dynamic v) {
if (v == null) return null;
if (v is String) return v.split('T').first;
@ -89,6 +134,13 @@ class EnfantParentLink {
EnfantParentLink({required this.parentId, this.parentName});
EnfantParentLink copyWith({String? parentId, String? parentName}) {
return EnfantParentLink(
parentId: parentId ?? this.parentId,
parentName: parentName ?? this.parentName,
);
}
factory EnfantParentLink.fromJson(Map<String, dynamic> json) {
final parentId =
(json['parentId'] ?? json['id_parent'] ?? '').toString();
@ -99,6 +151,12 @@ class EnfantParentLink {
if (user is Map<String, dynamic>) {
final u = AppUser.fromJson(user);
name = u.fullName.isNotEmpty ? u.fullName : u.email;
} else {
// Parfois le parent est aplati (prenom/nom) sans nested user.
final prenom = (parent['prenom'] ?? parent['first_name'] ?? '').toString().trim();
final nom = (parent['nom'] ?? parent['last_name'] ?? '').toString().trim();
final flat = '$prenom $nom'.trim();
if (flat.isNotEmpty) name = flat;
}
}
return EnfantParentLink(

View File

@ -460,7 +460,40 @@ class UserService {
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur chargement enfant'));
}
return EnfantAdminModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
final enfant = EnfantAdminModel.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
// GET /enfants/:id ne joint pas toujours parent.user noms manquants.
return enrichEnfantParentNames(enfant);
}
/// Complète [parentName] via GET parent quand l'API n'a pas fourni le nested user.
static Future<EnfantAdminModel> enrichEnfantParentNames(
EnfantAdminModel enfant,
) async {
final links = enfant.parentLinks;
if (links.isEmpty) return enfant;
if (links.every((l) => (l.parentName ?? '').trim().isNotEmpty)) {
return enfant;
}
final enriched = await Future.wait(
links.map((link) async {
if ((link.parentName ?? '').trim().isNotEmpty) return link;
final id = link.parentId.trim();
if (id.isEmpty) return link;
try {
final parent = await getParent(id);
final name = parent.user.fullName.trim();
if (name.isEmpty) return link;
return link.copyWith(parentName: name);
} catch (_) {
return link;
}
}),
);
return enfant.copyWith(parentLinks: enriched);
}
static Future<EnfantAdminModel> updateEnfant({
@ -475,7 +508,29 @@ class UserService {
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur mise à jour enfant'));
}
return EnfantAdminModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
final enfant = EnfantAdminModel.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
return enrichEnfantParentNames(enfant);
}
static Future<void> deleteEnfant(String enfantId) async {
final response = await http.delete(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
headers: await _headers(),
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception(_extractErrorMessage(response.body, 'Erreur suppression enfant'));
}
}
/// AM dont la liste d'enfants actifs contient [enfantId] (API actuelle).
static Future<AssistanteMaternelleModel?> findAmForEnfant(String enfantId) async {
final ams = await getAssistantesMaternelles();
for (final am in ams) {
if (am.children.any((c) => c.id == enfantId)) return am;
}
return null;
}
static ParentModel _parentModelFromBody(String body) {

View File

@ -14,10 +14,10 @@ String normalizeEnfantStatus(String? raw) {
return s;
}
String _scolariseAccordeAuGenre(String? gender) {
String _accordeAuGenre(String masculin, String feminin, String? gender) {
final g = (gender ?? '').trim().toUpperCase();
if (g == 'F') return 'Scolarisée';
return 'Scolarisé';
if (g == 'F') return feminin;
return masculin;
}
/// Libellé affiché pour un statut enfant.
@ -28,9 +28,9 @@ String enfantStatusLabel(String? status, {String? gender}) {
case 'sans_garde':
return 'Sans garde';
case 'garde':
return 'En garde';
return _accordeAuGenre('Gardé', 'Gardée', gender);
case 'scolarise':
return _scolariseAccordeAuGenre(gender);
return _accordeAuGenre('Scolarisé', 'Scolarisée', gender);
default:
return status?.trim().isNotEmpty == true ? status!.trim() : '';
}

View File

@ -38,15 +38,12 @@ class RepriseMapper {
? isoToDdMmYyyy(e.dueDate)
: isoToDdMmYyyy(e.birthDate);
final photo = e.photoUrl?.trim();
final hasPhoto = photo != null && photo.isNotEmpty;
return ChildData(
firstName: e.firstName ?? '',
lastName: e.lastName ?? '',
dob: dob,
genre: e.gender ?? '',
// Legacy : dossiers inscrits avant #144 avaient consent_photo=false malgré une photo.
// On pré-coche encore si photo en base pour ne pas bloquer la reprise.
photoConsent: e.consentPhoto || hasPhoto,
photoConsent: e.consentPhoto,
multipleBirth: e.estMultiple,
isUnbornChild: isUnborn,
cardColor: _childCardColors[index % _childCardColors.length],
@ -200,7 +197,7 @@ class RepriseMapper {
postalCode: dossier.codePostal ?? '',
city: dossier.ville ?? '',
existingPhotoUrl: displayPhoto,
consentementPhoto: dossier.consentementPhoto || hasPhoto,
consentementPhoto: dossier.consentementPhoto,
dateOfBirth: parseIsoDate(dossier.dateNaissance),
birthCity: dossier.lieuNaissanceVille ?? '',
birthCountry: dossier.lieuNaissancePays ?? '',

File diff suppressed because it is too large Load Diff

View File

@ -64,6 +64,7 @@ class AdminChildrenAffiliationPanel extends StatelessWidget {
final c = children[i];
return AdminEnfantUserCard.fromSummary(
c,
onCardTap: () => onOpen(c),
actions: [
IconButton(
icon: const Icon(Icons.visibility_outlined),

View File

@ -14,6 +14,7 @@ class AdminUserCard extends StatefulWidget {
final Color? infoColor;
final String? vigilanceTooltip;
final VoidCallback? onCardTap;
final EdgeInsetsGeometry? margin;
const AdminUserCard({
super.key,
@ -28,6 +29,7 @@ class AdminUserCard extends StatefulWidget {
this.infoColor,
this.vigilanceTooltip,
this.onCardTap,
this.margin,
});
@override
@ -59,7 +61,7 @@ class _AdminUserCardState extends State<AdminUserCard> {
borderRadius: BorderRadius.circular(10),
hoverColor: const Color(0x149CC5C0),
child: Card(
margin: const EdgeInsets.only(bottom: 12),
margin: widget.margin ?? const EdgeInsets.only(bottom: 12),
elevation: 0,
color: widget.backgroundColor,
shape: RoundedRectangleBorder(

View File

@ -215,7 +215,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
value: 'garde',
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('En garde', style: TextStyle(fontSize: 12)),
child: Text('Gardé', style: TextStyle(fontSize: 12)),
),
),
DropdownMenuItem<String?>(