378 lines
11 KiB
Dart
378 lines
11 KiB
Dart
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<void> 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<String, String>.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<T>(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<bool> hasInternetConnection() async {
|
|
final connectivityResult = await (Connectivity().checkConnectivity());
|
|
return connectivityResult.contains(ConnectivityResult.none) == false;
|
|
}
|
|
|
|
// Méthode POST générique
|
|
Future<Response> post(String path, {dynamic data}) async {
|
|
try {
|
|
return await _dio.post(path, data: data);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Méthode GET générique
|
|
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
|
|
try {
|
|
return await _dio.get(path, queryParameters: queryParameters);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Méthode PUT générique
|
|
Future<Response> put(String path, {dynamic data}) async {
|
|
try {
|
|
return await _dio.put(path, data: data);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Méthode DELETE générique
|
|
Future<Response> delete(String path) async {
|
|
try {
|
|
return await _dio.delete(path);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Authentification avec PHP session
|
|
Future<Map<String, dynamic>> 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<String, dynamic>;
|
|
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<void> 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<List<UserModel>> 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<UserModel> getUserById(int id) async {
|
|
try {
|
|
final response = await _dio.get('/users/$id');
|
|
return UserModel.fromJson(response.data);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<UserModel> 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<String, dynamic>;
|
|
|
|
// 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<UserModel> 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<void> deleteUser(String id) async {
|
|
try {
|
|
await _dio.delete('/users/$id');
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Synchronisation en batch
|
|
Future<Map<String, dynamic>> syncData({
|
|
List<UserModel>? users,
|
|
}) async {
|
|
try {
|
|
final Map<String, dynamic> 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<void> 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<int> 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<void> _downloadFileMobile(List<int> 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;
|
|
}
|
|
}
|