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>
992 lines
33 KiB
Dart
992 lines
33 KiB
Dart
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';
|
||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
||
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
||
|
||
class DocumentActifInfo {
|
||
final String id;
|
||
final String type;
|
||
final int version;
|
||
final String url;
|
||
|
||
const DocumentActifInfo({
|
||
required this.id,
|
||
required this.type,
|
||
required this.version,
|
||
required this.url,
|
||
});
|
||
|
||
factory DocumentActifInfo.fromJson(Map<String, dynamic> json) {
|
||
return DocumentActifInfo(
|
||
id: (json['id'] ?? '').toString(),
|
||
type: (json['type'] ?? '').toString(),
|
||
version: int.tryParse((json['version'] ?? '').toString()) ?? 0,
|
||
url: (json['url'] ?? '').toString(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class DocumentsActifsInfo {
|
||
final DocumentActifInfo cgu;
|
||
final DocumentActifInfo privacy;
|
||
|
||
const DocumentsActifsInfo({
|
||
required this.cgu,
|
||
required this.privacy,
|
||
});
|
||
}
|
||
|
||
class UserService {
|
||
static Future<Map<String, String>> _headers() async {
|
||
final token = await TokenService.getToken();
|
||
return token != null
|
||
? ApiConfig.authHeaders(token)
|
||
: Map<String, String>.from(ApiConfig.headers);
|
||
}
|
||
|
||
static String? _toStr(dynamic v) {
|
||
if (v == null) return null;
|
||
if (v is String) return v;
|
||
return v.toString();
|
||
}
|
||
|
||
static String _errMessage(dynamic err) {
|
||
if (err == null) return 'Erreur inconnue';
|
||
if (err is String) return err;
|
||
if (err is Map) {
|
||
final m = err['message'];
|
||
if (m is String) return m;
|
||
if (m is Map && m['message'] is String) return m['message'] as String;
|
||
if (m != null) return _toStr(m) ?? 'Erreur inconnue';
|
||
}
|
||
return _toStr(err) ?? 'Erreur inconnue';
|
||
}
|
||
|
||
/// Documents légaux actifs (CGU + privacy). GET /documents-legaux/actifs.
|
||
static Future<DocumentsActifsInfo> getDocumentsLegauxActifs() async {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}/documents-legaux/actifs'),
|
||
headers: ApiConfig.headers,
|
||
);
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception(
|
||
'Erreur chargement documents légaux (${response.statusCode})',
|
||
);
|
||
}
|
||
}
|
||
|
||
try {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is! Map<String, dynamic>) {
|
||
throw const FormatException('Réponse invalide');
|
||
}
|
||
final cgu = decoded['cgu'];
|
||
final privacy = decoded['privacy'];
|
||
if (cgu is! Map || privacy is! Map) {
|
||
throw const FormatException('Documents actifs manquants');
|
||
}
|
||
return DocumentsActifsInfo(
|
||
cgu: DocumentActifInfo.fromJson(Map<String, dynamic>.from(cgu)),
|
||
privacy: DocumentActifInfo.fromJson(Map<String, dynamic>.from(privacy)),
|
||
);
|
||
} catch (e) {
|
||
if (e is FormatException) rethrow;
|
||
throw Exception(
|
||
'Réponse invalide (documents légaux): ${e is Exception ? e.toString() : "format inattendu"}',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Utilisateurs en attente de validation (GET /users/pending). Ticket #107.
|
||
static Future<List<AppUser>> getPendingUsers({String? role}) async {
|
||
final query = role != null ? '?role=$role' : '';
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/pending$query'),
|
||
headers: await _headers(),
|
||
);
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur chargement comptes en attente (${response.statusCode})');
|
||
}
|
||
}
|
||
try {
|
||
final decoded = jsonDecode(response.body);
|
||
final data = decoded is List ? decoded as List<dynamic> : <dynamic>[];
|
||
return data
|
||
.where((e) => e is Map<String, dynamic>)
|
||
.map((e) => AppUser.fromJson(Map<String, dynamic>.from(e as Map)))
|
||
.toList();
|
||
} catch (e) {
|
||
throw Exception('Réponse invalide (comptes en attente): ${e is Exception ? e.toString() : "format inattendu"}');
|
||
}
|
||
}
|
||
|
||
/// Familles en attente (une entrée par famille). GET /parents/pending-families. Ticket #107.
|
||
static Future<List<PendingFamily>> getPendingFamilies() async {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}/pending-families'),
|
||
headers: await _headers(),
|
||
);
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur chargement familles en attente (${response.statusCode})');
|
||
}
|
||
}
|
||
try {
|
||
final decoded = jsonDecode(response.body);
|
||
final data = decoded is List ? decoded as List<dynamic> : <dynamic>[];
|
||
return data
|
||
.where((e) => e is Map)
|
||
.map((e) => PendingFamily.fromJson(Map<String, dynamic>.from(e as Map)))
|
||
.toList();
|
||
} catch (e) {
|
||
throw Exception('Réponse invalide (familles en attente): ${e is Exception ? e.toString() : "format inattendu"}');
|
||
}
|
||
}
|
||
|
||
/// Dossier unifié par numéro (AM ou famille). GET /dossiers/:numeroDossier. Ticket #119, #107.
|
||
static Future<DossierUnifie> getDossier(String numeroDossier) async {
|
||
final encoded = Uri.encodeComponent(numeroDossier);
|
||
final uri = Uri.parse('${ApiConfig.baseUrl}${ApiConfig.dossiers}/$encoded');
|
||
final response = await http.get(
|
||
uri,
|
||
headers: await _headers(),
|
||
);
|
||
if (response.statusCode == 404) {
|
||
throw Exception('Aucun dossier avec ce numéro.');
|
||
}
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur chargement dossier (${response.statusCode})');
|
||
}
|
||
}
|
||
try {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is! Map<String, dynamic>) {
|
||
throw FormatException('Réponse invalide');
|
||
}
|
||
final dossier = DossierUnifie.fromJson(Map<String, dynamic>.from(decoded));
|
||
return dossier;
|
||
} catch (e) {
|
||
if (e is FormatException) rethrow;
|
||
throw Exception('Réponse invalide (dossier): ${e is Exception ? e.toString() : "format inattendu"}');
|
||
}
|
||
}
|
||
|
||
/// Valider un utilisateur (AM). PATCH /users/:id/valider. Ticket #108.
|
||
static Future<AppUser> validateUser(String userId, {String? comment}) async {
|
||
final response = await http.patch(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId/valider'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(comment != null ? {'comment': comment} : {}),
|
||
);
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur validation (${response.statusCode})');
|
||
}
|
||
}
|
||
final data = jsonDecode(response.body);
|
||
return AppUser.fromJson(Map<String, dynamic>.from(data is Map ? data : {}));
|
||
}
|
||
|
||
/// Refuser un compte en attente (AM ou parent). PATCH /users/:id/refuser. Ticket #110.
|
||
static Future<AppUser> refuseUser(String userId, {String? comment}) async {
|
||
final response = await http.patch(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId/refuser'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(
|
||
comment != null && comment.trim().isNotEmpty
|
||
? {'comment': comment.trim()}
|
||
: {},
|
||
),
|
||
);
|
||
if (response.statusCode != 200) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur refus (${response.statusCode})');
|
||
}
|
||
}
|
||
final data = jsonDecode(response.body);
|
||
return AppUser.fromJson(Map<String, dynamic>.from(data is Map ? data : {}));
|
||
}
|
||
|
||
/// Valider tout le dossier famille. POST /parents/:parentId/valider-dossier. Ticket #108.
|
||
static Future<List<AppUser>> validerDossierFamille(String parentId, {String? comment}) async {
|
||
final response = await http.post(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}/$parentId/valider-dossier'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(comment != null ? {'comment': comment} : {}),
|
||
);
|
||
// NestJS renvoie souvent 201 Created sur un POST même avec corps JSON — le front ne doit pas traiter ça comme une erreur
|
||
// (sinon validation + emails déjà faits côté serveur mais SnackBar rouge côté UI).
|
||
final ok = response.statusCode == 200 || response.statusCode == 201;
|
||
if (!ok) {
|
||
try {
|
||
final err = jsonDecode(response.body);
|
||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||
} catch (e) {
|
||
if (e is Exception) rethrow;
|
||
throw Exception('Erreur validation dossier famille (${response.statusCode})');
|
||
}
|
||
}
|
||
final data = jsonDecode(response.body);
|
||
final list = data is List ? data : [];
|
||
return list.map((e) => AppUser.fromJson(Map<String, dynamic>.from(e as Map))).toList();
|
||
}
|
||
|
||
// Récupérer la liste des gestionnaires (endpoint dédié)
|
||
static Future<List<AppUser>> getGestionnaires() async {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||
headers: await _headers(),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||
throw Exception(
|
||
_toStr(err?['message']) ?? 'Erreur chargement gestionnaires');
|
||
}
|
||
|
||
final List<dynamic> data = jsonDecode(response.body);
|
||
return data.map((e) => AppUser.fromJson(e)).toList();
|
||
}
|
||
|
||
static Future<AppUser> createGestionnaire({
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
required String password,
|
||
required String telephone,
|
||
String? relaisId,
|
||
}) async {
|
||
final response = await http.post(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(<String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
'password': password,
|
||
'telephone': telephone,
|
||
'cguAccepted': true,
|
||
'relaisId': relaisId,
|
||
}),
|
||
);
|
||
|
||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur création gestionnaire');
|
||
}
|
||
throw Exception('Erreur création gestionnaire');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
static Future<AppUser> createAdministrateur({
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
required String password,
|
||
required String telephone,
|
||
}) async {
|
||
final response = await http.post(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/admin'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(<String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
'password': password,
|
||
'telephone': telephone,
|
||
}),
|
||
);
|
||
|
||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur création administrateur');
|
||
}
|
||
throw Exception('Erreur création administrateur');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
// Récupérer la liste des parents
|
||
static Future<List<ParentModel>> getParents() async {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}'),
|
||
headers: await _headers(),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parents');
|
||
}
|
||
|
||
final List<dynamic> data = jsonDecode(response.body);
|
||
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'));
|
||
}
|
||
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({
|
||
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'));
|
||
}
|
||
final enfant = EnfantAdminModel.fromJson(
|
||
jsonDecode(response.body) as Map<String, dynamic>,
|
||
);
|
||
return enrichEnfantParentNames(enfant);
|
||
}
|
||
|
||
/// Création enfant côté staff (#132) — nécessite `POST /enfants` étendu (voir mini-spec).
|
||
/// [parentUserId] : parent pivot du foyer (`parent_user_id`).
|
||
static Future<EnfantAdminModel> createEnfant({
|
||
required String parentUserId,
|
||
required Map<String, dynamic> body,
|
||
}) async {
|
||
final payload = <String, dynamic>{
|
||
...body,
|
||
'parent_user_id': parentUserId,
|
||
};
|
||
final response = await http.post(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(payload),
|
||
);
|
||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||
throw Exception(
|
||
_extractErrorMessage(response.body, 'Erreur création enfant'),
|
||
);
|
||
}
|
||
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) {
|
||
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
|
||
static Future<List<AssistanteMaternelleModel>>
|
||
getAssistantesMaternelles() async {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}'),
|
||
headers: await _headers(),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM');
|
||
}
|
||
|
||
final List<dynamic> data = jsonDecode(response.body);
|
||
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'));
|
||
}
|
||
final am = _amModelFromBody(response.body);
|
||
// Recalcule toujours places_available (capacité − enfants) après attachement.
|
||
return syncAmPlacesAvailable(am);
|
||
}
|
||
|
||
/// Aligne `places_available` sur capacité − enfants rattachés.
|
||
static Future<AssistanteMaternelleModel> syncAmPlacesAvailable(
|
||
AssistanteMaternelleModel am,
|
||
) async {
|
||
final expected = amExpectedPlacesAvailable(
|
||
maxChildren: am.maxChildren,
|
||
childrenCount: am.children.length,
|
||
);
|
||
if (expected == null || am.placesAvailable == expected) return am;
|
||
return updateAmFiche(
|
||
amUserId: am.user.id,
|
||
body: {'places_available': expected},
|
||
);
|
||
}
|
||
|
||
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)
|
||
// Pour l'instant on va utiliser /users et filtrer côté client si on est super admin
|
||
static Future<List<AppUser>> getAdministrateurs() async {
|
||
// TODO: Endpoint dédié ou filtrage
|
||
// En attendant, on retourne une liste vide ou on tente /users
|
||
try {
|
||
final response = await http.get(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}'),
|
||
headers: await _headers(),
|
||
);
|
||
|
||
if (response.statusCode == 200) {
|
||
final List<dynamic> data = jsonDecode(response.body);
|
||
return data
|
||
.map((e) => AppUser.fromJson(e))
|
||
.where((u) => u.role == 'administrateur' || u.role == 'super_admin')
|
||
.toList();
|
||
}
|
||
} catch (e) {
|
||
// On garde un fallback vide pour ne pas bloquer l'UI admin.
|
||
}
|
||
return [];
|
||
}
|
||
|
||
static Future<AppUser> createAdmin({
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
required String password,
|
||
required String telephone,
|
||
}) async {
|
||
final response = await http.post(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/admin'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(<String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
'password': password,
|
||
'telephone': telephone,
|
||
}),
|
||
);
|
||
|
||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur création administrateur');
|
||
}
|
||
throw Exception('Erreur création administrateur');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
static Future<AppUser> updateAdmin({
|
||
required String adminId,
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
required String telephone,
|
||
String? password,
|
||
}) async {
|
||
final body = <String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
'telephone': telephone,
|
||
};
|
||
|
||
if (password != null && password.trim().isNotEmpty) {
|
||
body['password'] = password.trim();
|
||
}
|
||
|
||
final response = await http.patch(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$adminId'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(body),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur modification administrateur');
|
||
}
|
||
throw Exception('Erreur modification administrateur');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
static Future<void> updateGestionnaireRelais({
|
||
required String gestionnaireId,
|
||
required String? relaisId,
|
||
}) async {
|
||
final response = await http.patch(
|
||
Uri.parse(
|
||
'${ApiConfig.baseUrl}${ApiConfig.gestionnaires}/$gestionnaireId'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(<String, dynamic>{'relaisId': relaisId}),
|
||
);
|
||
|
||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||
throw Exception(
|
||
_toStr(err?['message']) ?? 'Erreur rattachement relais au gestionnaire',
|
||
);
|
||
}
|
||
}
|
||
|
||
static Future<AppUser> updateGestionnaire({
|
||
required String gestionnaireId,
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
String? telephone,
|
||
required String? relaisId,
|
||
String? password,
|
||
}) async {
|
||
final body = <String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
'relaisId': relaisId,
|
||
};
|
||
|
||
if (telephone != null && telephone.trim().isNotEmpty) {
|
||
body['telephone'] = telephone.trim();
|
||
}
|
||
|
||
if (password != null && password.trim().isNotEmpty) {
|
||
body['password'] = password.trim();
|
||
}
|
||
|
||
final response = await http.patch(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}/$gestionnaireId'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(body),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur modification gestionnaire');
|
||
}
|
||
throw Exception('Erreur modification gestionnaire');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
static Future<AppUser> updateAdministrateur({
|
||
required String adminId,
|
||
required String nom,
|
||
required String prenom,
|
||
required String email,
|
||
String? telephone,
|
||
String? password,
|
||
}) async {
|
||
final body = <String, dynamic>{
|
||
'nom': nom,
|
||
'prenom': prenom,
|
||
'email': email,
|
||
};
|
||
|
||
if (telephone != null && telephone.trim().isNotEmpty) {
|
||
body['telephone'] = telephone.trim();
|
||
}
|
||
|
||
if (password != null && password.trim().isNotEmpty) {
|
||
body['password'] = password.trim();
|
||
}
|
||
|
||
final response = await http.patch(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$adminId'),
|
||
headers: await _headers(),
|
||
body: jsonEncode(body),
|
||
);
|
||
|
||
if (response.statusCode != 200) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur modification administrateur');
|
||
}
|
||
throw Exception('Erreur modification administrateur');
|
||
}
|
||
|
||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||
return AppUser.fromJson(data);
|
||
}
|
||
|
||
static Future<void> deleteUser(String userId) async {
|
||
final response = await http.delete(
|
||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}/$userId'),
|
||
headers: await _headers(),
|
||
);
|
||
|
||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||
final decoded = jsonDecode(response.body);
|
||
if (decoded is Map<String, dynamic>) {
|
||
final message = decoded['message'];
|
||
if (message is List && message.isNotEmpty) {
|
||
throw Exception(message.join(' - '));
|
||
}
|
||
throw Exception(_toStr(message) ?? 'Erreur suppression utilisateur');
|
||
}
|
||
throw Exception('Erreur suppression utilisateur');
|
||
}
|
||
}
|
||
}
|