From 3d748625762dbd7a8edacc2b1382e73f94f7f666 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Tue, 26 Aug 2025 09:16:03 +0800 Subject: [PATCH] upgrade settings screen to display proper and all data --- lib/screens/settings.dart | 659 +++++++++++++++++++++++++++----------- 1 file changed, 469 insertions(+), 190 deletions(-) diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 1d2ea1d..6dc2d30 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -12,16 +12,9 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - // 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 { 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 { _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 _manualDataSync() async { @@ -76,7 +93,6 @@ class _SettingsScreenState extends State { final auth = Provider.of(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 { } } - // 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 { final auth = Provider.of(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>(); + + 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>(); + + // 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>(); + + 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>(); + + // 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>(); + + // 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>(); return Scaffold( appBar: AppBar( @@ -151,8 +200,7 @@ class _SettingsScreenState extends State { 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 { _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( - 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, - ), - ), - ], - ), + child: Column( + children: [ + _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), - Text("Marine Manual Stations (${filteredManualStations?.length ?? 0})", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 16), + _buildSectionHeader(context, "Air Clients"), Card( margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - 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, - ), - ), - ], - ), - ), - ), - 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), - child: Column( - children: [ - TextField( - 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, - ), - ), - 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'}'), - ], + 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', ), - dense: true, - ), + 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), - Text("River Triennial Stations (${filteredRiverTriennialStations?.length ?? 0})", style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 16), + _buildSectionHeader(context, "Stations Info"), Card( margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - 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, - ), - ), - 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, - children: [ - Text('Code: ${station['triennial_station_code'] ?? 'N/A'}'), - Text('Basin: ${station['triennial_basin'] ?? 'N/A'}'), - Text('State: ${station['state_name'] ?? 'N/A'}'), - ], + 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', ), - dense: true, - ), + 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), - 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 { ); } - Widget _buildStationList( - List>? stations, - String noMatchText, - String noDataText, - Widget Function(Map) itemBuilder, - ) { - if (stations == null || stations.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - _isSyncingData ? 'Loading...' : (stations == null ? noDataText : noMatchText), - ), - ), + 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: [ + 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 SizedBox( - height: 250, - child: ListView.builder( - itemCount: stations.length, - itemBuilder: (context, index) { - final station = stations[index]; - return itemBuilder(station); - }, - ), + return Column( + children: items.map((item) => itemBuilder(item)).toList(), ); } @@ -436,4 +525,194 @@ class _SettingsScreenState extends State { dense: true, ); } -} \ No newline at end of file + + 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) { + 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 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>? 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, + ); + } +}