- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
644 lines
21 KiB
Dart
644 lines
21 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:injectable/injectable.dart';
|
|
import '../models/membre_model.dart';
|
|
import '../models/cotisation_model.dart';
|
|
import '../models/evenement_model.dart';
|
|
import '../models/wave_checkout_session_model.dart';
|
|
import '../network/dio_client.dart';
|
|
|
|
/// Service API principal pour communiquer avec le serveur UnionFlow
|
|
@singleton
|
|
class ApiService {
|
|
final DioClient _dioClient;
|
|
|
|
ApiService(this._dioClient);
|
|
|
|
Dio get _dio => _dioClient.dio;
|
|
|
|
// ========================================
|
|
// MEMBRES
|
|
// ========================================
|
|
|
|
/// Récupère la liste de tous les membres actifs
|
|
Future<List<MembreModel>> getMembres() async {
|
|
try {
|
|
final response = await _dio.get('/api/membres');
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => MembreModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la liste des membres');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des membres');
|
|
}
|
|
}
|
|
|
|
/// Récupère un membre par son ID
|
|
Future<MembreModel> getMembreById(String id) async {
|
|
try {
|
|
final response = await _dio.get('/api/membres/$id');
|
|
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération du membre');
|
|
}
|
|
}
|
|
|
|
/// Crée un nouveau membre
|
|
Future<MembreModel> createMembre(MembreModel membre) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'/api/membres',
|
|
data: membre.toJson(),
|
|
);
|
|
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la création du membre');
|
|
}
|
|
}
|
|
|
|
/// Met à jour un membre existant
|
|
Future<MembreModel> updateMembre(String id, MembreModel membre) async {
|
|
try {
|
|
final response = await _dio.put(
|
|
'/api/membres/$id',
|
|
data: membre.toJson(),
|
|
);
|
|
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la mise à jour du membre');
|
|
}
|
|
}
|
|
|
|
/// Désactive un membre
|
|
Future<void> deleteMembre(String id) async {
|
|
try {
|
|
await _dio.delete('/api/membres/$id');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la suppression du membre');
|
|
}
|
|
}
|
|
|
|
/// Recherche des membres par nom ou prénom
|
|
Future<List<MembreModel>> searchMembres(String query) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/membres/recherche',
|
|
queryParameters: {'q': query},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => MembreModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la recherche');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la recherche de membres');
|
|
}
|
|
}
|
|
|
|
/// Recherche avancée des membres avec filtres multiples
|
|
Future<List<MembreModel>> advancedSearchMembres(Map<String, dynamic> filters) async {
|
|
try {
|
|
// Nettoyer les filtres vides
|
|
final cleanFilters = <String, dynamic>{};
|
|
filters.forEach((key, value) {
|
|
if (value != null && value.toString().isNotEmpty) {
|
|
cleanFilters[key] = value;
|
|
}
|
|
});
|
|
|
|
final response = await _dio.get(
|
|
'/api/membres/recherche-avancee',
|
|
queryParameters: cleanFilters,
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => MembreModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la recherche avancée');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la recherche avancée de membres');
|
|
}
|
|
}
|
|
|
|
/// Récupère les statistiques des membres
|
|
Future<Map<String, dynamic>> getMembresStats() async {
|
|
try {
|
|
final response = await _dio.get('/api/membres/stats');
|
|
return response.data as Map<String, dynamic>;
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des statistiques');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// PAIEMENTS WAVE
|
|
// ========================================
|
|
|
|
/// Crée une session de paiement Wave
|
|
Future<WaveCheckoutSessionModel> createWaveSession({
|
|
required double montant,
|
|
required String devise,
|
|
required String successUrl,
|
|
required String errorUrl,
|
|
String? organisationId,
|
|
String? membreId,
|
|
String? typePaiement,
|
|
String? description,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'/api/paiements/wave/sessions',
|
|
data: {
|
|
'montant': montant,
|
|
'devise': devise,
|
|
'successUrl': successUrl,
|
|
'errorUrl': errorUrl,
|
|
if (organisationId != null) 'organisationId': organisationId,
|
|
if (membreId != null) 'membreId': membreId,
|
|
if (typePaiement != null) 'typePaiement': typePaiement,
|
|
if (description != null) 'description': description,
|
|
},
|
|
);
|
|
return WaveCheckoutSessionModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la création de la session Wave');
|
|
}
|
|
}
|
|
|
|
/// Récupère une session de paiement Wave par son ID
|
|
Future<WaveCheckoutSessionModel> getWaveSession(String sessionId) async {
|
|
try {
|
|
final response = await _dio.get('/api/paiements/wave/sessions/$sessionId');
|
|
return WaveCheckoutSessionModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération de la session Wave');
|
|
}
|
|
}
|
|
|
|
/// Vérifie le statut d'une session de paiement Wave
|
|
Future<String> checkWaveSessionStatus(String sessionId) async {
|
|
try {
|
|
final response = await _dio.get('/api/paiements/wave/sessions/$sessionId/status');
|
|
return response.data['statut'] as String;
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la vérification du statut Wave');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// COTISATIONS
|
|
// ========================================
|
|
|
|
/// Récupère la liste de toutes les cotisations avec pagination
|
|
Future<List<CotisationModel>> getCotisations({int page = 0, int size = 20}) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations', queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
});
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => CotisationModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la liste des cotisations');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des cotisations');
|
|
}
|
|
}
|
|
|
|
/// Récupère une cotisation par son ID
|
|
Future<CotisationModel> getCotisationById(String id) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/$id');
|
|
return CotisationModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération de la cotisation');
|
|
}
|
|
}
|
|
|
|
/// Récupère une cotisation par son numéro de référence
|
|
Future<CotisationModel> getCotisationByReference(String numeroReference) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/reference/$numeroReference');
|
|
return CotisationModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération de la cotisation');
|
|
}
|
|
}
|
|
|
|
/// Crée une nouvelle cotisation
|
|
Future<CotisationModel> createCotisation(CotisationModel cotisation) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'/api/cotisations',
|
|
data: cotisation.toJson(),
|
|
);
|
|
return CotisationModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la création de la cotisation');
|
|
}
|
|
}
|
|
|
|
/// Met à jour une cotisation existante
|
|
Future<CotisationModel> updateCotisation(String id, CotisationModel cotisation) async {
|
|
try {
|
|
final response = await _dio.put(
|
|
'/api/cotisations/$id',
|
|
data: cotisation.toJson(),
|
|
);
|
|
return CotisationModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la mise à jour de la cotisation');
|
|
}
|
|
}
|
|
|
|
/// Supprime une cotisation
|
|
Future<void> deleteCotisation(String id) async {
|
|
try {
|
|
await _dio.delete('/api/cotisations/$id');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la suppression de la cotisation');
|
|
}
|
|
}
|
|
|
|
/// Récupère les cotisations d'un membre
|
|
Future<List<CotisationModel>> getCotisationsByMembre(String membreId, {int page = 0, int size = 20}) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/membre/$membreId', queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
});
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => CotisationModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les cotisations du membre');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des cotisations du membre');
|
|
}
|
|
}
|
|
|
|
/// Récupère les cotisations par statut
|
|
Future<List<CotisationModel>> getCotisationsByStatut(String statut, {int page = 0, int size = 20}) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/statut/$statut', queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
});
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => CotisationModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les cotisations par statut');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des cotisations par statut');
|
|
}
|
|
}
|
|
|
|
/// Récupère les cotisations en retard
|
|
Future<List<CotisationModel>> getCotisationsEnRetard({int page = 0, int size = 20}) async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/en-retard', queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
});
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => CotisationModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les cotisations en retard');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des cotisations en retard');
|
|
}
|
|
}
|
|
|
|
/// Recherche avancée de cotisations
|
|
Future<List<CotisationModel>> rechercherCotisations({
|
|
String? membreId,
|
|
String? statut,
|
|
String? typeCotisation,
|
|
int? annee,
|
|
int? mois,
|
|
int page = 0,
|
|
int size = 20,
|
|
}) async {
|
|
try {
|
|
final queryParams = <String, dynamic>{
|
|
'page': page,
|
|
'size': size,
|
|
};
|
|
|
|
if (membreId != null) queryParams['membreId'] = membreId;
|
|
if (statut != null) queryParams['statut'] = statut;
|
|
if (typeCotisation != null) queryParams['typeCotisation'] = typeCotisation;
|
|
if (annee != null) queryParams['annee'] = annee;
|
|
if (mois != null) queryParams['mois'] = mois;
|
|
|
|
final response = await _dio.get('/api/cotisations/recherche', queryParameters: queryParams);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => CotisationModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la recherche de cotisations');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la recherche de cotisations');
|
|
}
|
|
}
|
|
|
|
/// Récupère les statistiques des cotisations
|
|
Future<Map<String, dynamic>> getCotisationsStats() async {
|
|
try {
|
|
final response = await _dio.get('/api/cotisations/stats');
|
|
return response.data as Map<String, dynamic>;
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des statistiques des cotisations');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// GESTION DES ERREURS
|
|
// ========================================
|
|
|
|
/// Gère les exceptions Dio et les convertit en messages d'erreur appropriés
|
|
Exception _handleDioException(DioException e, String defaultMessage) {
|
|
switch (e.type) {
|
|
case DioExceptionType.connectionTimeout:
|
|
case DioExceptionType.sendTimeout:
|
|
case DioExceptionType.receiveTimeout:
|
|
return Exception('Délai d\'attente dépassé. Vérifiez votre connexion internet.');
|
|
|
|
case DioExceptionType.badResponse:
|
|
final statusCode = e.response?.statusCode;
|
|
final responseData = e.response?.data;
|
|
|
|
if (statusCode == 400) {
|
|
if (responseData is Map && responseData.containsKey('message')) {
|
|
return Exception(responseData['message']);
|
|
}
|
|
return Exception('Données invalides');
|
|
} else if (statusCode == 401) {
|
|
return Exception('Non autorisé. Veuillez vous reconnecter.');
|
|
} else if (statusCode == 403) {
|
|
return Exception('Accès interdit');
|
|
} else if (statusCode == 404) {
|
|
return Exception('Ressource non trouvée');
|
|
} else if (statusCode == 500) {
|
|
return Exception('Erreur serveur. Veuillez réessayer plus tard.');
|
|
}
|
|
|
|
return Exception('$defaultMessage (Code: $statusCode)');
|
|
|
|
case DioExceptionType.cancel:
|
|
return Exception('Requête annulée');
|
|
|
|
case DioExceptionType.connectionError:
|
|
return Exception('Erreur de connexion. Vérifiez votre connexion internet.');
|
|
|
|
case DioExceptionType.badCertificate:
|
|
return Exception('Certificat SSL invalide');
|
|
|
|
case DioExceptionType.unknown:
|
|
default:
|
|
return Exception(defaultMessage);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// ÉVÉNEMENTS
|
|
// ========================================
|
|
|
|
/// Récupère la liste des événements à venir (optimisé mobile)
|
|
Future<List<EvenementModel>> getEvenementsAVenir({
|
|
int page = 0,
|
|
int size = 10,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/evenements/a-venir',
|
|
queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => EvenementModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les événements à venir');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des événements à venir');
|
|
}
|
|
}
|
|
|
|
/// Récupère la liste des événements publics (sans authentification)
|
|
Future<List<EvenementModel>> getEvenementsPublics({
|
|
int page = 0,
|
|
int size = 20,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/evenements/publics',
|
|
queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => EvenementModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les événements publics');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des événements publics');
|
|
}
|
|
}
|
|
|
|
/// Récupère tous les événements avec pagination
|
|
Future<List<EvenementModel>> getEvenements({
|
|
int page = 0,
|
|
int size = 20,
|
|
String sortField = 'dateDebut',
|
|
String sortDirection = 'asc',
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/evenements',
|
|
queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
'sort': sortField,
|
|
'direction': sortDirection,
|
|
},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => EvenementModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la liste des événements');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des événements');
|
|
}
|
|
}
|
|
|
|
/// Récupère un événement par son ID
|
|
Future<EvenementModel> getEvenementById(String id) async {
|
|
try {
|
|
final response = await _dio.get('/api/evenements/$id');
|
|
return EvenementModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération de l\'événement');
|
|
}
|
|
}
|
|
|
|
/// Recherche d'événements par terme
|
|
Future<List<EvenementModel>> rechercherEvenements(
|
|
String terme, {
|
|
int page = 0,
|
|
int size = 20,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/evenements/recherche',
|
|
queryParameters: {
|
|
'q': terme,
|
|
'page': page,
|
|
'size': size,
|
|
},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => EvenementModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour la recherche d\'événements');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la recherche d\'événements');
|
|
}
|
|
}
|
|
|
|
/// Récupère les événements par type
|
|
Future<List<EvenementModel>> getEvenementsByType(
|
|
TypeEvenement type, {
|
|
int page = 0,
|
|
int size = 20,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get(
|
|
'/api/evenements/type/${type.name.toUpperCase()}',
|
|
queryParameters: {
|
|
'page': page,
|
|
'size': size,
|
|
},
|
|
);
|
|
|
|
if (response.data is List) {
|
|
return (response.data as List)
|
|
.map((json) => EvenementModel.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Format de réponse invalide pour les événements par type');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des événements par type');
|
|
}
|
|
}
|
|
|
|
/// Crée un nouvel événement
|
|
Future<EvenementModel> createEvenement(EvenementModel evenement) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'/api/evenements',
|
|
data: evenement.toJson(),
|
|
);
|
|
return EvenementModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la création de l\'événement');
|
|
}
|
|
}
|
|
|
|
/// Met à jour un événement existant
|
|
Future<EvenementModel> updateEvenement(String id, EvenementModel evenement) async {
|
|
try {
|
|
final response = await _dio.put(
|
|
'/api/evenements/$id',
|
|
data: evenement.toJson(),
|
|
);
|
|
return EvenementModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la mise à jour de l\'événement');
|
|
}
|
|
}
|
|
|
|
/// Supprime un événement
|
|
Future<void> deleteEvenement(String id) async {
|
|
try {
|
|
await _dio.delete('/api/evenements/$id');
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la suppression de l\'événement');
|
|
}
|
|
}
|
|
|
|
/// Change le statut d'un événement
|
|
Future<EvenementModel> changerStatutEvenement(
|
|
String id,
|
|
StatutEvenement nouveauStatut,
|
|
) async {
|
|
try {
|
|
final response = await _dio.patch(
|
|
'/api/evenements/$id/statut',
|
|
queryParameters: {
|
|
'statut': nouveauStatut.name.toUpperCase(),
|
|
},
|
|
);
|
|
return EvenementModel.fromJson(response.data as Map<String, dynamic>);
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors du changement de statut');
|
|
}
|
|
}
|
|
|
|
/// Récupère les statistiques des événements
|
|
Future<Map<String, dynamic>> getStatistiquesEvenements() async {
|
|
try {
|
|
final response = await _dio.get('/api/evenements/statistiques');
|
|
return response.data as Map<String, dynamic>;
|
|
} on DioException catch (e) {
|
|
throw _handleDioException(e, 'Erreur lors de la récupération des statistiques');
|
|
}
|
|
}
|
|
}
|