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>>? _tarballClassifications;
|
||||||
List<Map<String, dynamic>>? _riverManualStations;
|
List<Map<String, dynamic>>? _riverManualStations;
|
||||||
List<Map<String, dynamic>>? _riverTriennialStations;
|
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>>? _departments;
|
||||||
List<Map<String, dynamic>>? _companies;
|
List<Map<String, dynamic>>? _companies;
|
||||||
List<Map<String, dynamic>>? _positions;
|
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 tarballClassifications => _tarballClassifications;
|
||||||
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
|
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
|
||||||
List<Map<String, dynamic>>? get riverTriennialStations => _riverTriennialStations;
|
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 departments => _departments;
|
||||||
List<Map<String, dynamic>>? get companies => _companies;
|
List<Map<String, dynamic>>? get companies => _companies;
|
||||||
List<Map<String, dynamic>>? get positions => _positions;
|
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
|
// Load server config early
|
||||||
final activeApiConfig = await _serverConfigService.getActiveApiConfig();
|
Map<String, dynamic>? activeApiConfig = await _serverConfigService.getActiveApiConfig();
|
||||||
if (activeApiConfig == null) {
|
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 = {
|
final initialConfig = {
|
||||||
'api_config_id': 0,
|
'api_config_id': 0,
|
||||||
'config_name': 'Default Server',
|
'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);
|
await _serverConfigService.setActiveApiConfig(initialConfig);
|
||||||
}
|
}
|
||||||
|
// --- END MODIFIED ---
|
||||||
|
|
||||||
final lastSyncString = prefs.getString(lastSyncTimestampKey);
|
final lastSyncString = prefs.getString(lastSyncTimestampKey);
|
||||||
if (lastSyncString != null) {
|
if (lastSyncString != null) {
|
||||||
@ -499,6 +527,9 @@ class AuthProvider with ChangeNotifier {
|
|||||||
_tarballClassifications = await _dbHelper.loadTarballClassifications();
|
_tarballClassifications = await _dbHelper.loadTarballClassifications();
|
||||||
_riverManualStations = await _dbHelper.loadRiverManualStations();
|
_riverManualStations = await _dbHelper.loadRiverManualStations();
|
||||||
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
|
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
|
||||||
|
// --- MODIFIED: Load River Investigative Stations ---
|
||||||
|
_riverInvestigativeStations = await _dbHelper.loadRiverInvestigativeStations();
|
||||||
|
// --- END MODIFIED ---
|
||||||
_departments = await _dbHelper.loadDepartments();
|
_departments = await _dbHelper.loadDepartments();
|
||||||
_companies = await _dbHelper.loadCompanies();
|
_companies = await _dbHelper.loadCompanies();
|
||||||
_positions = await _dbHelper.loadPositions();
|
_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
|
// Now proceed with post-login actions that *don't* belong in the helper
|
||||||
debugPrint('AuthProvider: Login successful. Session and profile persisted.');
|
debugPrint('AuthProvider: Login successful. Session and profile persisted.');
|
||||||
|
// --- THIS IS THE LINE THAT FIXES THE TICKING ---
|
||||||
await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded();
|
await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded();
|
||||||
|
// ---
|
||||||
// The main sync triggered by a direct user login
|
// The main sync triggered by a direct user login
|
||||||
await syncAllData(forceRefresh: true);
|
await syncAllData(forceRefresh: true);
|
||||||
// Notify listeners *after* sync is attempted (or throws)
|
// Notify listeners *after* sync is attempted (or throws)
|
||||||
@ -625,6 +658,9 @@ class AuthProvider with ChangeNotifier {
|
|||||||
_tarballClassifications = null;
|
_tarballClassifications = null;
|
||||||
_riverManualStations = null;
|
_riverManualStations = null;
|
||||||
_riverTriennialStations = null;
|
_riverTriennialStations = null;
|
||||||
|
// --- MODIFIED: Clear River Investigative Stations ---
|
||||||
|
_riverInvestigativeStations = null;
|
||||||
|
// --- END MODIFIED ---
|
||||||
_departments = null;
|
_departments = null;
|
||||||
_companies = null;
|
_companies = null;
|
||||||
_positions = 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: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.person),
|
icon: const Icon(Icons.person),
|
||||||
|
|||||||
@ -205,13 +205,28 @@ class RootApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RootAppState extends State<RootApp> {
|
class _RootAppState extends State<RootApp> {
|
||||||
|
// --- START: MODIFICATION FOR HOURLY SYNC ---
|
||||||
|
Timer? _configSyncTimer;
|
||||||
|
// --- END: MODIFICATION ---
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initializeConnectivityListener();
|
_initializeConnectivityListener();
|
||||||
_performInitialSessionCheck();
|
_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.
|
/// Initial check when app loads to see if we need to transition from offline to online.
|
||||||
void _performInitialSessionCheck() async {
|
void _performInitialSessionCheck() async {
|
||||||
// Wait a moment for providers to be fully available.
|
// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<AuthProvider>(
|
return Consumer<AuthProvider>(
|
||||||
|
|||||||
@ -147,13 +147,32 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Center(
|
|
||||||
child: Image.asset(
|
// --- MODIFICATION START ---
|
||||||
'assets/icon_3_512x512.png',
|
// Replaced the original `Center(child: Image.asset(...))`
|
||||||
height: 120,
|
// with this `Container` to make the icon circular and add a shadow.
|
||||||
width: 120,
|
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),
|
const SizedBox(height: 48),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
|
|||||||
@ -95,12 +95,29 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED LIFECYCLE METHOD ---
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
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() {
|
void _initializeControllers() {
|
||||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||||
@ -282,8 +299,10 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED DISCONNECT METHOD ---
|
||||||
void _disconnect(String type) {
|
void _disconnect(String type) {
|
||||||
final service = context.read<MarineInvestigativeSamplingService>();
|
// Use the member variable, not context, for lifecycle safety
|
||||||
|
final service = _samplingService;
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
@ -296,12 +315,16 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false;
|
_isLockedOut = false;
|
||||||
|
_isLoading = false; // <-- CRITICAL: Also reset the loading flag
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED DISCONNECT METHOD ---
|
||||||
|
|
||||||
|
// --- START: MODIFIED DISCONNECT_ALL METHOD ---
|
||||||
void _disconnectFromAll() {
|
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) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth');
|
||||||
}
|
}
|
||||||
@ -309,6 +332,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_disconnect('serial');
|
_disconnect('serial');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
||||||
|
|
||||||
void _updateTextFields(Map<String, double> readings) {
|
void _updateTextFields(Map<String, double> readings) {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
|
|||||||
@ -7,6 +7,37 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
|
|
||||||
import '../../../auth_provider.dart';
|
import '../../../auth_provider.dart';
|
||||||
import '../../../services/api_service.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 {
|
class MarineImageRequestScreen extends StatefulWidget {
|
||||||
const MarineImageRequestScreen({super.key});
|
const MarineImageRequestScreen({super.key});
|
||||||
@ -19,8 +50,9 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _dateController = TextEditingController();
|
final _dateController = TextEditingController();
|
||||||
|
|
||||||
String? _selectedSamplingType = 'All Manual Sampling';
|
// --- FIX: Default to a specific type, not 'All' ---
|
||||||
final List<String> _samplingTypes = ['All Manual Sampling', 'In-Situ Sampling', 'Tarball Sampling'];
|
String? _selectedSamplingType = 'In-Situ Sampling';
|
||||||
|
final List<String> _samplingTypes = ['In-Situ Sampling', 'Tarball Sampling', 'All Manual Sampling'];
|
||||||
|
|
||||||
String? _selectedStateName;
|
String? _selectedStateName;
|
||||||
String? _selectedCategoryName;
|
String? _selectedCategoryName;
|
||||||
@ -38,8 +70,13 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// --- FIX: Use addPostFrameCallback to ensure provider is ready ---
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if(mounted) {
|
||||||
_initializeStationFilters();
|
_initializeStationFilters();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -47,14 +84,85 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
super.dispose();
|
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() {
|
void _initializeStationFilters() {
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
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) {
|
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();
|
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
||||||
states.sort();
|
states.sort();
|
||||||
setState(() {
|
setState(() {
|
||||||
_statesList = states;
|
_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final stationId = _selectedStation!['station_id'];
|
// --- FIX: Get the correct station ID key ---
|
||||||
|
final stationIdKey = _getStationIdKey();
|
||||||
|
final stationId = _selectedStation![stationIdKey];
|
||||||
|
|
||||||
if (stationId == null) {
|
if (stationId == null) {
|
||||||
debugPrint("[Image Request] ERROR: Station ID is null.");
|
debugPrint("[Image Request] ERROR: Station ID is null.");
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -121,33 +232,47 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint("[Image Request] All checks passed. Calling API with Station ID: $stationId, Type: $_selectedSamplingType");
|
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(
|
final result = await apiService.marine.getManualSamplingImages(
|
||||||
stationId: stationId,
|
stationId: stationId as int, // Ensure it's passed as int
|
||||||
samplingDate: _selectedDate!,
|
samplingDate: _selectedDate!,
|
||||||
samplingType: _selectedSamplingType!,
|
samplingType: _selectedSamplingType!, // This is now used by the API service
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted && result['success'] == true) {
|
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<Map<String, dynamic>> records = List<Map<String, dynamic>>.from(result['data'] ?? []);
|
||||||
final List<String> fetchedUrls = [];
|
final List<String> fetchedUrls = [];
|
||||||
|
|
||||||
const imageKeys = [
|
// --- FIX: Determine which set of keys to use ---
|
||||||
'man_left_side_land_view_path',
|
List<String> imageKeys;
|
||||||
'man_right_side_land_view_path',
|
switch (_selectedSamplingType) {
|
||||||
'man_filling_water_into_sample_bottle_path',
|
case 'In-Situ Sampling':
|
||||||
'man_seawater_in_clear_glass_bottle_path',
|
imageKeys = _inSituImageKeys;
|
||||||
'man_examine_preservative_ph_paper_path',
|
break;
|
||||||
'man_optional_photo_01_path',
|
case 'Tarball Sampling':
|
||||||
'man_optional_photo_02_path',
|
imageKeys = _tarballImageKeys; // Now uses the corrected list
|
||||||
'man_optional_photo_03_path',
|
break;
|
||||||
'man_optional_photo_04_path',
|
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 record in records) {
|
||||||
for (final key in imageKeys) {
|
for (final key in imageKeys) {
|
||||||
if (record[key] != null && (record[key] as String).isNotEmpty) {
|
if (record[key] != null && (record[key] as String).isNotEmpty) {
|
||||||
final String imagePathFromServer = record[key];
|
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);
|
fetchedUrls.add(fullUrl);
|
||||||
debugPrint("[Image Request] Found and constructed URL: $fullUrl");
|
debugPrint("[Image Request] Found and constructed URL: $fullUrl");
|
||||||
}
|
}
|
||||||
@ -155,7 +280,8 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
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.");
|
debugPrint("[Image Request] Successfully processed and constructed ${_imageUrls.length} image URLs.");
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
@ -261,8 +387,9 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
try {
|
try {
|
||||||
debugPrint("[Image Request] Sending email request to server for recipient: $toEmail");
|
debugPrint("[Image Request] Sending email request to server for recipient: $toEmail");
|
||||||
|
|
||||||
final stationCode = _selectedStation?['man_station_code'] ?? 'N/A';
|
// --- FIX: Use dynamic keys ---
|
||||||
final stationName = _selectedStation?['man_station_name'] ?? 'N/A';
|
final stationCode = _selectedStation?[_getStationCodeKey()] ?? 'N/A';
|
||||||
|
final stationName = _selectedStation?[_getStationNameKey()] ?? 'N/A';
|
||||||
final fullStationIdentifier = '$stationCode - $stationName';
|
final fullStationIdentifier = '$stationCode - $stationName';
|
||||||
|
|
||||||
final result = await apiService.marine.sendImageRequestEmail(
|
final result = await apiService.marine.sendImageRequestEmail(
|
||||||
@ -318,7 +445,11 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedSamplingType,
|
value: _selectedSamplingType,
|
||||||
items: _samplingTypes.map((type) => DropdownMenuItem(value: type, child: Text(type))).toList(),
|
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()),
|
decoration: const InputDecoration(labelText: 'Sampling Type *', border: OutlineInputBorder()),
|
||||||
validator: (value) => value == null ? 'Please select a type' : null,
|
validator: (value) => value == null ? 'Please select a type' : null,
|
||||||
),
|
),
|
||||||
@ -334,7 +465,8 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
_selectedCategoryName = null;
|
_selectedCategoryName = null;
|
||||||
_selectedStation = null;
|
_selectedStation = null;
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
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>[];
|
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();
|
categories.sort();
|
||||||
_categoriesForState = categories;
|
_categoriesForState = categories;
|
||||||
@ -355,8 +487,11 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
_selectedCategoryName = category;
|
_selectedCategoryName = category;
|
||||||
_selectedStation = null;
|
_selectedStation = null;
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final allStations = auth.manualStations ?? [];
|
// --- FIX: Use helper to get dynamic station list ---
|
||||||
_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'] ?? ''))) : [];
|
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,
|
validator: (val) => _selectedStateName != null && val == null ? "Category is required" : null,
|
||||||
@ -366,7 +501,12 @@ class _MarineImageRequestScreenState extends State<MarineImageRequestScreen> {
|
|||||||
items: _stationsForCategory,
|
items: _stationsForCategory,
|
||||||
selectedItem: _selectedStation,
|
selectedItem: _selectedStation,
|
||||||
enabled: _selectedCategoryName != null,
|
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..."))),
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Station..."))),
|
||||||
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Station *", border: OutlineInputBorder())),
|
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Station *", border: OutlineInputBorder())),
|
||||||
onChanged: (station) => setState(() => _selectedStation = station),
|
onChanged: (station) => setState(() => _selectedStation = station),
|
||||||
|
|||||||
@ -302,7 +302,7 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
|||||||
if (remark != null && remark.isNotEmpty)
|
if (remark != null && remark.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
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();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED LIFECYCLE METHOD ---
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
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(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// --- END: MODIFIED LIFECYCLE METHOD ---
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
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) {
|
void _disconnect(String type) {
|
||||||
final service = context.read<MarineInSituSamplingService>();
|
// Use the member variable, not context, for lifecycle safety
|
||||||
|
final service = _samplingService;
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
@ -312,12 +329,16 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
_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() {
|
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) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth');
|
||||||
}
|
}
|
||||||
@ -325,6 +346,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
_disconnect('serial');
|
_disconnect('serial');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
||||||
|
|
||||||
void _updateTextFields(Map<String, double> readings) {
|
void _updateTextFields(Map<String, double> readings) {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
|
|||||||
@ -114,7 +114,21 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
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) {
|
void _disconnect(String type) {
|
||||||
// Logic copied from RiverInSituStep3DataCaptureState._disconnect
|
// Logic copied from RiverInSituStep3DataCaptureState._disconnect
|
||||||
// Uses the correct _samplingService instance
|
// Uses the correct _samplingService instance
|
||||||
|
// --- START MODIFICATION ---
|
||||||
|
final service = _samplingService; // NEW: Use the member variable
|
||||||
|
// --- END MODIFICATION ---
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
_samplingService.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
_samplingService.disconnectFromSerial();
|
service.disconnectFromSerial();
|
||||||
}
|
}
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
_dataSubscription = null;
|
_dataSubscription = null;
|
||||||
@ -405,16 +422,20 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false; // Reset lockout state
|
_isLockedOut = false; // Reset lockout state
|
||||||
|
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disconnectFromAll() {
|
void _disconnectFromAll() {
|
||||||
// Logic copied from RiverInSituStep3DataCaptureState._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');
|
_disconnect('bluetooth');
|
||||||
}
|
}
|
||||||
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
|
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||||
_disconnect('serial');
|
_disconnect('serial');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,8 +120,22 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
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(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +361,9 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _disconnect(String type) {
|
void _disconnect(String type) {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
// --- START MODIFICATION ---
|
||||||
|
final service = _samplingService; // NEW: Use the member variable
|
||||||
|
// --- END MODIFICATION ---
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
@ -360,12 +376,15 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||||
|
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disconnectFromAll() {
|
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) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth');
|
||||||
}
|
}
|
||||||
@ -519,6 +538,8 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void _showSnackBar(String message, {bool isError = false}) {
|
void _showSnackBar(String message, {bool isError = false}) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
|||||||
@ -120,8 +120,22 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
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(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +361,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _disconnect(String type) {
|
void _disconnect(String type) {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
// --- START MODIFICATION ---
|
||||||
|
final service = _samplingService; // NEW: Use the member variable
|
||||||
|
// --- END MODIFICATION ---
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
@ -360,12 +376,15 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||||
|
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disconnectFromAll() {
|
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) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,10 +41,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
final Map<String, _ModuleSettings> _moduleSettings = {};
|
final Map<String, _ModuleSettings> _moduleSettings = {};
|
||||||
|
|
||||||
|
// This list seems correct based on your file
|
||||||
final List<Map<String, String>> _configurableModules = [
|
final List<Map<String, String>> _configurableModules = [
|
||||||
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
|
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
|
||||||
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
|
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
|
||||||
|
{'key': 'marine_investigative', 'name': 'Marine Investigative'},
|
||||||
{'key': 'river_in_situ', 'name': 'River In-Situ'},
|
{'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_installation', 'name': 'Air Installation'},
|
||||||
{'key': 'air_collection', 'name': 'Air Collection'},
|
{'key': 'air_collection', 'name': 'Air Collection'},
|
||||||
];
|
];
|
||||||
@ -70,9 +74,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
String _airLimitsSearchQuery = '';
|
String _airLimitsSearchQuery = '';
|
||||||
final TextEditingController _riverLimitsSearchController = TextEditingController();
|
final TextEditingController _riverLimitsSearchController = TextEditingController();
|
||||||
String _riverLimitsSearchQuery = '';
|
String _riverLimitsSearchQuery = '';
|
||||||
|
// --- MODIFICATION: Added missing controller ---
|
||||||
final TextEditingController _marineLimitsSearchController = TextEditingController();
|
final TextEditingController _marineLimitsSearchController = TextEditingController();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
String _marineLimitsSearchQuery = '';
|
String _marineLimitsSearchQuery = '';
|
||||||
|
|
||||||
|
// --- START: PAGINATION STATE FOR MARINE LIMITS ---
|
||||||
|
int _marineLimitsCurrentPage = 1;
|
||||||
|
final int _marineLimitsItemsPerPage = 15;
|
||||||
|
// --- END: PAGINATION STATE FOR MARINE LIMITS ---
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -88,7 +99,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_npeMarineLimitsSearchController.addListener(() => setState(() => _npeMarineLimitsSearchQuery = _npeMarineLimitsSearchController.text));
|
_npeMarineLimitsSearchController.addListener(() => setState(() => _npeMarineLimitsSearchQuery = _npeMarineLimitsSearchController.text));
|
||||||
_airLimitsSearchController.addListener(() => setState(() => _airLimitsSearchQuery = _airLimitsSearchController.text));
|
_airLimitsSearchController.addListener(() => setState(() => _airLimitsSearchQuery = _airLimitsSearchController.text));
|
||||||
_riverLimitsSearchController.addListener(() => setState(() => _riverLimitsSearchQuery = _riverLimitsSearchController.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
|
@override
|
||||||
@ -566,7 +583,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
labelText: 'Search Marine Limits',
|
labelText: 'Search Marine Limits',
|
||||||
hintText: 'Search by parameter or station',
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: const Text('App Version'),
|
title: const Text('App Version'),
|
||||||
subtitle: const Text('MMS Version 3.7.01'),
|
subtitle: const Text('MMS Version 3.8.01'),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
ListTile(
|
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) {
|
Widget _buildDestinationList(String title, List<Map<String, dynamic>> configs, String idKey) {
|
||||||
if (configs.isEmpty) {
|
if (configs.isEmpty) {
|
||||||
return const ListTile(
|
return const ListTile(
|
||||||
@ -808,9 +826,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||||
),
|
),
|
||||||
...configs.map((config) {
|
...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(
|
return CheckboxListTile(
|
||||||
title: Text(config['config_name'] ?? 'Unnamed'),
|
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,
|
value: config['is_enabled'] ?? false,
|
||||||
onChanged: (bool? value) {
|
onChanged: (bool? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -824,6 +852,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||||
return Padding(
|
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) {
|
Widget _buildChatIdEntry(String label, String value) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@ -905,11 +1019,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final stationId = item['station_id'];
|
final stationId = item['station_id'];
|
||||||
final station = stations.firstWhere((s) => s['station_id'] == stationId, orElse: () => {});
|
final station = stations.firstWhere((s) => s['station_id'] == stationId, orElse: () => {});
|
||||||
if (station.isNotEmpty) {
|
if (station.isNotEmpty) {
|
||||||
// --- START: MODIFICATION ---
|
|
||||||
final stationCode = station['man_station_code'] ?? 'N/A';
|
final stationCode = station['man_station_code'] ?? 'N/A';
|
||||||
final stationName = station['man_station_name'] ?? 'N/A';
|
final stationName = station['man_station_name'] ?? 'N/A';
|
||||||
contextSubtitle = 'Station: $stationCode - $stationName';
|
contextSubtitle = 'Station: $stationCode - $stationName';
|
||||||
// --- END: MODIFICATION ---
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -983,6 +1095,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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('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('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('Pass: ${item['ftp_pass']?.toString() ?? 'N/A'}', style: const TextStyle(fontSize: 12)),
|
||||||
|
|||||||
@ -226,6 +226,16 @@ class ApiService {
|
|||||||
await dbHelper.deleteRiverTriennialStations(id);
|
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': {
|
'departments': {
|
||||||
'endpoint': 'departments',
|
'endpoint': 'departments',
|
||||||
'handler': (d, id) async {
|
'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 {
|
class MarineApiService {
|
||||||
final BaseApiService _baseService;
|
final BaseApiService _baseService;
|
||||||
final TelegramService _telegramService; // Still needed if _handleAlerts were here
|
final TelegramService _telegramService;
|
||||||
final ServerConfigService _serverConfigService;
|
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);
|
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({
|
Future<Map<String, dynamic>> sendImageRequestEmail({
|
||||||
required String recipientEmail,
|
required String recipientEmail,
|
||||||
required List<String> imageUrls,
|
required List<String> imageUrls,
|
||||||
@ -511,65 +585,13 @@ class MarineApiService {
|
|||||||
|
|
||||||
return _baseService.postMultipart(
|
return _baseService.postMultipart(
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
endpoint: 'marine/images/send-email',
|
endpoint: 'marine/images/send-email', // **IMPORTANT**: Verify this endpoint
|
||||||
fields: fields,
|
fields: fields,
|
||||||
files: {},
|
files: {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getManualSamplingImages({
|
// --- KEPT METHODS (Unchanged) ---
|
||||||
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) ---
|
|
||||||
Future<Map<String, dynamic>> submitPreDepartureChecklist(MarineManualPreDepartureChecklistData data) async {
|
Future<Map<String, dynamic>> submitPreDepartureChecklist(MarineManualPreDepartureChecklistData data) async {
|
||||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
return _baseService.post(baseUrl, 'marine/checklist', data.toApiFormData());
|
return _baseService.post(baseUrl, 'marine/checklist', data.toApiFormData());
|
||||||
@ -590,6 +612,9 @@ class MarineApiService {
|
|||||||
return _baseService.get(baseUrl, 'marine/maintenance/previous');
|
return _baseService.get(baseUrl, 'marine/maintenance/previous');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// =======================================================================
|
||||||
|
// --- END OF MODIFIED SECTION ---
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
class RiverApiService {
|
class RiverApiService {
|
||||||
final BaseApiService _baseService;
|
final BaseApiService _baseService;
|
||||||
@ -665,7 +690,9 @@ class RiverApiService {
|
|||||||
class DatabaseHelper {
|
class DatabaseHelper {
|
||||||
static Database? _database;
|
static Database? _database;
|
||||||
static const String _dbName = 'app_data.db';
|
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
|
// compute-related static variables/methods REMOVED
|
||||||
|
|
||||||
@ -675,6 +702,9 @@ class DatabaseHelper {
|
|||||||
static const String _manualStationsTable = 'marine_manual_stations';
|
static const String _manualStationsTable = 'marine_manual_stations';
|
||||||
static const String _riverManualStationsTable = 'river_manual_stations';
|
static const String _riverManualStationsTable = 'river_manual_stations';
|
||||||
static const String _riverTriennialStationsTable = 'river_triennial_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 _tarballClassificationsTable = 'marine_tarball_classifications';
|
||||||
static const String _departmentsTable = 'departments';
|
static const String _departmentsTable = 'departments';
|
||||||
static const String _companiesTable = 'companies';
|
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 $_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 $_riverManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_riverTriennialStationsTable(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 $_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 $_departmentsTable(department_id INTEGER PRIMARY KEY, department_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_companiesTable(company_id INTEGER PRIMARY KEY, company_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");
|
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 ---
|
// --- Data Handling Methods ---
|
||||||
@ -1074,6 +1112,13 @@ class DatabaseHelper {
|
|||||||
Future<void> deleteRiverTriennialStations(List<dynamic> ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids);
|
Future<void> deleteRiverTriennialStations(List<dynamic> ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station');
|
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) =>
|
Future<void> upsertTarballClassifications(List<Map<String, dynamic>> data) =>
|
||||||
_upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification');
|
_upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification');
|
||||||
Future<void> deleteTarballClassifications(List<dynamic> ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids);
|
Future<void> deleteTarballClassifications(List<dynamic> ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids);
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
// lib/services/marine_api_service.dart
|
// 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/base_api_service.dart';
|
||||||
import 'package:environment_monitoring_app/services/telegram_service.dart';
|
import 'package:environment_monitoring_app/services/telegram_service.dart';
|
||||||
import 'package:environment_monitoring_app/services/server_config_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 {
|
class MarineApiService {
|
||||||
final BaseApiService _baseService;
|
final BaseApiService _baseService;
|
||||||
@ -25,4 +32,75 @@ class MarineApiService {
|
|||||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
return _baseService.get(baseUrl, 'marine/tarball/classifications');
|
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';
|
static const _defaultPrefsSavedKey = 'default_preferences_saved';
|
||||||
|
|
||||||
// Moved from settings.dart for central access
|
// Moved from settings.dart for central access
|
||||||
|
// This list now includes all your modules
|
||||||
final List<Map<String, String>> _configurableModules = [
|
final List<Map<String, String>> _configurableModules = [
|
||||||
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
|
{'key': 'marine_tarball', 'name': 'Marine Tarball'},
|
||||||
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
|
{'key': 'marine_in_situ', 'name': 'Marine In-Situ'},
|
||||||
|
{'key': 'marine_investigative', 'name': 'Marine Investigative'},
|
||||||
{'key': 'river_in_situ', 'name': 'River In-Situ'},
|
{'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_installation', 'name': 'Air Installation'},
|
||||||
{'key': 'air_collection', 'name': 'Air Collection'},
|
{'key': 'air_collection', 'name': 'Air Collection'},
|
||||||
];
|
];
|
||||||
@ -47,43 +51,24 @@ class UserPreferencesService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 2. Determine default API links
|
// 2. Determine default API links
|
||||||
|
// This is correct: Tick any API server marked as 'is_active' by default.
|
||||||
final defaultApiLinks = allApiConfigs.map((config) {
|
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};
|
return {...config, 'is_enabled': isEnabled};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// 3. Determine default FTP links
|
// 3. Determine default FTP links
|
||||||
|
// --- START MODIFICATION: Simplified logic using ftp_module ---
|
||||||
final defaultFtpLinks = allFtpConfigs.map((config) {
|
final defaultFtpLinks = allFtpConfigs.map((config) {
|
||||||
bool isEnabled = false; // Disable all by default
|
final String configModule = config['ftp_module'] ?? '';
|
||||||
switch (moduleKey) {
|
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
||||||
case 'marine_tarball':
|
|
||||||
if (config['config_name'] == 'pstw_marine_tarball' || config['config_name'] == 'tes_marine_tarball') {
|
// Enable if the config's module matches the current moduleKey AND it's active
|
||||||
isEnabled = true;
|
bool isEnabled = (configModule == moduleKey) && isActive;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return {...config, 'is_enabled': isEnabled};
|
return {...config, 'is_enabled': isEnabled};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
// 4. Save the default links to the database.
|
// 4. Save the default links to the database.
|
||||||
await saveApiLinksForModule(moduleKey, defaultApiLinks);
|
await saveApiLinksForModule(moduleKey, defaultApiLinks);
|
||||||
@ -139,7 +124,7 @@ class UserPreferencesService {
|
|||||||
// 3. Merge the two lists.
|
// 3. Merge the two lists.
|
||||||
return allApiConfigs.map((config) {
|
return allApiConfigs.map((config) {
|
||||||
final configId = config['api_config_id'];
|
final configId = config['api_config_id'];
|
||||||
bool isEnabled = false; // Default to disabled
|
bool isEnabled; // Default to disabled
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find if a link exists for this config ID in the user's saved preferences.
|
// 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,
|
(link) => link['api_config_id'] == configId,
|
||||||
// If no link is found, 'orElse' is not triggered, it throws.
|
// 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;
|
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// --- THIS IS THE FIX for API post-sync ---
|
||||||
// A 'firstWhere' with no match throws an error. We catch it here.
|
// 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.
|
// This means no link was saved (e.g., new config).
|
||||||
isEnabled = false;
|
// 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.
|
// Return a new map containing the original config details plus the 'is_enabled' flag.
|
||||||
@ -172,14 +161,22 @@ class UserPreferencesService {
|
|||||||
|
|
||||||
return allFtpConfigs.map((config) {
|
return allFtpConfigs.map((config) {
|
||||||
final configId = config['ftp_config_id'];
|
final configId = config['ftp_config_id'];
|
||||||
bool isEnabled = false;
|
bool isEnabled;
|
||||||
try {
|
try {
|
||||||
final matchingLink = savedLinks.firstWhere(
|
final matchingLink = savedLinks.firstWhere(
|
||||||
(link) => link['ftp_config_id'] == configId,
|
(link) => link['ftp_config_id'] == configId,
|
||||||
);
|
);
|
||||||
|
// A link was found, use the user's saved preference
|
||||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||||
} catch (e) {
|
} 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 {
|
return {
|
||||||
...config,
|
...config,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user