import 'dart:convert'; import 'dart:math'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; class ApiService { static const String baseUrl = 'https://posyandu.oyi.web.id/api'; static const Duration _defaultTimeout = Duration(seconds: 10); static const bool _enableLogging = true; // Cache untuk mengurangi panggilan berulang static final Map _responseCache = {}; static const bool _enableCacheForPerkembangan = false; String? _token; bool _isLoadingToken = false; // Singleton pattern static final ApiService _instance = ApiService._internal(); factory ApiService() { return _instance; } ApiService._internal() { // Load token saat instance dibuat loadToken(); } /// Generic request method that handles all HTTP methods Future> request({ required String method, required String endpoint, Map? data, }) async { try { // Dapatkan token dan SharedPreferences di awal final prefs = await SharedPreferences.getInstance(); if (_token == null && !_isLoadingToken) { await loadToken(); print('Loaded token: ${_token != null ? 'Token ditemukan' : 'Token tidak ditemukan'}'); } final url = Uri.parse('$baseUrl/$endpoint'); final headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; // Tambahkan token Bearer jika tersedia if (_token != null) { headers['Authorization'] = 'Bearer $_token'; } // Tambahkan header identifikasi if (prefs.getString('nik') != null) { headers['X-User-NIK'] = prefs.getString('nik')!; } if (prefs.getInt('user_id') != null) { headers['X-User-ID'] = prefs.getInt('user_id').toString(); } // Tambahkan Basic Auth sebagai fallback String? basicAuth; if (_token == null && prefs.getString('nik') != null && prefs.getInt('user_id') != null) { String nik = prefs.getString('nik')!; String userId = prefs.getInt('user_id').toString(); basicAuth = 'Basic ' + base64Encode(utf8.encode('$nik:$userId')); headers['Authorization'] = basicAuth; print('Using basic auth as fallback'); } // Log request if enabled if (_enableLogging) { print('$method Request to: $url'); print('Headers: ${headers.toString()}'); if (data != null) print('Body: ${json.encode(data)}'); } http.Response response; // Gunakan timeout yang lebih panjang untuk server yang mungkin lambat final timeout = Duration(seconds: 30); switch (method.toLowerCase()) { case 'get': // Check cache for GET requests if (_responseCache.containsKey(endpoint) && !endpoint.startsWith('perkembangan') && _enableCacheForPerkembangan) { if (_enableLogging) print('Using cached response for $endpoint'); return _responseCache[endpoint]; } response = await http.get(url, headers: headers) .timeout(timeout); break; case 'post': response = await http.post( url, headers: headers, body: data != null ? json.encode(data) : null, ).timeout(timeout); break; case 'put': response = await http.put( url, headers: headers, body: data != null ? json.encode(data) : null, ).timeout(timeout); break; case 'delete': response = await http.delete(url, headers: headers) .timeout(timeout); break; default: throw Exception('Unsupported HTTP method: $method'); } if (_enableLogging) { print('Response status: ${response.statusCode}'); print('Response body: ${response.body.length > 300 ? response.body.substring(0, 300) + '...' : response.body}'); } // Handle 401 Unauthorized dengan mencoba beberapa alternatif autentikasi if (response.statusCode == 401) { print('401 Unauthorized - akan mencoba kredensial alternatif untuk: $endpoint'); // *** Khusus endpoint anak: coba dengan format token yang berbeda *** if (endpoint.startsWith('anak')) { if (_token != null) { print('Mencoba format token khusus untuk endpoint anak'); Map anakEndpointHeaders = { 'Accept': 'application/json', 'Content-Type': 'application/json', // Jangan gunakan 'Bearer ' prefix, hanya token saja 'Authorization': _token!, 'X-User-NIK': prefs.getString('nik') ?? '', 'X-User-ID': prefs.getInt('user_id')?.toString() ?? '', }; try { // Coba request dengan format token khusus http.Response anakResponse; switch (method.toLowerCase()) { case 'get': anakResponse = await http.get(url, headers: anakEndpointHeaders).timeout(timeout); break; case 'post': anakResponse = await http.post( url, headers: anakEndpointHeaders, body: data != null ? json.encode(data) : null, ).timeout(timeout); break; default: anakResponse = await http.get(url, headers: anakEndpointHeaders).timeout(timeout); } if (anakResponse.statusCode >= 200 && anakResponse.statusCode < 300) { print('Format token khusus berhasil: ${anakResponse.statusCode}'); response = anakResponse; // Jika ini berfungsi, jangan lakukan alternatif lain return json.decode(response.body); } else { print('Format token khusus gagal: ${anakResponse.statusCode}'); } } catch (e) { print('Error pada format token khusus: $e'); } } } // Metode alternatif 1: Tanpa token, hanya header X-User Map altHeaders1 = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; if (prefs.getString('nik') != null) { altHeaders1['X-User-NIK'] = prefs.getString('nik')!; } if (prefs.getInt('user_id') != null) { altHeaders1['X-User-ID'] = prefs.getInt('user_id').toString(); } print('Alternatif 1: Request dengan X-User headers saja: $altHeaders1'); // Coba request dengan Alternatif 1 http.Response alt1Response; try { switch (method.toLowerCase()) { case 'get': alt1Response = await http.get(url, headers: altHeaders1).timeout(timeout); break; case 'post': alt1Response = await http.post( url, headers: altHeaders1, body: data != null ? json.encode(data) : null, ).timeout(timeout); break; default: alt1Response = await http.get(url, headers: altHeaders1).timeout(timeout); } if (alt1Response.statusCode >= 200 && alt1Response.statusCode < 300) { print('Alternatif 1 berhasil: ${alt1Response.statusCode}'); response = alt1Response; } else { print('Alternatif 1 gagal: ${alt1Response.statusCode}'); } } catch (e) { print('Error pada Alternatif 1: $e'); } // Metode alternatif 2: Basic Auth if (response.statusCode == 401 && basicAuth != null) { Map altHeaders2 = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': basicAuth, }; print('Alternatif 2: Request dengan Basic Auth: $altHeaders2'); // Coba request dengan Alternatif 2 http.Response alt2Response; try { switch (method.toLowerCase()) { case 'get': alt2Response = await http.get(url, headers: altHeaders2).timeout(timeout); break; case 'post': alt2Response = await http.post( url, headers: altHeaders2, body: data != null ? json.encode(data) : null, ).timeout(timeout); break; default: alt2Response = await http.get(url, headers: altHeaders2).timeout(timeout); } if (alt2Response.statusCode >= 200 && alt2Response.statusCode < 300) { print('Alternatif 2 berhasil: ${alt2Response.statusCode}'); response = alt2Response; } else { print('Alternatif 2 gagal: ${alt2Response.statusCode}'); } } catch (e) { print('Error pada Alternatif 2: $e'); } } // ** PENTING: Jika ini adalah endpoint anak, jangan hapus token meskipun gagal ** if (response.statusCode == 401) { if (endpoint.startsWith('anak')) { print('Gagal autentikasi untuk endpoint anak, tetapi TIDAK menghapus token'); // Tetap kembalikan error, tetapi jangan hapus token if (response.body.isNotEmpty) { try { return json.decode(response.body); } catch (e) { return {'status': 'error', 'message': 'Gagal autentikasi untuk endpoint anak'}; } } else { return {'status': 'error', 'message': 'Gagal autentikasi untuk endpoint anak'}; } } else { // Hapus token hanya jika bukan endpoint anak print('Semua metode autentikasi gagal untuk endpoint: $endpoint'); print('Aplikasi mungkin perlu login ulang'); await clearToken(); clearAuthCache(); throw Exception('Session expired. Please login again.'); } } } if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = json.decode(response.body); // Cache GET responses (except for perkembangan if disabled) if (method.toLowerCase() == 'get' && !endpoint.startsWith('perkembangan') && _enableCacheForPerkembangan) { _responseCache[endpoint] = responseData; } // Invalidate cache for write operations if (method.toLowerCase() != 'get') { _invalidateRelatedCache(endpoint); } return responseData; } else { throw Exception('HTTP Error: ${response.statusCode} - ${response.body}'); } } catch (e) { if (_enableLogging) { print('API Error: $e'); if (e.toString().contains('TimeoutException')) { print('!!! KONEKSI KE SERVER API TIMEOUT !!!'); print('!!! Pastikan server API berjalan di $baseUrl !!!'); } else if (e.toString().contains('SocketException')) { print('!!! SERVER API TIDAK DAPAT DIJANGKAU !!!'); print('!!! Pastikan server API berjalan di $baseUrl !!!'); } } throw Exception('API Error: $e'); } } // Convenience methods that use the request method Future> get(String endpoint, {Map? queryParameters}) async { String finalEndpoint = endpoint; if (queryParameters != null && queryParameters.isNotEmpty) { // Tambahkan parameter query ke endpoint final queryString = queryParameters.entries .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}') .join('&'); finalEndpoint = '$endpoint?$queryString'; if (_enableLogging) { print('GET request with query parameters: $finalEndpoint'); } } final response = await request(method: 'get', endpoint: finalEndpoint); // Khusus endpoint artikel, berikan debug log tambahan if (endpoint.startsWith('artikel')) { print('Endpoint artikel: $finalEndpoint'); print('Response format untuk artikel: ${response.keys}'); if (response.containsKey('data')) { if (response['data'] is List) { print('Data adalah List dengan ${(response['data'] as List).length} items'); } else if (response['data'] is Map) { print('Data adalah Map dengan keys: ${(response['data'] as Map).keys}'); if ((response['data'] as Map).containsKey('data')) { final innerData = (response['data'] as Map)['data']; if (innerData is List) { print('Inner data adalah List dengan ${innerData.length} items'); } else { print('Inner data bukan List: ${innerData.runtimeType}'); } } } else { print('Data bukan List atau Map: ${response['data'].runtimeType}'); } } } return response; } Future> post(String endpoint, Map data) async { return request(method: 'post', endpoint: endpoint, data: data); } Future> put(String endpoint, Map data) async { return request(method: 'put', endpoint: endpoint, data: data); } Future> delete(String endpoint) async { return request(method: 'delete', endpoint: endpoint); } // Token management Future setToken(String token) async { _token = token; final prefs = await SharedPreferences.getInstance(); await prefs.setString('token', token); if (_enableLogging) print('Token saved successfully to SharedPreferences: ${token.substring(0, min(10, token.length))}...'); } Future clearToken() async { _token = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); if (_enableLogging) print('Token cleared from SharedPreferences'); } Future loadToken() async { if (_isLoadingToken) return; try { _isLoadingToken = true; final prefs = await SharedPreferences.getInstance(); _token = prefs.getString('token'); if (_enableLogging) { print('Token dari SharedPreferences: ${_token != null ? _token!.substring(0, min(10, _token!.length)) + "..." : "tidak ditemukan"}'); } } catch (e) { print('Error loading token: $e'); _token = null; } finally { _isLoadingToken = false; } } // Cache management void _invalidateRelatedCache(String endpoint) { if (endpoint.startsWith('anak')) { _responseCache.removeWhere((key, _) => key.startsWith('anak')); } else if (endpoint.startsWith('perkembangan')) { _responseCache.removeWhere((key, _) => key.startsWith('perkembangan')); } else if (endpoint.startsWith('auth') || endpoint.startsWith('profile')) { _responseCache.removeWhere((key, _) => key.startsWith('auth') || key.startsWith('profile') || key.startsWith('user') ); } } void clearCache() { _responseCache.clear(); if (_enableLogging) print('All API cache cleared'); } void clearAuthCache() { _responseCache.removeWhere((key, _) => key.startsWith('auth') || key.startsWith('profile') || key.startsWith('user') ); if (_enableLogging) print('Auth-related cache cleared'); } void clearChildCache() { _responseCache.removeWhere((key, _) => key.startsWith('anak') || key.contains('orangtua') || key.contains('pengguna') ); if (_enableLogging) print('Child-related cache cleared'); } /// Memeriksa apakah server API dapat dijangkau Future isServerReachable() async { try { if (_enableLogging) { print('Memeriksa koneksi ke server API: $baseUrl'); } final response = await http.get( Uri.parse('$baseUrl/ping'), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ).timeout(Duration(seconds: 5)); if (_enableLogging) { print('Status koneksi server: ${response.statusCode}'); } return response.statusCode >= 200 && response.statusCode < 300; } catch (e) { if (_enableLogging) { print('Gagal terhubung ke server API: $e'); if (e.toString().contains('TimeoutException')) { print('!!! KONEKSI KE SERVER API TIMEOUT !!!'); } else if (e.toString().contains('SocketException')) { print('!!! SERVER API TIDAK DAPAT DIJANGKAU !!!'); } } return false; } } }