import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; 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 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> _headers() async { final token = await TokenService.getToken(); return token != null ? ApiConfig.authHeaders(token) : Map.from(ApiConfig.headers); } static String? _toStr(dynamic v) { if (v == null) return null; if (v is String) return v; return v.toString(); } /// MIME pour upload photo enfant (filtre Nest : jpg/jpeg/png/gif). static MediaType _imageMediaType(String filename, List bytes) { if (bytes.length >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) { return MediaType('image', 'jpeg'); } if (bytes.length >= 8 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) { return MediaType('image', 'png'); } if (bytes.length >= 6 && bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) { return MediaType('image', 'gif'); } final lower = filename.toLowerCase(); if (lower.endsWith('.png')) return MediaType('image', 'png'); if (lower.endsWith('.gif')) return MediaType('image', 'gif'); return MediaType('image', 'jpeg'); } 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 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) { 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.from(cgu)), privacy: DocumentActifInfo.fromJson(Map.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> 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 : []; return data .where((e) => e is Map) .map((e) => AppUser.fromJson(Map.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> 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 : []; return data .where((e) => e is Map) .map((e) => PendingFamily.fromJson(Map.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 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) { throw FormatException('Réponse invalide'); } final dossier = DossierUnifie.fromJson(Map.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 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.from(data is Map ? data : {})); } /// Refuser un compte en attente (AM ou parent). PATCH /users/:id/refuser. Ticket #110. static Future 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.from(data is Map ? data : {})); } /// Valider tout le dossier famille. POST /parents/:parentId/valider-dossier. Ticket #108. static Future> 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.from(e as Map))).toList(); } // Récupérer la liste des gestionnaires (endpoint dédié) static Future> 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?; throw Exception( _toStr(err?['message']) ?? 'Erreur chargement gestionnaires'); } final List data = jsonDecode(response.body); return data.map((e) => AppUser.fromJson(e)).toList(); } static Future 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({ '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) { 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; return AppUser.fromJson(data); } static Future 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({ '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) { 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; return AppUser.fromJson(data); } // Récupérer la liste des parents static Future> 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?; throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parents'); } final List data = jsonDecode(response.body); return data .map((e) => ParentModel.fromJson(Map.from(e as Map))) .toList(); } static Future 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?; throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parent'); } final decoded = jsonDecode(response.body); return ParentModel.fromJson( Map.from(decoded is Map ? decoded : {}), ); } static Future updateParentFiche({ required String parentUserId, required Map 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 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 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> 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 data = jsonDecode(response.body); return data .whereType>() .map(EnfantAdminModel.fromJson) .toList(); } static Future 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, ); // 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 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 updateEnfant({ required String enfantId, required Map 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, ); 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`). /// Avec [photoBytes] : `multipart/form-data` (champ fichier `photo`), sinon JSON. static Future createEnfant({ required String parentUserId, required Map body, List? photoBytes, String? photoFilename, }) async { final hasPhoto = photoBytes != null && photoBytes.isNotEmpty; final http.Response response; if (hasPhoto) { final token = await TokenService.getToken(); final req = http.MultipartRequest( 'POST', Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}'), ); req.headers['Accept'] = 'application/json'; if (token != null) { req.headers['Authorization'] = 'Bearer $token'; } body.forEach((key, value) { if (value == null) return; req.fields[key] = value is bool ? (value ? 'true' : 'false') : value.toString(); }); req.fields['parent_user_id'] = parentUserId; final name = (photoFilename ?? '').trim(); final filename = name.isNotEmpty ? name : 'photo.jpg'; req.files.add( http.MultipartFile.fromBytes( 'photo', photoBytes, filename: filename, contentType: _imageMediaType(filename, photoBytes), ), ); final streamed = await req.send(); response = await http.Response.fromStream(streamed); } else { final payload = { ...body, 'parent_user_id': parentUserId, }; 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, ); return enrichEnfantParentNames(enfant); } static Future 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 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.from(decoded is Map ? decoded : {}), ); } static String _extractErrorMessage(String body, String fallback) { try { final decoded = jsonDecode(body); if (decoded is Map) { 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> 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?; throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM'); } final List data = jsonDecode(response.body); return data .map((e) => AssistanteMaternelleModel.fromJson( Map.from(e as Map), )) .toList(); } static Future 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.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?; throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM'); } /// Mise à jour fiche AM (identité + champs pro). Ticket #131. static Future updateAmFiche({ required String amUserId, required Map 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 = {}; 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 = {}; 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 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 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 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.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> 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 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 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({ '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) { 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; return AppUser.fromJson(data); } static Future updateAdmin({ required String adminId, required String nom, required String prenom, required String email, required String telephone, String? password, }) async { final body = { '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) { 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; return AppUser.fromJson(data); } static Future 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({'relaisId': relaisId}), ); if (response.statusCode != 200 && response.statusCode != 204) { final err = jsonDecode(response.body) as Map?; throw Exception( _toStr(err?['message']) ?? 'Erreur rattachement relais au gestionnaire', ); } } static Future updateGestionnaire({ required String gestionnaireId, required String nom, required String prenom, required String email, String? telephone, required String? relaisId, String? password, }) async { final body = { '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) { 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; return AppUser.fromJson(data); } static Future updateAdministrateur({ required String adminId, required String nom, required String prenom, required String email, String? telephone, String? password, }) async { final body = { '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) { 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; return AppUser.fromJson(data); } static Future 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) { 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'); } } }