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> login(String email, String password) { return _baseService.post('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, }) { 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('auth/register', body); } Future> post(String endpoint, Map data) { return _baseService.post(endpoint, data); } Future> getProfile() => _baseService.get('profile'); Future> getAllUsers() => _baseService.get('users'); Future> getAllDepartments() => _baseService.get('departments'); Future> getAllCompanies() => _baseService.get('companies'); Future> getAllPositions() => _baseService.get('positions'); Future> getAllStates() => _baseService.get('states'); Future> sendTelegramAlert({ required String chatId, required String message, }) { return _baseService.post('marine/telegram-alert', { '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) { return _baseService.postMultipart( 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; } /// Orchestrates a full data sync from the server to the local database. Future> 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 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>.from(syncedData['allUsers'])); if (syncedData['tarballStations'] != null) await _dbHelper.saveTarballStations(List>.from(syncedData['tarballStations'])); if (syncedData['manualStations'] != null) await _dbHelper.saveManualStations(List>.from(syncedData['manualStations'])); if (syncedData['tarballClassifications'] != null) await _dbHelper.saveTarballClassifications(List>.from(syncedData['tarballClassifications'])); if (syncedData['riverManualStations'] != null) await _dbHelper.saveRiverManualStations(List>.from(syncedData['riverManualStations'])); if (syncedData['riverTriennialStations'] != null) await _dbHelper.saveRiverTriennialStations(List>.from(syncedData['riverTriennialStations'])); if (syncedData['departments'] != null) await _dbHelper.saveDepartments(List>.from(syncedData['departments'])); if (syncedData['companies'] != null) await _dbHelper.saveCompanies(List>.from(syncedData['companies'])); if (syncedData['positions'] != null) await _dbHelper.savePositions(List>.from(syncedData['positions'])); if (syncedData['airManualStations'] != null) await _dbHelper.saveAirManualStations(List>.from(syncedData['airManualStations'])); if (syncedData['airClients'] != null) await _dbHelper.saveAirClients(List>.from(syncedData['airClients'])); if (syncedData['states'] != null) await _dbHelper.saveStates(List>.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> getManualStations() => _baseService.get('air/manual-stations'); Future> getClients() => _baseService.get('air/clients'); // NEW: Added dedicated method for uploading installation images Future> uploadInstallationImages({ required String airManId, required Map files, }) { return _baseService.postMultipart( endpoint: 'air/manual/installation-images', fields: {'air_man_id': airManId}, files: files, ); } } class MarineApiService { final BaseApiService _baseService; MarineApiService(this._baseService); Future> getTarballStations() => _baseService.get('marine/tarball/stations'); Future> getManualStations() => _baseService.get('marine/manual/stations'); Future> getTarballClassifications() => _baseService.get('marine/tarball/classifications'); Future> submitTarballSample({ required Map formData, required Map 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 = {}; 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> getManualStations() => _baseService.get('river/manual-stations'); Future> 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 get database async { if (_database != null) return _database!; _database = await _initDB(); return _database!; } Future _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 _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 _saveData(String table, String idKey, List> 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>?> _loadData(String table, String idKey) async { final db = await database; final List> maps = await db.query(table); if (maps.isNotEmpty) { return maps.map((map) => jsonDecode(map['${idKey}_json']) as Map).toList(); } return null; } Future saveProfile(Map profile) async { final db = await database; await db.insert(_profileTable, {'user_id': profile['user_id'], 'profile_json': jsonEncode(profile)}, conflictAlgorithm: ConflictAlgorithm.replace); } Future?> loadProfile() async { final db = await database; final List> maps = await db.query(_profileTable); if(maps.isNotEmpty) return jsonDecode(maps.first['profile_json']); return null; } Future saveUsers(List> users) => _saveData(_usersTable, 'user', users); Future>?> loadUsers() => _loadData(_usersTable, 'user'); Future saveTarballStations(List> stations) => _saveData(_tarballStationsTable, 'station', stations); Future>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station'); Future saveManualStations(List> stations) => _saveData(_manualStationsTable, 'station', stations); Future>?> loadManualStations() => _loadData(_manualStationsTable, 'station'); Future saveRiverManualStations(List> stations) => _saveData(_riverManualStationsTable, 'station', stations); Future>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station'); Future saveRiverTriennialStations(List> stations) => _saveData(_riverTriennialStationsTable, 'station', stations); Future>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station'); Future saveTarballClassifications(List> data) => _saveData(_tarballClassificationsTable, 'classification', data); Future>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification'); Future saveDepartments(List> data) => _saveData(_departmentsTable, 'department', data); Future>?> loadDepartments() => _loadData(_departmentsTable, 'department'); Future saveCompanies(List> data) => _saveData(_companiesTable, 'company', data); Future>?> loadCompanies() => _loadData(_companiesTable, 'company'); Future savePositions(List> data) => _saveData(_positionsTable, 'position', data); Future>?> loadPositions() => _loadData(_positionsTable, 'position'); Future saveAirManualStations(List> stations) => _saveData(_airManualStationsTable, 'station', stations); Future>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station'); Future saveAirClients(List> clients) => _saveData(_airClientsTable, 'client', clients); Future>?> loadAirClients() => _loadData(_airClientsTable, 'client'); Future saveStates(List> states) => _saveData(_statesTable, 'state', states); Future>?> loadStates() => _loadData(_statesTable, 'state'); }