From 18c2bf3ec084c378e16f7679468c5d930a6e25db Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Thu, 2 Oct 2025 21:10:23 +0800 Subject: [PATCH] fix parameter limit for river and marine --- lib/auth_provider.dart | 29 +- .../widgets/in_situ_step_3_data_capture.dart | 6 +- .../widgets/in_situ_step_4_summary.dart | 6 +- .../river_in_situ_step_3_data_capture.dart | 28 +- .../widgets/river_in_situ_step_5_summary.dart | 5 +- lib/screens/settings.dart | 268 +++++++++++++----- lib/services/api_service.dart | 59 +++- 7 files changed, 302 insertions(+), 99 deletions(-) diff --git a/lib/auth_provider.dart b/lib/auth_provider.dart index 0c68437..7163974 100644 --- a/lib/auth_provider.dart +++ b/lib/auth_provider.dart @@ -50,7 +50,12 @@ class AuthProvider with ChangeNotifier { List>? _airManualStations; List>? _states; List>? _appSettings; - List>? _parameterLimits; + // --- START: MODIFIED PARAMETER LIMITS PROPERTIES --- + // The old generic list has been removed and replaced with three specific lists. + List>? _npeParameterLimits; + List>? _marineParameterLimits; + List>? _riverParameterLimits; + // --- END: MODIFIED PARAMETER LIMITS PROPERTIES --- List>? _apiConfigs; List>? _ftpConfigs; List>? _documents; @@ -70,7 +75,11 @@ class AuthProvider with ChangeNotifier { List>? get airManualStations => _airManualStations; List>? get states => _states; List>? get appSettings => _appSettings; - List>? get parameterLimits => _parameterLimits; + // --- START: GETTERS FOR NEW PARAMETER LIMITS --- + List>? get npeParameterLimits => _npeParameterLimits; + List>? get marineParameterLimits => _marineParameterLimits; + List>? get riverParameterLimits => _riverParameterLimits; + // --- END: GETTERS FOR NEW PARAMETER LIMITS --- List>? get apiConfigs => _apiConfigs; List>? get ftpConfigs => _ftpConfigs; List>? get documents => _documents; @@ -326,7 +335,13 @@ class AuthProvider with ChangeNotifier { _airManualStations = await _dbHelper.loadAirManualStations(); _states = await _dbHelper.loadStates(); _appSettings = await _dbHelper.loadAppSettings(); - _parameterLimits = await _dbHelper.loadParameterLimits(); + + // --- START: LOAD DATA FROM NEW PARAMETER LIMIT TABLES --- + _npeParameterLimits = await _dbHelper.loadNpeParameterLimits(); + _marineParameterLimits = await _dbHelper.loadMarineParameterLimits(); + _riverParameterLimits = await _dbHelper.loadRiverParameterLimits(); + // --- END: LOAD DATA FROM NEW PARAMETER LIMIT TABLES --- + _documents = await _dbHelper.loadDocuments(); _apiConfigs = await _dbHelper.loadApiConfigs(); _ftpConfigs = await _dbHelper.loadFtpConfigs(); @@ -468,7 +483,13 @@ class AuthProvider with ChangeNotifier { _airManualStations = null; _states = null; _appSettings = null; - _parameterLimits = null; + + // --- START: Clear new parameter limit lists --- + _npeParameterLimits = null; + _marineParameterLimits = null; + _riverParameterLimits = null; + // --- END: Clear new parameter limit lists --- + _documents = null; _apiConfigs = null; _ftpConfigs = null; diff --git a/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart b/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart index 0ef79d0..a6daca9 100644 --- a/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart +++ b/lib/screens/marine/manual/widgets/in_situ_step_3_data_capture.dart @@ -315,7 +315,11 @@ class _InSituStep3DataCaptureState extends State with Wi final currentReadings = _captureReadingsToMap(); final authProvider = Provider.of(context, listen: false); - final marineLimits = (authProvider.parameterLimits ?? []).where((limit) => limit['department_id'] == 4).toList(); + // --- START: MODIFICATION --- + // The `parameterLimits` getter was removed from AuthProvider. + // This now correctly uses the new `marineParameterLimits` getter. + final marineLimits = authProvider.marineParameterLimits ?? []; + // --- END: MODIFICATION --- final outOfBoundsParams = _validateParameters(currentReadings, marineLimits); setState(() { diff --git a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart index fbffc91..dcaa2de 100644 --- a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart +++ b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart @@ -39,7 +39,11 @@ class InSituStep4Summary extends StatelessWidget { /// Re-validates the final parameters against the defined limits. Set _getOutOfBoundsKeys(BuildContext context) { final authProvider = Provider.of(context, listen: false); - final marineLimits = (authProvider.parameterLimits ?? []).where((limit) => limit['department_id'] == 4).toList(); + // --- START MODIFICATION --- + // The `parameterLimits` getter was removed from AuthProvider. + // This now correctly uses the new `marineParameterLimits` getter. + final marineLimits = authProvider.marineParameterLimits ?? []; + // --- END MODIFICATION --- final Set invalidKeys = {}; final readings = { diff --git a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart index 3f53f81..868bb12 100644 --- a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart +++ b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart @@ -9,6 +9,7 @@ import 'package:intl/intl.dart'; import '../../../../auth_provider.dart'; import '../../../../models/river_in_situ_sampling_data.dart'; +import '../../../../services/api_service.dart'; // Import to access DatabaseHelper import '../../../../services/river_in_situ_sampling_service.dart'; import '../../../../bluetooth/bluetooth_manager.dart'; import '../../../../serial/serial_manager.dart'; @@ -35,11 +36,12 @@ class _RiverInSituStep3DataCaptureState extends State? _previousReadingsForComparison; Set _outOfBoundsKeys = {}; @@ -55,7 +57,6 @@ class _RiverInSituStep3DataCaptureState extends State> _parameters = []; @@ -85,9 +86,7 @@ class _RiverInSituStep3DataCaptureState extends State(context, listen: false); - // --- END FIX --- _initializeControllers(); _initializeFlowrateControllers(); WidgetsBinding.instance.addObserver(this); @@ -97,14 +96,12 @@ class _RiverInSituStep3DataCaptureState extends State(context, listen: false); - final allLimits = authProvider.parameterLimits ?? []; - // Use department_id 3 for River - final riverLimits = allLimits.where((limit) => limit['department_id'] == 3).toList(); + // Directly load river-specific limits from the new table via DatabaseHelper. + final List> riverLimits = await _dbHelper.loadRiverParameterLimits() ?? []; + final outOfBoundsParams = _validateParameters(currentReadings, riverLimits); setState(() { @@ -384,6 +380,7 @@ class _RiverInSituStep3DataCaptureState extends State _captureReadingsToMap() { final Map readings = {}; @@ -475,7 +472,6 @@ class _RiverInSituStep3DataCaptureState extends State _getOutOfBoundsKeys(BuildContext context) { final authProvider = Provider.of(context, listen: false); - // Filter for River department (id: 3) - final riverLimits = (authProvider.parameterLimits ?? []).where((limit) => limit['department_id'] == 3).toList(); + // --- MODIFICATION: Use the new river-specific parameter limits list --- + final riverLimits = authProvider.riverParameterLimits ?? []; + // --- END MODIFICATION --- final Set invalidKeys = {}; final readings = { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index f6d3125..14189e6 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -5,11 +5,8 @@ import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/services/settings_service.dart'; -// START CHANGE: Import the new UserPreferencesService to manage submission settings import 'package:environment_monitoring_app/services/user_preferences_service.dart'; -// END CHANGE -// START CHANGE: A helper class to manage the state of each module's settings in the UI class _ModuleSettings { bool isApiEnabled; bool isFtpEnabled; @@ -23,7 +20,6 @@ class _ModuleSettings { required this.ftpConfigs, }); } -// END CHANGE class SettingsScreen extends StatefulWidget { @@ -37,15 +33,12 @@ class _SettingsScreenState extends State { final SettingsService _settingsService = SettingsService(); bool _isSyncingData = false; - // START CHANGE: New state variables for managing submission preferences UI final UserPreferencesService _preferencesService = UserPreferencesService(); bool _isLoadingSettings = true; bool _isSaving = false; - // This map holds the live state of the settings UI for each module final Map _moduleSettings = {}; - // This list defines which modules will appear in the new settings section final List> _configurableModules = [ {'key': 'marine_tarball', 'name': 'Marine Tarball'}, {'key': 'marine_in_situ', 'name': 'Marine In-Situ'}, @@ -53,7 +46,6 @@ class _SettingsScreenState extends State { {'key': 'air_installation', 'name': 'Air Installation'}, {'key': 'air_collection', 'name': 'Air Collection'}, ]; - // END CHANGE final TextEditingController _tarballSearchController = TextEditingController(); String _tarballSearchQuery = ''; @@ -68,16 +60,33 @@ class _SettingsScreenState extends State { final TextEditingController _airClientSearchController = TextEditingController(); String _airClientSearchQuery = ''; + final TextEditingController _npeRiverLimitsSearchController = TextEditingController(); + String _npeRiverLimitsSearchQuery = ''; + final TextEditingController _npeMarineLimitsSearchController = TextEditingController(); + String _npeMarineLimitsSearchQuery = ''; + final TextEditingController _airLimitsSearchController = TextEditingController(); + String _airLimitsSearchQuery = ''; + final TextEditingController _riverLimitsSearchController = TextEditingController(); + String _riverLimitsSearchQuery = ''; + final TextEditingController _marineLimitsSearchController = TextEditingController(); + String _marineLimitsSearchQuery = ''; + @override void initState() { super.initState(); - _loadAllModuleSettings(); // Load the new submission preferences on init + _loadAllModuleSettings(); _tarballSearchController.addListener(_onTarballSearchChanged); _manualSearchController.addListener(_onManualSearchChanged); _riverManualSearchController.addListener(_onRiverManualSearchChanged); _riverTriennialSearchController.addListener(_onRiverTriennialSearchChanged); _airStationSearchController.addListener(_onAirStationSearchChanged); _airClientSearchController.addListener(_onAirClientSearchChanged); + + _npeRiverLimitsSearchController.addListener(() => setState(() => _npeRiverLimitsSearchQuery = _npeRiverLimitsSearchController.text)); + _npeMarineLimitsSearchController.addListener(() => setState(() => _npeMarineLimitsSearchQuery = _npeMarineLimitsSearchController.text)); + _airLimitsSearchController.addListener(() => setState(() => _airLimitsSearchQuery = _airLimitsSearchController.text)); + _riverLimitsSearchController.addListener(() => setState(() => _riverLimitsSearchQuery = _riverLimitsSearchController.text)); + _marineLimitsSearchController.addListener(() => setState(() => _marineLimitsSearchQuery = _marineLimitsSearchController.text)); } @override @@ -88,6 +97,12 @@ class _SettingsScreenState extends State { _riverTriennialSearchController.dispose(); _airStationSearchController.dispose(); _airClientSearchController.dispose(); + + _npeRiverLimitsSearchController.dispose(); + _npeMarineLimitsSearchController.dispose(); + _airLimitsSearchController.dispose(); + _riverLimitsSearchController.dispose(); + _marineLimitsSearchController.dispose(); super.dispose(); } @@ -127,7 +142,6 @@ class _SettingsScreenState extends State { }); } - // START CHANGE: New methods for loading and saving the submission preferences Future _loadAllModuleSettings() async { setState(() => _isLoadingSettings = true); for (var module in _configurableModules) { @@ -136,26 +150,17 @@ class _SettingsScreenState extends State { final apiConfigsWithPrefs = await _preferencesService.getAllApiConfigsWithModulePreferences(moduleKey); final ftpConfigsWithPrefs = await _preferencesService.getAllFtpConfigsWithModulePreferences(moduleKey); - // START MODIFICATION: Apply default settings for submission preferences - // This logic checks if the main toggle for a submission type is on but no - // specific destination is checked. If so, it applies a default selection. - // This ensures a default configuration without overriding saved user choices. - - // Check if any API config is already enabled from preferences. final bool isAnyApiConfigEnabled = apiConfigsWithPrefs.any((c) => c['is_enabled'] == true); - // If the main API toggle is on but no specific API is selected, apply the default. if (prefs['is_api_enabled'] == true && !isAnyApiConfigEnabled) { - final pstwHqApi = apiConfigsWithPrefs.firstWhere((c) => c['config_name'] == 'pstw_hq', orElse: () => {}); + final pstwHqApi = apiConfigsWithPrefs.firstWhere((c) => c['config_name'] == 'PSTW_HQ', orElse: () => {}); if (pstwHqApi.isNotEmpty) { pstwHqApi['is_enabled'] = true; } } - // Check if any FTP config is already enabled from preferences. final bool isAnyFtpConfigEnabled = ftpConfigsWithPrefs.any((c) => c['is_enabled'] == true); - // If the main FTP toggle is on but no specific FTP is selected, apply the defaults for the module. if (prefs['is_ftp_enabled'] == true && !isAnyFtpConfigEnabled) { switch (moduleKey) { case 'marine_tarball': @@ -195,7 +200,6 @@ class _SettingsScreenState extends State { break; } } - // END MODIFICATION _moduleSettings[moduleKey] = _ModuleSettings( isApiEnabled: prefs['is_api_enabled'], @@ -235,7 +239,6 @@ class _SettingsScreenState extends State { } } } - // END CHANGE Future _manualDataSync() async { if (_isSyncingData) return; @@ -245,7 +248,6 @@ class _SettingsScreenState extends State { try { await auth.syncAllData(forceRefresh: true); - // MODIFIED: After syncing, also reload module settings to reflect any new server configurations. await _loadAllModuleSettings(); if (mounted) { @@ -278,25 +280,69 @@ class _SettingsScreenState extends State { final auth = Provider.of(context); final lastSync = auth.lastSyncTimestamp; - // Get the synced data from the provider. final appSettings = auth.appSettings; - final parameterLimits = auth.parameterLimits; + final npeParameterLimits = auth.npeParameterLimits; + final marineParameterLimits = auth.marineParameterLimits; + final riverParameterLimits = auth.riverParameterLimits; final apiConfigs = auth.apiConfigs; final ftpConfigs = auth.ftpConfigs; final airClients = auth.airClients; final departments = auth.departments; + final allManualStations = auth.manualStations; - // Find Department IDs final int? airDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'Air', orElse: () => {})?['department_id']; final int? riverDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'River', orElse: () => {})?['department_id']; final int? marineDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'Marine', orElse: () => {})?['department_id']; - // Filter Parameter Limits by Department ID - final filteredAirLimits = parameterLimits?.where((limit) => limit['department_id'] == airDepartmentId).toList(); - final filteredRiverLimits = parameterLimits?.where((limit) => limit['department_id'] == riverDepartmentId).toList(); - final filteredMarineLimits = parameterLimits?.where((limit) => limit['department_id'] == marineDepartmentId).toList(); + final filteredNpeRiverLimits = npeParameterLimits?.where((limit) { + final isRiverNpe = riverDepartmentId != null && limit['department_id'] == riverDepartmentId; + if (!isRiverNpe) return false; + final paramName = limit['param_parameter_list']?.toLowerCase() ?? ''; + final query = _npeRiverLimitsSearchQuery.toLowerCase(); + return paramName.contains(query); + }).toList(); + + final filteredNpeMarineLimits = npeParameterLimits?.where((limit) { + final isMarineNpe = marineDepartmentId != null && limit['department_id'] == marineDepartmentId; + if (!isMarineNpe) return false; + final paramName = limit['param_parameter_list']?.toLowerCase() ?? ''; + final query = _npeMarineLimitsSearchQuery.toLowerCase(); + return paramName.contains(query); + }).toList(); + + final filteredAirLimits = npeParameterLimits?.where((limit) { + final isAirLimit = airDepartmentId != null && limit['department_id'] == airDepartmentId; + if (!isAirLimit) return false; + final paramName = limit['param_parameter_list']?.toLowerCase() ?? ''; + final query = _airLimitsSearchQuery.toLowerCase(); + return paramName.contains(query); + }).toList(); + + final filteredRiverLimits = riverParameterLimits?.where((limit) { + final paramName = limit['param_parameter_list']?.toLowerCase() ?? ''; + final query = _riverLimitsSearchQuery.toLowerCase(); + return paramName.contains(query); + }).toList(); + + final filteredMarineLimits = marineParameterLimits?.where((limit) { + final paramName = limit['param_parameter_list']?.toLowerCase() ?? ''; + final query = _marineLimitsSearchQuery.toLowerCase(); + if (paramName.contains(query)) return true; + + final stationId = limit['station_id']; + if (stationId != null && allManualStations != null) { + final station = allManualStations.firstWhere((s) => s['station_id'] == stationId, orElse: () => {}); + if (station.isNotEmpty) { + final stationName = station['man_station_name']?.toLowerCase() ?? ''; + final stationCode = station['man_station_code']?.toLowerCase() ?? ''; + if (stationName.contains(query) || stationCode.contains(query)) { + return true; + } + } + } + return false; + }).toList(); - // Filter Marine Stations final filteredTarballStations = (auth.tarballStations?.where((station) { final stationName = station['tbl_station_name']?.toLowerCase() ?? ''; final stationCode = station['tbl_station_code']?.toLowerCase() ?? ''; @@ -311,7 +357,6 @@ class _SettingsScreenState extends State { return stationName.contains(query) || stationCode.contains(query); }).toList())?.cast>(); - // Filter River Stations final filteredRiverManualStations = (auth.riverManualStations?.where((station) { final riverName = station['sampling_river']?.toLowerCase() ?? ''; final stationCode = station['sampling_station_code']?.toLowerCase() ?? ''; @@ -328,7 +373,6 @@ class _SettingsScreenState extends State { return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query); }).toList())?.cast>(); - // Filter Air Stations final filteredAirStations = (auth.airManualStations?.where((station) { final stationName = station['station_name']?.toLowerCase() ?? ''; final stationCode = station['station_code']?.toLowerCase() ?? ''; @@ -336,7 +380,6 @@ class _SettingsScreenState extends State { return stationName.contains(query) || stationCode.contains(query); }).toList())?.cast>(); - // Filter Air Clients final filteredAirClients = (auth.airClients?.where((client) { final clientName = client['client_name']?.toLowerCase() ?? ''; final clientId = client['client_id']?.toString().toLowerCase() ?? ''; @@ -347,7 +390,6 @@ class _SettingsScreenState extends State { return Scaffold( appBar: AppBar( title: const Text("Settings"), - // START CHANGE: Add a save button to the AppBar actions: [ Padding( padding: const EdgeInsets.only(right: 8.0), @@ -360,7 +402,6 @@ class _SettingsScreenState extends State { ), ) ], - // END CHANGE ), body: SingleChildScrollView( padding: const EdgeInsets.all(24.0), @@ -391,7 +432,6 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 32), - // START CHANGE: Insert the new Submission Preferences section _buildSectionHeader(context, "Submission Preferences"), _isLoadingSettings ? const Center(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())) @@ -410,7 +450,6 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 32), - // END CHANGE Text("Telegram Alert Settings", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 16), @@ -457,20 +496,75 @@ class _SettingsScreenState extends State { margin: EdgeInsets.zero, child: Column( children: [ + _buildExpansionTile( + title: 'NPE River Parameter Limits', + leadingIcon: Icons.science_outlined, + child: Column( + children: [ + _buildSearchBar( + controller: _npeRiverLimitsSearchController, + labelText: 'Search NPE River Limits', + hintText: 'Search by parameter name', + ), + _buildInfoList(filteredNpeRiverLimits, (item) => _buildParameterLimitEntry(item, departments: departments)), + ], + ), + ), + _buildExpansionTile( + title: 'NPE Marine Parameter Limits', + leadingIcon: Icons.science_outlined, + child: Column( + children: [ + _buildSearchBar( + controller: _npeMarineLimitsSearchController, + labelText: 'Search NPE Marine Limits', + hintText: 'Search by parameter name', + ), + _buildInfoList(filteredNpeMarineLimits, (item) => _buildParameterLimitEntry(item, departments: departments)), + ], + ), + ), _buildExpansionTile( title: 'Air Parameter Limits', - leadingIcon: Icons.poll, - child: _buildInfoList(filteredAirLimits, (item) => _buildParameterLimitEntry(item)), + leadingIcon: Icons.air, + child: Column( + children: [ + _buildSearchBar( + controller: _airLimitsSearchController, + labelText: 'Search Air Limits', + hintText: 'Search by parameter name', + ), + _buildInfoList(filteredAirLimits, (item) => _buildParameterLimitEntry(item, departments: departments)), + ], + ), ), _buildExpansionTile( title: 'River Parameter Limits', - leadingIcon: Icons.poll, - child: _buildInfoList(filteredRiverLimits, (item) => _buildParameterLimitEntry(item)), + leadingIcon: Icons.water, + child: Column( + children: [ + _buildSearchBar( + controller: _riverLimitsSearchController, + labelText: 'Search River Limits', + hintText: 'Search by parameter name', + ), + _buildInfoList(filteredRiverLimits, (item) => _buildParameterLimitEntry(item)), + ], + ), ), _buildExpansionTile( title: 'Marine Parameter Limits', - leadingIcon: Icons.poll, - child: _buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item)), + leadingIcon: Icons.waves, + child: Column( + children: [ + _buildSearchBar( + controller: _marineLimitsSearchController, + labelText: 'Search Marine Limits', + hintText: 'Search by parameter or station', + ), + _buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item, stations: allManualStations)), + ], + ), ), _buildExpansionTile( title: 'API Configurations', @@ -668,7 +762,6 @@ class _SettingsScreenState extends State { ); } - // START CHANGE: New helper widgets for the preferences UI Widget _buildModulePreferenceTile(String title, String moduleKey, _ModuleSettings settings) { return ExpansionTile( title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), @@ -727,7 +820,6 @@ class _SettingsScreenState extends State { ), ); } - // END CHANGE Widget _buildSectionHeader(BuildContext context, String title) { return Padding( @@ -748,7 +840,10 @@ class _SettingsScreenState extends State { leading: Icon(leadingIcon), title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), children: [ - child, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: child, + ), ], ); } @@ -784,13 +879,37 @@ class _SettingsScreenState extends State { ); } - Widget _buildParameterLimitEntry(Map item) { + Widget _buildParameterLimitEntry( + Map item, { + List>? departments, + List>? stations, + }) { final paramName = item['param_parameter_list']?.toString() ?? 'N/A'; final upperLimit = item['param_upper_limit']?.toString() ?? 'N/A'; final lowerLimit = item['param_lower_limit']?.toString() ?? 'N/A'; - String unit = ''; + String contextSubtitle = ''; - // Hardcoded units as they are not available in the provided data + if (item.containsKey('department_id') && item['department_id'] != null && departments != null) { + final deptId = item['department_id']; + final dept = departments.firstWhere((d) => d['department_id'] == deptId, orElse: () => {}); + if (dept.isNotEmpty) { + contextSubtitle = 'Dept: ${dept['department_name']}'; + } + } + + if (item.containsKey('station_id') && item['station_id'] != null && stations != null) { + final stationId = item['station_id']; + final station = stations.firstWhere((s) => s['station_id'] == stationId, orElse: () => {}); + if (station.isNotEmpty) { + // --- START: MODIFICATION --- + final stationCode = station['man_station_code'] ?? 'N/A'; + final stationName = station['man_station_name'] ?? 'N/A'; + contextSubtitle = 'Station: $stationCode - $stationName'; + // --- END: MODIFICATION --- + } + } + + String unit = ''; if (paramName.toLowerCase() == 'ph') { unit = 'pH units'; } else if (paramName.toLowerCase() == 'temp') { @@ -798,7 +917,7 @@ class _SettingsScreenState extends State { } return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -819,16 +938,24 @@ class _SettingsScreenState extends State { children: [ const Icon(Icons.science_outlined, size: 16), const SizedBox(width: 8), - Text( - paramName, - style: const TextStyle(fontSize: 14), + Flexible( + child: Text( + paramName, + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.center, + ), ), ], ), - Text( - unit, - style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey), - ), + if (contextSubtitle.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + contextSubtitle, + style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey), + textAlign: TextAlign.center, + ), + ), ], ), ), @@ -868,18 +995,21 @@ class _SettingsScreenState extends State { required String labelText, required String hintText, }) { - return TextField( - controller: controller, - decoration: InputDecoration( - labelText: labelText, - hintText: hintText, - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), - suffixIcon: controller.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear), onPressed: () => controller.clear()) - : null, + return Padding( + padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: labelText, + hintText: hintText, + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), + suffixIcon: controller.text.isNotEmpty + ? IconButton(icon: const Icon(Icons.clear), onPressed: () => controller.clear()) + : null, + ), + style: const TextStyle(fontSize: 14), ), - style: const TextStyle(fontSize: 14), ); } diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index c8318fb..fd03c4d 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -276,13 +276,29 @@ class ApiService { await dbHelper.deleteAppSettings(id); } }, - 'parameterLimits': { - 'endpoint': 'parameter-limits', + // --- START: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS --- + 'npeParameterLimits': { + 'endpoint': 'npe-parameter-limits', 'handler': (d, id) async { - await dbHelper.upsertParameterLimits(d); - await dbHelper.deleteParameterLimits(id); + 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); + } + }, + // --- END: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS --- 'apiConfigs': { 'endpoint': 'api-configs', 'handler': (d, id) async { @@ -781,7 +797,9 @@ class RiverApiService { class DatabaseHelper { static Database? _database; static const String _dbName = 'app_data.db'; - static const int _dbVersion = 21; + // --- START: INCREMENTED DB VERSION --- + static const int _dbVersion = 23; + // --- END: INCREMENTED DB VERSION --- static const String _profileTable = 'user_profile'; static const String _usersTable = 'all_users'; @@ -799,6 +817,11 @@ class DatabaseHelper { static const String _statesTable = 'states'; static const String _appSettingsTable = 'app_settings'; static const String _parameterLimitsTable = 'manual_parameter_limits'; + // --- START: ADDED NEW TABLE CONSTANTS --- + static const String _npeParameterLimitsTable = 'npe_parameter_limits'; + static const String _marineParameterLimitsTable = 'marine_parameter_limits'; + static const String _riverParameterLimitsTable = 'river_parameter_limits'; + // --- END: ADDED NEW TABLE CONSTANTS --- static const String _apiConfigsTable = 'api_configurations'; static const String _ftpConfigsTable = 'ftp_configurations'; static const String _retryQueueTable = 'retry_queue'; @@ -844,6 +867,11 @@ class DatabaseHelper { await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)'); await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)'); await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + // --- START: ADDED CREATE TABLE FOR NEW LIMITS --- + await db.execute('CREATE TABLE $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + await db.execute('CREATE TABLE $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + await db.execute('CREATE TABLE $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + // --- END: ADDED CREATE TABLE FOR NEW LIMITS --- await db.execute('CREATE TABLE $_apiConfigsTable(api_config_id INTEGER PRIMARY KEY, config_json TEXT)'); await db.execute('CREATE TABLE $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)'); await db.execute(''' @@ -987,6 +1015,13 @@ class DatabaseHelper { debugPrint("Upgrade warning: Failed to add password_hash column to users table (may already exist): $e"); } } + // --- START: ADDED UPGRADE LOGIC FOR NEW TABLES --- + if (oldVersion < 23) { + await db.execute('CREATE TABLE IF NOT EXISTS $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + await db.execute('CREATE TABLE IF NOT EXISTS $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + await db.execute('CREATE TABLE IF NOT EXISTS $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); + } + // --- END: ADDED UPGRADE LOGIC FOR NEW TABLES --- } /// Performs an "upsert": inserts new records or replaces existing ones. @@ -1199,6 +1234,20 @@ class DatabaseHelper { Future deleteParameterLimits(List ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids); Future>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit'); + // --- START: ADDED NEW DB METHODS FOR PARAMETER LIMITS --- + Future upsertNpeParameterLimits(List> data) => _upsertData(_npeParameterLimitsTable, 'param_autoid', data, 'limit'); + Future deleteNpeParameterLimits(List ids) => _deleteData(_npeParameterLimitsTable, 'param_autoid', ids); + Future>?> loadNpeParameterLimits() => _loadData(_npeParameterLimitsTable, 'limit'); + + Future upsertMarineParameterLimits(List> data) => _upsertData(_marineParameterLimitsTable, 'param_autoid', data, 'limit'); + Future deleteMarineParameterLimits(List ids) => _deleteData(_marineParameterLimitsTable, 'param_autoid', ids); + Future>?> loadMarineParameterLimits() => _loadData(_marineParameterLimitsTable, 'limit'); + + Future upsertRiverParameterLimits(List> data) => _upsertData(_riverParameterLimitsTable, 'param_autoid', data, 'limit'); + Future deleteRiverParameterLimits(List ids) => _deleteData(_riverParameterLimitsTable, 'param_autoid', ids); + Future>?> loadRiverParameterLimits() => _loadData(_riverParameterLimitsTable, 'limit'); + // --- END: ADDED NEW DB METHODS FOR PARAMETER LIMITS --- + Future upsertApiConfigs(List> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config'); Future deleteApiConfigs(List ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids); Future>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config');