// 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; static const String imageBaseUrl = 'https://dev14.pstw.com.my/'; ApiService() { marine = MarineApiService(_baseService); river = RiverApiService(_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'); // --- ADDED: Method to send a queued Telegram alert to your server --- Future> sendTelegramAlert({ required String chatId, required String message, }) { return _baseService.post('marine/telegram-alert', { 'chat_id': chatId, 'message': message, }); } // --- END --- 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} ); } /// --- A dedicated method to refresh only the profile --- 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(), ]); 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, }; 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'])); 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 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'; // FIXED: Incremented database version to trigger onUpgrade and create the new table static const int _dbVersion = 10; 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'; // --- ADDED: Table name for the alert queue --- static const String _alertQueueTable = 'alert_queue'; 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)'); // --- ADDED: Create the alert_queue table --- 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 ) '''); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 10) { await db.execute('DROP TABLE IF EXISTS $_profileTable'); await db.execute('DROP TABLE IF EXISTS $_usersTable'); await db.execute('DROP TABLE IF EXISTS $_tarballStationsTable'); await db.execute('DROP TABLE IF EXISTS $_manualStationsTable'); await db.execute('DROP TABLE IF EXISTS $_riverManualStationsTable'); await db.execute('DROP TABLE IF EXISTS $_riverTriennialStationsTable'); await db.execute('DROP TABLE IF EXISTS $_tarballClassificationsTable'); await db.execute('DROP TABLE IF EXISTS $_departmentsTable'); await db.execute('DROP TABLE IF EXISTS $_companiesTable'); await db.execute('DROP TABLE IF EXISTS $_positionsTable'); // --- ADDED: Drop the alert_queue table during upgrade --- await db.execute('DROP TABLE IF EXISTS $_alertQueueTable'); await _onCreate(db, newVersion); } } 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], '${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'); }