environment_monitoring_app/lib/services/api_service.dart

387 lines
18 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:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:environment_monitoring_app/services/base_api_service.dart';
// =======================================================================
// Part 1: Unified API Service
// =======================================================================
/// A unified service that consolidates all API interactions for the application.
/// It is organized by feature (e.g., marine, river) for clarity and provides
/// a central point for data synchronization.
class ApiService {
final BaseApiService _baseService = BaseApiService();
final DatabaseHelper _dbHelper = DatabaseHelper();
late final MarineApiService marine;
late final RiverApiService river;
late final AirApiService air;
static const String imageBaseUrl = 'https://dev14.pstw.com.my/';
ApiService() {
marine = MarineApiService(_baseService);
river = RiverApiService(_baseService);
air = AirApiService(_baseService);
}
// --- Core API Methods ---
Future<Map<String, dynamic>> login(String email, String password) {
return _baseService.post('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,
}) {
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('auth/register', body);
}
Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) {
return _baseService.post(endpoint, data);
}
Future<Map<String, dynamic>> getProfile() => _baseService.get('profile');
Future<Map<String, dynamic>> getAllUsers() => _baseService.get('users');
Future<Map<String, dynamic>> getAllDepartments() => _baseService.get('departments');
Future<Map<String, dynamic>> getAllCompanies() => _baseService.get('companies');
Future<Map<String, dynamic>> getAllPositions() => _baseService.get('positions');
Future<Map<String, dynamic>> getAllStates() => _baseService.get('states');
Future<Map<String, dynamic>> sendTelegramAlert({
required String chatId,
required String message,
}) {
return _baseService.post('marine/telegram-alert', {
'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) {
return _baseService.postMultipart(
endpoint: 'profile/upload-picture',
fields: {},
files: {'profile_picture': imageFile}
);
}
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;
}
/// Orchestrates a full data sync from the server to the local database.
Future<Map<String, dynamic>> syncAllData() async {
debugPrint('ApiService: Starting full data sync from server...');
try {
final results = await Future.wait([
getProfile(),
getAllUsers(),
marine.getTarballStations(),
marine.getManualStations(),
marine.getTarballClassifications(),
river.getManualStations(),
river.getTriennialStations(),
getAllDepartments(),
getAllCompanies(),
getAllPositions(),
air.getManualStations(),
air.getClients(),
getAllStates(),
]);
final Map<String, dynamic> syncedData = {
'profile': results[0]['success'] == true ? results[0]['data'] : null,
'allUsers': results[1]['success'] == true ? results[1]['data'] : null,
'tarballStations': results[2]['success'] == true ? results[2]['data'] : null,
'manualStations': results[3]['success'] == true ? results[3]['data'] : null,
'tarballClassifications': results[4]['success'] == true ? results[4]['data'] : null,
'riverManualStations': results[5]['success'] == true ? results[5]['data'] : null,
'riverTriennialStations': results[6]['success'] == true ? results[6]['data'] : null,
'departments': results[7]['success'] == true ? results[7]['data'] : null,
'companies': results[8]['success'] == true ? results[8]['data'] : null,
'positions': results[9]['success'] == true ? results[9]['data'] : null,
'airManualStations': results[10]['success'] == true ? results[10]['data'] : null,
'airClients': results[11]['success'] == true ? results[11]['data'] : null,
'states': results[12]['success'] == true ? results[12]['data'] : null,
};
if (syncedData['profile'] != null) await _dbHelper.saveProfile(syncedData['profile']);
if (syncedData['allUsers'] != null) await _dbHelper.saveUsers(List<Map<String, dynamic>>.from(syncedData['allUsers']));
if (syncedData['tarballStations'] != null) await _dbHelper.saveTarballStations(List<Map<String, dynamic>>.from(syncedData['tarballStations']));
if (syncedData['manualStations'] != null) await _dbHelper.saveManualStations(List<Map<String, dynamic>>.from(syncedData['manualStations']));
if (syncedData['tarballClassifications'] != null) await _dbHelper.saveTarballClassifications(List<Map<String, dynamic>>.from(syncedData['tarballClassifications']));
if (syncedData['riverManualStations'] != null) await _dbHelper.saveRiverManualStations(List<Map<String, dynamic>>.from(syncedData['riverManualStations']));
if (syncedData['riverTriennialStations'] != null) await _dbHelper.saveRiverTriennialStations(List<Map<String, dynamic>>.from(syncedData['riverTriennialStations']));
if (syncedData['departments'] != null) await _dbHelper.saveDepartments(List<Map<String, dynamic>>.from(syncedData['departments']));
if (syncedData['companies'] != null) await _dbHelper.saveCompanies(List<Map<String, dynamic>>.from(syncedData['companies']));
if (syncedData['positions'] != null) await _dbHelper.savePositions(List<Map<String, dynamic>>.from(syncedData['positions']));
if (syncedData['airManualStations'] != null) await _dbHelper.saveAirManualStations(List<Map<String, dynamic>>.from(syncedData['airManualStations']));
if (syncedData['airClients'] != null) await _dbHelper.saveAirClients(List<Map<String, dynamic>>.from(syncedData['airClients']));
if (syncedData['states'] != null) await _dbHelper.saveStates(List<Map<String, dynamic>>.from(syncedData['states']));
debugPrint('ApiService: Sync complete. Data saved to local DB.');
return {'success': true, 'data': syncedData};
} catch (e) {
debugPrint('ApiService: Full data sync failed: $e');
return {'success': false, 'message': 'Data sync failed: $e'};
}
}
}
// =======================================================================
// Part 2: Feature-Specific API Services
// =======================================================================
class AirApiService {
final BaseApiService _baseService;
AirApiService(this._baseService);
Future<Map<String, dynamic>> getManualStations() => _baseService.get('air/manual-stations');
Future<Map<String, dynamic>> getClients() => _baseService.get('air/clients');
// NEW: Added dedicated method for uploading installation images
Future<Map<String, dynamic>> uploadInstallationImages({
required String airManId,
required Map<String, File> files,
}) {
return _baseService.postMultipart(
endpoint: 'air/manual/installation-images',
fields: {'air_man_id': airManId},
files: files,
);
}
// NECESSARY FIX: Added dedicated method for uploading collection images
Future<Map<String, dynamic>> uploadCollectionImages({
required String airManId,
required Map<String, File> files,
}) {
return _baseService.postMultipart(
// Note: Please verify this endpoint path with your backend developer.
endpoint: 'air/manual/collection-images',
fields: {'air_man_id': airManId},
files: files,
);
}
}
class MarineApiService {
final BaseApiService _baseService;
MarineApiService(this._baseService);
Future<Map<String, dynamic>> getTarballStations() => _baseService.get('marine/tarball/stations');
Future<Map<String, dynamic>> getManualStations() => _baseService.get('marine/manual/stations');
Future<Map<String, dynamic>> getTarballClassifications() => _baseService.get('marine/tarball/classifications');
Future<Map<String, dynamic>> submitTarballSample({
required Map<String, String> formData,
required Map<String, File?> imageFiles,
}) async {
final dataResult = await _baseService.post('marine/tarball/sample', formData);
if (dataResult['success'] != true) return {'status': 'L1', 'success': false, 'message': 'Failed to submit data: ${dataResult['message']}'};
final recordId = dataResult['data']?['autoid'];
if (recordId == null) return {'status': 'L2', 'success': false, 'message': 'Data submitted, but failed to get a record ID.'};
final filesToUpload = <String, File>{};
imageFiles.forEach((key, value) { if (value != null) filesToUpload[key] = value; });
if (filesToUpload.isEmpty) return {'status': 'L3', 'success': true, 'message': 'Data submitted successfully.', 'reportId': recordId};
final imageResult = await _baseService.postMultipart(endpoint: 'marine/tarball/images', fields: {'autoid': recordId.toString()}, files: filesToUpload);
if (imageResult['success'] != true) return {'status': 'L2', 'success': false, 'message': 'Data submitted, but image upload failed: ${imageResult['message']}', 'reportId': recordId};
return {'status': 'L3', 'success': true, 'message': 'Data and images submitted successfully.', 'reportId': recordId};
}
}
class RiverApiService {
final BaseApiService _baseService;
RiverApiService(this._baseService);
Future<Map<String, dynamic>> getManualStations() => _baseService.get('river/manual-stations');
Future<Map<String, dynamic>> getTriennialStations() => _baseService.get('river/triennial-stations');
}
// =======================================================================
// Part 3: Local Database Helper
// =======================================================================
class DatabaseHelper {
static Database? _database;
static const String _dbName = 'app_data.db';
static const int _dbVersion = 12;
static const String _profileTable = 'user_profile';
static const String _usersTable = 'all_users';
static const String _tarballStationsTable = 'marine_tarball_stations';
static const String _manualStationsTable = 'marine_manual_stations';
static const String _riverManualStationsTable = 'river_manual_stations';
static const String _riverTriennialStationsTable = 'river_triennial_stations';
static const String _tarballClassificationsTable = 'marine_tarball_classifications';
static const String _departmentsTable = 'departments';
static const String _companiesTable = 'companies';
static const String _positionsTable = 'positions';
static const String _alertQueueTable = 'alert_queue';
static const String _airManualStationsTable = 'air_manual_stations';
static const String _airClientsTable = 'air_clients';
static const String _statesTable = 'states';
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
Future<Database> _initDB() async {
String dbPath = p.join(await getDatabasesPath(), _dbName);
return await openDatabase(dbPath, version: _dbVersion, onCreate: _onCreate, onUpgrade: _onUpgrade);
}
Future _onCreate(Database db, int version) async {
await db.execute('CREATE TABLE $_profileTable(user_id INTEGER PRIMARY KEY, profile_json TEXT)');
await db.execute('CREATE TABLE $_usersTable(user_id INTEGER PRIMARY KEY, user_json TEXT)');
await db.execute('CREATE TABLE $_tarballStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE $_manualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE $_riverManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE $_riverTriennialStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE $_tarballClassificationsTable(classification_id INTEGER PRIMARY KEY, classification_json TEXT)');
await db.execute('CREATE TABLE $_departmentsTable(department_id INTEGER PRIMARY KEY, department_json TEXT)');
await db.execute('CREATE TABLE $_companiesTable(company_id INTEGER PRIMARY KEY, company_json TEXT)');
await db.execute('CREATE TABLE $_positionsTable(position_id INTEGER PRIMARY KEY, position_json TEXT)');
await db.execute('''CREATE TABLE $_alertQueueTable (id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id TEXT NOT NULL, message TEXT NOT NULL, created_at TEXT NOT NULL)''');
await db.execute('CREATE TABLE $_airManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE $_airClientsTable(client_id INTEGER PRIMARY KEY, client_json TEXT)');
await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 11) {
await db.execute('CREATE TABLE IF NOT EXISTS $_airManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
await db.execute('CREATE TABLE IF NOT EXISTS $_airClientsTable(client_id INTEGER PRIMARY KEY, client_json TEXT)');
}
if (oldVersion < 12) {
await db.execute('CREATE TABLE IF NOT EXISTS $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)');
}
}
Future<void> _saveData(String table, String idKey, List<Map<String, dynamic>> data) async {
final db = await database;
await db.delete(table);
for (var item in data) {
await db.insert(table, {'${idKey}_id': item['${idKey}_id'], '${idKey}_json': jsonEncode(item)}, conflictAlgorithm: ConflictAlgorithm.replace);
}
}
Future<List<Map<String, dynamic>>?> _loadData(String table, String idKey) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(table);
if (maps.isNotEmpty) {
return maps.map((map) => jsonDecode(map['${idKey}_json']) as Map<String, dynamic>).toList();
}
return null;
}
Future<void> saveProfile(Map<String, dynamic> profile) async {
final db = await database;
await db.insert(_profileTable, {'user_id': profile['user_id'], 'profile_json': jsonEncode(profile)}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<Map<String, dynamic>?> loadProfile() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(_profileTable);
if(maps.isNotEmpty) return jsonDecode(maps.first['profile_json']);
return null;
}
Future<void> saveUsers(List<Map<String, dynamic>> users) => _saveData(_usersTable, 'user', users);
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
Future<void> saveTarballStations(List<Map<String, dynamic>> stations) => _saveData(_tarballStationsTable, 'station', stations);
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');
Future<void> saveManualStations(List<Map<String, dynamic>> stations) => _saveData(_manualStationsTable, 'station', stations);
Future<List<Map<String, dynamic>>?> loadManualStations() => _loadData(_manualStationsTable, 'station');
Future<void> saveRiverManualStations(List<Map<String, dynamic>> stations) => _saveData(_riverManualStationsTable, 'station', stations);
Future<List<Map<String, dynamic>>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station');
Future<void> saveRiverTriennialStations(List<Map<String, dynamic>> stations) => _saveData(_riverTriennialStationsTable, 'station', stations);
Future<List<Map<String, dynamic>>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station');
Future<void> saveTarballClassifications(List<Map<String, dynamic>> data) => _saveData(_tarballClassificationsTable, 'classification', data);
Future<List<Map<String, dynamic>>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification');
Future<void> saveDepartments(List<Map<String, dynamic>> data) => _saveData(_departmentsTable, 'department', data);
Future<List<Map<String, dynamic>>?> loadDepartments() => _loadData(_departmentsTable, 'department');
Future<void> saveCompanies(List<Map<String, dynamic>> data) => _saveData(_companiesTable, 'company', data);
Future<List<Map<String, dynamic>>?> loadCompanies() => _loadData(_companiesTable, 'company');
Future<void> savePositions(List<Map<String, dynamic>> data) => _saveData(_positionsTable, 'position', data);
Future<List<Map<String, dynamic>>?> loadPositions() => _loadData(_positionsTable, 'position');
Future<void> saveAirManualStations(List<Map<String, dynamic>> stations) => _saveData(_airManualStationsTable, 'station', stations);
Future<List<Map<String, dynamic>>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station');
Future<void> saveAirClients(List<Map<String, dynamic>> clients) => _saveData(_airClientsTable, 'client', clients);
Future<List<Map<String, dynamic>>?> loadAirClients() => _loadData(_airClientsTable, 'client');
Future<void> saveStates(List<Map<String, dynamic>> states) => _saveData(_statesTable, 'state', states);
Future<List<Map<String, dynamic>>?> loadStates() => _loadData(_statesTable, 'state');
}