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>>? _airManualStations;
List<Map<String, dynamic>>? _states; List<Map<String, dynamic>>? _states;
List<Map<String, dynamic>>? _appSettings; 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>>? _apiConfigs;
List<Map<String, dynamic>>? _ftpConfigs; List<Map<String, dynamic>>? _ftpConfigs;
List<Map<String, dynamic>>? _documents; 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 airManualStations => _airManualStations;
List<Map<String, dynamic>>? get states => _states; List<Map<String, dynamic>>? get states => _states;
List<Map<String, dynamic>>? get appSettings => _appSettings; 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 apiConfigs => _apiConfigs;
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs; List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
List<Map<String, dynamic>>? get documents => _documents; List<Map<String, dynamic>>? get documents => _documents;
@ -326,7 +335,13 @@ class AuthProvider with ChangeNotifier {
_airManualStations = await _dbHelper.loadAirManualStations(); _airManualStations = await _dbHelper.loadAirManualStations();
_states = await _dbHelper.loadStates(); _states = await _dbHelper.loadStates();
_appSettings = await _dbHelper.loadAppSettings(); _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(); _documents = await _dbHelper.loadDocuments();
_apiConfigs = await _dbHelper.loadApiConfigs(); _apiConfigs = await _dbHelper.loadApiConfigs();
_ftpConfigs = await _dbHelper.loadFtpConfigs(); _ftpConfigs = await _dbHelper.loadFtpConfigs();
@ -468,7 +483,13 @@ class AuthProvider with ChangeNotifier {
_airManualStations = null; _airManualStations = null;
_states = null; _states = null;
_appSettings = 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; _documents = null;
_apiConfigs = null; _apiConfigs = null;
_ftpConfigs = null; _ftpConfigs = null;

View File

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

View File

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

View File

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

View File

@ -39,8 +39,9 @@ class RiverInSituStep5Summary extends StatelessWidget {
/// Re-validates the final parameters against the defined limits. /// Re-validates the final parameters against the defined limits.
Set<String> _getOutOfBoundsKeys(BuildContext context) { Set<String> _getOutOfBoundsKeys(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context, listen: false); final authProvider = Provider.of<AuthProvider>(context, listen: false);
// Filter for River department (id: 3) // --- MODIFICATION: Use the new river-specific parameter limits list ---
final riverLimits = (authProvider.parameterLimits ?? []).where((limit) => limit['department_id'] == 3).toList(); final riverLimits = authProvider.riverParameterLimits ?? [];
// --- END MODIFICATION ---
final Set<String> invalidKeys = {}; final Set<String> invalidKeys = {};
final readings = { final readings = {

View File

@ -5,11 +5,8 @@ import 'package:provider/provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/auth_provider.dart';
import 'package:environment_monitoring_app/services/settings_service.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'; 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 { class _ModuleSettings {
bool isApiEnabled; bool isApiEnabled;
bool isFtpEnabled; bool isFtpEnabled;
@ -23,7 +20,6 @@ class _ModuleSettings {
required this.ftpConfigs, required this.ftpConfigs,
}); });
} }
// END CHANGE
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
@ -37,15 +33,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
final SettingsService _settingsService = SettingsService(); final SettingsService _settingsService = SettingsService();
bool _isSyncingData = false; bool _isSyncingData = false;
// START CHANGE: New state variables for managing submission preferences UI
final UserPreferencesService _preferencesService = UserPreferencesService(); final UserPreferencesService _preferencesService = UserPreferencesService();
bool _isLoadingSettings = true; bool _isLoadingSettings = true;
bool _isSaving = false; bool _isSaving = false;
// This map holds the live state of the settings UI for each module
final Map<String, _ModuleSettings> _moduleSettings = {}; final Map<String, _ModuleSettings> _moduleSettings = {};
// This list defines which modules will appear in the new settings section
final List<Map<String, String>> _configurableModules = [ final List<Map<String, String>> _configurableModules = [
{'key': 'marine_tarball', 'name': 'Marine Tarball'}, {'key': 'marine_tarball', 'name': 'Marine Tarball'},
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'}, {'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_installation', 'name': 'Air Installation'},
{'key': 'air_collection', 'name': 'Air Collection'}, {'key': 'air_collection', 'name': 'Air Collection'},
]; ];
// END CHANGE
final TextEditingController _tarballSearchController = TextEditingController(); final TextEditingController _tarballSearchController = TextEditingController();
String _tarballSearchQuery = ''; String _tarballSearchQuery = '';
@ -68,16 +60,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _airClientSearchController = TextEditingController(); final TextEditingController _airClientSearchController = TextEditingController();
String _airClientSearchQuery = ''; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_loadAllModuleSettings(); // Load the new submission preferences on init _loadAllModuleSettings();
_tarballSearchController.addListener(_onTarballSearchChanged); _tarballSearchController.addListener(_onTarballSearchChanged);
_manualSearchController.addListener(_onManualSearchChanged); _manualSearchController.addListener(_onManualSearchChanged);
_riverManualSearchController.addListener(_onRiverManualSearchChanged); _riverManualSearchController.addListener(_onRiverManualSearchChanged);
_riverTriennialSearchController.addListener(_onRiverTriennialSearchChanged); _riverTriennialSearchController.addListener(_onRiverTriennialSearchChanged);
_airStationSearchController.addListener(_onAirStationSearchChanged); _airStationSearchController.addListener(_onAirStationSearchChanged);
_airClientSearchController.addListener(_onAirClientSearchChanged); _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 @override
@ -88,6 +97,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
_riverTriennialSearchController.dispose(); _riverTriennialSearchController.dispose();
_airStationSearchController.dispose(); _airStationSearchController.dispose();
_airClientSearchController.dispose(); _airClientSearchController.dispose();
_npeRiverLimitsSearchController.dispose();
_npeMarineLimitsSearchController.dispose();
_airLimitsSearchController.dispose();
_riverLimitsSearchController.dispose();
_marineLimitsSearchController.dispose();
super.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 { Future<void> _loadAllModuleSettings() async {
setState(() => _isLoadingSettings = true); setState(() => _isLoadingSettings = true);
for (var module in _configurableModules) { for (var module in _configurableModules) {
@ -136,26 +150,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
final apiConfigsWithPrefs = await _preferencesService.getAllApiConfigsWithModulePreferences(moduleKey); final apiConfigsWithPrefs = await _preferencesService.getAllApiConfigsWithModulePreferences(moduleKey);
final ftpConfigsWithPrefs = await _preferencesService.getAllFtpConfigsWithModulePreferences(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); 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) { 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) { if (pstwHqApi.isNotEmpty) {
pstwHqApi['is_enabled'] = true; pstwHqApi['is_enabled'] = true;
} }
} }
// Check if any FTP config is already enabled from preferences.
final bool isAnyFtpConfigEnabled = ftpConfigsWithPrefs.any((c) => c['is_enabled'] == true); 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) { if (prefs['is_ftp_enabled'] == true && !isAnyFtpConfigEnabled) {
switch (moduleKey) { switch (moduleKey) {
case 'marine_tarball': case 'marine_tarball':
@ -195,7 +200,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
break; break;
} }
} }
// END MODIFICATION
_moduleSettings[moduleKey] = _ModuleSettings( _moduleSettings[moduleKey] = _ModuleSettings(
isApiEnabled: prefs['is_api_enabled'], isApiEnabled: prefs['is_api_enabled'],
@ -235,7 +239,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
} }
// END CHANGE
Future<void> _manualDataSync() async { Future<void> _manualDataSync() async {
if (_isSyncingData) return; if (_isSyncingData) return;
@ -245,7 +248,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
try { try {
await auth.syncAllData(forceRefresh: true); await auth.syncAllData(forceRefresh: true);
// MODIFIED: After syncing, also reload module settings to reflect any new server configurations.
await _loadAllModuleSettings(); await _loadAllModuleSettings();
if (mounted) { if (mounted) {
@ -278,25 +280,69 @@ class _SettingsScreenState extends State<SettingsScreen> {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final lastSync = auth.lastSyncTimestamp; final lastSync = auth.lastSyncTimestamp;
// Get the synced data from the provider.
final appSettings = auth.appSettings; 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 apiConfigs = auth.apiConfigs;
final ftpConfigs = auth.ftpConfigs; final ftpConfigs = auth.ftpConfigs;
final airClients = auth.airClients; final airClients = auth.airClients;
final departments = auth.departments; 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? 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? riverDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'River', orElse: () => {})?['department_id'];
final int? marineDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'Marine', orElse: () => {})?['department_id']; final int? marineDepartmentId = departments?.firstWhere((d) => d['department_name'] == 'Marine', orElse: () => {})?['department_id'];
// Filter Parameter Limits by Department ID final filteredNpeRiverLimits = npeParameterLimits?.where((limit) {
final filteredAirLimits = parameterLimits?.where((limit) => limit['department_id'] == airDepartmentId).toList(); final isRiverNpe = riverDepartmentId != null && limit['department_id'] == riverDepartmentId;
final filteredRiverLimits = parameterLimits?.where((limit) => limit['department_id'] == riverDepartmentId).toList(); if (!isRiverNpe) return false;
final filteredMarineLimits = parameterLimits?.where((limit) => limit['department_id'] == marineDepartmentId).toList(); 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 filteredTarballStations = (auth.tarballStations?.where((station) {
final stationName = station['tbl_station_name']?.toLowerCase() ?? ''; final stationName = station['tbl_station_name']?.toLowerCase() ?? '';
final stationCode = station['tbl_station_code']?.toLowerCase() ?? ''; final stationCode = station['tbl_station_code']?.toLowerCase() ?? '';
@ -311,7 +357,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
return stationName.contains(query) || stationCode.contains(query); return stationName.contains(query) || stationCode.contains(query);
}).toList())?.cast<Map<String, dynamic>>(); }).toList())?.cast<Map<String, dynamic>>();
// Filter River Stations
final filteredRiverManualStations = (auth.riverManualStations?.where((station) { final filteredRiverManualStations = (auth.riverManualStations?.where((station) {
final riverName = station['sampling_river']?.toLowerCase() ?? ''; final riverName = station['sampling_river']?.toLowerCase() ?? '';
final stationCode = station['sampling_station_code']?.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); return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query);
}).toList())?.cast<Map<String, dynamic>>(); }).toList())?.cast<Map<String, dynamic>>();
// Filter Air Stations
final filteredAirStations = (auth.airManualStations?.where((station) { final filteredAirStations = (auth.airManualStations?.where((station) {
final stationName = station['station_name']?.toLowerCase() ?? ''; final stationName = station['station_name']?.toLowerCase() ?? '';
final stationCode = station['station_code']?.toLowerCase() ?? ''; final stationCode = station['station_code']?.toLowerCase() ?? '';
@ -336,7 +380,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
return stationName.contains(query) || stationCode.contains(query); return stationName.contains(query) || stationCode.contains(query);
}).toList())?.cast<Map<String, dynamic>>(); }).toList())?.cast<Map<String, dynamic>>();
// Filter Air Clients
final filteredAirClients = (auth.airClients?.where((client) { final filteredAirClients = (auth.airClients?.where((client) {
final clientName = client['client_name']?.toLowerCase() ?? ''; final clientName = client['client_name']?.toLowerCase() ?? '';
final clientId = client['client_id']?.toString().toLowerCase() ?? ''; final clientId = client['client_id']?.toString().toLowerCase() ?? '';
@ -347,7 +390,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Settings"), title: const Text("Settings"),
// START CHANGE: Add a save button to the AppBar
actions: [ actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
@ -360,7 +402,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
) )
], ],
// END CHANGE
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
@ -391,7 +432,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// START CHANGE: Insert the new Submission Preferences section
_buildSectionHeader(context, "Submission Preferences"), _buildSectionHeader(context, "Submission Preferences"),
_isLoadingSettings _isLoadingSettings
? const Center(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())) ? const Center(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator()))
@ -410,7 +450,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// END CHANGE
Text("Telegram Alert Settings", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), Text("Telegram Alert Settings", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -457,20 +496,75 @@ class _SettingsScreenState extends State<SettingsScreen> {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Column( child: Column(
children: [ 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( _buildExpansionTile(
title: 'Air Parameter Limits', title: 'Air Parameter Limits',
leadingIcon: Icons.poll, leadingIcon: Icons.air,
child: _buildInfoList(filteredAirLimits, (item) => _buildParameterLimitEntry(item)), child: Column(
children: [
_buildSearchBar(
controller: _airLimitsSearchController,
labelText: 'Search Air Limits',
hintText: 'Search by parameter name',
),
_buildInfoList(filteredAirLimits, (item) => _buildParameterLimitEntry(item, departments: departments)),
],
),
), ),
_buildExpansionTile( _buildExpansionTile(
title: 'River Parameter Limits', title: 'River Parameter Limits',
leadingIcon: Icons.poll, leadingIcon: Icons.water,
child: _buildInfoList(filteredRiverLimits, (item) => _buildParameterLimitEntry(item)), child: Column(
children: [
_buildSearchBar(
controller: _riverLimitsSearchController,
labelText: 'Search River Limits',
hintText: 'Search by parameter name',
),
_buildInfoList(filteredRiverLimits, (item) => _buildParameterLimitEntry(item)),
],
),
), ),
_buildExpansionTile( _buildExpansionTile(
title: 'Marine Parameter Limits', title: 'Marine Parameter Limits',
leadingIcon: Icons.poll, leadingIcon: Icons.waves,
child: _buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item)), child: Column(
children: [
_buildSearchBar(
controller: _marineLimitsSearchController,
labelText: 'Search Marine Limits',
hintText: 'Search by parameter or station',
),
_buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item, stations: allManualStations)),
],
),
), ),
_buildExpansionTile( _buildExpansionTile(
title: 'API Configurations', 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) { Widget _buildModulePreferenceTile(String title, String moduleKey, _ModuleSettings settings) {
return ExpansionTile( return ExpansionTile(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), 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) { Widget _buildSectionHeader(BuildContext context, String title) {
return Padding( return Padding(
@ -748,7 +840,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
leading: Icon(leadingIcon), leading: Icon(leadingIcon),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
children: [ 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 paramName = item['param_parameter_list']?.toString() ?? 'N/A';
final upperLimit = item['param_upper_limit']?.toString() ?? 'N/A'; final upperLimit = item['param_upper_limit']?.toString() ?? 'N/A';
final lowerLimit = item['param_lower_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') { if (paramName.toLowerCase() == 'ph') {
unit = 'pH units'; unit = 'pH units';
} else if (paramName.toLowerCase() == 'temp') { } else if (paramName.toLowerCase() == 'temp') {
@ -798,7 +917,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -819,16 +938,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [ children: [
const Icon(Icons.science_outlined, size: 16), const Icon(Icons.science_outlined, size: 16),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
paramName, child: Text(
style: const TextStyle(fontSize: 14), paramName,
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
), ),
], ],
), ),
Text( if (contextSubtitle.isNotEmpty)
unit, Padding(
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey), 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 labelText,
required String hintText, required String hintText,
}) { }) {
return TextField( return Padding(
controller: controller, padding: const EdgeInsets.only(bottom: 8.0, top: 8.0),
decoration: InputDecoration( child: TextField(
labelText: labelText, controller: controller,
hintText: hintText, decoration: InputDecoration(
prefixIcon: const Icon(Icons.search), labelText: labelText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), hintText: hintText,
suffixIcon: controller.text.isNotEmpty prefixIcon: const Icon(Icons.search),
? IconButton(icon: const Icon(Icons.clear), onPressed: () => controller.clear()) border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
: null, 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); await dbHelper.deleteAppSettings(id);
} }
}, },
'parameterLimits': { // --- START: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS ---
'endpoint': 'parameter-limits', 'npeParameterLimits': {
'endpoint': 'npe-parameter-limits',
'handler': (d, id) async { 'handler': (d, id) async {
await dbHelper.upsertParameterLimits(d); await dbHelper.upsertNpeParameterLimits(d);
await dbHelper.deleteParameterLimits(id); 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': { 'apiConfigs': {
'endpoint': 'api-configs', 'endpoint': 'api-configs',
'handler': (d, id) async { 'handler': (d, id) async {
@ -781,7 +797,9 @@ class RiverApiService {
class DatabaseHelper { class DatabaseHelper {
static Database? _database; static Database? _database;
static const String _dbName = 'app_data.db'; 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 _profileTable = 'user_profile';
static const String _usersTable = 'all_users'; static const String _usersTable = 'all_users';
@ -799,6 +817,11 @@ class DatabaseHelper {
static const String _statesTable = 'states'; static const String _statesTable = 'states';
static const String _appSettingsTable = 'app_settings'; static const String _appSettingsTable = 'app_settings';
static const String _parameterLimitsTable = 'manual_parameter_limits'; 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 _apiConfigsTable = 'api_configurations';
static const String _ftpConfigsTable = 'ftp_configurations'; static const String _ftpConfigsTable = 'ftp_configurations';
static const String _retryQueueTable = 'retry_queue'; 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 $_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 $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)');
await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_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 $_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('CREATE TABLE $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)');
await db.execute(''' 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"); 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. /// 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<void> deleteParameterLimits(List<dynamic> ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit'); 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> 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<void> deleteApiConfigs(List<dynamic> ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids);
Future<List<Map<String, dynamic>>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config'); Future<List<Map<String, dynamic>>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config');