separate settings screen into sub screen
This commit is contained in:
parent
da1de869ed
commit
d77a0ed8e9
@ -36,6 +36,15 @@ import 'package:environment_monitoring_app/home_page.dart';
|
||||
import 'package:environment_monitoring_app/screens/profile.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings.dart';
|
||||
|
||||
// --- START: New Settings Screen Imports ---
|
||||
import 'package:environment_monitoring_app/screens/settings/submission_preferences_settings.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings/telegram_alert_settings.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings/api_ftp_configurations_settings.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings/parameter_limits_settings.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings/air_clients_settings.dart';
|
||||
import 'package:environment_monitoring_app/screens/settings/station_info_settings.dart';
|
||||
// --- END: New Settings Screen Imports ---
|
||||
|
||||
// Department Home Pages
|
||||
import 'package:environment_monitoring_app/screens/air/air_home_page.dart';
|
||||
import 'package:environment_monitoring_app/screens/river/river_home_page.dart';
|
||||
@ -227,6 +236,7 @@ class _RootAppState extends State<RootApp> {
|
||||
}
|
||||
// --- END: MODIFICATION ---
|
||||
|
||||
|
||||
/// Initial check when app loads to see if we need to transition from offline to online.
|
||||
void _performInitialSessionCheck() async {
|
||||
// Wait a moment for providers to be fully available.
|
||||
@ -350,6 +360,21 @@ class _RootAppState extends State<RootApp> {
|
||||
'/profile': (context) => const ProfileScreen(),
|
||||
'/settings': (context) => const SettingsScreen(),
|
||||
|
||||
// --- START: New Settings Routes (const removed) ---
|
||||
'/settings/submission-prefs': (context) =>
|
||||
SubmissionPreferencesSettingsScreen(),
|
||||
'/settings/telegram-alerts': (context) =>
|
||||
TelegramAlertSettingsScreen(),
|
||||
'/settings/api-ftp-configs': (context) =>
|
||||
ApiFtpConfigurationsSettingsScreen(),
|
||||
'/settings/parameter-limits': (context) =>
|
||||
ParameterLimitsSettingsScreen(),
|
||||
'/settings/air-clients': (context) =>
|
||||
AirClientsSettingsScreen(),
|
||||
'/settings/station-info': (context) =>
|
||||
StationInfoSettingsScreen(),
|
||||
// --- END: New Settings Routes ---
|
||||
|
||||
// Department Home Pages
|
||||
'/air/home': (context) => const AirHomePage(),
|
||||
'/river/home': (context) => const RiverHomePage(),
|
||||
@ -462,7 +487,7 @@ class _SessionAwareWrapperState extends State<SessionAwareWrapper> {
|
||||
// --- MODIFICATION END ---
|
||||
|
||||
// Call initial check here if needed, or rely on RootApp's check.
|
||||
// _checkAndShowDialogIfNeeded(_authProvider.isSessionExpired);
|
||||
_checkAndShowDialogIfNeeded(_authProvider.isSessionExpired);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
189
lib/screens/settings/air_clients_settings.dart
Normal file
189
lib/screens/settings/air_clients_settings.dart
Normal file
@ -0,0 +1,189 @@
|
||||
// lib/screens/settings/air_clients_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||
|
||||
class AirClientsSettingsScreen extends StatefulWidget {
|
||||
const AirClientsSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AirClientsSettingsScreen> createState() =>
|
||||
_AirClientsSettingsScreenState();
|
||||
}
|
||||
|
||||
class _AirClientsSettingsScreenState extends State<AirClientsSettingsScreen> {
|
||||
final TextEditingController _airClientSearchController =
|
||||
TextEditingController();
|
||||
String _airClientSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_airClientSearchController.addListener(_onAirClientSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_airClientSearchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onAirClientSearchChanged() {
|
||||
setState(() {
|
||||
_airClientSearchQuery = _airClientSearchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 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)),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 _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(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Air Clients"),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
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 ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_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: 400, // Increased height
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
138
lib/screens/settings/api_ftp_configurations_settings.dart
Normal file
138
lib/screens/settings/api_ftp_configurations_settings.dart
Normal file
@ -0,0 +1,138 @@
|
||||
// lib/screens/settings/api_ftp_configurations_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||
|
||||
class ApiFtpConfigurationsSettingsScreen extends StatelessWidget {
|
||||
const ApiFtpConfigurationsSettingsScreen({super.key});
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 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,
|
||||
bool initiallyExpanded = false,
|
||||
}) {
|
||||
return ExpansionTile(
|
||||
leading: Icon(leadingIcon),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: 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(),
|
||||
);
|
||||
}
|
||||
|
||||
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 _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('Module: ${item['ftp_module']?.toString() ?? 'N/A'}',
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("API/FTP Configurations"),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
final apiConfigs = auth.apiConfigs;
|
||||
final ftpConfigs = auth.ftpConfigs;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_buildSectionHeader(context, "Server Configurations"),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildExpansionTile(
|
||||
title: 'API Configurations',
|
||||
leadingIcon: Icons.cloud,
|
||||
initiallyExpanded: true,
|
||||
child: _buildInfoList(
|
||||
apiConfigs,
|
||||
(item) =>
|
||||
_buildKeyValueEntry(item, 'config_name', 'api_url')),
|
||||
),
|
||||
_buildExpansionTile(
|
||||
title: 'FTP Configurations',
|
||||
leadingIcon: Icons.folder,
|
||||
initiallyExpanded: true,
|
||||
child: _buildInfoList(
|
||||
ftpConfigs, (item) => _buildFtpConfigEntry(item)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
485
lib/screens/settings/parameter_limits_settings.dart
Normal file
485
lib/screens/settings/parameter_limits_settings.dart
Normal file
@ -0,0 +1,485 @@
|
||||
// lib/screens/settings/parameter_limits_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||
|
||||
class ParameterLimitsSettingsScreen extends StatefulWidget {
|
||||
const ParameterLimitsSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ParameterLimitsSettingsScreen> createState() =>
|
||||
_ParameterLimitsSettingsScreenState();
|
||||
}
|
||||
|
||||
class _ParameterLimitsSettingsScreenState
|
||||
extends State<ParameterLimitsSettingsScreen> {
|
||||
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 = '';
|
||||
|
||||
int _marineLimitsCurrentPage = 1;
|
||||
final int _marineLimitsItemsPerPage = 15;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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;
|
||||
_marineLimitsCurrentPage = 1; // Reset to page 1
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_npeRiverLimitsSearchController.dispose();
|
||||
_npeMarineLimitsSearchController.dispose();
|
||||
_airLimitsSearchController.dispose();
|
||||
_riverLimitsSearchController.dispose();
|
||||
_marineLimitsSearchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 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<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(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaginatedMarineLimitsList(
|
||||
List<Map<String, dynamic>>? filteredList,
|
||||
List<Map<String, dynamic>>? allStations,
|
||||
) {
|
||||
if (filteredList == null || filteredList.isEmpty) {
|
||||
final originalList = context.read<AuthProvider>().marineParameterLimits;
|
||||
String message = 'No matching parameter limits found.';
|
||||
if (originalList == null || originalList.isEmpty) {
|
||||
message = 'No data available. Sync to download.';
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(message, textAlign: TextAlign.center),
|
||||
dense: true,
|
||||
);
|
||||
}
|
||||
|
||||
final totalItems = filteredList.length;
|
||||
final totalPages = (totalItems / _marineLimitsItemsPerPage).ceil();
|
||||
if (totalPages > 0 && _marineLimitsCurrentPage > totalPages) {
|
||||
_marineLimitsCurrentPage = totalPages;
|
||||
}
|
||||
|
||||
final startIndex =
|
||||
(_marineLimitsCurrentPage - 1) * _marineLimitsItemsPerPage;
|
||||
final endIndex = (startIndex + _marineLimitsItemsPerPage > totalItems)
|
||||
? totalItems
|
||||
: startIndex + _marineLimitsItemsPerPage;
|
||||
|
||||
final paginatedItems = filteredList.sublist(startIndex, endIndex);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
children: paginatedItems
|
||||
.map((item) =>
|
||||
_buildParameterLimitEntry(item, stations: allStations))
|
||||
.toList(),
|
||||
),
|
||||
if (totalPages > 1) ...[
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
iconSize: 18.0,
|
||||
tooltip: 'Previous Page',
|
||||
onPressed: _marineLimitsCurrentPage > 1
|
||||
? () {
|
||||
setState(() {
|
||||
_marineLimitsCurrentPage--;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
'Page $_marineLimitsCurrentPage of $totalPages',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward_ios),
|
||||
iconSize: 18.0,
|
||||
tooltip: 'Next Page',
|
||||
onPressed: _marineLimitsCurrentPage < totalPages
|
||||
? () {
|
||||
setState(() {
|
||||
_marineLimitsCurrentPage++;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParameterLimitEntry(
|
||||
Map<String, dynamic> item, {
|
||||
List<Map<String, dynamic>>? departments,
|
||||
List<Map<String, dynamic>>? stations,
|
||||
}) {
|
||||
final paramName = item['param_parameter_list']?.toString() ?? 'N/A';
|
||||
final upperLimit = item['param_upper_limit']?.toString() ?? 'N/A';
|
||||
final lowerLimit = item['param_lower_limit']?.toString() ?? 'N/A';
|
||||
String 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) {
|
||||
final stationCode = station['man_station_code'] ?? 'N/A';
|
||||
final stationName = station['man_station_name'] ?? 'N/A';
|
||||
contextSubtitle = 'Station: $stationCode - $stationName';
|
||||
}
|
||||
}
|
||||
|
||||
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 _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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Parameter Limits"),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
final npeParameterLimits = auth.npeParameterLimits;
|
||||
final marineParameterLimits = auth.marineParameterLimits;
|
||||
final riverParameterLimits = auth.riverParameterLimits;
|
||||
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();
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_buildSectionHeader(context, "Parameter Limits"),
|
||||
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',
|
||||
),
|
||||
_buildPaginatedMarineLimitsList(
|
||||
filteredMarineLimits, allManualStations),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
372
lib/screens/settings/station_info_settings.dart
Normal file
372
lib/screens/settings/station_info_settings.dart
Normal file
@ -0,0 +1,372 @@
|
||||
// lib/screens/settings/station_info_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||
|
||||
class StationInfoSettingsScreen extends StatefulWidget {
|
||||
const StationInfoSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<StationInfoSettingsScreen> createState() =>
|
||||
_StationInfoSettingsScreenState();
|
||||
}
|
||||
|
||||
class _StationInfoSettingsScreenState extends State<StationInfoSettingsScreen> {
|
||||
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 = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tarballSearchController.addListener(_onTarballSearchChanged);
|
||||
_manualSearchController.addListener(_onManualSearchChanged);
|
||||
_riverManualSearchController.addListener(_onRiverManualSearchChanged);
|
||||
_riverTriennialSearchController.addListener(_onRiverTriennialSearchChanged);
|
||||
_airStationSearchController.addListener(_onAirStationSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tarballSearchController.dispose();
|
||||
_manualSearchController.dispose();
|
||||
_riverManualSearchController.dispose();
|
||||
_riverTriennialSearchController.dispose();
|
||||
_airStationSearchController.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;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 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)),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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<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(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Station Info"),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
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<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())
|
||||
?.cast<Map<String, dynamic>>();
|
||||
|
||||
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<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())
|
||||
?.cast<Map<String, dynamic>>();
|
||||
|
||||
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>>();
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
255
lib/screens/settings/submission_preferences_settings.dart
Normal file
255
lib/screens/settings/submission_preferences_settings.dart
Normal file
@ -0,0 +1,255 @@
|
||||
// lib/screens/settings/submission_preferences_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:environment_monitoring_app/services/user_preferences_service.dart';
|
||||
|
||||
class _ModuleSettings {
|
||||
bool isApiEnabled;
|
||||
bool isFtpEnabled;
|
||||
List<Map<String, dynamic>> apiConfigs;
|
||||
List<Map<String, dynamic>> ftpConfigs;
|
||||
|
||||
_ModuleSettings({
|
||||
this.isApiEnabled = true,
|
||||
this.isFtpEnabled = true,
|
||||
required this.apiConfigs,
|
||||
required this.ftpConfigs,
|
||||
});
|
||||
}
|
||||
|
||||
class SubmissionPreferencesSettingsScreen extends StatefulWidget {
|
||||
const SubmissionPreferencesSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SubmissionPreferencesSettingsScreen> createState() =>
|
||||
_SubmissionPreferencesSettingsScreenState();
|
||||
}
|
||||
|
||||
class _SubmissionPreferencesSettingsScreenState
|
||||
extends State<SubmissionPreferencesSettingsScreen> {
|
||||
final UserPreferencesService _preferencesService = UserPreferencesService();
|
||||
bool _isLoadingSettings = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
final Map<String, _ModuleSettings> _moduleSettings = {};
|
||||
|
||||
final List<Map<String, String>> _configurableModules = [
|
||||
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
|
||||
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
|
||||
{'key': 'marine_investigative', 'name': 'Marine Investigative'},
|
||||
{'key': 'river_in_situ', 'name': 'River In-Situ'},
|
||||
{'key': 'river_triennial', 'name': 'River Triennial'},
|
||||
{'key': 'river_investigative', 'name': 'River Investigative'},
|
||||
{'key': 'air_installation', 'name': 'Air Installation'},
|
||||
{'key': 'air_collection', 'name': 'Air Collection'},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAllModuleSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadAllModuleSettings() async {
|
||||
setState(() => _isLoadingSettings = true);
|
||||
for (var module in _configurableModules) {
|
||||
final moduleKey = module['key']!;
|
||||
|
||||
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<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 16.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Submission Preferences"),
|
||||
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: _isLoadingSettings
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator()))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_buildSectionHeader(context, "Module Settings"),
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModulePreferenceTile(
|
||||
String title, String moduleKey, _ModuleSettings settings) {
|
||||
return ExpansionTile(
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: false, // Start collapsed
|
||||
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<Map<String, dynamic>> 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) {
|
||||
bool isFtp = config.containsKey('ftp_module');
|
||||
String subtitleText;
|
||||
|
||||
if (isFtp) {
|
||||
subtitleText =
|
||||
'Module: ${config['ftp_module'] ?? 'N/A'} | Host: ${config['ftp_host'] ?? 'N/A'}';
|
||||
} else {
|
||||
subtitleText = config['api_url'] ?? 'No URL';
|
||||
}
|
||||
|
||||
return CheckboxListTile(
|
||||
title: Text(config['config_name'] ?? 'Unnamed'),
|
||||
subtitle:
|
||||
Text(subtitleText, style: const TextStyle(fontSize: 12)),
|
||||
value: config['is_enabled'] ?? false,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
config['is_enabled'] = value ?? false;
|
||||
});
|
||||
},
|
||||
dense: true,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/screens/settings/telegram_alert_settings.dart
Normal file
114
lib/screens/settings/telegram_alert_settings.dart
Normal file
@ -0,0 +1,114 @@
|
||||
// lib/screens/settings/telegram_alert_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||
import 'package:environment_monitoring_app/services/settings_service.dart';
|
||||
|
||||
class TelegramAlertSettingsScreen extends StatelessWidget {
|
||||
// --- START MODIFICATION ---
|
||||
// Removed 'const' from the constructor to fix the error.
|
||||
TelegramAlertSettingsScreen({super.key});
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
// Helper service for parsing settings
|
||||
final SettingsService _settingsService = SettingsService();
|
||||
|
||||
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 _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 16.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Telegram Alert Settings"),
|
||||
),
|
||||
body: Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
final appSettings = auth.appSettings;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
_buildSectionHeader(context, "Telegram Alerts"),
|
||||
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: true,
|
||||
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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -58,13 +58,20 @@ class UserPreferencesService {
|
||||
}).toList();
|
||||
|
||||
// 3. Determine default FTP links
|
||||
// --- START MODIFICATION: Simplified logic using ftp_module ---
|
||||
// Handle mapping 'marine_in_situ' -> 'marine_manual'
|
||||
String expectedFtpModuleKey = moduleKey;
|
||||
if (moduleKey == 'marine_in_situ') {
|
||||
expectedFtpModuleKey = 'marine_manual';
|
||||
} else if (moduleKey == 'river_in_situ') {
|
||||
expectedFtpModuleKey = 'river_manual';
|
||||
}
|
||||
|
||||
final defaultFtpLinks = allFtpConfigs.map((config) {
|
||||
final String configModule = config['ftp_module'] ?? '';
|
||||
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
|
||||
// Enable if the config's module matches the current moduleKey AND it's active
|
||||
bool isEnabled = (configModule == moduleKey) && isActive;
|
||||
bool isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
||||
|
||||
return {...config, 'is_enabled': isEnabled};
|
||||
}).toList();
|
||||
@ -124,25 +131,28 @@ class UserPreferencesService {
|
||||
// 3. Merge the two lists.
|
||||
return allApiConfigs.map((config) {
|
||||
final configId = config['api_config_id'];
|
||||
bool isEnabled; // Default to disabled
|
||||
bool isEnabled;
|
||||
|
||||
// --- START MODIFICATION: Corrected Merge Logic ---
|
||||
Map<String, dynamic>? matchingLink;
|
||||
try {
|
||||
// Find if a link exists for this config ID in the user's saved preferences.
|
||||
final matchingLink = savedLinks.firstWhere(
|
||||
matchingLink = savedLinks.firstWhere(
|
||||
(link) => link['api_config_id'] == configId,
|
||||
// If no link is found, 'orElse' is not triggered, it throws.
|
||||
);
|
||||
// A link was found, use the user's saved preference
|
||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||
} catch (e) {
|
||||
// --- THIS IS THE FIX for API post-sync ---
|
||||
// A 'firstWhere' with no match throws an error. We catch it here.
|
||||
// This means no link was saved (e.g., new config).
|
||||
// Default to the 'is_active' flag from the server.
|
||||
isEnabled = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
// --- END ---
|
||||
matchingLink = null; // No match found
|
||||
}
|
||||
|
||||
if (matchingLink != null) {
|
||||
// A preference exists. Use the saved value.
|
||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||
} else {
|
||||
// No preference saved for this config. Apply default logic.
|
||||
// (This handles newly synced configs automatically)
|
||||
isEnabled = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
// Return a new map containing the original config details plus the 'is_enabled' flag.
|
||||
return {
|
||||
...config,
|
||||
@ -159,25 +169,40 @@ class UserPreferencesService {
|
||||
|
||||
final savedLinks = await _dbHelper.getAllFtpLinksForModule(moduleName);
|
||||
|
||||
// Handle mapping 'marine_in_situ' -> 'marine_manual'
|
||||
String expectedFtpModuleKey = moduleName;
|
||||
if (moduleName == 'marine_in_situ') {
|
||||
expectedFtpModuleKey = 'marine_manual';
|
||||
} else if (moduleName == 'river_in_situ') {
|
||||
expectedFtpModuleKey = 'river_manual';
|
||||
}
|
||||
|
||||
return allFtpConfigs.map((config) {
|
||||
final configId = config['ftp_config_id'];
|
||||
bool isEnabled;
|
||||
|
||||
// --- START MODIFICATION: Corrected Merge Logic ---
|
||||
Map<String, dynamic>? matchingLink;
|
||||
try {
|
||||
final matchingLink = savedLinks.firstWhere(
|
||||
matchingLink = savedLinks.firstWhere(
|
||||
(link) => link['ftp_config_id'] == configId,
|
||||
);
|
||||
// A link was found, use the user's saved preference
|
||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||
} catch (e) {
|
||||
// --- START MODIFICATION: Use ftp_module for defaults ---
|
||||
// No matching link was found (e.g., new FTP config synced).
|
||||
// Default to 'enabled' if its module matches the one we're checking
|
||||
// and the config is marked 'is_active' from the server.
|
||||
matchingLink = null; // No match found
|
||||
}
|
||||
|
||||
if (matchingLink != null) {
|
||||
// A preference exists. Use the saved value.
|
||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||
} else {
|
||||
// No preference saved for this config. Apply default logic.
|
||||
final String configModule = config['ftp_module'] ?? '';
|
||||
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
isEnabled = (configModule == moduleName) && isActive;
|
||||
// --- END MODIFICATION ---
|
||||
// Use the mapped key for comparison
|
||||
isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
return {
|
||||
...config,
|
||||
'is_enabled': isEnabled,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user