import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:retry/retry.dart'; import 'package:universal_html/html.dart' as html; import 'package:geosector_app/core/utils/api_exception.dart'; class ApiService { static ApiService? _instance; static final Object _lock = Object(); // Propriétés existantes conservées final Dio _dio = Dio(); late final String _baseUrl; late final String _appIdentifier; String? _sessionId; // Singleton thread-safe static ApiService get instance { if (_instance == null) { throw Exception('ApiService non initialisé. Appelez initialize() d\'abord.'); } return _instance!; } static Future initialize() async { if (_instance == null) { synchronized(_lock, () { if (_instance == null) { _instance = ApiService._internal(); debugPrint('✅ ApiService singleton initialisé'); } }); } } // Constructeur privé avec toute la logique existante ApiService._internal() { _configureEnvironment(); _dio.options.baseUrl = _baseUrl; _dio.options.connectTimeout = AppKeys.connectionTimeout; _dio.options.receiveTimeout = AppKeys.receiveTimeout; final headers = Map.from(AppKeys.defaultHeaders); headers['X-App-Identifier'] = _appIdentifier; _dio.options.headers.addAll(headers); _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { if (_sessionId != null) { options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId'; } handler.next(options); }, onError: (DioException error, handler) { if (error.response?.statusCode == 401) { _sessionId = null; } handler.next(error); }, )); debugPrint('🔗 ApiService configuré pour $_baseUrl'); } // Fonction synchronized simple pour éviter les imports supplémentaires static T synchronized(Object lock, T Function() computation) { return computation(); } // Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL String _determineEnvironment() { if (!kIsWeb) { // En mode non-web, utiliser l'environnement de développement par défaut return 'DEV'; } final currentUrl = html.window.location.href.toLowerCase(); if (currentUrl.contains('dapp.geosector.fr')) { return 'DEV'; } else if (currentUrl.contains('rapp.geosector.fr')) { return 'REC'; } else { return 'PROD'; } } // Configure l'URL de base API et l'identifiant d'application selon l'environnement void _configureEnvironment() { final env = _determineEnvironment(); switch (env) { case 'DEV': _baseUrl = AppKeys.baseApiUrlDev; _appIdentifier = AppKeys.appIdentifierDev; break; case 'REC': _baseUrl = AppKeys.baseApiUrlRec; _appIdentifier = AppKeys.appIdentifierRec; break; default: // PROD _baseUrl = AppKeys.baseApiUrlProd; _appIdentifier = AppKeys.appIdentifierProd; } debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl'); } // Définir l'ID de session void setSessionId(String? sessionId) { _sessionId = sessionId; } // Obtenir l'environnement actuel (utile pour le débogage) String getCurrentEnvironment() { return _determineEnvironment(); } // Obtenir l'URL API actuelle (utile pour le débogage) String getCurrentApiUrl() { return _baseUrl; } // Obtenir l'identifiant d'application actuel (utile pour le débogage) String getCurrentAppIdentifier() { return _appIdentifier; } // Vérifier la connectivité réseau Future hasInternetConnection() async { final connectivityResult = await (Connectivity().checkConnectivity()); return connectivityResult.contains(ConnectivityResult.none) == false; } // Méthode POST générique Future post(String path, {dynamic data}) async { try { return await _dio.post(path, data: data); } catch (e) { rethrow; } } // Méthode GET générique Future get(String path, {Map? queryParameters}) async { try { return await _dio.get(path, queryParameters: queryParameters); } catch (e) { rethrow; } } // Méthode PUT générique Future put(String path, {dynamic data}) async { try { return await _dio.put(path, data: data); } catch (e) { rethrow; } } // Méthode DELETE générique Future delete(String path) async { try { return await _dio.delete(path); } catch (e) { rethrow; } } // Authentification avec PHP session Future> login(String username, String password, {required String type}) async { try { final response = await _dio.post(AppKeys.loginEndpoint, data: { 'username': username, 'password': password, 'type': type, }); final data = response.data as Map; final status = data['status'] as String?; // Si le statut n'est pas 'success', créer une exception avec le message de l'API if (status != 'success') { final message = data['message'] as String? ?? 'Erreur de connexion'; throw ApiException(message); } // Si succès, configurer la session if (data.containsKey('session_id')) { final sessionId = data['session_id']; if (sessionId != null) { setSessionId(sessionId); } } return data; } on DioException catch (e) { throw ApiException.fromDioException(e); } catch (e) { if (e is ApiException) rethrow; throw ApiException('Erreur inattendue lors de la connexion', originalError: e); } } // Déconnexion Future logout() async { try { if (_sessionId != null) { await _dio.post(AppKeys.logoutEndpoint); _sessionId = null; } } catch (e) { // Même en cas d'erreur, on réinitialise la session _sessionId = null; rethrow; } } // Utilisateurs Future> getUsers() async { try { final response = await retry( () => _dio.get('/users'), retryIf: (e) => e is SocketException || e is TimeoutException, ); return (response.data as List).map((json) => UserModel.fromJson(json)).toList(); } catch (e) { // Gérer les erreurs rethrow; } } Future getUserById(int id) async { try { final response = await _dio.get('/users/$id'); return UserModel.fromJson(response.data); } catch (e) { rethrow; } } Future updateUser(UserModel user) async { try { final response = await _dio.put('/users/${user.id}', data: user.toJson()); // Vérifier la structure de la réponse final data = response.data as Map; // Si l'API retourne {status: "success", message: "..."} if (data.containsKey('status') && data['status'] == 'success') { // L'API confirme le succès mais ne retourne pas l'objet user // On retourne l'utilisateur original qui a été envoyé debugPrint('✅ API updateUser success: ${data['message']}'); return user; } // Si l'API retourne directement un UserModel (fallback) return UserModel.fromJson(data); } on DioException catch (e) { throw ApiException.fromDioException(e); } catch (e) { throw ApiException('Erreur inattendue lors de la mise à jour', originalError: e); } } // Appliquer la même logique aux autres méthodes Future createUser(UserModel user) async { try { final response = await _dio.post('/users', data: user.toJson()); return UserModel.fromJson(response.data); } on DioException catch (e) { throw ApiException.fromDioException(e); } catch (e) { throw ApiException('Erreur inattendue lors de la création', originalError: e); } } Future deleteUser(String id) async { try { await _dio.delete('/users/$id'); } catch (e) { rethrow; } } // Synchronisation en batch Future> syncData({ List? users, }) async { try { final Map payload = { if (users != null) 'users': users.map((u) => u.toJson()).toList(), }; final response = await _dio.post('/sync', data: payload); return response.data; } catch (e) { rethrow; } } // Export Excel d'une opération Future downloadOperationExcel(int operationId, String fileName) async { try { debugPrint('📊 Téléchargement Excel pour opération $operationId'); final response = await _dio.get( '/operations/$operationId/export/excel', options: Options( responseType: ResponseType.bytes, // Important pour les fichiers binaires headers: { 'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, ), ); if (response.statusCode == 200) { debugPrint('✅ Fichier Excel reçu (${response.data.length} bytes)'); if (kIsWeb) { // Pour le web : déclencher le téléchargement via le navigateur _downloadFileWeb(response.data, fileName); } else { // Pour mobile : sauvegarder dans le dossier de téléchargements await _downloadFileMobile(response.data, fileName); } debugPrint('✅ Export Excel terminé: $fileName'); } else { throw ApiException('Erreur lors du téléchargement: ${response.statusCode}'); } } on DioException catch (e) { throw ApiException.fromDioException(e); } catch (e) { if (e is ApiException) rethrow; throw ApiException('Erreur inattendue lors de l\'export Excel', originalError: e); } } // Téléchargement pour le web void _downloadFileWeb(List bytes, String fileName) { final blob = html.Blob([bytes]); final url = html.Url.createObjectUrlFromBlob(blob); final anchor = html.AnchorElement(href: url) ..setAttribute('download', fileName) ..click(); html.Url.revokeObjectUrl(url); debugPrint('🌐 Téléchargement web déclenché: $fileName'); } // Téléchargement pour mobile Future _downloadFileMobile(List bytes, String fileName) async { try { // Pour mobile, on pourrait utiliser path_provider pour obtenir le dossier de téléchargements // et file_picker ou similar pour sauvegarder le fichier // Pour l'instant, on lance juste une exception informative throw const ApiException('Téléchargement mobile non implémenté. Utilisez la version web.'); } catch (e) { rethrow; } } // Méthode de nettoyage pour les tests static void reset() { _instance = null; } }