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:
@@ -1,30 +1,102 @@
|
||||
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
|
||||
class AssistanteMaternelleModel {
|
||||
final AppUser user;
|
||||
final String? approvalNumber;
|
||||
final String? nir;
|
||||
final String? residenceCity;
|
||||
final int? maxChildren;
|
||||
final int? placesAvailable;
|
||||
final String? biography;
|
||||
final bool? available;
|
||||
final String? agreementDate;
|
||||
final List<ParentChildSummary> children;
|
||||
|
||||
AssistanteMaternelleModel({
|
||||
required this.user,
|
||||
this.approvalNumber,
|
||||
this.nir,
|
||||
this.residenceCity,
|
||||
this.maxChildren,
|
||||
this.placesAvailable,
|
||||
this.biography,
|
||||
this.available,
|
||||
this.agreementDate,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
factory AssistanteMaternelleModel.fromJson(Map<String, dynamic> json) {
|
||||
final userJson = json['user'] ?? json;
|
||||
final root = Map<String, dynamic>.from(json);
|
||||
final userJson = Map<String, dynamic>.from(root['user'] ?? root);
|
||||
if (root['numero_dossier'] != null && userJson['numero_dossier'] == null) {
|
||||
userJson['numero_dossier'] = root['numero_dossier'];
|
||||
}
|
||||
final user = AppUser.fromJson(userJson);
|
||||
|
||||
final children = _parseChildren(root);
|
||||
|
||||
return AssistanteMaternelleModel(
|
||||
user: user,
|
||||
approvalNumber: json['numero_agrement'] as String?,
|
||||
residenceCity: json['ville_residence'] as String?,
|
||||
maxChildren: json['nb_max_enfants'] as int?,
|
||||
placesAvailable: json['place_disponible'] as int?,
|
||||
approvalNumber: _str(
|
||||
root['approval_number'] ?? root['numero_agrement'],
|
||||
),
|
||||
nir: _str(root['nir'] ?? root['nir_chiffre']),
|
||||
residenceCity: _str(
|
||||
root['residence_city'] ?? root['ville_residence'],
|
||||
),
|
||||
maxChildren: _int(root['max_children'] ?? root['nb_max_enfants']),
|
||||
placesAvailable: _int(
|
||||
root['places_available'] ?? root['place_disponible'],
|
||||
),
|
||||
biography: _str(root['biography'] ?? root['biographie']),
|
||||
available: root['available'] as bool? ?? root['disponible'] as bool?,
|
||||
agreementDate: _dateString(
|
||||
root['agreement_date'] ?? root['date_agrement'],
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _str(dynamic v) {
|
||||
if (v == null) return null;
|
||||
final s = v.toString().trim();
|
||||
return s.isEmpty ? null : s;
|
||||
}
|
||||
|
||||
static int? _int(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is int) return v;
|
||||
return int.tryParse(v.toString());
|
||||
}
|
||||
|
||||
static String? _dateString(dynamic v) {
|
||||
if (v == null) return null;
|
||||
return v.toString().split('T').first;
|
||||
}
|
||||
|
||||
static List<ParentChildSummary> _parseChildren(Map<String, dynamic> json) {
|
||||
final children = <ParentChildSummary>[];
|
||||
final seen = <String>{};
|
||||
|
||||
void add(ParentChildSummary? child) {
|
||||
if (child == null || child.id.isEmpty || seen.contains(child.id)) return;
|
||||
seen.add(child.id);
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
final links = json['amChildren'] ??
|
||||
json['am_children'] ??
|
||||
json['assistanteChildren'] ??
|
||||
json['assistante_children'];
|
||||
if (links is List) {
|
||||
for (final link in links) {
|
||||
if (link is! Map) continue;
|
||||
add(ParentChildSummary.fromParentChildLink(
|
||||
Map<String, dynamic>.from(link),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||
|
||||
/// Réponse unifiée GET /dossiers/:numeroDossier. Ticket #119, #107.
|
||||
class DossierUnifie {
|
||||
@@ -220,7 +221,7 @@ class EnfantDossier {
|
||||
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
||||
birthDate: json['birth_date']?.toString(),
|
||||
gender: (json['gender'] ?? json['genre'])?.toString(),
|
||||
status: json['status']?.toString(),
|
||||
status: normalizeEnfantStatus(json['status']?.toString()),
|
||||
dueDate: json['due_date']?.toString(),
|
||||
photoUrl: resolvedPhoto,
|
||||
consentPhoto:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||
|
||||
/// Enfant tel que renvoyé par `GET /enfants` (dashboard admin).
|
||||
class EnfantAdminModel {
|
||||
final String id;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? gender;
|
||||
final String? birthDate;
|
||||
final String? dueDate;
|
||||
final String status;
|
||||
final String? photoUrl;
|
||||
final bool consentPhoto;
|
||||
final bool isMultiple;
|
||||
final List<EnfantParentLink> parentLinks;
|
||||
|
||||
EnfantAdminModel({
|
||||
required this.id,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.gender,
|
||||
this.birthDate,
|
||||
this.dueDate,
|
||||
required this.status,
|
||||
this.photoUrl,
|
||||
this.consentPhoto = false,
|
||||
this.isMultiple = false,
|
||||
this.parentLinks = const [],
|
||||
});
|
||||
|
||||
String get fullName {
|
||||
final fn = (firstName ?? '').trim();
|
||||
final ln = (lastName ?? '').trim();
|
||||
if (fn.isEmpty && ln.isEmpty) return 'Enfant';
|
||||
if (ln.isEmpty) return fn;
|
||||
return '$fn $ln';
|
||||
}
|
||||
|
||||
factory EnfantAdminModel.fromJson(Map<String, dynamic> json) {
|
||||
final linksRaw = json['parentLinks'] as List?;
|
||||
final links = <EnfantParentLink>[];
|
||||
if (linksRaw != null) {
|
||||
for (final item in linksRaw) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
links.add(EnfantParentLink.fromJson(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
parentLinks: links,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toUpdateJson() {
|
||||
return {
|
||||
if (firstName != null) 'first_name': firstName,
|
||||
if (lastName != null) 'last_name': lastName,
|
||||
if (gender != null && gender!.isNotEmpty) 'gender': gender,
|
||||
'status': status,
|
||||
if (birthDate != null && birthDate!.isNotEmpty) 'birth_date': birthDate,
|
||||
if (dueDate != null && dueDate!.isNotEmpty) 'due_date': dueDate,
|
||||
'consent_photo': consentPhoto,
|
||||
'is_multiple': isMultiple,
|
||||
};
|
||||
}
|
||||
|
||||
static String? _dateString(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is String) return v.split('T').first;
|
||||
return v.toString().split('T').first;
|
||||
}
|
||||
}
|
||||
|
||||
class EnfantParentLink {
|
||||
final String parentId;
|
||||
final String? parentName;
|
||||
|
||||
EnfantParentLink({required this.parentId, this.parentName});
|
||||
|
||||
factory EnfantParentLink.fromJson(Map<String, dynamic> json) {
|
||||
final parentId =
|
||||
(json['parentId'] ?? json['id_parent'] ?? '').toString();
|
||||
String? name;
|
||||
final parent = json['parent'];
|
||||
if (parent is Map<String, dynamic>) {
|
||||
final user = parent['user'];
|
||||
if (user is Map<String, dynamic>) {
|
||||
final u = AppUser.fromJson(user);
|
||||
name = u.fullName.isNotEmpty ? u.fullName : u.email;
|
||||
}
|
||||
}
|
||||
return EnfantParentLink(
|
||||
parentId: parentId,
|
||||
parentName: name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||
|
||||
/// Résumé enfant affiché dans la fiche parent (dashboard admin).
|
||||
class ParentChildSummary {
|
||||
final String id;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String status;
|
||||
final String? photoUrl;
|
||||
final String? birthDate;
|
||||
final String? dueDate;
|
||||
|
||||
ParentChildSummary({
|
||||
required this.id,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
required this.status,
|
||||
this.photoUrl,
|
||||
this.birthDate,
|
||||
this.dueDate,
|
||||
});
|
||||
|
||||
String get fullName {
|
||||
final fn = (firstName ?? '').trim();
|
||||
final ln = (lastName ?? '').trim();
|
||||
if (fn.isEmpty && ln.isEmpty) return 'Enfant';
|
||||
if (ln.isEmpty) return fn;
|
||||
return '$fn $ln';
|
||||
}
|
||||
|
||||
factory ParentChildSummary.fromJson(Map<String, dynamic> json) {
|
||||
return ParentChildSummary(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
firstName: (json['first_name'] ?? json['prenom'])?.toString(),
|
||||
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
||||
status: normalizeEnfantStatus(
|
||||
(json['status'] ?? json['statut'])?.toString(),
|
||||
),
|
||||
photoUrl: json['photo_url']?.toString(),
|
||||
birthDate: _dateString(json['birth_date']),
|
||||
dueDate: _dateString(json['due_date']),
|
||||
);
|
||||
}
|
||||
|
||||
factory ParentChildSummary.fromEnfant(EnfantAdminModel enfant) {
|
||||
return ParentChildSummary(
|
||||
id: enfant.id,
|
||||
firstName: enfant.firstName,
|
||||
lastName: enfant.lastName,
|
||||
status: enfant.status,
|
||||
photoUrl: enfant.photoUrl,
|
||||
birthDate: enfant.birthDate,
|
||||
dueDate: enfant.dueDate,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _dateString(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is String) return v.split('T').first;
|
||||
return v.toString().split('T').first;
|
||||
}
|
||||
|
||||
/// Parse un lien `parentChildren` (objet enfant imbriqué ou id seul).
|
||||
static ParentChildSummary? fromParentChildLink(Map<String, dynamic> link) {
|
||||
final childRaw = link['child'] ?? link['enfant'];
|
||||
if (childRaw is Map) {
|
||||
return ParentChildSummary.fromJson(Map<String, dynamic>.from(childRaw));
|
||||
}
|
||||
|
||||
if (link.containsKey('first_name') ||
|
||||
link.containsKey('prenom') ||
|
||||
link.containsKey('id')) {
|
||||
final id = (link['id'] ?? '').toString();
|
||||
if (id.isNotEmpty &&
|
||||
(link.containsKey('first_name') || link.containsKey('prenom'))) {
|
||||
return ParentChildSummary.fromJson(link);
|
||||
}
|
||||
}
|
||||
|
||||
final enfantId =
|
||||
link['enfantId'] ?? link['id_enfant'] ?? link['enfant_id'];
|
||||
if (enfantId != null && enfantId.toString().isNotEmpty) {
|
||||
return ParentChildSummary(
|
||||
id: enfantId.toString(),
|
||||
status: '',
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,65 @@
|
||||
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
|
||||
class ParentModel {
|
||||
final AppUser user;
|
||||
final AppUser? coParent;
|
||||
final int childrenCount;
|
||||
final List<ParentChildSummary> children;
|
||||
|
||||
ParentModel({required this.user, this.childrenCount = 0});
|
||||
ParentModel({
|
||||
required this.user,
|
||||
this.coParent,
|
||||
this.childrenCount = 0,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
factory ParentModel.fromJson(Map<String, dynamic> json) {
|
||||
final userJson = json['user'] ?? json;
|
||||
final root = Map<String, dynamic>.from(json);
|
||||
final userJson = Map<String, dynamic>.from(root['user'] ?? root);
|
||||
if (root['numero_dossier'] != null && userJson['numero_dossier'] == null) {
|
||||
userJson['numero_dossier'] = root['numero_dossier'];
|
||||
}
|
||||
final user = AppUser.fromJson(userJson);
|
||||
final children = json['parentChildren'] as List?;
|
||||
|
||||
AppUser? coParent;
|
||||
final coParentRaw = root['co_parent'];
|
||||
if (coParentRaw is Map) {
|
||||
coParent = AppUser.fromJson(Map<String, dynamic>.from(coParentRaw));
|
||||
}
|
||||
|
||||
final children = _parseChildren(root);
|
||||
final links = root['parentChildren'] ?? root['parent_children'];
|
||||
final linkCount = links is List ? links.length : 0;
|
||||
|
||||
return ParentModel(
|
||||
user: user,
|
||||
childrenCount: children?.length ?? 0,
|
||||
coParent: coParent,
|
||||
childrenCount: children.isNotEmpty ? children.length : linkCount,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
static List<ParentChildSummary> _parseChildren(Map<String, dynamic> json) {
|
||||
final children = <ParentChildSummary>[];
|
||||
final seen = <String>{};
|
||||
|
||||
void add(ParentChildSummary? child) {
|
||||
if (child == null || child.id.isEmpty || seen.contains(child.id)) return;
|
||||
seen.add(child.id);
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
final links = json['parentChildren'] ?? json['parent_children'];
|
||||
if (links is List) {
|
||||
for (final link in links) {
|
||||
if (link is! Map) continue;
|
||||
add(ParentChildSummary.fromParentChildLink(
|
||||
Map<String, dynamic>.from(link),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user