import 'dart:io'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/foundation.dart'; import '../config/api_config.dart'; import 'package:flutter/material.dart'; import '../login_page.dart'; class ApiService { final Dio _dio = Dio(); static ApiService? _instance; static bool _isRefreshing = false; static VoidCallback? onTokenExpired; // Add getter for dio Dio get dio => _dio; factory ApiService() { _instance ??= ApiService._internal(); return _instance!; } ApiService._internal() { _dio.options.baseUrl = ApiConfig.apiUrl; _dio.options.connectTimeout = Duration(seconds: 30); // Tambah waktu timeout menjadi 30 detik _dio.options.receiveTimeout = Duration(seconds: 30); // Tambah waktu timeout menjadi 30 detik _dio.options.followRedirects = false; _dio.options.validateStatus = (status) { return status! < 500; }; _dio.interceptors.add(AuthInterceptor()); _dio.interceptors.add(RetryInterceptor()); // Tambah retry interceptor if (kDebugMode) { _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); } } // Auth Endpoints Future login(String email, String password) async { try { final response = await _dio.post('/login', data: { 'email': email, 'password': password, }); if (response.statusCode == 200) { final token = response.data['data']['token']; final prefs = await SharedPreferences.getInstance(); await prefs.setString('token', token); } return response; } catch (e) { rethrow; } } Future register(String name, String email, String password) async { try { return await _dio.post('/auth/register', data: { 'name': name, 'email': email, 'password': password, }); } catch (e) { rethrow; } } Future logout() async { try { final response = await _dio.post('/auth/logout'); if (response.statusCode == 200) { final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); } return response; } catch (e) { rethrow; } } Future getProfile() async { try { return await _dio.get('/profile'); } catch (e) { rethrow; } } // Laporan Endpoints Future laporJalan({ required String lokasi, required double latitude, required double longitude, required String jenisRusak, String? deskripsi, required File foto, String? waktu, }) async { try { _dio.options.headers = { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data', }; FormData formData = FormData.fromMap({ 'lokasi': lokasi, 'latitude': latitude, 'longitude': longitude, 'jenis_rusak': jenisRusak, 'deskripsi': deskripsi ?? '', 'waktu': waktu ?? DateTime.now().toUtc().add(Duration(hours: 7)).toIso8601String(), 'foto': await MultipartFile.fromFile( foto.path, filename: 'foto_jalan_${DateTime.now().millisecondsSinceEpoch}.jpg' ), }); // Tambahkan logging untuk memeriksa data yang dikirim print('FormData yang dikirim:'); print('lokasi: $lokasi'); print('latitude: $latitude'); print('longitude: $longitude'); print('jenis_rusak: $jenisRusak'); print('deskripsi: $deskripsi'); print('waktu sebelum konversi: $waktu'); print('waktu default: ${DateTime.now().toUtc().add(Duration(hours: 7)).toIso8601String()}'); print('waktu yang dikirim: ${formData.fields.firstWhere((field) => field.key == 'waktu').value}'); print('foto: ${foto.path}'); final response = await _dio.post('/lapor/jalan', data: formData); return response; } catch (e) { print('Error dalam laporJalan: $e'); rethrow; } } Future laporBencana({ required String lokasi, required double latitude, required double longitude, required String jenisBencana, String? deskripsi, required File foto, }) async { try { _dio.options.headers = { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data', }; FormData formData = FormData.fromMap({ 'lokasi': lokasi, 'latitude': latitude, 'longitude': longitude, 'jenis_bencana': jenisBencana, 'deskripsi': deskripsi ?? '', 'foto': await MultipartFile.fromFile( foto.path, filename: 'foto_bencana_${DateTime.now().millisecondsSinceEpoch}.jpg' ), }); // Tambahkan logging untuk memeriksa data yang dikirim print('FormData yang dikirim:'); print('lokasi: $lokasi'); print('latitude: $latitude'); print('longitude: $longitude'); print('jenis_bencana: $jenisBencana'); print('deskripsi: $deskripsi'); print('foto: ${foto.path}'); final response = await _dio.post('/lapor/bencana', data: formData); return response; } catch (e) { print('Error dalam laporBencana: $e'); rethrow; } } Future getLaporanJalan() async { try { return await _dio.get('/laporan-jalan'); } catch (e) { rethrow; } } Future getLaporanBencana() async { try { return await _dio.get('/laporan-bencana'); } catch (e) { rethrow; } } Future getRiwayatLaporan() async { try { return await _dio.get('/laporan/riwayat'); } catch (e) { rethrow; } } Future refreshToken() async { try { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('token'); if (token == null) throw Exception('No token found'); final response = await _dio.post('/refresh', options: Options(headers: {'Authorization': 'Bearer $token'}) ); if (response.statusCode == 200) { final newToken = response.data['data']['token']; await prefs.setString('token', newToken); } return response; } catch (e) { rethrow; } } // Fungsi untuk menangani token expired static Future handleTokenExpired(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); await prefs.remove('id_pengguna'); await prefs.remove('username'); await prefs.remove('surel'); // Tampilkan pesan error if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Sesi Anda telah berakhir. Silakan login kembali.'), backgroundColor: Colors.red, duration: Duration(seconds: 3), ), ); // Navigasi ke halaman login Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => LoginPage()), (route) => false, ); } } } // Interceptor untuk menambahkan token ke setiap request class AuthInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('token'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; options.headers['Accept'] = 'application/json'; } super.onRequest(options, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { String userFriendlyMessage = ''; switch (err.type) { case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.connectionTimeout: userFriendlyMessage = 'Waktu tunggu habis. Mohon periksa koneksi internet Anda dan coba lagi.'; break; case DioExceptionType.connectionError: userFriendlyMessage = 'Tidak dapat terhubung ke server. Mohon periksa koneksi internet Anda.'; break; case DioExceptionType.badResponse: if (err.response?.statusCode == 401) { final responseData = err.response?.data; final message = (responseData as Map)['message']?.toString(); if (message != null && message.toLowerCase().contains('expired')) { userFriendlyMessage = 'Sesi Anda telah berakhir. Silakan login kembali.'; if (ApiService.onTokenExpired != null) { ApiService.onTokenExpired!(); } } } else { userFriendlyMessage = 'Terjadi kesalahan pada server. Silakan coba lagi nanti.'; } break; default: userFriendlyMessage = 'Terjadi kesalahan. Silakan coba lagi.'; } // Buat DioException baru dengan pesan yang lebih user friendly final newError = DioException( requestOptions: err.requestOptions, error: userFriendlyMessage, type: err.type, response: err.response, ); return handler.next(newError); } } // Tambahkan class RetryInterceptor class RetryInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.receiveTimeout || err.type == DioExceptionType.sendTimeout || err.type == DioExceptionType.connectionError) { // Coba request ulang maksimal 3 kali final options = err.requestOptions; int retryCount = options.extra['retryCount'] ?? 0; if (retryCount < 3) { options.extra['retryCount'] = retryCount + 1; // Tunggu 2 detik sebelum retry await Future.delayed(Duration(seconds: 2)); try { final dio = Dio(BaseOptions( baseUrl: options.baseUrl, connectTimeout: options.connectTimeout, receiveTimeout: options.receiveTimeout, headers: options.headers, )); final response = await dio.fetch(options); return handler.resolve(response); } catch (e) { return handler.next(err); } } } return handler.next(err); } }