fix parameter limit for river and marine

This commit is contained in:
ALim Aidrus 2025-10-02 21:10:23 +08:00
parent 31b64fc203
commit 18c2bf3ec0
7 changed files with 302 additions and 99 deletions

View File

@ -50,7 +50,12 @@ class AuthProvider with ChangeNotifier {
List<Map<String, dynamic>>? _airManualStations;
List<Map<String, dynamic>>? _states;
List<Map<String, dynamic>>? _appSettings;
List<Map<String, dynamic>>? _parameterLimits;
// --- START: MODIFIED PARAMETER LIMITS PROPERTIES ---
// The old generic list has been removed and replaced with three specific lists.
List<Map<String, dynamic>>? _npeParameterLimits;
List<Map<String, dynamic>>? _marineParameterLimits;
List<Map<String, dynamic>>? _riverParameterLimits;
// --- END: MODIFIED PARAMETER LIMITS PROPERTIES ---
List<Map<String, dynamic>>? _apiConfigs;
List<Map<String, dynamic>>? _ftpConfigs;
List<Map<String, dynamic>>? _documents;
@ -70,7 +75,11 @@ class AuthProvider with ChangeNotifier {
List<Map<String, dynamic>>? get airManualStations => _airManualStations;
List<Map<String, dynamic>>? get states => _states;
List<Map<String, dynamic>>? get appSettings => _appSettings;
List<Map<String, dynamic>>? get parameterLimits => _parameterLimits;
// --- START: GETTERS FOR NEW PARAMETER LIMITS ---
List<Map<String, dynamic>>? get npeParameterLimits => _npeParameterLimits;
List<Map<String, dynamic>>? get marineParameterLimits => _marineParameterLimits;
List<Map<String, dynamic>>? get riverParameterLimits => _riverParameterLimits;
// --- END: GETTERS FOR NEW PARAMETER LIMITS ---
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
List<Map<String, dynamic>>? 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;

View File

@ -315,7 +315,11 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
final currentReadings = _captureReadingsToMap();
final authProvider = Provider.of<AuthProvider>(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(() {

View File

@ -39,7 +39,11 @@ class InSituStep4Summary extends StatelessWidget {
/// Re-validates the final parameters against the defined limits.
Set<String> _getOutOfBoundsKeys(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(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<String> invalidKeys = {};
final readings = {

View File

@ -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<RiverInSituStep3DataCaptur
bool _isAutoReading = false;
StreamSubscription? _dataSubscription;
// --- START FIX: Declare service variable for safe disposal ---
late final RiverInSituSamplingService _samplingService;
// --- END FIX ---
// --- START: Added for Parameter Validation Feature ---
// --- START: Added for direct database access ---
final DatabaseHelper _dbHelper = DatabaseHelper();
// --- END: Added for direct database access ---
Map<String, double>? _previousReadingsForComparison;
Set<String> _outOfBoundsKeys = {};
@ -55,7 +57,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
'ammonia': 'Ammonia',
'batteryVoltage': 'Battery',
};
// --- END: Added for Parameter Validation Feature ---
final List<Map<String, dynamic>> _parameters = [];
@ -85,9 +86,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
@override
void initState() {
super.initState();
// --- START FIX: Initialize service variable safely ---
_samplingService = Provider.of<RiverInSituSamplingService>(context, listen: false);
// --- END FIX ---
_initializeControllers();
_initializeFlowrateControllers();
WidgetsBinding.instance.addObserver(this);
@ -97,14 +96,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
void dispose() {
_dataSubscription?.cancel();
// --- START FIX: Properly disconnect from active connections on dispose ---
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
_samplingService.disconnectFromBluetooth();
}
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
_samplingService.disconnectFromSerial();
}
// --- END FIX ---
_disposeControllers();
_disposeFlowrateControllers();
@ -354,8 +351,8 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
});
}
// --- START: New Validation Flow ---
void _validateAndProceed() {
// --- START: MODIFIED VALIDATION FLOW ---
void _validateAndProceed() async {
if (_isAutoReading) {
_showStopReadingDialog();
return;
@ -367,11 +364,10 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
_formKey.currentState!.save();
final currentReadings = _captureReadingsToMap();
final authProvider = Provider.of<AuthProvider>(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<Map<String, dynamic>> riverLimits = await _dbHelper.loadRiverParameterLimits() ?? [];
final outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
setState(() {
@ -384,6 +380,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
_saveDataAndMoveOn(currentReadings);
}
}
// --- END: MODIFIED VALIDATION FLOW ---
Map<String, double> _captureReadingsToMap() {
final Map<String, double> readings = {};
@ -475,7 +472,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
widget.onNext();
}
// --- END: New Validation Flow ---
void _showSnackBar(String message, {bool isError = false}) {
if (mounted) {
@ -670,7 +666,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
);
}
// --- START: New UI Widgets for Validation Feature ---
Widget _buildComparisonView() {
final previousReadings = _previousReadingsForComparison!;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
@ -833,7 +828,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
},
);
}
// --- END: New UI Widgets for Validation Feature ---
Widget _buildFlowrateSection() {
return Card(

View File

@ -39,8 +39,9 @@ class RiverInSituStep5Summary extends StatelessWidget {
/// Re-validates the final parameters against the defined limits.
Set<String> _getOutOfBoundsKeys(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(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<String> invalidKeys = {};
final readings = {

View File

@ -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<SettingsScreen> {
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<String, _ModuleSettings> _moduleSettings = {};
// This list defines which modules will appear in the new settings section
final List<Map<String, String>> _configurableModules = [
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
@ -53,7 +46,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
{'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<SettingsScreen> {
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<SettingsScreen> {
_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<SettingsScreen> {
});
}
// START CHANGE: New methods for loading and saving the submission preferences
Future<void> _loadAllModuleSettings() async {
setState(() => _isLoadingSettings = true);
for (var module in _configurableModules) {
@ -136,26 +150,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
break;
}
}
// END MODIFICATION
_moduleSettings[moduleKey] = _ModuleSettings(
isApiEnabled: prefs['is_api_enabled'],
@ -235,7 +239,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
}
// END CHANGE
Future<void> _manualDataSync() async {
if (_isSyncingData) return;
@ -245,7 +248,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
final auth = Provider.of<AuthProvider>(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<SettingsScreen> {
return stationName.contains(query) || stationCode.contains(query);
}).toList())?.cast<Map<String, dynamic>>();
// 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<SettingsScreen> {
return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query);
}).toList())?.cast<Map<String, dynamic>>();
// 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<SettingsScreen> {
return stationName.contains(query) || stationCode.contains(query);
}).toList())?.cast<Map<String, dynamic>>();
// 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<SettingsScreen> {
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<SettingsScreen> {
),
)
],
// END CHANGE
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
@ -391,7 +432,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
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<SettingsScreen> {
),
),
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<SettingsScreen> {
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<SettingsScreen> {
);
}
// 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<SettingsScreen> {
),
);
}
// END CHANGE
Widget _buildSectionHeader(BuildContext context, String title) {
return Padding(
@ -748,7 +840,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
);
}
Widget _buildParameterLimitEntry(Map<String, dynamic> item) {
Widget _buildParameterLimitEntry(
Map<String, dynamic> item, {
List<Map<String, dynamic>>? departments,
List<Map<String, dynamic>>? 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<SettingsScreen> {
}
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<SettingsScreen> {
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<SettingsScreen> {
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),
);
}

View File

@ -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<void> deleteParameterLimits(List<dynamic> ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit');
// --- START: ADDED NEW DB METHODS FOR PARAMETER LIMITS ---
Future<void> upsertNpeParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_npeParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteNpeParameterLimits(List<dynamic> ids) => _deleteData(_npeParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadNpeParameterLimits() => _loadData(_npeParameterLimitsTable, 'limit');
Future<void> upsertMarineParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_marineParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteMarineParameterLimits(List<dynamic> ids) => _deleteData(_marineParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadMarineParameterLimits() => _loadData(_marineParameterLimitsTable, 'limit');
Future<void> upsertRiverParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_riverParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteRiverParameterLimits(List<dynamic> ids) => _deleteData(_riverParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadRiverParameterLimits() => _loadData(_riverParameterLimitsTable, 'limit');
// --- END: ADDED NEW DB METHODS FOR PARAMETER LIMITS ---
Future<void> upsertApiConfigs(List<Map<String, dynamic>> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config');
Future<void> deleteApiConfigs(List<dynamic> ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids);
Future<List<Map<String, dynamic>>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config');