environment_monitoring_app/lib/services/api_service.dart

462 lines
17 KiB
Dart

// 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<Map<String, dynamic>> login(String email, String password) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password});
}
Future<Map<String, dynamic>> 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<String, dynamic> 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<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, endpoint, data);
}
Future<Map<String, dynamic>> getProfile() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'profile');
}
Future<Map<String, dynamic>> getAllUsers() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'users');
}
Future<Map<String, dynamic>> getAllDepartments() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'departments');
}
Future<Map<String, dynamic>> getAllCompanies() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'companies');
}
Future<Map<String, dynamic>> getAllPositions() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'positions');
}
Future<Map<String, dynamic>> getAllStates() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'states');
}
Future<Map<String, dynamic>> 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<File?> 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<Map<String, dynamic>> uploadProfilePicture(File imageFile) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'profile/upload-picture',
fields: {},
files: {'profile_picture': imageFile});
}
// --- ADDED THIS NEW METHOD ---
/// Uploads the user's signature as a transparent PNG file.
Future<Map<String, dynamic>> uploadSignature(File imageFile) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
// We assume the endpoint is 'profile/upload-signature'
// and the server expects the file field name to be 'signature'.
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'profile/upload-signature',
fields: {},
files: {'signature': imageFile});
}
// --- END OF NEW METHOD ---
Future<Map<String, dynamic>> 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<void> validateToken() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
await _baseService.get(baseUrl, 'profile');
}
Future<Map<String, dynamic>> _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<Map<String, dynamic>> 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<String, dynamic>) {
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<Map<String, dynamic>>.from(result['data']['updated'] ?? []);
final deleted = List<dynamic>.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<Map<String, dynamic>> 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<Map<String, dynamic>> allData = [];
if (result['data'] is List) {
allData = List<Map<String, dynamic>>.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<Map<String, dynamic>>.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.
// =======================================================================