// lib/screens/settings.dart import 'package:flutter/material.dart'; 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'; import 'package:environment_monitoring_app/services/user_preferences_service.dart'; import 'package:environment_monitoring_app/services/api_service.dart'; // Import for DatabaseHelper access class _ModuleSettings { bool isApiEnabled; bool isFtpEnabled; List> apiConfigs; List> ftpConfigs; _ModuleSettings({ this.isApiEnabled = true, this.isFtpEnabled = true, required this.apiConfigs, required this.ftpConfigs, }); } class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { final SettingsService _settingsService = SettingsService(); bool _isSyncingData = false; final UserPreferencesService _preferencesService = UserPreferencesService(); final DatabaseHelper _dbHelper = DatabaseHelper(); // Add instance for direct access bool _isLoadingSettings = true; bool _isSaving = false; final Map _moduleSettings = {}; final List> _configurableModules = [ {'key': 'marine_tarball', 'name': 'Marine Tarball'}, {'key': 'marine_in_situ', 'name': 'Marine In-Situ'}, {'key': 'river_in_situ', 'name': 'River In-Situ'}, {'key': 'air_installation', 'name': 'Air Installation'}, {'key': 'air_collection', 'name': 'Air Collection'}, ]; final TextEditingController _tarballSearchController = TextEditingController(); String _tarballSearchQuery = ''; final TextEditingController _manualSearchController = TextEditingController(); String _manualSearchQuery = ''; final TextEditingController _riverManualSearchController = TextEditingController(); String _riverManualSearchQuery = ''; final TextEditingController _riverTriennialSearchController = TextEditingController(); String _riverTriennialSearchQuery = ''; final TextEditingController _airStationSearchController = TextEditingController(); String _airStationSearchQuery = ''; 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(); _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 void dispose() { _tarballSearchController.dispose(); _manualSearchController.dispose(); _riverManualSearchController.dispose(); _riverTriennialSearchController.dispose(); _airStationSearchController.dispose(); _airClientSearchController.dispose(); _npeRiverLimitsSearchController.dispose(); _npeMarineLimitsSearchController.dispose(); _airLimitsSearchController.dispose(); _riverLimitsSearchController.dispose(); _marineLimitsSearchController.dispose(); super.dispose(); } void _onTarballSearchChanged() { setState(() { _tarballSearchQuery = _tarballSearchController.text; }); } void _onManualSearchChanged() { setState(() { _manualSearchQuery = _manualSearchController.text; }); } void _onRiverManualSearchChanged() { setState(() { _riverManualSearchQuery = _riverManualSearchController.text; }); } void _onRiverTriennialSearchChanged() { setState(() { _riverTriennialSearchQuery = _riverTriennialSearchController.text; }); } void _onAirStationSearchChanged() { setState(() { _airStationSearchQuery = _airStationSearchController.text; }); } void _onAirClientSearchChanged() { setState(() { _airClientSearchQuery = _airClientSearchController.text; }); } Future _loadAllModuleSettings() async { setState(() => _isLoadingSettings = true); for (var module in _configurableModules) { final moduleKey = module['key']!; // This method now simply loads whatever preferences are saved in the database. // The auto-saving of defaults is handled by AuthProvider upon login/app start. final prefs = await _preferencesService.getModulePreference(moduleKey); final apiConfigsWithPrefs = await _preferencesService.getAllApiConfigsWithModulePreferences(moduleKey); final ftpConfigsWithPrefs = await _preferencesService.getAllFtpConfigsWithModulePreferences(moduleKey); _moduleSettings[moduleKey] = _ModuleSettings( isApiEnabled: prefs?['is_api_enabled'] ?? true, // Fallback to true if null isFtpEnabled: prefs?['is_ftp_enabled'] ?? true, // Fallback to true if null apiConfigs: apiConfigsWithPrefs, ftpConfigs: ftpConfigsWithPrefs, ); } if (mounted) { setState(() => _isLoadingSettings = false); } } Future _saveAllModuleSettings() async { setState(() => _isSaving = true); try { for (var module in _configurableModules) { final moduleKey = module['key']!; final settings = _moduleSettings[moduleKey]!; await _preferencesService.saveModulePreference( moduleName: moduleKey, isApiEnabled: settings.isApiEnabled, isFtpEnabled: settings.isFtpEnabled, ); await _preferencesService.saveApiLinksForModule(moduleKey, settings.apiConfigs); await _preferencesService.saveFtpLinksForModule(moduleKey, settings.ftpConfigs); } _showSnackBar('Submission preferences saved successfully.', isError: false); } catch (e) { _showSnackBar('Failed to save settings: $e', isError: true); } finally { if (mounted) { setState(() => _isSaving = false); } } } Future _manualDataSync() async { if (_isSyncingData) return; setState(() => _isSyncingData = true); final auth = Provider.of(context, listen: false); // 1. Pre-sync checks if (!await auth.isConnected()) { _showSnackBar('Sync failed: No internet connection.', isError: true); setState(() => _isSyncingData = false); return; } if (auth.isSessionExpired) { _showSessionExpiredDialog(); setState(() => _isSyncingData = false); return; } // 2. Attempt the sync operation try { // AuthProvider's syncAllData will internally handle token validation and attempt a silent re-login if necessary. await auth.syncAllData(forceRefresh: true); await _loadAllModuleSettings(); // Reload settings on successful sync if (mounted) { _showSnackBar('Data synced successfully.', isError: false); } } catch (e) { // 3. Handle failures if (mounted) { // If the sync failed, check if the session is now marked as expired. // This indicates that the silent re-login attempt during the sync also failed. if (auth.isSessionExpired) { _showSessionExpiredDialog(); } else { // A different error occurred (e.g., server down, network issue during sync) _showSnackBar('Data sync failed: ${e.toString()}', isError: true); } } } finally { if (mounted) { setState(() => _isSyncingData = false); } } } void _showSessionExpiredDialog() { if (!mounted) return; showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text("Session Expired"), content: const Text("Your session has expired and automatic re-login failed. Please log out and log in again to sync data."), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text("OK"), ), ElevatedButton( onPressed: () { final auth = Provider.of(context, listen: false); Navigator.pop(dialogContext); auth.logout(); // Navigate to the root, which will then redirect to the login screen. Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false); }, child: const Text("Logout"), ), ], ), ); } void _showSnackBar(String message, {bool isError = false}) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Theme.of(context).colorScheme.error : Colors.green, ), ); } } @override Widget build(BuildContext context) { final auth = Provider.of(context); final lastSync = auth.lastSyncTimestamp; final appSettings = auth.appSettings; 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; 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']; 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(); final filteredTarballStations = (auth.tarballStations?.where((station) { final stationName = station['tbl_station_name']?.toLowerCase() ?? ''; final stationCode = station['tbl_station_code']?.toLowerCase() ?? ''; final query = _tarballSearchQuery.toLowerCase(); return stationName.contains(query) || stationCode.contains(query); }).toList())?.cast>(); final filteredManualStations = (auth.manualStations?.where((station) { final stationName = station['man_station_name']?.toLowerCase() ?? ''; final stationCode = station['man_station_code']?.toLowerCase() ?? ''; final query = _manualSearchQuery.toLowerCase(); return stationName.contains(query) || stationCode.contains(query); }).toList())?.cast>(); final filteredRiverManualStations = (auth.riverManualStations?.where((station) { final riverName = station['sampling_river']?.toLowerCase() ?? ''; final stationCode = station['sampling_station_code']?.toLowerCase() ?? ''; final basinName = station['sampling_basin']?.toLowerCase() ?? ''; final query = _riverManualSearchQuery.toLowerCase(); return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query); }).toList())?.cast>(); final filteredRiverTriennialStations = (auth.riverTriennialStations?.where((station) { final riverName = station['triennial_river']?.toLowerCase() ?? ''; final stationCode = station['triennial_station_code']?.toLowerCase() ?? ''; final basinName = station['triennial_basin']?.toLowerCase() ?? ''; final query = _riverTriennialSearchQuery.toLowerCase(); return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query); }).toList())?.cast>(); final filteredAirStations = (auth.airManualStations?.where((station) { final stationName = station['station_name']?.toLowerCase() ?? ''; final stationCode = station['station_code']?.toLowerCase() ?? ''; final query = _airStationSearchQuery.toLowerCase(); return stationName.contains(query) || stationCode.contains(query); }).toList())?.cast>(); final filteredAirClients = (auth.airClients?.where((client) { final clientName = client['client_name']?.toLowerCase() ?? ''; final clientId = client['client_id']?.toString().toLowerCase() ?? ''; final query = _airClientSearchQuery.toLowerCase(); return clientName.contains(query) || clientId.contains(query); }).toList())?.cast>(); return Scaffold( appBar: AppBar( title: const Text("Settings"), actions: [ Padding( padding: const EdgeInsets.only(right: 8.0), child: _isSaving ? const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.white))) : IconButton( icon: const Icon(Icons.save), onPressed: _isLoadingSettings ? null : _saveAllModuleSettings, tooltip: 'Save Submission Preferences', ), ) ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader(context, "Synchronization"), Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text("Last Data Sync:", style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text(lastSync != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(lastSync.toLocal()) : 'Never', style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 16), ElevatedButton.icon( onPressed: _isSyncingData ? null : _manualDataSync, icon: _isSyncingData ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.cloud_sync), label: Text(_isSyncingData ? 'Syncing Data...' : 'Sync App Data'), style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 50)), ), ], ), ), ), const SizedBox(height: 32), _buildSectionHeader(context, "Submission Preferences"), _isLoadingSettings ? const Center(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())) : Card( margin: EdgeInsets.zero, child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _configurableModules.length, itemBuilder: (context, index) { final module = _configurableModules[index]; final settings = _moduleSettings[module['key']]; if (settings == null) return const SizedBox.shrink(); return _buildModulePreferenceTile(module['name']!, module['key']!, settings); }, ), ), const SizedBox(height: 32), Text("Telegram Alert Settings", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 16), Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ ExpansionTile( title: const Text('Marine Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ _buildChatIdEntry('In-Situ', _settingsService.getInSituChatId(appSettings)), _buildChatIdEntry('Tarball', _settingsService.getTarballChatId(appSettings)), _buildChatIdEntry('Investigative', _settingsService.getMarineInvestigativeChatId(appSettings)), ], ), ExpansionTile( title: const Text('River Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ _buildChatIdEntry('In-Situ', _settingsService.getRiverInSituChatId(appSettings)), _buildChatIdEntry('Triennial', _settingsService.getRiverTriennialChatId(appSettings)), _buildChatIdEntry('Investigative', _settingsService.getRiverInvestigativeChatId(appSettings)), ], ), ExpansionTile( title: const Text('Air Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ _buildChatIdEntry('Manual', _settingsService.getAirManualChatId(appSettings)), _buildChatIdEntry('Investigative', _settingsService.getAirInvestigativeChatId(appSettings)), ], ), ], ), ), ), const SizedBox(height: 32), _buildSectionHeader(context, "Configurations"), Card( 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.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.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.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', leadingIcon: Icons.cloud, child: _buildInfoList(apiConfigs, (item) => _buildKeyValueEntry(item, 'config_name', 'api_url')), ), _buildExpansionTile( title: 'FTP Configurations', leadingIcon: Icons.folder, child: _buildInfoList(ftpConfigs, (item) => _buildFtpConfigEntry(item)), ), ], ), ), const SizedBox(height: 32), _buildSectionHeader(context, "Air Clients"), Card( margin: EdgeInsets.zero, child: Column( children: [ _buildExpansionTile( title: 'Air Clients', leadingIcon: Icons.air, child: Column( children: [ _buildSearchBar( controller: _airClientSearchController, labelText: 'Search Air Clients', hintText: 'Search by name or ID', ), const SizedBox(height: 16), _buildClientList( filteredAirClients, 'No matching air clients found.', 'No air clients available. Sync to download.', (client) => _buildClientTile( title: client['client_name'] ?? 'N/A', subtitle: 'ID: ${client['client_id'] ?? 'N/A'}', ), height: 250, ), ], ), ), ], ), ), const SizedBox(height: 32), _buildSectionHeader(context, "Stations Info"), Card( margin: EdgeInsets.zero, child: Column( children: [ _buildExpansionTile( title: 'Marine Stations', leadingIcon: Icons.waves, child: Column( children: [ _buildSearchBar( controller: _tarballSearchController, labelText: 'Search Tarball Stations', hintText: 'Search by name or code', ), const SizedBox(height: 16), _buildStationList( filteredTarballStations, 'No matching tarball stations found.', 'No tarball stations available. Sync to download.', (station) => _buildStationTile( title: station['tbl_station_name'] ?? 'N/A', subtitle: 'Code: ${station['tbl_station_code'] ?? 'N/A'}', type: 'Tarball' ), height: 250, ), const SizedBox(height: 16), _buildSearchBar( controller: _manualSearchController, labelText: 'Search Manual Stations', hintText: 'Search by name or code', ), const SizedBox(height: 16), _buildStationList( filteredManualStations, 'No matching manual stations found.', 'No manual stations available. Sync to download.', (station) => _buildStationTile( title: station['man_station_name'] ?? 'N/A', subtitle: 'Code: ${station['man_station_code'] ?? 'N/A'}', type: 'Manual' ), height: 250, ), ], ), ), _buildExpansionTile( title: 'River Stations', leadingIcon: Icons.water, child: Column( children: [ _buildSearchBar( controller: _riverManualSearchController, labelText: 'Search River Manual Stations', hintText: 'Search by name, code, or basin', ), const SizedBox(height: 16), _buildStationList( filteredRiverManualStations, 'No matching river manual stations found.', 'No river manual stations available. Sync to download.', (station) => _buildStationTile( title: station['sampling_river'] ?? 'N/A', subtitle: 'Code: ${station['sampling_station_code'] ?? 'N/A'}, Basin: ${station['sampling_basin'] ?? 'N/A'}', type: 'River Manual' ), height: 250, ), const SizedBox(height: 16), _buildSearchBar( controller: _riverTriennialSearchController, labelText: 'Search River Triennial Stations', hintText: 'Search by name, code, or basin', ), const SizedBox(height: 16), _buildStationList( filteredRiverTriennialStations, 'No matching river triennial stations found.', 'No river triennial stations available. Sync to download.', (station) => _buildStationTile( title: station['triennial_river'] ?? 'N/A', subtitle: 'Code: ${station['triennial_station_code'] ?? 'N/A'}, Basin: ${station['triennial_basin'] ?? 'N/A'}', type: 'River Triennial' ), height: 250, ), ], ), ), _buildExpansionTile( title: 'Air Stations', leadingIcon: Icons.air, child: Column( children: [ _buildSearchBar( controller: _airStationSearchController, labelText: 'Search Air Stations', hintText: 'Search by name or code', ), const SizedBox(height: 16), _buildStationList( filteredAirStations, 'No matching air stations found.', 'No air stations available. Sync to download.', (station) => _buildStationTile( title: station['station_name'] ?? 'N/A', subtitle: 'Code: ${station['station_code'] ?? 'N/A'}', type: 'Air' ), height: 250, ), ], ), ), ], ), ), const SizedBox(height: 32), _buildSectionHeader(context, "Other Information"), Card( margin: EdgeInsets.zero, child: Column( children: [ ListTile( leading: const Icon(Icons.info_outline), title: const Text('App Version'), subtitle: const Text('MMS V4 1.2.11'), dense: true, ), ListTile( leading: const Icon(Icons.privacy_tip_outlined), title: const Text('Privacy Policy'), onTap: () {}, dense: true, ), ], ), ), ], ), ), ); } Widget _buildModulePreferenceTile(String title, String moduleKey, _ModuleSettings settings) { return ExpansionTile( title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), children: [ SwitchListTile( title: const Text('Enable API Submission'), value: settings.isApiEnabled, onChanged: (value) => setState(() => settings.isApiEnabled = value), ), if (settings.isApiEnabled) _buildDestinationList('API Destinations', settings.apiConfigs, 'api_config_id'), const Divider(), SwitchListTile( title: const Text('Enable FTP Submission'), value: settings.isFtpEnabled, onChanged: (value) => setState(() => settings.isFtpEnabled = value), ), if (settings.isFtpEnabled) _buildDestinationList('FTP Destinations', settings.ftpConfigs, 'ftp_config_id'), ], ); } Widget _buildDestinationList(String title, List> configs, String idKey) { if (configs.isEmpty) { return const ListTile( dense: true, title: Center(child: Text('No destinations configured. Sync to fetch.')), ); } return Padding( padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), child: Text(title, style: Theme.of(context).textTheme.titleMedium), ), ...configs.map((config) { return CheckboxListTile( title: Text(config['config_name'] ?? 'Unnamed'), subtitle: Text(config['api_url'] ?? config['ftp_host'] ?? 'No URL/Host'), value: config['is_enabled'] ?? false, onChanged: (bool? value) { setState(() { config['is_enabled'] = value ?? false; }); }, dense: true, ); }).toList(), ], ), ); } Widget _buildSectionHeader(BuildContext context, String title) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Text( title, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), ); } Widget _buildExpansionTile({ required String title, required IconData leadingIcon, required Widget child, }) { return ExpansionTile( leading: Icon(leadingIcon), title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: child, ), ], ); } Widget _buildInfoList(List>? items, Widget Function(Map) itemBuilder) { if (items == null || items.isEmpty) { return const ListTile( title: Text('No data available. Sync to download.'), dense: true, ); } return Column( children: items.map((item) => itemBuilder(item)).toList(), ); } Widget _buildChatIdEntry(String label, String value) { return ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.telegram, size: 20), title: Text('$label Chat ID'), subtitle: Text(value.isNotEmpty ? value : 'Not Set'), dense: true, ); } Widget _buildKeyValueEntry(Map item, String key1, String key2) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 4.0), title: Text(item[key1]?.toString() ?? 'N/A', style: const TextStyle(fontSize: 14)), subtitle: Text(item[key2]?.toString() ?? 'N/A', style: const TextStyle(fontSize: 12)), dense: true, ); } Widget _buildParameterLimitEntry( Map item, { List>? departments, List>? stations, }) { final paramName = item['param_parameter_list']?.toString() ?? 'N/A'; final upperLimit = item['param_upper_limit']?.toString() ?? 'N/A'; final lowerLimit = item['param_lower_limit']?.toString() ?? 'N/A'; String contextSubtitle = ''; 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') { unit = '°C'; } return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 2, child: Text( lowerLimit, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), textAlign: TextAlign.start, ), ), Expanded( flex: 5, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.science_outlined, size: 16), const SizedBox(width: 8), Flexible( child: Text( paramName, style: const TextStyle(fontSize: 14), textAlign: TextAlign.center, ), ), ], ), 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, ), ), ], ), ), Expanded( flex: 2, child: Text( upperLimit, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), textAlign: TextAlign.end, ), ), ], ), ); } Widget _buildFtpConfigEntry(Map item) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 4.0), title: Text(item['config_name']?.toString() ?? 'N/A', style: const TextStyle(fontSize: 14)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Host: ${item['ftp_host']?.toString() ?? 'N/A'}', style: const TextStyle(fontSize: 12)), Text('User: ${item['ftp_user']?.toString() ?? 'N/A'}', style: const TextStyle(fontSize: 12)), Text('Pass: ${item['ftp_pass']?.toString() ?? 'N/A'}', style: const TextStyle(fontSize: 12)), Text('Port: ${item['ftp_port']?.toString() ?? 'N/A'}', style: const TextStyle(fontSize: 12)), ], ), leading: const Icon(Icons.folder_shared_outlined), dense: true, ); } Widget _buildSearchBar({ required TextEditingController controller, required String labelText, required String hintText, }) { 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), ), ); } Widget _buildStationList( List>? stations, String noMatchText, String noDataText, Widget Function(Map) itemBuilder, {double height = 250}) { if (stations == null || stations.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( _isSyncingData ? 'Loading...' : (stations == null ? noDataText : noMatchText), textAlign: TextAlign.center, ), ), ); } return SizedBox( height: height, child: ListView.builder( itemCount: stations.length, itemBuilder: (context, index) { final station = stations[index]; return itemBuilder(station); }, ), ); } Widget _buildStationTile({required String title, required String subtitle, required String type}) { return ListTile( title: Text(title, style: const TextStyle(fontSize: 14)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(subtitle, style: const TextStyle(fontSize: 12)), Text('Type: $type', style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic)), ], ), dense: true, ); } Widget _buildClientList( List>? clients, String noMatchText, String noDataText, Widget Function(Map) itemBuilder, {double height = 250}) { if (clients == null || clients.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( _isSyncingData ? 'Loading...' : (clients == null ? noDataText : noMatchText), textAlign: TextAlign.center, ), ), ); } return SizedBox( height: height, child: ListView.builder( itemCount: clients.length, itemBuilder: (context, index) { final client = clients[index]; return itemBuilder(client); }, ), ); } Widget _buildClientTile({required String title, required String subtitle}) { return ListTile( title: Text(title, style: const TextStyle(fontSize: 14)), subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)), dense: true, ); } }