// lib/services/api_service.dart import 'dart:io'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:environment_monitoring_app/services/base_api_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; // Import the new separated files import 'package:environment_monitoring_app/services/database_helper.dart'; import 'package:environment_monitoring_app/services/marine_api_service.dart'; import 'package:environment_monitoring_app/services/river_api_service.dart'; import 'package:environment_monitoring_app/services/air_api_service.dart'; // Removed: Models that are no longer directly used by this top-level class // import 'package:environment_monitoring_app/models/air_collection_data.dart'; // import 'package:environment_monitoring_app/models/air_installation_data.dart'; // import 'package:environment_monitoring_app/models/marine_manual_pre_departure_checklist_data.dart'; // import 'package:environment_monitoring_app/models/marine_manual_sonde_calibration_data.dart'; // import 'package:environment_monitoring_app/models/marine_manual_equipment_maintenance_data.dart'; // ======================================================================= // Part 1: Unified API Service // ======================================================================= class ApiService { final BaseApiService _baseService = BaseApiService(); final DatabaseHelper dbHelper = DatabaseHelper(); final ServerConfigService _serverConfigService = ServerConfigService(); late final MarineApiService marine; late final RiverApiService river; late final AirApiService air; static const String imageBaseUrl = 'https://mms-apiv4.pstw.com.my/'; ApiService({required TelegramService telegramService}) { // --- MODIFIED CONSTRUCTOR --- // Note that marine and river no longer take dbHelper, matching your new files marine = MarineApiService(_baseService, telegramService, _serverConfigService); river = RiverApiService(_baseService, telegramService, _serverConfigService); // AirApiService also doesn't need the dbHelper air = AirApiService(_baseService, telegramService, _serverConfigService); } // --- Core API Methods --- Future> login(String email, String password) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password}); } Future> register({ required String username, String? firstName, String? lastName, required String email, required String password, String? phoneNumber, int? departmentId, int? companyId, int? positionId, }) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); final Map body = { 'username': username, 'email': email, 'password': password, 'first_name': firstName ?? '', 'last_name': lastName ?? '', 'phone_number': phoneNumber ?? '', 'department_id': departmentId, 'company_id': companyId, 'position_id': positionId, }; body.removeWhere((key, value) => value == null); return _baseService.post(baseUrl, 'auth/register', body); } Future> post(String endpoint, Map data) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.post(baseUrl, endpoint, data); } Future> getProfile() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'profile'); } Future> getAllUsers() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'users'); } Future> getAllDepartments() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'departments'); } Future> getAllCompanies() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'companies'); } Future> getAllPositions() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'positions'); } Future> getAllStates() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.get(baseUrl, 'states'); } Future> sendTelegramAlert({ required String chatId, required String message, }) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.post(baseUrl, 'marine/telegram-alert', { // Note: Endpoint might need generalization if used by other modules 'chat_id': chatId, 'message': message, }); } Future downloadProfilePicture(String imageUrl, String localPath) async { try { final response = await http.get(Uri.parse(imageUrl)); if (response.statusCode == 200) { final File file = File(localPath); await file.parent.create(recursive: true); await file.writeAsBytes(response.bodyBytes); return file; } } catch (e) { debugPrint('Error downloading profile picture: $e'); } return null; } Future> uploadProfilePicture(File imageFile) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.postMultipart( baseUrl: baseUrl, endpoint: 'profile/upload-picture', fields: {}, files: {'profile_picture': imageFile}); } Future> refreshProfile() async { debugPrint('ApiService: Refreshing profile data from server...'); final result = await getProfile(); if (result['success'] == true && result['data'] != null) { await dbHelper.saveProfile(result['data']); debugPrint('ApiService: Profile data refreshed and saved to local DB.'); } return result; } Future validateToken() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); await _baseService.get(baseUrl, 'profile'); } Future> _fetchDelta(String endpoint, String? lastSyncTimestamp) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); String url = endpoint; if (lastSyncTimestamp != null) { url += '?since=$lastSyncTimestamp'; } return _baseService.get(baseUrl, url); } Future> syncAllData({String? lastSyncTimestamp}) async { debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp'); try { final syncTasks = { 'profile': { 'endpoint': 'profile', 'handler': (d, id) async { if (d.isNotEmpty) await dbHelper.saveProfile(d.first); } }, 'allUsers': { 'endpoint': 'users', 'handler': (d, id) async { await dbHelper.upsertUsers(d); await dbHelper.deleteUsers(id); } }, 'documents': { 'endpoint': 'documents', 'handler': (d, id) async { await dbHelper.upsertDocuments(d); await dbHelper.deleteDocuments(id); } }, 'tarballStations': { 'endpoint': 'marine/tarball/stations', 'handler': (d, id) async { await dbHelper.upsertTarballStations(d); await dbHelper.deleteTarballStations(id); } }, 'manualStations': { 'endpoint': 'marine/manual/stations', 'handler': (d, id) async { await dbHelper.upsertManualStations(d); await dbHelper.deleteManualStations(id); } }, 'tarballClassifications': { 'endpoint': 'marine/tarball/classifications', 'handler': (d, id) async { await dbHelper.upsertTarballClassifications(d); await dbHelper.deleteTarballClassifications(id); } }, 'riverManualStations': { 'endpoint': 'river/manual-stations', 'handler': (d, id) async { await dbHelper.upsertRiverManualStations(d); await dbHelper.deleteRiverManualStations(id); } }, 'riverTriennialStations': { 'endpoint': 'river/triennial-stations', 'handler': (d, id) async { await dbHelper.upsertRiverTriennialStations(d); await dbHelper.deleteRiverTriennialStations(id); } }, // --- REMOVED: River Investigative Stations Sync --- // The 'riverInvestigativeStations' task has been removed // as per the request to use river manual stations instead. // --- END REMOVED --- 'departments': { 'endpoint': 'departments', 'handler': (d, id) async { await dbHelper.upsertDepartments(d); await dbHelper.deleteDepartments(id); } }, 'companies': { 'endpoint': 'companies', 'handler': (d, id) async { await dbHelper.upsertCompanies(d); await dbHelper.deleteCompanies(id); } }, 'positions': { 'endpoint': 'positions', 'handler': (d, id) async { await dbHelper.upsertPositions(d); await dbHelper.deletePositions(id); } }, 'airManualStations': { 'endpoint': 'air/manual-stations', 'handler': (d, id) async { await dbHelper.upsertAirManualStations(d); await dbHelper.deleteAirManualStations(id); } }, 'airClients': { 'endpoint': 'air/clients', 'handler': (d, id) async { await dbHelper.upsertAirClients(d); await dbHelper.deleteAirClients(id); } }, 'states': { 'endpoint': 'states', 'handler': (d, id) async { await dbHelper.upsertStates(d); await dbHelper.deleteStates(id); } }, 'appSettings': { 'endpoint': 'settings', 'handler': (d, id) async { await dbHelper.upsertAppSettings(d); await dbHelper.deleteAppSettings(id); } }, 'npeParameterLimits': { 'endpoint': 'npe-parameter-limits', 'handler': (d, id) async { await dbHelper.upsertNpeParameterLimits(d); await dbHelper.deleteNpeParameterLimits(id); } }, 'marineParameterLimits': { 'endpoint': 'marine-parameter-limits', 'handler': (d, id) async { await dbHelper.upsertMarineParameterLimits(d); await dbHelper.deleteMarineParameterLimits(id); } }, 'riverParameterLimits': { 'endpoint': 'river-parameter-limits', 'handler': (d, id) async { await dbHelper.upsertRiverParameterLimits(d); await dbHelper.deleteRiverParameterLimits(id); } }, 'apiConfigs': { 'endpoint': 'api-configs', 'handler': (d, id) async { await dbHelper.upsertApiConfigs(d); await dbHelper.deleteApiConfigs(id); } }, 'ftpConfigs': { 'endpoint': 'ftp-configs', 'handler': (d, id) async { await dbHelper.upsertFtpConfigs(d); await dbHelper.deleteFtpConfigs(id); } }, }; // Fetch all deltas in parallel final fetchFutures = syncTasks.map((key, value) => MapEntry(key, _fetchDelta(value['endpoint'] as String, lastSyncTimestamp))); final results = await Future.wait(fetchFutures.values); final resultData = Map.fromIterables(fetchFutures.keys, results); // Process and save all changes for (var entry in resultData.entries) { final key = entry.key; final result = entry.value; if (result['success'] == true && result['data'] != null) { if (key == 'profile') { // Handle potential non-list response for profile final profileData = result['data']; if (profileData is Map) { await (syncTasks[key]!['handler'] as Function)([profileData], []); } else if (profileData is List && profileData.isNotEmpty) { await (syncTasks[key]!['handler'] as Function)([profileData.first], []); } } else { // --- REVERTED TO ORIGINAL --- // The special logic to handle List vs Map is no longer needed // since the endpoint causing the problem is no longer being called. final updated = List>.from(result['data']['updated'] ?? []); final deleted = List.from(result['data']['deleted'] ?? []); await (syncTasks[key]!['handler'] as Function)(updated, deleted); // --- END REVERTED --- } } else { debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}'); } } debugPrint('ApiService: Delta sync complete.'); return {'success': true, 'message': 'Delta sync successful.'}; } catch (e) { debugPrint('ApiService: Delta data sync failed: $e'); rethrow; } } Future> syncRegistrationData() async { debugPrint('ApiService: Starting registration data sync...'); try { final syncTasks = { 'departments': { 'endpoint': 'departments', 'handler': (d, id) async { await dbHelper.upsertDepartments(d); await dbHelper.deleteDepartments(id); } }, 'companies': { 'endpoint': 'companies', 'handler': (d, id) async { await dbHelper.upsertCompanies(d); await dbHelper.deleteCompanies(id); } }, 'positions': { 'endpoint': 'positions', 'handler': (d, id) async { await dbHelper.upsertPositions(d); await dbHelper.deletePositions(id); } }, }; final fetchFutures = syncTasks.map((key, value) => MapEntry(key, _fetchDelta(value['endpoint'] as String, null))); // Fetch all data final results = await Future.wait(fetchFutures.values); final resultData = Map.fromIterables(fetchFutures.keys, results); for (var entry in resultData.entries) { final key = entry.key; final result = entry.value; // Assuming the full list is returned in 'data' when lastSyncTimestamp is null if (result['success'] == true && result['data'] != null) { // Ensure 'data' is treated as a list, even if API might sometimes return a map for single results List> allData = []; if (result['data'] is List) { allData = List>.from(result['data']); } else if (result['data'] is Map) { // Handle cases where the API might return just a map if only one item exists // Or if the structure is like {'data': [...]} incorrectly var potentialList = (result['data'] as Map).values.firstWhere((v) => v is List, orElse: () => null); if (potentialList != null) { allData = List>.from(potentialList); } else { debugPrint('ApiService: Unexpected data format for $key. Expected List, got Map.'); } } // Since it's a full sync, we just upsert everything and don't delete if (allData.isNotEmpty) { await (syncTasks[key]!['handler'] as Function)(allData, []); } else if (result['data'] is Map && allData.isEmpty) { // If it was a map and we couldn't extract a list, log it. debugPrint('ApiService: Data for $key was a map, but could not extract list for handler.'); } } else { debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}'); } } debugPrint('ApiService: Registration data sync complete.'); return {'success': true, 'message': 'Registration data sync successful.'}; } catch (e) { debugPrint('ApiService: Registration data sync failed: $e'); return {'success': false, 'message': 'Registration data sync failed: $e'}; } } } // ======================================================================= // Part 2 & 3: Marine, River, Air, and DatabaseHelper classes // // ... All of these class definitions have been REMOVED from this file // and placed in their own respective files. // =======================================================================