fix parameter limit for river and marine
This commit is contained in:
parent
31b64fc203
commit
18c2bf3ec0
@ -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;
|
||||
|
||||
@ -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(() {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user