upgrade settings screen to display proper and all data
This commit is contained in:
parent
0c37669725
commit
3d74862576
@ -12,16 +12,9 @@ class SettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// SettingsService is now a utility, it doesn't hold state.
|
||||
final SettingsService _settingsService = SettingsService();
|
||||
bool _isSyncingData = false;
|
||||
|
||||
// REMOVED: Redundant state variable for settings sync
|
||||
// bool _isSyncingSettings = false;
|
||||
|
||||
// REMOVED: Chat ID state variables are no longer needed,
|
||||
// we will read directly from the provider in the build method.
|
||||
|
||||
final TextEditingController _tarballSearchController = TextEditingController();
|
||||
String _tarballSearchQuery = '';
|
||||
final TextEditingController _manualSearchController = TextEditingController();
|
||||
@ -30,15 +23,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
String _riverManualSearchQuery = '';
|
||||
final TextEditingController _riverTriennialSearchController = TextEditingController();
|
||||
String _riverTriennialSearchQuery = '';
|
||||
final TextEditingController _airStationSearchController = TextEditingController();
|
||||
String _airStationSearchQuery = '';
|
||||
final TextEditingController _airClientSearchController = TextEditingController();
|
||||
String _airClientSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// REMOVED: _loadCurrentSettings() is no longer needed as we read from the provider.
|
||||
_tarballSearchController.addListener(_onTarballSearchChanged);
|
||||
_manualSearchController.addListener(_onManualSearchChanged);
|
||||
_riverManualSearchController.addListener(_onRiverManualSearchChanged);
|
||||
_riverTriennialSearchController.addListener(_onRiverTriennialSearchChanged);
|
||||
_airStationSearchController.addListener(_onAirStationSearchChanged);
|
||||
_airClientSearchController.addListener(_onAirClientSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -47,26 +45,45 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_manualSearchController.dispose();
|
||||
_riverManualSearchController.dispose();
|
||||
_riverTriennialSearchController.dispose();
|
||||
_airStationSearchController.dispose();
|
||||
_airClientSearchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// REMOVED: _loadCurrentSettings is obsolete. The build method will now
|
||||
// get the latest settings directly from AuthProvider.
|
||||
|
||||
void _onTarballSearchChanged() {
|
||||
setState(() { _tarballSearchQuery = _tarballSearchController.text; });
|
||||
setState(() {
|
||||
_tarballSearchQuery = _tarballSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _onManualSearchChanged() {
|
||||
setState(() { _manualSearchQuery = _manualSearchController.text; });
|
||||
setState(() {
|
||||
_manualSearchQuery = _manualSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _onRiverManualSearchChanged() {
|
||||
setState(() { _riverManualSearchQuery = _riverManualSearchController.text; });
|
||||
setState(() {
|
||||
_riverManualSearchQuery = _riverManualSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _onRiverTriennialSearchChanged() {
|
||||
setState(() { _riverTriennialSearchQuery = _riverTriennialSearchController.text; });
|
||||
setState(() {
|
||||
_riverTriennialSearchQuery = _riverTriennialSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _onAirStationSearchChanged() {
|
||||
setState(() {
|
||||
_airStationSearchQuery = _airStationSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _onAirClientSearchChanged() {
|
||||
setState(() {
|
||||
_airClientSearchQuery = _airClientSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _manualDataSync() async {
|
||||
@ -76,7 +93,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
try {
|
||||
// This now syncs ALL data, including settings.
|
||||
await auth.syncAllData(forceRefresh: true);
|
||||
|
||||
if (mounted) {
|
||||
@ -93,9 +109,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVED: _manualSettingsSync is obsolete because the main data sync
|
||||
// now handles settings as well.
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -112,35 +125,71 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final lastSync = auth.lastSyncTimestamp;
|
||||
|
||||
// Get the synced app settings from the provider.
|
||||
// Get the synced data from the provider.
|
||||
final appSettings = auth.appSettings;
|
||||
final parameterLimits = auth.parameterLimits;
|
||||
final apiConfigs = auth.apiConfigs;
|
||||
final ftpConfigs = auth.ftpConfigs;
|
||||
final airClients = auth.airClients;
|
||||
final departments = auth.departments;
|
||||
|
||||
final filteredTarballStations = auth.tarballStations?.where((station) {
|
||||
// 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();
|
||||
|
||||
// Filter Marine Stations
|
||||
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();
|
||||
final filteredManualStations = auth.manualStations?.where((station) {
|
||||
}).toList())?.cast<Map<String, dynamic>>();
|
||||
|
||||
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();
|
||||
final filteredRiverManualStations = auth.riverManualStations?.where((station) {
|
||||
}).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() ?? '';
|
||||
final basinName = station['sampling_basin']?.toLowerCase() ?? '';
|
||||
final query = _riverManualSearchQuery.toLowerCase();
|
||||
return riverName.contains(query) || stationCode.contains(query) || basinName.contains(query);
|
||||
}).toList();
|
||||
final filteredRiverTriennialStations = auth.riverTriennialStations?.where((station) {
|
||||
}).toList())?.cast<Map<String, dynamic>>();
|
||||
|
||||
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();
|
||||
}).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() ?? '';
|
||||
final query = _airStationSearchQuery.toLowerCase();
|
||||
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() ?? '';
|
||||
final query = _airClientSearchQuery.toLowerCase();
|
||||
return clientName.contains(query) || clientId.contains(query);
|
||||
}).toList())?.cast<Map<String, dynamic>>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@ -151,8 +200,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Synchronization", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader(context, "Synchronization"),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
@ -210,169 +258,203 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildChatIdEntry('Investigative', _settingsService.getAirInvestigativeChatId(appSettings)),
|
||||
],
|
||||
),
|
||||
// REMOVED: The separate sync button for settings is no longer needed.
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text("Marine Tarball Stations (${filteredTarballStations?.length ?? 0})", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader(context, "Configurations"),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
_buildExpansionTile(
|
||||
title: 'Air Parameter Limits',
|
||||
leadingIcon: Icons.poll,
|
||||
child: _buildInfoList(filteredAirLimits, (item) => _buildParameterLimitEntry(item)),
|
||||
),
|
||||
_buildExpansionTile(
|
||||
title: 'River Parameter Limits',
|
||||
leadingIcon: Icons.poll,
|
||||
child: _buildInfoList(filteredRiverLimits, (item) => _buildParameterLimitEntry(item)),
|
||||
),
|
||||
_buildExpansionTile(
|
||||
title: 'Marine Parameter Limits',
|
||||
leadingIcon: Icons.poll,
|
||||
child: _buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item)),
|
||||
),
|
||||
_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,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search Tarball Stations',
|
||||
hintText: 'Search by name or code',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
suffixIcon: _tarballSearchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear), onPressed: () => _tarballSearchController.clear()) : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStationList(
|
||||
filteredTarballStations,
|
||||
'No matching tarball stations found.',
|
||||
'No tarball stations available. Sync to download.',
|
||||
(station) => ListTile(
|
||||
title: Text(station['tbl_station_name'] ?? 'N/A'),
|
||||
subtitle: Text('Code: ${station['tbl_station_code'] ?? 'N/A'}'),
|
||||
dense: true,
|
||||
(station) => _buildStationTile(
|
||||
title: station['tbl_station_name'] ?? 'N/A',
|
||||
subtitle: 'Code: ${station['tbl_station_code'] ?? 'N/A'}',
|
||||
type: 'Tarball'
|
||||
),
|
||||
height: 250,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text("Marine Manual Stations (${filteredManualStations?.length ?? 0})", 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: [
|
||||
TextField(
|
||||
_buildSearchBar(
|
||||
controller: _manualSearchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search Manual Stations',
|
||||
hintText: 'Search by name or code',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
suffixIcon: _manualSearchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear), onPressed: () => _manualSearchController.clear()) : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStationList(
|
||||
filteredManualStations,
|
||||
'No matching manual stations found.',
|
||||
'No manual stations available. Sync to download.',
|
||||
(station) => ListTile(
|
||||
title: Text(station['man_station_name'] ?? 'N/A'),
|
||||
subtitle: Text('Code: ${station['man_station_code'] ?? 'N/A'}'),
|
||||
dense: true,
|
||||
(station) => _buildStationTile(
|
||||
title: station['man_station_name'] ?? 'N/A',
|
||||
subtitle: 'Code: ${station['man_station_code'] ?? 'N/A'}',
|
||||
type: 'Manual'
|
||||
),
|
||||
height: 250,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text("River Manual Stations (${filteredRiverManualStations?.length ?? 0})", 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),
|
||||
_buildExpansionTile(
|
||||
title: 'River Stations',
|
||||
leadingIcon: Icons.water,
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
_buildSearchBar(
|
||||
controller: _riverManualSearchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search River Manual Stations',
|
||||
hintText: 'Search by river, basin, or code',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
suffixIcon: _riverManualSearchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear), onPressed: () => _riverManualSearchController.clear()) : null,
|
||||
),
|
||||
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) => ListTile(
|
||||
title: Text(station['sampling_river'] ?? 'N/A'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Code: ${station['sampling_station_code'] ?? 'N/A'}'),
|
||||
Text('Basin: ${station['sampling_basin'] ?? 'N/A'}'),
|
||||
Text('State: ${station['state_name'] ?? 'N/A'}'),
|
||||
],
|
||||
(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'
|
||||
),
|
||||
dense: true,
|
||||
height: 250,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text("River Triennial Stations (${filteredRiverTriennialStations?.length ?? 0})", 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: [
|
||||
TextField(
|
||||
_buildSearchBar(
|
||||
controller: _riverTriennialSearchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search River Triennial Stations',
|
||||
hintText: 'Search by river, basin, or code',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
suffixIcon: _riverTriennialSearchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear), onPressed: () => _riverTriennialSearchController.clear()) : null,
|
||||
),
|
||||
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) => ListTile(
|
||||
title: Text(station['triennial_river'] ?? 'N/A'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
(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: [
|
||||
Text('Code: ${station['triennial_station_code'] ?? 'N/A'}'),
|
||||
Text('Basin: ${station['triennial_basin'] ?? 'N/A'}'),
|
||||
Text('State: ${station['state_name'] ?? 'N/A'}'),
|
||||
],
|
||||
_buildSearchBar(
|
||||
controller: _airStationSearchController,
|
||||
labelText: 'Search Air Stations',
|
||||
hintText: 'Search by name or code',
|
||||
),
|
||||
dense: true,
|
||||
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),
|
||||
|
||||
Text("General Settings", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader(context, "Other Information"),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
@ -398,32 +480,39 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStationList(
|
||||
List<Map<String, dynamic>>? stations,
|
||||
String noMatchText,
|
||||
String noDataText,
|
||||
Widget Function(Map<String, dynamic>) itemBuilder,
|
||||
) {
|
||||
if (stations == null || stations.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_isSyncingData ? 'Loading...' : (stations == null ? noDataText : noMatchText),
|
||||
),
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: ListView.builder(
|
||||
itemCount: stations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final station = stations[index];
|
||||
return itemBuilder(station);
|
||||
},
|
||||
),
|
||||
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: [
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoList(List<Map<String, dynamic>>? items, Widget Function(Map<String, dynamic>) 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(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -436,4 +525,194 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
dense: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeyValueEntry(Map<String, dynamic> 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<String, dynamic> item) {
|
||||
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 = '';
|
||||
|
||||
// Hardcoded units as they are not available in the provided data
|
||||
if (paramName.toLowerCase() == 'ph') {
|
||||
unit = 'pH units';
|
||||
} else if (paramName.toLowerCase() == 'temp') {
|
||||
unit = '°C';
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.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),
|
||||
Text(
|
||||
paramName,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
unit,
|
||||
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
upperLimit,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFtpConfigEntry(Map<String, dynamic> 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 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<Map<String, dynamic>>? stations,
|
||||
String noMatchText,
|
||||
String noDataText,
|
||||
Widget Function(Map<String, dynamic>) 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<Map<String, dynamic>>? clients,
|
||||
String noMatchText,
|
||||
String noDataText,
|
||||
Widget Function(Map<String, dynamic>) 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user