import 'package:firebase_auth/firebase_auth.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:em2rp/config/api_config.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; /// Interface abstraite pour les opérations API /// Permet de changer facilement de backend (Firebase Functions, REST API personnalisé, etc.) abstract class ApiService { Future> call(String functionName, Map data); Future get(String endpoint, {Map? params}); Future post(String endpoint, Map data); Future put(String endpoint, Map data); Future delete(String endpoint, {Map? data}); } /// Implémentation pour Firebase Cloud Functions class FirebaseFunctionsApiService implements ApiService { // URL de base - gérée par ApiConfig String get _baseUrl => ApiConfig.baseUrl; /// Récupère le token d'authentification Firebase Future _getAuthToken() async { final user = FirebaseAuth.instance.currentUser; if (user == null) return null; return await user.getIdToken(); } /// Headers par défaut avec authentification Future> _getHeaders() async { final token = await _getAuthToken(); return { 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; } /// Convertit récursivement les Timestamps Firestore, DocumentReference et GeoPoint en formats encodables dynamic _convertTimestamps(dynamic value) { if (value == null) return null; if (value is Timestamp) { // Convertir Timestamp en ISO string return value.toDate().toIso8601String(); } else if (value is DateTime) { // Convertir DateTime en ISO string return value.toIso8601String(); } else if (value is DocumentReference) { // Convertir DocumentReference en path string return value.path; } else if (value is GeoPoint) { // Convertir GeoPoint en objet avec latitude et longitude return { 'latitude': value.latitude, 'longitude': value.longitude, }; } else if (value is Map) { // Parcourir récursivement les Maps et créer une nouvelle Map typée final Map result = {}; value.forEach((key, val) { result[key.toString()] = _convertTimestamps(val); }); return result; } else if (value is List) { // Parcourir récursivement les Lists return value.map((item) => _convertTimestamps(item)).toList(); } return value; } @override Future> call(String functionName, Map data) async { final url = Uri.parse('$_baseUrl/$functionName'); final headers = await _getHeaders(); // Convertir les Timestamps avant l'envoi final convertedData = _convertTimestamps(data) as Map; // Log pour débogage print('[API] Calling $functionName with eventId: ${convertedData['eventId']}'); final response = await http.post( url, headers: headers, body: jsonEncode({'data': convertedData}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData is Map ? responseData : {}; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future get(String endpoint, {Map? params}) async { final url = Uri.parse('$_baseUrl/$endpoint').replace(queryParameters: params); final headers = await _getHeaders(); final response = await http.get(url, headers: headers); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T?; } else if (response.statusCode == 404) { return null; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future post(String endpoint, Map data) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Convertir les Timestamps avant l'envoi final convertedData = _convertTimestamps(data) as Map; final response = await http.post( url, headers: headers, body: jsonEncode({'data': convertedData}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future put(String endpoint, Map data) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Convertir les Timestamps avant l'envoi final convertedData = _convertTimestamps(data) as Map; final response = await http.put( url, headers: headers, body: jsonEncode({'data': convertedData}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = jsonDecode(response.body); return responseData as T; } else { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } @override Future delete(String endpoint, {Map? data}) async { final url = Uri.parse('$_baseUrl/$endpoint'); final headers = await _getHeaders(); // Convertir les Timestamps avant l'envoi si data existe final convertedData = data != null ? _convertTimestamps(data) as Map : null; final response = await http.delete( url, headers: headers, body: convertedData != null ? jsonEncode({'data': convertedData}) : null, ); if (response.statusCode < 200 || response.statusCode >= 300) { final error = jsonDecode(response.body); throw ApiException( message: error['error'] ?? 'Unknown error', statusCode: response.statusCode, ); } } } /// Exception personnalisée pour les erreurs API class ApiException implements Exception { final String message; final int statusCode; ApiException({ required this.message, required this.statusCode, }); @override String toString() => 'ApiException($statusCode): $message'; bool get isForbidden => statusCode == 403; bool get isUnauthorized => statusCode == 401; bool get isNotFound => statusCode == 404; bool get isConflict => statusCode == 409; } /// Instance singleton du service API final ApiService apiService = FirebaseFunctionsApiService();