492 lines
19 KiB
Dart
492 lines
19 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');
|
|
}
|
|
|
|
// --- NEW METHOD FOR FIRST TIME LOGIN ---
|
|
/// Fetches critical configuration data (API and FTP settings) needed for
|
|
/// immediate operation. This should be called during the login/splash sequence.
|
|
Future<void> fetchInitialConfigurations() async {
|
|
debugPrint("ApiService: Fetching initial API and FTP configurations...");
|
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
|
|
|
try {
|
|
// 1. Fetch API Configs
|
|
final apiResult = await _baseService.get(baseUrl, 'api-configs');
|
|
if (apiResult['success'] == true && apiResult['data'] != null) {
|
|
final apiData = List<Map<String, dynamic>>.from(apiResult['data']['updated'] ?? []);
|
|
await dbHelper.upsertApiConfigs(apiData);
|
|
debugPrint("Initial API Configs synced: ${apiData.length} items.");
|
|
}
|
|
|
|
// 2. Fetch FTP Configs
|
|
final ftpResult = await _baseService.get(baseUrl, 'ftp-configs');
|
|
if (ftpResult['success'] == true && ftpResult['data'] != null) {
|
|
final ftpData = List<Map<String, dynamic>>.from(ftpResult['data']['updated'] ?? []);
|
|
await dbHelper.upsertFtpConfigs(ftpData);
|
|
debugPrint("Initial FTP Configs synced: ${ftpData.length} items.");
|
|
}
|
|
} catch (e) {
|
|
debugPrint("ApiService: Error fetching initial configurations: $e");
|
|
// We don't rethrow here to allow login to proceed, but UserPreferencesService
|
|
// will handle the missing data gracefully (by not saving defaults yet).
|
|
}
|
|
}
|
|
|
|
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.
|
|
// ======================================================================= |