Squash merge de la branche develop dans master : un seul commit sur master pour regrouper la livraison liée au ticket #50 (affichage dynamique des CGU et de la politique de confidentialité à l'inscription) ainsi que la documentation et les ajustements associés. Frontend (P'titsPas) : modale de validation bloquante avec onglets CGU / confidentialité, chargement des PDF depuis l'API des documents légaux actifs, affichage via pdfx (PdfViewPinch et barre de progression latérale sur le web, repli PdfView sous Windows). Branchement dans le flux d'inscription (écran de présentation), enrichissement du UserService et correction des chemins média pour les URL absolues. Documentation: jeux de fichiers juridiques (markdown et PDF de référence), réorganisation (dossier docs/juridique, archives, renommage du briefing), mises à jour index, liste de tickets et décisions projet. Scripts et métadonnées mineures (Gitea, pubspec, index web). Made-with: Cursor
590 lines
19 KiB
Dart
590 lines
19 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/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';
|
|
|
|
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 : {}));
|
|
}
|
|
|
|
/// 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} : {}),
|
|
);
|
|
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 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(e)).toList();
|
|
}
|
|
|
|
// 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(e)).toList();
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
}
|
|
}
|