repair api n ftp settings to be automatically selected during installation
This commit is contained in:
parent
0a0c31b405
commit
da1de869ed
@ -57,6 +57,9 @@ class AuthProvider with ChangeNotifier {
|
||||
List<Map<String, dynamic>>? _tarballClassifications;
|
||||
List<Map<String, dynamic>>? _riverManualStations;
|
||||
List<Map<String, dynamic>>? _riverTriennialStations;
|
||||
// --- ADDED: River Investigative Stations ---
|
||||
List<Map<String, dynamic>>? _riverInvestigativeStations;
|
||||
// --- END ADDED ---
|
||||
List<Map<String, dynamic>>? _departments;
|
||||
List<Map<String, dynamic>>? _companies;
|
||||
List<Map<String, dynamic>>? _positions;
|
||||
@ -79,6 +82,9 @@ class AuthProvider with ChangeNotifier {
|
||||
List<Map<String, dynamic>>? get tarballClassifications => _tarballClassifications;
|
||||
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
|
||||
List<Map<String, dynamic>>? get riverTriennialStations => _riverTriennialStations;
|
||||
// --- ADDED: Getter for River Investigative Stations ---
|
||||
List<Map<String, dynamic>>? get riverInvestigativeStations => _riverInvestigativeStations;
|
||||
// --- END ADDED ---
|
||||
List<Map<String, dynamic>>? get departments => _departments;
|
||||
List<Map<String, dynamic>>? get companies => _companies;
|
||||
List<Map<String, dynamic>>? get positions => _positions;
|
||||
@ -141,17 +147,39 @@ class AuthProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// --- START MODIFIED: Load server config with is_active check ---
|
||||
// Load server config early
|
||||
final activeApiConfig = await _serverConfigService.getActiveApiConfig();
|
||||
Map<String, dynamic>? activeApiConfig = await _serverConfigService.getActiveApiConfig();
|
||||
if (activeApiConfig == null) {
|
||||
debugPrint("AuthProvider: No active API config found. Setting default bootstrap URL.");
|
||||
debugPrint("AuthProvider: No active config in SharedPreferences. Checking local DB for 'is_active' flag...");
|
||||
// Load configs directly from DB helper (since cache isn't loaded yet)
|
||||
final allApiConfigs = await _dbHelper.loadApiConfigs();
|
||||
if (allApiConfigs != null && allApiConfigs.isNotEmpty) {
|
||||
try {
|
||||
// Find the first config marked as active. Note: 'is_active' might be 1 or true.
|
||||
activeApiConfig = allApiConfigs.firstWhere(
|
||||
(c) => c['is_active'] == 1 || c['is_active'] == true,
|
||||
orElse: () => allApiConfigs.first, // Fallback to just the first config if none are active
|
||||
);
|
||||
debugPrint("AuthProvider: Found active config in DB: ${activeApiConfig['config_name']}");
|
||||
await _serverConfigService.setActiveApiConfig(activeApiConfig);
|
||||
} catch (e) {
|
||||
debugPrint("AuthProvider: Error finding active config in DB. $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still no config (empty DB, first launch), set hardcoded default
|
||||
if (activeApiConfig == null) {
|
||||
debugPrint("AuthProvider: No active config found in DB. Setting default bootstrap URL.");
|
||||
final initialConfig = {
|
||||
'api_config_id': 0,
|
||||
'config_name': 'Default Server',
|
||||
'api_url': 'https://mms-apiv4.pstw.com.my/v1', // Use actual default if needed
|
||||
'api_url': 'https://mms-apiv4.pstw.com.my/v1', // This is your bootstrap URL
|
||||
};
|
||||
await _serverConfigService.setActiveApiConfig(initialConfig);
|
||||
}
|
||||
// --- END MODIFIED ---
|
||||
|
||||
final lastSyncString = prefs.getString(lastSyncTimestampKey);
|
||||
if (lastSyncString != null) {
|
||||
@ -499,6 +527,9 @@ class AuthProvider with ChangeNotifier {
|
||||
_tarballClassifications = await _dbHelper.loadTarballClassifications();
|
||||
_riverManualStations = await _dbHelper.loadRiverManualStations();
|
||||
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
|
||||
// --- MODIFIED: Load River Investigative Stations ---
|
||||
_riverInvestigativeStations = await _dbHelper.loadRiverInvestigativeStations();
|
||||
// --- END MODIFIED ---
|
||||
_departments = await _dbHelper.loadDepartments();
|
||||
_companies = await _dbHelper.loadCompanies();
|
||||
_positions = await _dbHelper.loadPositions();
|
||||
@ -528,7 +559,9 @@ class AuthProvider with ChangeNotifier {
|
||||
|
||||
// Now proceed with post-login actions that *don't* belong in the helper
|
||||
debugPrint('AuthProvider: Login successful. Session and profile persisted.');
|
||||
// --- THIS IS THE LINE THAT FIXES THE TICKING ---
|
||||
await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded();
|
||||
// ---
|
||||
// The main sync triggered by a direct user login
|
||||
await syncAllData(forceRefresh: true);
|
||||
// Notify listeners *after* sync is attempted (or throws)
|
||||
@ -625,6 +658,9 @@ class AuthProvider with ChangeNotifier {
|
||||
_tarballClassifications = null;
|
||||
_riverManualStations = null;
|
||||
_riverTriennialStations = null;
|
||||
// --- MODIFIED: Clear River Investigative Stations ---
|
||||
_riverInvestigativeStations = null;
|
||||
// --- END MODIFIED ---
|
||||
_departments = null;
|
||||
_companies = null;
|
||||
_positions = null;
|
||||
|
||||
@ -29,7 +29,7 @@ class _HomePageState extends State<HomePage> {
|
||||
});
|
||||
},
|
||||
),
|
||||
title: const Text("MMS Version 3.7.01"),
|
||||
title: const Text("MMS Version 3.8.01"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person),
|
||||
|
||||
@ -205,13 +205,28 @@ class RootApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RootAppState extends State<RootApp> {
|
||||
// --- START: MODIFICATION FOR HOURLY SYNC ---
|
||||
Timer? _configSyncTimer;
|
||||
// --- END: MODIFICATION ---
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeConnectivityListener();
|
||||
_performInitialSessionCheck();
|
||||
// --- START: MODIFICATION FOR HOURLY SYNC ---
|
||||
_initializePeriodicSync(); // Start the hourly sync timer
|
||||
// --- END: MODIFICATION ---
|
||||
}
|
||||
|
||||
// --- START: MODIFICATION FOR HOURLY SYNC ---
|
||||
@override
|
||||
void dispose() {
|
||||
_configSyncTimer?.cancel(); // Cancel the timer when the app closes
|
||||
super.dispose();
|
||||
}
|
||||
// --- 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.
|
||||
@ -259,6 +274,33 @@ class _RootAppState extends State<RootApp> {
|
||||
});
|
||||
}
|
||||
|
||||
// --- START: MODIFICATION FOR HOURLY SYNC ---
|
||||
/// Initializes a recurring timer to sync data periodically.
|
||||
void _initializePeriodicSync() {
|
||||
// Start a timer for 1 hour (as requested). You can change this duration.
|
||||
_configSyncTimer = Timer.periodic(const Duration(hours: 1), (timer) {
|
||||
debugPrint("[Main] Periodic 1-hour sync triggered.");
|
||||
|
||||
// Use 'context.read' for a safe, one-time read inside a timer
|
||||
if (mounted) {
|
||||
final authProvider = context.read<AuthProvider>();
|
||||
|
||||
// Only sync if the user is logged in and the session isn't expired
|
||||
if (authProvider.isLoggedIn && !authProvider.isSessionExpired) {
|
||||
debugPrint("[Main] User is logged in. Starting periodic data sync...");
|
||||
|
||||
// Run syncAllData but don't block. Catch errors to prevent the timer from stopping.
|
||||
authProvider.syncAllData().catchError((e) {
|
||||
debugPrint("[Main] Error during periodic 1-hour sync: $e");
|
||||
});
|
||||
} else {
|
||||
debugPrint("[Main] Skipping periodic sync: User not logged in or session expired.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- END: MODIFICATION ---
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AuthProvider>(
|
||||
|
||||
@ -147,13 +147,32 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/icon_3_512x512.png',
|
||||
height: 120,
|
||||
width: 120,
|
||||
|
||||
// --- MODIFICATION START ---
|
||||
// Replaced the original `Center(child: Image.asset(...))`
|
||||
// with this `Container` to make the icon circular and add a shadow.
|
||||
Container(
|
||||
width: 130,
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white, // Background color in case icon has transparency
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2), // Shadow color
|
||||
spreadRadius: 2, // How far the shadow spreads
|
||||
blurRadius: 8, // The blurriness of the shadow
|
||||
offset: const Offset(0, 4), // Position of shadow (x, y)
|
||||
),
|
||||
],
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/icon_3_512x512.png'),
|
||||
fit: BoxFit.cover, // Ensures the image fills the circle
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- MODIFICATION END ---
|
||||
|
||||
const SizedBox(height: 48),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
|
||||
@ -95,12 +95,29 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- START: MODIFIED LIFECYCLE METHOD ---
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) {
|
||||
// Use the member variable, not context
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||
|
||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
||||
if (_isLoading || btConnecting || serialConnecting) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED LIFECYCLE METHOD ---
|
||||
|
||||
void _initializeControllers() {
|
||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||
@ -282,8 +299,10 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
});
|
||||
}
|
||||
|
||||
// --- START: MODIFIED DISCONNECT METHOD ---
|
||||
void _disconnect(String type) {
|
||||
final service = context.read<MarineInvestigativeSamplingService>();
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (type == 'bluetooth') {
|
||||
service.disconnectFromBluetooth();
|
||||
} else {
|
||||
@ -296,12 +315,16 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false;
|
||||
_isLoading = false; // <-- CRITICAL: Also reset the loading flag
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT METHOD ---
|
||||
|
||||
// --- START: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
void _disconnectFromAll() {
|
||||
final service = context.read<MarineInvestigativeSamplingService>();
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
}
|
||||
@ -309,6 +332,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
_disconnect('serial');
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
|
||||
void _updateTextFields(Map<String, double> readings) {
|
||||
const defaultValue = -999.0;
|
||||
|
||||
@ -7,6 +7,37 @@ import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../../auth_provider.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
// --- FIX: Import the new dedicated service ---
|
||||
import '../../../services/marine_api_service.dart';
|
||||
|
||||
// --- FIX: Define image key lists ---
|
||||
const List<String> _inSituImageKeys = [
|
||||
'man_left_side_land_view_path',
|
||||
'man_right_side_land_view_path',
|
||||
'man_filling_water_into_sample_bottle_path',
|
||||
'man_seawater_in_clear_glass_bottle_path',
|
||||
'man_examine_preservative_ph_paper_path',
|
||||
'man_optional_photo_01_path',
|
||||
'man_optional_photo_02_path',
|
||||
'man_optional_photo_03_path',
|
||||
'man_optional_photo_04_path',
|
||||
];
|
||||
|
||||
|
||||
// --- START: MODIFIED TO FIX 0 IMAGE URLS ---
|
||||
// These keys are now an exact match for the keys in your server log
|
||||
const List<String> _tarballImageKeys = [
|
||||
'left_side_coastal_view',
|
||||
'right_side_coastal_view',
|
||||
'drawing_vertical_lines',
|
||||
'drawing_horizontal_line',
|
||||
'optional_photo_01',
|
||||
'optional_photo_02',
|
||||
'optional_photo_03',
|
||||
'optional_photo_04',
|
||||
];
|
||||
// --- END: MODIFIED TO FIX 0 IMAGE URLS ---
|
||||
|
||||
|
||||
class MarineImageRequestScreen extends StatefulWidget {
|
||||
const MarineImageRequestScreen({super.key});
|
||||
@ -19,8 +50,9 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _dateController = TextEditingController();
|
||||
|
||||
String? _selectedSamplingType = 'All Manual Sampling';
|
||||
final List<String> _samplingTypes = ['All Manual Sampling', 'In-Situ Sampling', 'Tarball Sampling'];
|
||||
// --- FIX: Default to a specific type, not 'All' ---
|
||||
String? _selectedSamplingType = 'In-Situ Sampling';
|
||||
final List<String> _samplingTypes = ['In-Situ Sampling', 'Tarball Sampling', 'All Manual Sampling'];
|
||||
|
||||
String? _selectedStateName;
|
||||
String? _selectedCategoryName;
|
||||
@ -38,8 +70,13 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// --- FIX: Use addPostFrameCallback to ensure provider is ready ---
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(mounted) {
|
||||
_initializeStationFilters();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -47,14 +84,85 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- FIX: New function to get the correct station list from AuthProvider ---
|
||||
List<Map<String, dynamic>> _getStationsForType(AuthProvider auth) {
|
||||
switch (_selectedSamplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
return auth.manualStations ?? [];
|
||||
case 'Tarball Sampling':
|
||||
return auth.tarballStations ?? [];
|
||||
case 'All Manual Sampling':
|
||||
// 'All' is complex. Defaulting to manual stations as a fallback.
|
||||
return auth.manualStations ?? [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- FIX: New function to get the correct station ID key ---
|
||||
String _getStationIdKey() {
|
||||
switch (_selectedSamplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
return 'station_id'; // from manualStations
|
||||
case 'Tarball Sampling':
|
||||
return 'station_id'; // from tarballStations (assuming 'station_id')
|
||||
default:
|
||||
return 'station_id';
|
||||
}
|
||||
}
|
||||
|
||||
// --- FIX: New function to get the correct station code key ---
|
||||
String _getStationCodeKey() {
|
||||
switch (_selectedSamplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
return 'man_station_code';
|
||||
case 'Tarball Sampling':
|
||||
return 'tbl_station_code'; // Please verify this key
|
||||
default:
|
||||
return 'man_station_code';
|
||||
}
|
||||
}
|
||||
|
||||
// --- FIX: New function to get the correct station name key ---
|
||||
String _getStationNameKey() {
|
||||
switch (_selectedSamplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
return 'man_station_name';
|
||||
case 'Tarball Sampling':
|
||||
return 'tbl_station_name'; // Please verify this key
|
||||
default:
|
||||
return 'man_station_name';
|
||||
}
|
||||
}
|
||||
|
||||
// --- FIX: Function modified to be dynamic ---
|
||||
void _initializeStationFilters() {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final allStations = auth.manualStations ?? [];
|
||||
// --- FIX: Use helper to get dynamic station list ---
|
||||
final allStations = _getStationsForType(auth);
|
||||
|
||||
if (allStations.isNotEmpty) {
|
||||
// Assuming 'state_name' exists on all station types
|
||||
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
||||
states.sort();
|
||||
setState(() {
|
||||
_statesList = states;
|
||||
// --- FIX: Reset dependent fields on change ---
|
||||
_selectedStateName = null;
|
||||
_selectedCategoryName = null;
|
||||
_selectedStation = null;
|
||||
_categoriesForState = [];
|
||||
_stationsForCategory = [];
|
||||
});
|
||||
} else {
|
||||
// --- FIX: Handle empty list ---
|
||||
setState(() {
|
||||
_statesList = [];
|
||||
_selectedStateName = null;
|
||||
_selectedCategoryName = null;
|
||||
_selectedStation = null;
|
||||
_categoriesForState = [];
|
||||
_stationsForCategory = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -102,7 +210,10 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
final stationId = _selectedStation!['station_id'];
|
||||
// --- FIX: Get the correct station ID key ---
|
||||
final stationIdKey = _getStationIdKey();
|
||||
final stationId = _selectedStation![stationIdKey];
|
||||
|
||||
if (stationId == null) {
|
||||
debugPrint("[Image Request] ERROR: Station ID is null.");
|
||||
if (mounted) {
|
||||
@ -121,33 +232,47 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
|
||||
try {
|
||||
debugPrint("[Image Request] All checks passed. Calling API with Station ID: $stationId, Type: $_selectedSamplingType");
|
||||
|
||||
// This call is correct, as apiService.marine holds the MarineApiService
|
||||
final result = await apiService.marine.getManualSamplingImages(
|
||||
stationId: stationId,
|
||||
stationId: stationId as int, // Ensure it's passed as int
|
||||
samplingDate: _selectedDate!,
|
||||
samplingType: _selectedSamplingType!,
|
||||
samplingType: _selectedSamplingType!, // This is now used by the API service
|
||||
);
|
||||
|
||||
if (mounted && result['success'] == true) {
|
||||
// --- FIX: The data key from your new log is just 'data' ---
|
||||
final List<Map<String, dynamic>> records = List<Map<String, dynamic>>.from(result['data'] ?? []);
|
||||
final List<String> fetchedUrls = [];
|
||||
|
||||
const imageKeys = [
|
||||
'man_left_side_land_view_path',
|
||||
'man_right_side_land_view_path',
|
||||
'man_filling_water_into_sample_bottle_path',
|
||||
'man_seawater_in_clear_glass_bottle_path',
|
||||
'man_examine_preservative_ph_paper_path',
|
||||
'man_optional_photo_01_path',
|
||||
'man_optional_photo_02_path',
|
||||
'man_optional_photo_03_path',
|
||||
'man_optional_photo_04_path',
|
||||
];
|
||||
// --- FIX: Determine which set of keys to use ---
|
||||
List<String> imageKeys;
|
||||
switch (_selectedSamplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
imageKeys = _inSituImageKeys;
|
||||
break;
|
||||
case 'Tarball Sampling':
|
||||
imageKeys = _tarballImageKeys; // Now uses the corrected list
|
||||
break;
|
||||
case 'All Manual Sampling':
|
||||
default:
|
||||
// Default to In-Situ keys as a fallback
|
||||
imageKeys = _inSituImageKeys;
|
||||
break;
|
||||
}
|
||||
// If 'All Manual Sampling' is selected, you could merge keys:
|
||||
// imageKeys = [..._inSituImageKeys, ..._tarballImageKeys];
|
||||
|
||||
for (final record in records) {
|
||||
for (final key in imageKeys) {
|
||||
if (record[key] != null && (record[key] as String).isNotEmpty) {
|
||||
final String imagePathFromServer = record[key];
|
||||
final fullUrl = ApiService.imageBaseUrl + imagePathFromServer;
|
||||
|
||||
// --- FIX: Make URL construction robust ---
|
||||
final fullUrl = imagePathFromServer.startsWith('http')
|
||||
? imagePathFromServer
|
||||
: ApiService.imageBaseUrl + imagePathFromServer;
|
||||
|
||||
fetchedUrls.add(fullUrl);
|
||||
debugPrint("[Image Request] Found and constructed URL: $fullUrl");
|
||||
}
|
||||
@ -155,7 +280,8 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_imageUrls = fetchedUrls;
|
||||
// --- FIX: Use toSet() to remove potential duplicates ---
|
||||
_imageUrls = fetchedUrls.toSet().toList();
|
||||
});
|
||||
debugPrint("[Image Request] Successfully processed and constructed ${_imageUrls.length} image URLs.");
|
||||
} else if (mounted) {
|
||||
@ -261,8 +387,9 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
try {
|
||||
debugPrint("[Image Request] Sending email request to server for recipient: $toEmail");
|
||||
|
||||
final stationCode = _selectedStation?['man_station_code'] ?? 'N/A';
|
||||
final stationName = _selectedStation?['man_station_name'] ?? 'N/A';
|
||||
// --- FIX: Use dynamic keys ---
|
||||
final stationCode = _selectedStation?[_getStationCodeKey()] ?? 'N/A';
|
||||
final stationName = _selectedStation?[_getStationNameKey()] ?? 'N/A';
|
||||
final fullStationIdentifier = '$stationCode - $stationName';
|
||||
|
||||
final result = await apiService.marine.sendImageRequestEmail(
|
||||
@ -318,7 +445,11 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedSamplingType,
|
||||
items: _samplingTypes.map((type) => DropdownMenuItem(value: type, child: Text(type))).toList(),
|
||||
onChanged: (value) => setState(() => _selectedSamplingType = value),
|
||||
onChanged: (value) => setState(() {
|
||||
_selectedSamplingType = value;
|
||||
// --- FIX: Re-initialize filters when type changes ---
|
||||
_initializeStationFilters();
|
||||
}),
|
||||
decoration: const InputDecoration(labelText: 'Sampling Type *', border: OutlineInputBorder()),
|
||||
validator: (value) => value == null ? 'Please select a type' : null,
|
||||
),
|
||||
@ -334,7 +465,8 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
_selectedCategoryName = null;
|
||||
_selectedStation = null;
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final allStations = auth.manualStations ?? [];
|
||||
// --- FIX: Use helper to get dynamic station list ---
|
||||
final allStations = _getStationsForType(auth);
|
||||
final categories = state != null ? allStations.where((s) => s['state_name'] == state).map((s) => s['category_name'] as String?).whereType<String>().toSet().toList() : <String>[];
|
||||
categories.sort();
|
||||
_categoriesForState = categories;
|
||||
@ -355,8 +487,11 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
_selectedCategoryName = category;
|
||||
_selectedStation = null;
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final allStations = auth.manualStations ?? [];
|
||||
_stationsForCategory = category != null ? (allStations.where((s) => s['state_name'] == _selectedStateName && s['category_name'] == category).toList()..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''))) : [];
|
||||
// --- FIX: Use helper to get dynamic station list ---
|
||||
final allStations = _getStationsForType(auth);
|
||||
// --- FIX: Use dynamic key for sorting ---
|
||||
final stationCodeKey = _getStationCodeKey();
|
||||
_stationsForCategory = category != null ? (allStations.where((s) => s['state_name'] == _selectedStateName && s['category_name'] == category).toList()..sort((a, b) => (a[stationCodeKey] ?? '').compareTo(b[stationCodeKey] ?? ''))) : [];
|
||||
});
|
||||
},
|
||||
validator: (val) => _selectedStateName != null && val == null ? "Category is required" : null,
|
||||
@ -366,7 +501,12 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
||||
items: _stationsForCategory,
|
||||
selectedItem: _selectedStation,
|
||||
enabled: _selectedCategoryName != null,
|
||||
itemAsString: (station) => "${station['man_station_code']} - ${station['man_station_name']}",
|
||||
// --- FIX: Use dynamic keys for display string ---
|
||||
itemAsString: (station) {
|
||||
final code = station[_getStationCodeKey()] ?? 'N/A';
|
||||
final name = station[_getStationNameKey()] ?? 'N/A';
|
||||
return "$code - $name";
|
||||
},
|
||||
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Station..."))),
|
||||
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Station *", border: OutlineInputBorder())),
|
||||
onChanged: (station) => setState(() => _selectedStation = station),
|
||||
|
||||
@ -302,7 +302,7 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
if (remark != null && remark.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text('Remark: $remark', style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.black54)),
|
||||
child: Text('Remark: $remark', style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.white70)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -107,14 +107,29 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- START: MODIFIED LIFECYCLE METHOD ---
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) {
|
||||
// Use the member variable, not context
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||
|
||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
||||
if (_isLoading || btConnecting || serialConnecting) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED LIFECYCLE METHOD ---
|
||||
|
||||
void _initializeControllers() {
|
||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||
@ -298,8 +313,10 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
||||
});
|
||||
}
|
||||
|
||||
// --- START: MODIFIED DISCONNECT METHOD ---
|
||||
void _disconnect(String type) {
|
||||
final service = context.read<MarineInSituSamplingService>();
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (type == 'bluetooth') {
|
||||
service.disconnectFromBluetooth();
|
||||
} else {
|
||||
@ -312,12 +329,16 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||
_isLoading = false; // <-- CRITICAL: Also reset the loading flag
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT METHOD ---
|
||||
|
||||
// --- START: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
void _disconnectFromAll() {
|
||||
final service = context.read<MarineInSituSamplingService>();
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
}
|
||||
@ -325,6 +346,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
||||
_disconnect('serial');
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
|
||||
void _updateTextFields(Map<String, double> readings) {
|
||||
const defaultValue = -999.0;
|
||||
|
||||
@ -114,7 +114,21 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) {
|
||||
setState(() {}); // Refresh UI, e.g., to re-check connection state
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||
|
||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
||||
if (_isLoading || btConnecting || serialConnecting) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -393,10 +407,13 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
void _disconnect(String type) {
|
||||
// Logic copied from RiverInSituStep3DataCaptureState._disconnect
|
||||
// Uses the correct _samplingService instance
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (type == 'bluetooth') {
|
||||
_samplingService.disconnectFromBluetooth();
|
||||
service.disconnectFromBluetooth();
|
||||
} else {
|
||||
_samplingService.disconnectFromSerial();
|
||||
service.disconnectFromSerial();
|
||||
}
|
||||
_dataSubscription?.cancel();
|
||||
_dataSubscription = null;
|
||||
@ -405,16 +422,20 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false; // Reset lockout state
|
||||
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _disconnectFromAll() {
|
||||
// Logic copied from RiverInSituStep3DataCaptureState._disconnectFromAll
|
||||
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
}
|
||||
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||
_disconnect('serial');
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,8 +120,22 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) {
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||
|
||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
||||
if (_isLoading || btConnecting || serialConnecting) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,7 +361,9 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
}
|
||||
|
||||
void _disconnect(String type) {
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (type == 'bluetooth') {
|
||||
service.disconnectFromBluetooth();
|
||||
} else {
|
||||
@ -360,12 +376,15 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _disconnectFromAll() {
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
}
|
||||
@ -519,6 +538,8 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
widget.onNext();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
|
||||
@ -120,8 +120,22 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) {
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||
|
||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
||||
if (_isLoading || btConnecting || serialConnecting) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,7 +361,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
}
|
||||
|
||||
void _disconnect(String type) {
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (type == 'bluetooth') {
|
||||
service.disconnectFromBluetooth();
|
||||
} else {
|
||||
@ -360,12 +376,15 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _disconnectFromAll() {
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
// --- START MODIFICATION ---
|
||||
final service = _samplingService; // NEW: Use the member variable
|
||||
// --- END MODIFICATION ---
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
}
|
||||
|
||||
@ -41,10 +41,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
final Map<String, _ModuleSettings> _moduleSettings = {};
|
||||
|
||||
// This list seems correct based on your file
|
||||
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'},
|
||||
];
|
||||
@ -70,9 +74,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
String _airLimitsSearchQuery = '';
|
||||
final TextEditingController _riverLimitsSearchController = TextEditingController();
|
||||
String _riverLimitsSearchQuery = '';
|
||||
// --- MODIFICATION: Added missing controller ---
|
||||
final TextEditingController _marineLimitsSearchController = TextEditingController();
|
||||
// --- END MODIFICATION ---
|
||||
String _marineLimitsSearchQuery = '';
|
||||
|
||||
// --- START: PAGINATION STATE FOR MARINE LIMITS ---
|
||||
int _marineLimitsCurrentPage = 1;
|
||||
final int _marineLimitsItemsPerPage = 15;
|
||||
// --- END: PAGINATION STATE FOR MARINE LIMITS ---
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -88,7 +99,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_npeMarineLimitsSearchController.addListener(() => setState(() => _npeMarineLimitsSearchQuery = _npeMarineLimitsSearchController.text));
|
||||
_airLimitsSearchController.addListener(() => setState(() => _airLimitsSearchQuery = _airLimitsSearchController.text));
|
||||
_riverLimitsSearchController.addListener(() => setState(() => _riverLimitsSearchQuery = _riverLimitsSearchController.text));
|
||||
_marineLimitsSearchController.addListener(() => setState(() => _marineLimitsSearchQuery = _marineLimitsSearchController.text));
|
||||
|
||||
_marineLimitsSearchController.addListener(() {
|
||||
setState(() {
|
||||
_marineLimitsSearchQuery = _marineLimitsSearchController.text;
|
||||
_marineLimitsCurrentPage = 1; // Reset to page 1 when search query changes
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -566,7 +583,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
labelText: 'Search Marine Limits',
|
||||
hintText: 'Search by parameter or station',
|
||||
),
|
||||
_buildInfoList(filteredMarineLimits, (item) => _buildParameterLimitEntry(item, stations: allManualStations)),
|
||||
_buildPaginatedMarineLimitsList(filteredMarineLimits, allManualStations),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -748,7 +765,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('App Version'),
|
||||
subtitle: const Text('MMS Version 3.7.01'),
|
||||
subtitle: const Text('MMS Version 3.8.01'),
|
||||
dense: true,
|
||||
),
|
||||
ListTile(
|
||||
@ -791,6 +808,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// --- START MODIFICATION: This widget is updated to show ftp_module ---
|
||||
Widget _buildDestinationList(String title, List<Map<String, dynamic>> configs, String idKey) {
|
||||
if (configs.isEmpty) {
|
||||
return const ListTile(
|
||||
@ -808,9 +826,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
...configs.map((config) {
|
||||
// Check if this is an FTP config by looking for the 'ftp_module' key
|
||||
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(config['api_url'] ?? config['ftp_host'] ?? 'No URL/Host'),
|
||||
subtitle: Text(subtitleText, style: const TextStyle(fontSize: 12)),
|
||||
value: config['is_enabled'] ?? false,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
@ -824,6 +852,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
@ -864,6 +893,91 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaginatedMarineLimitsList(
|
||||
List<Map<String, dynamic>>? filteredList,
|
||||
List<Map<String, dynamic>>? allStations,
|
||||
) {
|
||||
// 1. Handle null or empty list after filtering
|
||||
if (filteredList == null || filteredList.isEmpty) {
|
||||
// Check if the original list (pre-filter) was also empty
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Pagination Calculations
|
||||
final totalItems = filteredList.length;
|
||||
final totalPages = (totalItems / _marineLimitsItemsPerPage).ceil();
|
||||
if (totalPages > 0 && _marineLimitsCurrentPage > totalPages) {
|
||||
_marineLimitsCurrentPage = totalPages;
|
||||
}
|
||||
|
||||
|
||||
// 3. Get items for the current page
|
||||
final startIndex = (_marineLimitsCurrentPage - 1) * _marineLimitsItemsPerPage;
|
||||
final endIndex = (startIndex + _marineLimitsItemsPerPage > totalItems)
|
||||
? totalItems
|
||||
: startIndex + _marineLimitsItemsPerPage;
|
||||
|
||||
final paginatedItems = filteredList.sublist(startIndex, endIndex);
|
||||
|
||||
// 4. Build the UI
|
||||
return Column(
|
||||
children: [
|
||||
// 4.1. The list of items for the current page
|
||||
Column(
|
||||
children: paginatedItems
|
||||
.map((item) => _buildParameterLimitEntry(item, stations: allStations))
|
||||
.toList(),
|
||||
),
|
||||
|
||||
// 4.2. The pagination controls (only show if more than one page)
|
||||
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, // Disable button if on first page
|
||||
),
|
||||
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, // Disable button if on last page
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChatIdEntry(String label, String value) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@ -905,11 +1019,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final stationId = item['station_id'];
|
||||
final station = stations.firstWhere((s) => s['station_id'] == stationId, orElse: () => {});
|
||||
if (station.isNotEmpty) {
|
||||
// --- START: MODIFICATION ---
|
||||
final stationCode = station['man_station_code'] ?? 'N/A';
|
||||
final stationName = station['man_station_name'] ?? 'N/A';
|
||||
contextSubtitle = 'Station: $stationCode - $stationName';
|
||||
// --- END: MODIFICATION ---
|
||||
}
|
||||
}
|
||||
|
||||
@ -983,6 +1095,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- MODIFICATION: Added ftp_module display ---
|
||||
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)),
|
||||
|
||||
@ -226,6 +226,16 @@ class ApiService {
|
||||
await dbHelper.deleteRiverTriennialStations(id);
|
||||
}
|
||||
},
|
||||
// --- ADDED: River Investigative Stations Sync ---
|
||||
'riverInvestigativeStations': {
|
||||
// IMPORTANT: Make sure this endpoint matches your server's route
|
||||
'endpoint': 'river/investigative-stations',
|
||||
'handler': (d, id) async {
|
||||
await dbHelper.upsertRiverInvestigativeStations(d);
|
||||
await dbHelper.deleteRiverInvestigativeStations(id);
|
||||
}
|
||||
},
|
||||
// --- END ADDED ---
|
||||
'departments': {
|
||||
'endpoint': 'departments',
|
||||
'handler': (d, id) async {
|
||||
@ -485,15 +495,79 @@ class AirApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// --- START OF MODIFIED SECTION ---
|
||||
// The entire MarineApiService class is replaced with the corrected version.
|
||||
// =======================================================================
|
||||
class MarineApiService {
|
||||
final BaseApiService _baseService;
|
||||
final TelegramService _telegramService; // Still needed if _handleAlerts were here
|
||||
final TelegramService _telegramService;
|
||||
final ServerConfigService _serverConfigService;
|
||||
final DatabaseHelper _dbHelper; // Still needed for parameter limit lookups if alerts were here
|
||||
final DatabaseHelper _dbHelper; // Kept to match constructor
|
||||
|
||||
MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
|
||||
|
||||
// --- KEPT METHODS ---
|
||||
// --- KEPT METHODS (Unchanged) ---
|
||||
Future<Map<String, dynamic>> getTarballStations() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/tarball/stations');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getManualStations() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/manual/stations');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTarballClassifications() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/tarball/classifications');
|
||||
}
|
||||
|
||||
// --- REPLACED/FIXED METHOD ---
|
||||
Future<Map<String, dynamic>> getManualSamplingImages({
|
||||
required int stationId,
|
||||
required DateTime samplingDate,
|
||||
required String samplingType, // This parameter is NOW USED
|
||||
}) async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
|
||||
|
||||
String endpoint;
|
||||
// Determine the correct endpoint based on the sampling type
|
||||
switch (samplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
case 'Tarball Sampling':
|
||||
// **IMPORTANT**: Please verify this is the correct endpoint for tarball records
|
||||
endpoint = 'marine/tarball/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
case 'All Manual Sampling':
|
||||
default:
|
||||
// 'All' is complex. Defaulting to 'manual' (in-situ) as a fallback.
|
||||
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
}
|
||||
|
||||
// This new debug print will help you confirm the fix is working
|
||||
debugPrint("MarineApiService: Calling API endpoint: $endpoint");
|
||||
|
||||
final response = await _baseService.get(baseUrl, endpoint);
|
||||
|
||||
// Adjusting response parsing based on observed structure
|
||||
if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
|
||||
return {
|
||||
'success': true,
|
||||
'data': response['data']['data'], // Return the inner 'data' list
|
||||
'message': response['message'],
|
||||
};
|
||||
}
|
||||
// Return original response if structure doesn't match
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
// --- ADDED METHOD ---
|
||||
Future<Map<String, dynamic>> sendImageRequestEmail({
|
||||
required String recipientEmail,
|
||||
required List<String> imageUrls,
|
||||
@ -511,65 +585,13 @@ class MarineApiService {
|
||||
|
||||
return _baseService.postMultipart(
|
||||
baseUrl: baseUrl,
|
||||
endpoint: 'marine/images/send-email',
|
||||
endpoint: 'marine/images/send-email', // **IMPORTANT**: Verify this endpoint
|
||||
fields: fields,
|
||||
files: {},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getManualSamplingImages({
|
||||
required int stationId,
|
||||
required DateTime samplingDate,
|
||||
required String samplingType, // This parameter seems unused in the current endpoint
|
||||
}) async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
|
||||
|
||||
// Endpoint seems specific to 'manual' records, might need adjustment if other types needed
|
||||
final String endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
|
||||
debugPrint("ApiService: Calling real API endpoint: $endpoint");
|
||||
|
||||
final response = await _baseService.get(baseUrl, endpoint);
|
||||
|
||||
// Adjusting response parsing based on observed structure
|
||||
if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
|
||||
return {
|
||||
'success': true,
|
||||
'data': response['data']['data'], // Return the inner 'data' list
|
||||
'message': response['message'],
|
||||
};
|
||||
}
|
||||
// Return original response if structure doesn't match
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getTarballStations() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/tarball/stations');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getManualStations() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/manual/stations');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTarballClassifications() async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/tarball/classifications');
|
||||
}
|
||||
|
||||
// --- REMOVED METHODS (Logic moved to feature services) ---
|
||||
// - submitInSituSample
|
||||
// - _handleInSituSuccessAlert
|
||||
// - _generateInSituAlertMessage
|
||||
// - _getOutOfBoundsAlertSection (InSitu version)
|
||||
// - submitTarballSample
|
||||
// - _handleTarballSuccessAlert
|
||||
// - _generateTarballAlertMessage
|
||||
|
||||
// --- KEPT METHODS (Simple POSTs called by specific services) ---
|
||||
// --- KEPT METHODS (Unchanged) ---
|
||||
Future<Map<String, dynamic>> submitPreDepartureChecklist(MarineManualPreDepartureChecklistData data) async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.post(baseUrl, 'marine/checklist', data.toApiFormData());
|
||||
@ -590,6 +612,9 @@ class MarineApiService {
|
||||
return _baseService.get(baseUrl, 'marine/maintenance/previous');
|
||||
}
|
||||
}
|
||||
// =======================================================================
|
||||
// --- END OF MODIFIED SECTION ---
|
||||
// =======================================================================
|
||||
|
||||
class RiverApiService {
|
||||
final BaseApiService _baseService;
|
||||
@ -665,7 +690,9 @@ class RiverApiService {
|
||||
class DatabaseHelper {
|
||||
static Database? _database;
|
||||
static const String _dbName = 'app_data.db';
|
||||
static const int _dbVersion = 23; // Keep version updated if schema changes
|
||||
// --- MODIFIED: Incremented DB version ---
|
||||
static const int _dbVersion = 24; // Keep version updated if schema changes
|
||||
// --- END MODIFIED ---
|
||||
|
||||
// compute-related static variables/methods REMOVED
|
||||
|
||||
@ -675,6 +702,9 @@ class DatabaseHelper {
|
||||
static const String _manualStationsTable = 'marine_manual_stations';
|
||||
static const String _riverManualStationsTable = 'river_manual_stations';
|
||||
static const String _riverTriennialStationsTable = 'river_triennial_stations';
|
||||
// --- ADDED: River Investigative Stations Table Name ---
|
||||
static const String _riverInvestigativeStationsTable = 'river_investigative_stations';
|
||||
// --- END ADDED ---
|
||||
static const String _tarballClassificationsTable = 'marine_tarball_classifications';
|
||||
static const String _departmentsTable = 'departments';
|
||||
static const String _companiesTable = 'companies';
|
||||
@ -726,6 +756,9 @@ class DatabaseHelper {
|
||||
await db.execute('CREATE TABLE $_manualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||
await db.execute('CREATE TABLE $_riverManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||
await db.execute('CREATE TABLE $_riverTriennialStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||
// --- ADDED: River Investigative Stations Table Create ---
|
||||
await db.execute('CREATE TABLE $_riverInvestigativeStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||
// --- END ADDED ---
|
||||
await db.execute('CREATE TABLE $_tarballClassificationsTable(classification_id INTEGER PRIMARY KEY, classification_json TEXT)');
|
||||
await db.execute('CREATE TABLE $_departmentsTable(department_id INTEGER PRIMARY KEY, department_json TEXT)');
|
||||
await db.execute('CREATE TABLE $_companiesTable(company_id INTEGER PRIMARY KEY, company_json TEXT)');
|
||||
@ -894,6 +927,11 @@ class DatabaseHelper {
|
||||
debugPrint("Upgrade warning: Failed to drop old parameter limits table (may not exist): $e");
|
||||
}
|
||||
}
|
||||
// --- ADDED: Upgrade step for new table ---
|
||||
if (oldVersion < 24) {
|
||||
await db.execute('CREATE TABLE IF NOT EXISTS $_riverInvestigativeStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||
}
|
||||
// --- END ADDED ---
|
||||
}
|
||||
|
||||
// --- Data Handling Methods ---
|
||||
@ -1074,6 +1112,13 @@ class DatabaseHelper {
|
||||
Future<void> deleteRiverTriennialStations(List<dynamic> ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station');
|
||||
|
||||
// --- ADDED: River Investigative Stations DB Methods ---
|
||||
Future<void> upsertRiverInvestigativeStations(List<Map<String, dynamic>> data) =>
|
||||
_upsertData(_riverInvestigativeStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteRiverInvestigativeStations(List<dynamic> ids) => _deleteData(_riverInvestigativeStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadRiverInvestigativeStations() => _loadData(_riverInvestigativeStationsTable, 'station');
|
||||
// --- END ADDED ---
|
||||
|
||||
Future<void> upsertTarballClassifications(List<Map<String, dynamic>> data) =>
|
||||
_upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification');
|
||||
Future<void> deleteTarballClassifications(List<dynamic> ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids);
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
// lib/services/marine_api_service.dart
|
||||
|
||||
import 'dart:convert'; // Added: Necessary for jsonEncode
|
||||
import 'package:flutter/foundation.dart'; // Added: Necessary for debugPrint
|
||||
import 'package:intl/intl.dart'; // Added: Necessary for DateFormat
|
||||
|
||||
import 'package:environment_monitoring_app/services/base_api_service.dart';
|
||||
import 'package:environment_monitoring_app/services/telegram_service.dart';
|
||||
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
||||
// Added: Necessary for ApiService.imageBaseUrl, assuming it's defined there.
|
||||
// If not, you may need to adjust this.
|
||||
import 'package:environment_monitoring_app/services/api_service.dart';
|
||||
|
||||
class MarineApiService {
|
||||
final BaseApiService _baseService;
|
||||
@ -25,4 +32,75 @@ class MarineApiService {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
return _baseService.get(baseUrl, 'marine/tarball/classifications');
|
||||
}
|
||||
|
||||
// --- ADDED: Method to fetch images (Fixes the issue) ---
|
||||
/// Fetches image records for either In-Situ or Tarball sampling.
|
||||
Future<Map<String, dynamic>> getManualSamplingImages({
|
||||
required int stationId,
|
||||
required DateTime samplingDate,
|
||||
required String samplingType, // This parameter is now used
|
||||
}) async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
|
||||
|
||||
String endpoint;
|
||||
// Determine the correct endpoint based on the sampling type
|
||||
switch (samplingType) {
|
||||
case 'In-Situ Sampling':
|
||||
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
case 'Tarball Sampling':
|
||||
// **IMPORTANT**: Please verify this is the correct endpoint for tarball records
|
||||
endpoint = 'marine/tarball/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
case 'All Manual Sampling':
|
||||
default:
|
||||
// 'All' is complex. Defaulting to 'manual' (in-situ) as a fallback.
|
||||
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
|
||||
break;
|
||||
}
|
||||
|
||||
debugPrint("MarineApiService: Calling API endpoint: $endpoint");
|
||||
|
||||
final response = await _baseService.get(baseUrl, endpoint);
|
||||
|
||||
// This parsing logic assumes the server nests the list inside {'data': {'data': [...]}}
|
||||
// Adjust if your API response is different
|
||||
if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
|
||||
return {
|
||||
'success': true,
|
||||
'data': response['data']['data'], // Return the inner 'data' list
|
||||
'message': response['message'],
|
||||
};
|
||||
}
|
||||
|
||||
// Return original response if structure doesn't match
|
||||
return response;
|
||||
}
|
||||
|
||||
// --- ADDED: Method to send email request (Fixes the issue) ---
|
||||
/// Sends the selected image URLs to the server for emailing.
|
||||
Future<Map<String, dynamic>> sendImageRequestEmail({
|
||||
required String recipientEmail,
|
||||
required List<String> imageUrls,
|
||||
required String stationName,
|
||||
required String samplingDate,
|
||||
}) async {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
|
||||
final Map<String, String> fields = {
|
||||
'recipientEmail': recipientEmail,
|
||||
'imageUrls': jsonEncode(imageUrls), // Encode list as JSON string
|
||||
'stationName': stationName,
|
||||
'samplingDate': samplingDate,
|
||||
};
|
||||
|
||||
// Use postMultipart (even with no files) as it's common for this kind of endpoint
|
||||
return _baseService.postMultipart(
|
||||
baseUrl: baseUrl,
|
||||
endpoint: 'marine/images/send-email', // **IMPORTANT**: Verify this endpoint
|
||||
fields: fields,
|
||||
files: {}, // No files being uploaded, just data
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -11,10 +11,14 @@ class UserPreferencesService {
|
||||
static const _defaultPrefsSavedKey = 'default_preferences_saved';
|
||||
|
||||
// Moved from settings.dart for central access
|
||||
// This list now includes all your modules
|
||||
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'},
|
||||
];
|
||||
@ -47,43 +51,24 @@ class UserPreferencesService {
|
||||
);
|
||||
|
||||
// 2. Determine default API links
|
||||
// This is correct: Tick any API server marked as 'is_active' by default.
|
||||
final defaultApiLinks = allApiConfigs.map((config) {
|
||||
bool isEnabled = config['config_name'] == 'PSTW_HQ';
|
||||
bool isEnabled = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
return {...config, 'is_enabled': isEnabled};
|
||||
}).toList();
|
||||
|
||||
// 3. Determine default FTP links
|
||||
// --- START MODIFICATION: Simplified logic using ftp_module ---
|
||||
final defaultFtpLinks = allFtpConfigs.map((config) {
|
||||
bool isEnabled = false; // Disable all by default
|
||||
switch (moduleKey) {
|
||||
case 'marine_tarball':
|
||||
if (config['config_name'] == 'pstw_marine_tarball' || config['config_name'] == 'tes_marine_tarball') {
|
||||
isEnabled = true;
|
||||
}
|
||||
break;
|
||||
case 'marine_in_situ':
|
||||
if (config['config_name'] == 'pstw_marine_manual' || config['config_name'] == 'tes_marine_manual') {
|
||||
isEnabled = true;
|
||||
}
|
||||
break;
|
||||
case 'river_in_situ':
|
||||
if (config['config_name'] == 'pstw_river_manual' || config['config_name'] == 'tes_river_manual') {
|
||||
isEnabled = true;
|
||||
}
|
||||
break;
|
||||
case 'air_collection':
|
||||
if (config['config_name'] == 'pstw_air_collect' || config['config_name'] == 'tes_air_collect') {
|
||||
isEnabled = true;
|
||||
}
|
||||
break;
|
||||
case 'air_installation':
|
||||
if (config['config_name'] == 'pstw_air_install' || config['config_name'] == 'tes_air_install') {
|
||||
isEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
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;
|
||||
|
||||
return {...config, 'is_enabled': isEnabled};
|
||||
}).toList();
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
// 4. Save the default links to the database.
|
||||
await saveApiLinksForModule(moduleKey, defaultApiLinks);
|
||||
@ -139,7 +124,7 @@ class UserPreferencesService {
|
||||
// 3. Merge the two lists.
|
||||
return allApiConfigs.map((config) {
|
||||
final configId = config['api_config_id'];
|
||||
bool isEnabled = false; // Default to disabled
|
||||
bool isEnabled; // Default to disabled
|
||||
|
||||
try {
|
||||
// Find if a link exists for this config ID in the user's saved preferences.
|
||||
@ -147,11 +132,15 @@ class UserPreferencesService {
|
||||
(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 for this config, so it remains disabled.
|
||||
isEnabled = false;
|
||||
// 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 ---
|
||||
}
|
||||
|
||||
// Return a new map containing the original config details plus the 'is_enabled' flag.
|
||||
@ -172,14 +161,22 @@ class UserPreferencesService {
|
||||
|
||||
return allFtpConfigs.map((config) {
|
||||
final configId = config['ftp_config_id'];
|
||||
bool isEnabled = false;
|
||||
bool isEnabled;
|
||||
try {
|
||||
final 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) {
|
||||
isEnabled = false;
|
||||
// --- 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.
|
||||
final String configModule = config['ftp_module'] ?? '';
|
||||
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
||||
isEnabled = (configModule == moduleName) && isActive;
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user