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:
2026-07-11 22:49:57 +02:00
co-authored by Cursor
parent b99745e0fe
commit 003fe6b762
69 changed files with 6290 additions and 417 deletions
+278 -2
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:p_tits_pas/models/user.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/models/parent_model.dart';
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
import 'package:p_tits_pas/models/pending_family.dart';
@@ -366,7 +367,136 @@ class UserService {
}
final List<dynamic> data = jsonDecode(response.body);
return data.map((e) => ParentModel.fromJson(e)).toList();
return data
.map((e) => ParentModel.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
static Future<ParentModel> getParent(String userId) async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}/$userId'),
headers: await _headers(),
);
if (response.statusCode != 200) {
final err = jsonDecode(response.body) as Map<String, dynamic>?;
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parent');
}
final decoded = jsonDecode(response.body);
return ParentModel.fromJson(
Map<String, dynamic>.from(decoded is Map ? decoded : {}),
);
}
static Future<ParentModel> updateParentFiche({
required String parentUserId,
required Map<String, dynamic> body,
}) async {
final response = await http.patch(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}/$parentUserId/fiche'),
headers: await _headers(),
body: jsonEncode(body),
);
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur mise à jour parent'));
}
return _parentModelFromBody(response.body);
}
static Future<ParentModel> attachEnfantToParent({
required String parentUserId,
required String enfantId,
}) async {
final response = await http.post(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.parents}/$parentUserId/enfants/$enfantId',
),
headers: await _headers(),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception(_extractErrorMessage(response.body, 'Erreur rattachement enfant'));
}
return _parentModelFromBody(response.body);
}
static Future<ParentModel> detachEnfantFromParent({
required String parentUserId,
required String enfantId,
}) async {
final response = await http.delete(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.parents}/$parentUserId/enfants/$enfantId',
),
headers: await _headers(),
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception(_extractErrorMessage(response.body, 'Erreur détachement enfant'));
}
if (response.body.isEmpty) {
return getParent(parentUserId);
}
return _parentModelFromBody(response.body);
}
static Future<List<EnfantAdminModel>> getEnfants() async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}'),
headers: await _headers(),
);
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur chargement enfants'));
}
final List<dynamic> data = jsonDecode(response.body);
return data
.whereType<Map<String, dynamic>>()
.map(EnfantAdminModel.fromJson)
.toList();
}
static Future<EnfantAdminModel> getEnfant(String enfantId) async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
headers: await _headers(),
);
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur chargement enfant'));
}
return EnfantAdminModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
static Future<EnfantAdminModel> updateEnfant({
required String enfantId,
required Map<String, dynamic> body,
}) async {
final response = await http.patch(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
headers: await _headers(),
body: jsonEncode(body),
);
if (response.statusCode != 200) {
throw Exception(_extractErrorMessage(response.body, 'Erreur mise à jour enfant'));
}
return EnfantAdminModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
static ParentModel _parentModelFromBody(String body) {
final decoded = jsonDecode(body);
return ParentModel.fromJson(
Map<String, dynamic>.from(decoded is Map ? decoded : {}),
);
}
static String _extractErrorMessage(String body, String fallback) {
try {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
final message = decoded['message'];
if (message is List && message.isNotEmpty) {
return message.join(' - ');
}
return _toStr(message) ?? fallback;
}
} catch (_) {}
return fallback;
}
// Récupérer la liste des assistantes maternelles
@@ -383,7 +513,153 @@ class UserService {
}
final List<dynamic> data = jsonDecode(response.body);
return data.map((e) => AssistanteMaternelleModel.fromJson(e)).toList();
return data
.map((e) => AssistanteMaternelleModel.fromJson(
Map<String, dynamic>.from(e as Map),
))
.toList();
}
static Future<AssistanteMaternelleModel> getAssistanteMaternelle(
String userId,
) async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}/$userId'),
headers: await _headers(),
);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
return AssistanteMaternelleModel.fromJson(
Map<String, dynamic>.from(decoded is Map ? decoded : {}),
);
}
if (response.statusCode == 403 || response.statusCode == 404) {
final all = await getAssistantesMaternelles();
return all.firstWhere(
(a) => a.user.id == userId,
orElse: () => throw Exception('Assistante maternelle introuvable'),
);
}
final err = jsonDecode(response.body) as Map<String, dynamic>?;
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM');
}
/// Mise à jour fiche AM (identité + champs pro). Ticket #131.
static Future<AssistanteMaternelleModel> updateAmFiche({
required String amUserId,
required Map<String, dynamic> body,
}) async {
final ficheResponse = await http.patch(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}/$amUserId/fiche',
),
headers: await _headers(),
body: jsonEncode(body),
);
if (ficheResponse.statusCode == 200) {
return _amModelFromBody(ficheResponse.body);
}
if (ficheResponse.statusCode != 404) {
throw Exception(
_extractErrorMessage(ficheResponse.body, 'Erreur mise à jour AM'),
);
}
final userFields = <String, dynamic>{};
for (final k in [
'nom',
'prenom',
'email',
'telephone',
'adresse',
'ville',
'code_postal',
'statut',
'date_naissance',
'lieu_naissance_ville',
'lieu_naissance_pays',
]) {
if (body.containsKey(k)) userFields[k] = body[k];
}
final proFields = <String, dynamic>{};
for (final entry in body.entries) {
if (!userFields.containsKey(entry.key)) {
proFields[entry.key] = entry.value;
}
}
if (userFields.isNotEmpty) {
final userResponse = await http.patch(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$amUserId'),
headers: await _headers(),
body: jsonEncode(userFields),
);
if (userResponse.statusCode != 200) {
throw Exception(
_extractErrorMessage(userResponse.body, 'Erreur mise à jour identité'),
);
}
}
if (proFields.isNotEmpty) {
final proResponse = await http.patch(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}/$amUserId',
),
headers: await _headers(),
body: jsonEncode(proFields),
);
if (proResponse.statusCode != 200) {
throw Exception(
_extractErrorMessage(proResponse.body, 'Erreur mise à jour fiche pro'),
);
}
}
return getAssistanteMaternelle(amUserId);
}
static Future<AssistanteMaternelleModel> attachEnfantToAm({
required String amUserId,
required String enfantId,
}) async {
final response = await http.post(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}/$amUserId/enfants/$enfantId',
),
headers: await _headers(),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception(_extractErrorMessage(response.body, 'Erreur rattachement enfant'));
}
return _amModelFromBody(response.body);
}
static Future<AssistanteMaternelleModel> detachEnfantFromAm({
required String amUserId,
required String enfantId,
}) async {
final response = await http.delete(
Uri.parse(
'${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}/$amUserId/enfants/$enfantId',
),
headers: await _headers(),
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception(_extractErrorMessage(response.body, 'Erreur détachement enfant'));
}
if (response.body.isEmpty) {
return getAssistanteMaternelle(amUserId);
}
return _amModelFromBody(response.body);
}
static AssistanteMaternelleModel _amModelFromBody(String body) {
final decoded = jsonDecode(body);
return AssistanteMaternelleModel.fromJson(
Map<String, dynamic>.from(decoded is Map ? decoded : {}),
);
}
// Récupérer la liste des administrateurs (via /users filtré ou autre)