import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:wisata_app/config/api_config.dart'; import 'package:wisata_app/models/destination.dart'; class DestinationService { const DestinationService({http.Client? client}) : _client = client; final http.Client? _client; static List localDestinations = []; static List get destinations => localDestinations; Future> getAllDestinations() async { final client = _client ?? http.Client(); try { final items = []; var currentPage = 1; var lastPage = 1; do { final uri = ApiConfig.apiUri('/destinations').replace( queryParameters: { 'per_page': '100', 'page': currentPage.toString(), }, ); final response = await client.get(uri).timeout( const Duration(seconds: 15), ); if (response.statusCode < 200 || response.statusCode >= 300) { throw DestinationServiceException( 'API destinasi gagal dimuat. HTTP ${response.statusCode}.', ); } final decoded = jsonDecode(response.body); items.addAll(_extractItems(decoded)); lastPage = _lastPageFrom(decoded); currentPage++; } while (currentPage <= lastPage); final destinations = items .whereType>() .map(Destination.fromJson) .where((destination) => destination.title.isNotEmpty) .toList(); localDestinations = destinations; return destinations; } finally { if (_client == null) client.close(); } } Future> getDestinations() => getAllDestinations(); Future getDestinationDetail(String id) async { try { return await getDestinationById(id); } on DestinationServiceException { return _findCachedById(id); } } Future getDestinationById(String id) async { final client = _client ?? http.Client(); try { final response = await client .get(ApiConfig.apiUri('/destinations/$id')) .timeout(const Duration(seconds: 15)); if (response.statusCode < 200 || response.statusCode >= 300) { throw DestinationServiceException( 'Detail destinasi gagal dimuat. HTTP ${response.statusCode}.', ); } final decoded = jsonDecode(response.body); final data = decoded is Map ? decoded['data'] : decoded; if (data is! Map) { throw const DestinationServiceException( 'Format detail destinasi dari API tidak valid.', ); } final destination = Destination.fromJson(data); final nextDestinations = [...localDestinations]; final index = nextDestinations.indexWhere((item) => item.id == id); if (index >= 0) { nextDestinations[index] = destination; } else { nextDestinations.add(destination); } localDestinations = nextDestinations; return destination; } finally { if (_client == null) client.close(); } } List get localDestinationsSnapshot => localDestinations; Destination findById(String id) { final destination = _findCachedById(id); if (destination != null) return destination; throw DestinationServiceException( 'Destinasi dengan ID $id tidak ditemukan.', ); } static Destination? _findCachedById(String id) { for (final item in localDestinations) { if (item.id == id) return item; } return null; } List _extractItems(Object? decoded) { if (decoded is List) return decoded; if (decoded is Map) { final data = decoded['data']; if (data is List) return data; if (data is Map) { final nestedData = data['data']; if (nestedData is List) return nestedData; } } throw const DestinationServiceException( 'Format daftar destinasi dari API tidak valid.', ); } int _lastPageFrom(Object? decoded) { if (decoded is Map) { final meta = decoded['meta']; if (meta is Map) { final lastPage = meta['last_page']; if (lastPage is num) return lastPage.toInt(); if (lastPage is String) return int.tryParse(lastPage) ?? 1; } } return 1; } } class DestinationServiceException implements Exception { const DestinationServiceException(this.message); final String message; @override String toString() => message; }