repair separate department parameter limit for river and marine
This commit is contained in:
parent
18c2bf3ec0
commit
fc14740b01
@ -70,7 +70,7 @@ import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.da
|
|||||||
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
|
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/report.dart' as marineManualReport;
|
import 'package:environment_monitoring_app/screens/marine/manual/report.dart' as marineManualReport;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/data_status_log.dart' as marineManualDataStatusLog;
|
import 'package:environment_monitoring_app/screens/marine/manual/data_status_log.dart' as marineManualDataStatusLog;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/image_request.dart' as marineManualImageRequest;
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest;
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry;
|
import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry;
|
||||||
@ -281,7 +281,8 @@ class _RootAppState extends State<RootApp> {
|
|||||||
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
||||||
'/marine/manual/report': (context) => marineManualReport.MarineManualReport(),
|
'/marine/manual/report': (context) => marineManualReport.MarineManualReport(),
|
||||||
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
||||||
'/marine/manual/image-request': (context) => marineManualImageRequest.MarineManualImageRequest(),
|
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
||||||
|
|
||||||
|
|
||||||
// Marine Continuous
|
// Marine Continuous
|
||||||
'/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(),
|
'/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(),
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'dart:io'; // Add this line at the top of these files
|
|
||||||
|
|
||||||
|
|
||||||
class MarineManualImageRequest extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<MarineManualImageRequest> createState() => _MarineManualImageRequestState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarineManualImageRequestState extends State<MarineManualImageRequest> {
|
|
||||||
XFile? _image;
|
|
||||||
final picker = ImagePicker();
|
|
||||||
final _descriptionController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
|
||||||
final pickedFile = await picker.pickImage(source: ImageSource.camera);
|
|
||||||
setState(() => _image = pickedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text("Marine Manual Image Request")),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: Icon(Icons.camera_alt),
|
|
||||||
label: Text("Capture Image"),
|
|
||||||
onPressed: _pickImage,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
if (_image != null)
|
|
||||||
Image.file(
|
|
||||||
File(_image!.path),
|
|
||||||
height: 200,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
decoration: InputDecoration(labelText: "Description"),
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
SizedBox(height: 24),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Submit logic here
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text("Image request submitted")),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text("Submit Request"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
493
lib/screens/marine/manual/marine_image_request.dart
Normal file
493
lib/screens/marine/manual/marine_image_request.dart
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
import '../../../services/api_service.dart';
|
||||||
|
|
||||||
|
class MarineImageRequestScreen extends StatefulWidget {
|
||||||
|
const MarineImageRequestScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MarineImageRequestScreen> createState() => _MarineImageRequestScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
String? _selectedStateName;
|
||||||
|
String? _selectedCategoryName;
|
||||||
|
Map<String, dynamic>? _selectedStation;
|
||||||
|
DateTime? _selectedDate;
|
||||||
|
|
||||||
|
List<String> _statesList = [];
|
||||||
|
List<String> _categoriesForState = [];
|
||||||
|
List<Map<String, dynamic>> _stationsForCategory = [];
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
List<String> _imageUrls = [];
|
||||||
|
final Set<String> _selectedImageUrls = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeStationFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_dateController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeStationFilters() {
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final allStations = auth.manualStations ?? [];
|
||||||
|
if (allStations.isNotEmpty) {
|
||||||
|
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
||||||
|
states.sort();
|
||||||
|
setState(() {
|
||||||
|
_statesList = states;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate() async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _selectedDate ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null && picked != _selectedDate) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDate = picked;
|
||||||
|
_dateController.text = DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _searchImages() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_imageUrls = [];
|
||||||
|
_selectedImageUrls.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPrint("[Image Request] Search button pressed. Starting validation.");
|
||||||
|
debugPrint("[Image Request] Selected Station: ${_selectedStation}");
|
||||||
|
debugPrint("[Image Request] Date: ${_selectedDate}");
|
||||||
|
debugPrint("[Image Request] Sampling Type: $_selectedSamplingType");
|
||||||
|
|
||||||
|
if (_selectedStation == null || _selectedDate == null || _selectedSamplingType == null) {
|
||||||
|
debugPrint("[Image Request] ERROR: Station, date, or sampling type is missing. Aborting search.");
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Error: Station, date, or sampling type is missing.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stationId = _selectedStation!['station_id'];
|
||||||
|
if (stationId == null) {
|
||||||
|
debugPrint("[Image Request] ERROR: Station ID is null.");
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Error: Invalid station data.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint("[Image Request] All checks passed. Calling API with Station ID: $stationId, Type: $_selectedSamplingType");
|
||||||
|
final result = await apiService.marine.getManualSamplingImages(
|
||||||
|
stationId: stationId,
|
||||||
|
samplingDate: _selectedDate!,
|
||||||
|
samplingType: _selectedSamplingType!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted && result['success'] == true) {
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
fetchedUrls.add(fullUrl);
|
||||||
|
debugPrint("[Image Request] Found and constructed URL: $fullUrl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_imageUrls = fetchedUrls;
|
||||||
|
});
|
||||||
|
debugPrint("[Image Request] Successfully processed and constructed ${_imageUrls.length} image URLs.");
|
||||||
|
} else if (mounted) {
|
||||||
|
debugPrint("[Image Request] API call failed. Message: ${result['message']}");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(result['message'] ?? 'Failed to fetch images.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[Image Request] An exception occurred during API call: $e");
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('An error occurred: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint("[Image Request] Form validation failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showEmailDialog() async {
|
||||||
|
final emailController = TextEditingController();
|
||||||
|
final dialogFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
bool isSending = false;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Send Images via Email'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isSending)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(width: 24),
|
||||||
|
Text("Sending..."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Form(
|
||||||
|
key: dialogFormKey,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Recipient Email Address',
|
||||||
|
hintText: 'user@example.com',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty || !RegExp(r'\S+@\S+\.\S+').hasMatch(value)) {
|
||||||
|
return 'Please enter a valid email address.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: isSending ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: isSending ? null : () async {
|
||||||
|
if (dialogFormKey.currentState!.validate()) {
|
||||||
|
setDialogState(() => isSending = true);
|
||||||
|
await _sendEmailRequestToServer(emailController.text);
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Send'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendEmailRequestToServer(String toEmail) async {
|
||||||
|
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||||
|
|
||||||
|
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';
|
||||||
|
final fullStationIdentifier = '$stationCode - $stationName';
|
||||||
|
|
||||||
|
final result = await apiService.marine.sendImageRequestEmail(
|
||||||
|
recipientEmail: toEmail,
|
||||||
|
imageUrls: _selectedImageUrls.toList(),
|
||||||
|
stationName: fullStationIdentifier,
|
||||||
|
samplingDate: _dateController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if(mounted) {
|
||||||
|
if (result['success'] == true) {
|
||||||
|
debugPrint("[Image Request] Server responded with success for email request.");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Success! Email is being sent by the server.'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint("[Image Request] Server responded with failure for email request. Message: ${result['message']}");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error: ${result['message']}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[Image Request] An exception occurred while sending email request: $e");
|
||||||
|
if(mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('An error occurred: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Marine Image Request")),
|
||||||
|
body: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
children: [
|
||||||
|
Text("Image Search Filters", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedSamplingType,
|
||||||
|
items: _samplingTypes.map((type) => DropdownMenuItem(value: type, child: Text(type))).toList(),
|
||||||
|
onChanged: (value) => setState(() => _selectedSamplingType = value),
|
||||||
|
decoration: const InputDecoration(labelText: 'Sampling Type *', border: OutlineInputBorder()),
|
||||||
|
validator: (value) => value == null ? 'Please select a type' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownSearch<String>(
|
||||||
|
items: _statesList,
|
||||||
|
selectedItem: _selectedStateName,
|
||||||
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
||||||
|
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select State *", border: OutlineInputBorder())),
|
||||||
|
onChanged: (state) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStateName = state;
|
||||||
|
_selectedCategoryName = null;
|
||||||
|
_selectedStation = null;
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final allStations = auth.manualStations ?? [];
|
||||||
|
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;
|
||||||
|
_stationsForCategory = [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validator: (val) => val == null ? "State is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownSearch<String>(
|
||||||
|
items: _categoriesForState,
|
||||||
|
selectedItem: _selectedCategoryName,
|
||||||
|
enabled: _selectedStateName != null,
|
||||||
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Category..."))),
|
||||||
|
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Category *", border: OutlineInputBorder())),
|
||||||
|
onChanged: (category) {
|
||||||
|
setState(() {
|
||||||
|
_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'] ?? ''))) : [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validator: (val) => _selectedStateName != null && val == null ? "Category is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownSearch<Map<String, dynamic>>(
|
||||||
|
items: _stationsForCategory,
|
||||||
|
selectedItem: _selectedStation,
|
||||||
|
enabled: _selectedCategoryName != null,
|
||||||
|
itemAsString: (station) => "${station['man_station_code']} - ${station['man_station_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),
|
||||||
|
validator: (val) => _selectedCategoryName != null && val == null ? "Station is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _dateController,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Select Date *',
|
||||||
|
hintText: 'Tap to pick a date',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: _selectDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: _selectDate,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Date is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
label: const Text('Search Images'),
|
||||||
|
onPressed: _isLoading ? null : _searchImages,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Divider(thickness: 1),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text("Results", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildResults(),
|
||||||
|
if (_selectedImageUrls.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.email_outlined),
|
||||||
|
label: Text('Send (${_selectedImageUrls.length}) Selected Image(s)'),
|
||||||
|
onPressed: _showEmailDialog,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildResults() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (_imageUrls.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'No images found. Please adjust your filters and search again.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 1.0,
|
||||||
|
),
|
||||||
|
itemCount: _imageUrls.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final imageUrl = _imageUrls[index];
|
||||||
|
final isSelected = _selectedImageUrls.contains(imageUrl);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (isSelected) {
|
||||||
|
_selectedImageUrls.remove(imageUrl);
|
||||||
|
} else {
|
||||||
|
_selectedImageUrls.add(imageUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
elevation: 2.0,
|
||||||
|
child: GridTile(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Image.network(
|
||||||
|
imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Icon(Icons.broken_image, color: Colors.grey, size: 40);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Container(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
child: const Icon(Icons.check_circle, color: Colors.white, size: 40),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -262,6 +262,12 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
|
// --- START DEBUG ---
|
||||||
|
debugPrint("--- Nearby Station Selected in Step 1 ---");
|
||||||
|
debugPrint("Selected Station Data: $station");
|
||||||
|
debugPrint("Selected Station ID (man_station_id): ${station['man_station_id']}");
|
||||||
|
// --- END DEBUG ---
|
||||||
|
|
||||||
// Recalculate distance
|
// Recalculate distance
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
@ -459,6 +465,17 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
widget.data.stationLongitude = station?['man_longitude']?.toString();
|
widget.data.stationLongitude = station?['man_longitude']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
|
// --- START DEBUG ---
|
||||||
|
if (station != null) {
|
||||||
|
debugPrint("--- Station Selected in Step 1 ---");
|
||||||
|
debugPrint("Selected Station Data: $station");
|
||||||
|
debugPrint("Selected Station ID (man_station_id): ${station['man_station_id']}");
|
||||||
|
} else {
|
||||||
|
debugPrint("--- Station Deselected in Step 1 ---");
|
||||||
|
}
|
||||||
|
// --- END DEBUG ---
|
||||||
|
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
}),
|
}),
|
||||||
validator: (val) => widget.data.selectedCategoryName != null && val == null ? "Station is required" : null,
|
validator: (val) => widget.data.selectedCategoryName != null && val == null ? "Station is required" : null,
|
||||||
|
|||||||
@ -315,11 +315,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
|
|
||||||
final currentReadings = _captureReadingsToMap();
|
final currentReadings = _captureReadingsToMap();
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
// --- START: MODIFICATION ---
|
|
||||||
// The `parameterLimits` getter was removed from AuthProvider.
|
|
||||||
// This now correctly uses the new `marineParameterLimits` getter.
|
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
// --- END: MODIFICATION ---
|
|
||||||
final outOfBoundsParams = _validateParameters(currentReadings, marineLimits);
|
final outOfBoundsParams = _validateParameters(currentReadings, marineLimits);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -345,6 +341,19 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
|
final int? stationId = widget.data.selectedStation?['station_id'];
|
||||||
|
|
||||||
|
debugPrint("--- Parameter Validation Start ---");
|
||||||
|
debugPrint("Selected Station ID: $stationId");
|
||||||
|
debugPrint("Total Marine Limits Loaded: ${limits.length}");
|
||||||
|
|
||||||
|
// --- START DEBUG: Add type inspection ---
|
||||||
|
if (limits.isNotEmpty && stationId != null) {
|
||||||
|
debugPrint("Inspecting the first loaded limit record: ${limits.first}");
|
||||||
|
debugPrint("Type of Selected Station ID ($stationId): ${stationId.runtimeType}");
|
||||||
|
debugPrint("Type of man_station_id in first limit record: ${limits.first['man_station_id']?.runtimeType}");
|
||||||
|
}
|
||||||
|
// --- END DEBUG ---
|
||||||
|
|
||||||
double? _parseLimitValue(dynamic value) {
|
double? _parseLimitValue(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@ -359,10 +368,24 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
final limitData = limits.firstWhere(
|
debugPrint("Checking parameter: '$limitName' (key: '$key')");
|
||||||
(l) => l['param_parameter_list'] == limitName,
|
|
||||||
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
|
if (stationId != null) {
|
||||||
|
// --- START FIX: Use type-safe comparison ---
|
||||||
|
limitData = limits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
|
// --- END FIX ---
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
debugPrint(" > Found station-specific limit for Station ID $stationId: $limitData");
|
||||||
|
} else {
|
||||||
|
debugPrint(" > No station-specific limit found for Station ID $stationId. Skipping check for this parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
if (limitData.isNotEmpty) {
|
if (limitData.isNotEmpty) {
|
||||||
final lowerLimit = _parseLimitValue(limitData['param_lower_limit']);
|
final lowerLimit = _parseLimitValue(limitData['param_lower_limit']);
|
||||||
@ -379,6 +402,9 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debugPrint("--- Parameter Validation End ---");
|
||||||
|
|
||||||
return invalidParams;
|
return invalidParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,12 +39,9 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
/// Re-validates the final parameters against the defined limits.
|
/// Re-validates the final parameters against the defined limits.
|
||||||
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
// --- START MODIFICATION ---
|
|
||||||
// The `parameterLimits` getter was removed from AuthProvider.
|
|
||||||
// This now correctly uses the new `marineParameterLimits` getter.
|
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
// --- END MODIFICATION ---
|
|
||||||
final Set<String> invalidKeys = {};
|
final Set<String> invalidKeys = {};
|
||||||
|
final int? stationId = data.selectedStation?['station_id'];
|
||||||
|
|
||||||
final readings = {
|
final readings = {
|
||||||
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
||||||
@ -66,7 +63,16 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
final limitData = marineLimits.firstWhere((l) => l['param_parameter_list'] == limitName, orElse: () => {});
|
// START MODIFICATION: Only check for station-specific limits
|
||||||
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
|
if (stationId != null) {
|
||||||
|
limitData = marineLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['station_id'] == stationId,
|
||||||
|
orElse: () => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// END MODIFICATION
|
||||||
|
|
||||||
if (limitData.isNotEmpty) {
|
if (limitData.isNotEmpty) {
|
||||||
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class MarineHomePage extends StatelessWidget {
|
|||||||
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
|
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
|
||||||
|
|
||||||
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
||||||
//SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
||||||
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -34,10 +34,11 @@ class ApiService {
|
|||||||
static const String imageBaseUrl = 'https://mms-apiv4.pstw.com.my/';
|
static const String imageBaseUrl = 'https://mms-apiv4.pstw.com.my/';
|
||||||
|
|
||||||
ApiService({required TelegramService telegramService}) {
|
ApiService({required TelegramService telegramService}) {
|
||||||
marine = MarineApiService(_baseService, telegramService, _serverConfigService);
|
marine = MarineApiService(_baseService, telegramService, _serverConfigService, dbHelper);
|
||||||
river = RiverApiService(_baseService, telegramService, _serverConfigService);
|
river = RiverApiService(_baseService, telegramService, _serverConfigService, dbHelper);
|
||||||
air = AirApiService(_baseService, telegramService, _serverConfigService);
|
air = AirApiService(_baseService, telegramService, _serverConfigService);
|
||||||
}
|
}
|
||||||
|
// --- END: FIX FOR CONSTRUCTOR ERROR ---
|
||||||
|
|
||||||
// --- Core API Methods ---
|
// --- Core API Methods ---
|
||||||
|
|
||||||
@ -470,8 +471,63 @@ class MarineApiService {
|
|||||||
final BaseApiService _baseService;
|
final BaseApiService _baseService;
|
||||||
final TelegramService _telegramService;
|
final TelegramService _telegramService;
|
||||||
final ServerConfigService _serverConfigService;
|
final ServerConfigService _serverConfigService;
|
||||||
|
final DatabaseHelper _dbHelper;
|
||||||
|
|
||||||
MarineApiService(this._baseService, this._telegramService, this._serverConfigService);
|
MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
|
||||||
|
|
||||||
|
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),
|
||||||
|
'stationName': stationName,
|
||||||
|
'samplingDate': samplingDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
return _baseService.postMultipart(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
endpoint: 'marine/images/send-email',
|
||||||
|
fields: fields,
|
||||||
|
files: {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- START: FIX - Replaced mock with a real API call ---
|
||||||
|
Future<Map<String, dynamic>> getManualSamplingImages({
|
||||||
|
required int stationId,
|
||||||
|
required DateTime samplingDate,
|
||||||
|
required String samplingType,
|
||||||
|
}) async {
|
||||||
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
|
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// The backend now returns a root 'data' key which the base service handles.
|
||||||
|
// However, the PHP controller wraps the results again in a 'data' key inside the main data object.
|
||||||
|
// We need to extract this nested list.
|
||||||
|
if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'data': response['data']['data'],
|
||||||
|
'message': response['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original response if the structure isn't as expected, or if it's an error.
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
// --- END: FIX ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getTarballStations() async {
|
Future<Map<String, dynamic>> getTarballStations() async {
|
||||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
@ -567,7 +623,7 @@ class MarineApiService {
|
|||||||
Future<void> _handleInSituSuccessAlert(InSituSamplingData data,
|
Future<void> _handleInSituSuccessAlert(InSituSamplingData data,
|
||||||
List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
||||||
try {
|
try {
|
||||||
final message = data.generateTelegramAlertMessage(isDataOnly: isDataOnly);
|
final message = await _generateInSituAlertMessage(data, isDataOnly: isDataOnly);
|
||||||
final bool wasSent = await _telegramService.sendAlertImmediately('marine_in_situ', message, appSettings);
|
final bool wasSent = await _telegramService.sendAlertImmediately('marine_in_situ', message, appSettings);
|
||||||
if (!wasSent) {
|
if (!wasSent) {
|
||||||
await _telegramService.queueMessage('marine_in_situ', message, appSettings);
|
await _telegramService.queueMessage('marine_in_situ', message, appSettings);
|
||||||
@ -577,6 +633,111 @@ class MarineApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _generateInSituAlertMessage(InSituSamplingData data, {required bool isDataOnly}) async {
|
||||||
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
|
final stationName = data.selectedStation?['man_station_name'] ?? 'N/A';
|
||||||
|
final stationCode = data.selectedStation?['man_station_code'] ?? 'N/A';
|
||||||
|
final distanceKm = data.distanceDifferenceInKm ?? 0;
|
||||||
|
final distanceMeters = (distanceKm * 1000).toStringAsFixed(0);
|
||||||
|
final distanceRemarks = data.distanceDifferenceRemarks ?? 'N/A';
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('✅ *Marine In-Situ Sample ${submissionType} Submitted:*')
|
||||||
|
..writeln()
|
||||||
|
..writeln('*Station Name & Code:* $stationName ($stationCode)')
|
||||||
|
..writeln('*Date of Submitted:* ${data.samplingDate}')
|
||||||
|
..writeln('*Submitted by User:* ${data.firstSamplerName}')
|
||||||
|
..writeln('*Sonde ID:* ${data.sondeId ?? 'N/A'}')
|
||||||
|
..writeln('*Status of Submission:* Successful');
|
||||||
|
|
||||||
|
if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
|
||||||
|
buffer
|
||||||
|
..writeln()
|
||||||
|
..writeln('🔔 *Distance Alert:*')
|
||||||
|
..writeln('*Distance from station:* $distanceMeters meters');
|
||||||
|
if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
|
||||||
|
buffer.writeln('*Remarks for distance:* $distanceRemarks');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data);
|
||||||
|
if (outOfBoundsAlert.isNotEmpty) {
|
||||||
|
buffer.write(outOfBoundsAlert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getOutOfBoundsAlertSection(InSituSamplingData data) async {
|
||||||
|
const Map<String, String> _parameterKeyToLimitName = {
|
||||||
|
'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH',
|
||||||
|
'salinity': 'Salinity', 'electricalConductivity': 'Conductivity', 'temperature': 'Temperature',
|
||||||
|
'tds': 'TDS', 'turbidity': 'Turbidity', 'tss': 'TSS', 'batteryVoltage': 'Battery',
|
||||||
|
};
|
||||||
|
|
||||||
|
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
|
||||||
|
if (allLimits.isEmpty) return "";
|
||||||
|
|
||||||
|
final int? stationId = data.selectedStation?['station_id'];
|
||||||
|
final readings = {
|
||||||
|
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
||||||
|
'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity,
|
||||||
|
'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity,
|
||||||
|
'tss': data.tss, 'batteryVoltage': data.batteryVoltage,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<String> outOfBoundsMessages = [];
|
||||||
|
|
||||||
|
double? parseLimitValue(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readings.forEach((key, value) {
|
||||||
|
if (value == null || value == -999.0) return;
|
||||||
|
|
||||||
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
// START MODIFICATION: Only check for station-specific limits
|
||||||
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
|
if (stationId != null) {
|
||||||
|
limitData = allLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['station_id'] == stationId,
|
||||||
|
orElse: () => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// END MODIFICATION
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
final upperLimit = parseLimitValue(limitData['param_upper_limit']);
|
||||||
|
|
||||||
|
if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) {
|
||||||
|
final valueStr = value.toStringAsFixed(5);
|
||||||
|
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
outOfBoundsMessages.add('- *$limitName*: `$valueStr` (Limit: `$lowerStr` - `$upperStr`)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outOfBoundsMessages.isEmpty) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln()
|
||||||
|
..writeln('⚠️ *Parameter Limit Alert:*')
|
||||||
|
..writeln('The following parameters were outside their defined limits:');
|
||||||
|
buffer.writeAll(outOfBoundsMessages, '\n');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitTarballSample({
|
Future<Map<String, dynamic>> submitTarballSample({
|
||||||
required Map<String, String> formData,
|
required Map<String, String> formData,
|
||||||
required Map<String, File?> imageFiles,
|
required Map<String, File?> imageFiles,
|
||||||
@ -666,8 +827,9 @@ class RiverApiService {
|
|||||||
final BaseApiService _baseService;
|
final BaseApiService _baseService;
|
||||||
final TelegramService _telegramService;
|
final TelegramService _telegramService;
|
||||||
final ServerConfigService _serverConfigService;
|
final ServerConfigService _serverConfigService;
|
||||||
|
final DatabaseHelper _dbHelper;
|
||||||
|
|
||||||
RiverApiService(this._baseService, this._telegramService, this._serverConfigService);
|
RiverApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getManualStations() async {
|
Future<Map<String, dynamic>> getManualStations() async {
|
||||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
@ -749,6 +911,17 @@ class RiverApiService {
|
|||||||
Future<void> _handleInSituSuccessAlert(
|
Future<void> _handleInSituSuccessAlert(
|
||||||
Map<String, String> formData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
Map<String, String> formData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
||||||
try {
|
try {
|
||||||
|
final String message = await _generateInSituAlertMessage(formData, isDataOnly: isDataOnly);
|
||||||
|
final bool wasSent = await _telegramService.sendAlertImmediately('river_in_situ', message, appSettings);
|
||||||
|
if (!wasSent) {
|
||||||
|
await _telegramService.queueMessage('river_in_situ', message, appSettings);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to handle River Telegram alert: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _generateInSituAlertMessage(Map<String, String> formData, {required bool isDataOnly}) async {
|
||||||
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
final stationName = formData['r_man_station_name'] ?? 'N/A';
|
final stationName = formData['r_man_station_name'] ?? 'N/A';
|
||||||
final stationCode = formData['r_man_station_code'] ?? 'N/A';
|
final stationCode = formData['r_man_station_code'] ?? 'N/A';
|
||||||
@ -771,22 +944,89 @@ class RiverApiService {
|
|||||||
if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
|
if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
|
||||||
buffer
|
buffer
|
||||||
..writeln()
|
..writeln()
|
||||||
..writeln('🔔 *Alert:*')
|
..writeln('🔔 *Distance Alert:*')
|
||||||
..writeln('*Distance from station:* $distanceMeters meters');
|
..writeln('*Distance from station:* $distanceMeters meters');
|
||||||
if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
|
if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
|
||||||
buffer.writeln('*Remarks for distance:* $distanceRemarks');
|
buffer.writeln('*Remarks for distance:* $distanceRemarks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String message = buffer.toString();
|
final outOfBoundsAlert = await _getOutOfBoundsAlertSection(formData);
|
||||||
|
if (outOfBoundsAlert.isNotEmpty) {
|
||||||
|
buffer.write(outOfBoundsAlert);
|
||||||
|
}
|
||||||
|
|
||||||
final bool wasSent = await _telegramService.sendAlertImmediately('river_in_situ', message, appSettings);
|
return buffer.toString();
|
||||||
if (!wasSent) {
|
|
||||||
await _telegramService.queueMessage('river_in_situ', message, appSettings);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Failed to handle River Telegram alert: $e");
|
Future<String> _getOutOfBoundsAlertSection(Map<String, String> formData) async {
|
||||||
|
const Map<String, String> _formKeyToLimitName = {
|
||||||
|
'r_man_ph': 'pH',
|
||||||
|
'r_man_temperature': 'Temperature',
|
||||||
|
'r_man_dissolved_oxygen': 'Dissolved Oxygen',
|
||||||
|
'r_man_conductivity': 'Conductivity',
|
||||||
|
'r_man_salinity': 'Salinity',
|
||||||
|
'r_man_turbidity': 'Turbidity',
|
||||||
|
'r_man_tds': 'TDS',
|
||||||
|
'r_man_sonde_battery': 'Sonde Battery',
|
||||||
|
};
|
||||||
|
|
||||||
|
final allLimits = await _dbHelper.loadRiverParameterLimits() ?? [];
|
||||||
|
if (allLimits.isEmpty) return "";
|
||||||
|
|
||||||
|
final int? stationId = int.tryParse(formData['r_man_station_id'] ?? '');
|
||||||
|
final List<String> outOfBoundsMessages = [];
|
||||||
|
|
||||||
|
double? parseLimitValue(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formData.forEach((key, valueStr) {
|
||||||
|
final double? value = double.tryParse(valueStr);
|
||||||
|
if (value == null || value == -999.0) return;
|
||||||
|
|
||||||
|
final limitName = _formKeyToLimitName[key];
|
||||||
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
Map<String, dynamic> limitData = {};
|
||||||
|
if (stationId != null) {
|
||||||
|
limitData = allLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['r_man_station_id'] == stationId,
|
||||||
|
orElse: () => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (limitData.isEmpty) {
|
||||||
|
limitData = allLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['r_man_station_id'] == null,
|
||||||
|
orElse: () => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
final upperLimit = parseLimitValue(limitData['param_upper_limit']);
|
||||||
|
|
||||||
|
if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) {
|
||||||
|
final valueFmt = value.toStringAsFixed(5);
|
||||||
|
final lowerFmt = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
final upperFmt = upperLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
outOfBoundsMessages.add('- *$limitName*: `$valueFmt` (Limit: `$lowerFmt` - `$upperFmt`)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outOfBoundsMessages.isEmpty) return "";
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln()
|
||||||
|
..writeln('⚠️ *Parameter Limit Alert:*')
|
||||||
|
..writeln('The following parameters were outside their defined limits:');
|
||||||
|
buffer.writeAll(outOfBoundsMessages, '\n');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -797,10 +1037,7 @@ 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';
|
||||||
// --- START: INCREMENTED DB VERSION ---
|
|
||||||
static const int _dbVersion = 23;
|
static const int _dbVersion = 23;
|
||||||
// --- END: INCREMENTED DB VERSION ---
|
|
||||||
|
|
||||||
static const String _profileTable = 'user_profile';
|
static const String _profileTable = 'user_profile';
|
||||||
static const String _usersTable = 'all_users';
|
static const String _usersTable = 'all_users';
|
||||||
static const String _tarballStationsTable = 'marine_tarball_stations';
|
static const String _tarballStationsTable = 'marine_tarball_stations';
|
||||||
@ -817,11 +1054,9 @@ class DatabaseHelper {
|
|||||||
static const String _statesTable = 'states';
|
static const String _statesTable = 'states';
|
||||||
static const String _appSettingsTable = 'app_settings';
|
static const String _appSettingsTable = 'app_settings';
|
||||||
static const String _parameterLimitsTable = 'manual_parameter_limits';
|
static const String _parameterLimitsTable = 'manual_parameter_limits';
|
||||||
// --- START: ADDED NEW TABLE CONSTANTS ---
|
|
||||||
static const String _npeParameterLimitsTable = 'npe_parameter_limits';
|
static const String _npeParameterLimitsTable = 'npe_parameter_limits';
|
||||||
static const String _marineParameterLimitsTable = 'marine_parameter_limits';
|
static const String _marineParameterLimitsTable = 'marine_parameter_limits';
|
||||||
static const String _riverParameterLimitsTable = 'river_parameter_limits';
|
static const String _riverParameterLimitsTable = 'river_parameter_limits';
|
||||||
// --- END: ADDED NEW TABLE CONSTANTS ---
|
|
||||||
static const String _apiConfigsTable = 'api_configurations';
|
static const String _apiConfigsTable = 'api_configurations';
|
||||||
static const String _ftpConfigsTable = 'ftp_configurations';
|
static const String _ftpConfigsTable = 'ftp_configurations';
|
||||||
static const String _retryQueueTable = 'retry_queue';
|
static const String _retryQueueTable = 'retry_queue';
|
||||||
@ -867,11 +1102,9 @@ class DatabaseHelper {
|
|||||||
await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)');
|
await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)');
|
await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
// --- START: ADDED CREATE TABLE FOR NEW LIMITS ---
|
|
||||||
await db.execute('CREATE TABLE $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
// --- END: ADDED CREATE TABLE FOR NEW LIMITS ---
|
|
||||||
await db.execute('CREATE TABLE $_apiConfigsTable(api_config_id INTEGER PRIMARY KEY, config_json TEXT)');
|
await db.execute('CREATE TABLE $_apiConfigsTable(api_config_id INTEGER PRIMARY KEY, config_json TEXT)');
|
||||||
await db.execute('CREATE TABLE $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)');
|
await db.execute('CREATE TABLE $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)');
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
@ -1015,16 +1248,13 @@ class DatabaseHelper {
|
|||||||
debugPrint("Upgrade warning: Failed to add password_hash column to users table (may already exist): $e");
|
debugPrint("Upgrade warning: Failed to add password_hash column to users table (may already exist): $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- START: ADDED UPGRADE LOGIC FOR NEW TABLES ---
|
|
||||||
if (oldVersion < 23) {
|
if (oldVersion < 23) {
|
||||||
await db.execute('CREATE TABLE IF NOT EXISTS $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE IF NOT EXISTS $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
await db.execute('CREATE TABLE IF NOT EXISTS $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE IF NOT EXISTS $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
await db.execute('CREATE TABLE IF NOT EXISTS $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
await db.execute('CREATE TABLE IF NOT EXISTS $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
|
||||||
}
|
}
|
||||||
// --- END: ADDED UPGRADE LOGIC FOR NEW TABLES ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an "upsert": inserts new records or replaces existing ones.
|
|
||||||
Future<void> _upsertData(String table, String idKeyName, List<Map<String, dynamic>> data, String jsonKeyName) async {
|
Future<void> _upsertData(String table, String idKeyName, List<Map<String, dynamic>> data, String jsonKeyName) async {
|
||||||
if (data.isEmpty) return;
|
if (data.isEmpty) return;
|
||||||
final db = await database;
|
final db = await database;
|
||||||
@ -1040,7 +1270,6 @@ class DatabaseHelper {
|
|||||||
debugPrint("Upserted ${data.length} items into $table");
|
debugPrint("Upserted ${data.length} items into $table");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a list of records from a table by their primary keys.
|
|
||||||
Future<void> _deleteData(String table, String idKeyName, List<dynamic> ids) async {
|
Future<void> _deleteData(String table, String idKeyName, List<dynamic> ids) async {
|
||||||
if (ids.isEmpty) return;
|
if (ids.isEmpty) return;
|
||||||
final db = await database;
|
final db = await database;
|
||||||
@ -1075,9 +1304,6 @@ class DatabaseHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: Offline Authentication and User Upsert Methods ---
|
|
||||||
|
|
||||||
/// Retrieves a user's profile JSON from the users table by email.
|
|
||||||
Future<Map<String, dynamic>?> loadProfileByEmail(String email) async {
|
Future<Map<String, dynamic>?> loadProfileByEmail(String email) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
@ -1097,8 +1323,6 @@ class DatabaseHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts or replaces a user's profile and credentials.
|
|
||||||
/// This ensures the record exists when caching credentials during login.
|
|
||||||
Future<void> upsertUserWithCredentials({
|
Future<void> upsertUserWithCredentials({
|
||||||
required Map<String, dynamic> profile,
|
required Map<String, dynamic> profile,
|
||||||
required String passwordHash,
|
required String passwordHash,
|
||||||
@ -1117,7 +1341,6 @@ class DatabaseHelper {
|
|||||||
debugPrint("Upserted user credentials for ${profile['email']}");
|
debugPrint("Upserted user credentials for ${profile['email']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the stored password hash for a user by email.
|
|
||||||
Future<String?> getUserPasswordHashByEmail(String email) async {
|
Future<String?> getUserPasswordHashByEmail(String email) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final List<Map<String, dynamic>> result = await db.query(
|
final List<Map<String, dynamic>> result = await db.query(
|
||||||
@ -1132,18 +1355,14 @@ class DatabaseHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: Custom upsert method for user sync to prevent hash overwrite ---
|
|
||||||
/// Upserts user data from sync without overwriting the local password hash.
|
|
||||||
Future<void> upsertUsers(List<Map<String, dynamic>> data) async {
|
Future<void> upsertUsers(List<Map<String, dynamic>> data) async {
|
||||||
if (data.isEmpty) return;
|
if (data.isEmpty) return;
|
||||||
final db = await database;
|
final db = await database;
|
||||||
for (var item in data) {
|
for (var item in data) {
|
||||||
final updateData = {
|
final updateData = {
|
||||||
//'email': item['email'],
|
|
||||||
'user_json': jsonEncode(item),
|
'user_json': jsonEncode(item),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to update existing record first, preserving other columns like password_hash
|
|
||||||
int count = await db.update(
|
int count = await db.update(
|
||||||
_usersTable,
|
_usersTable,
|
||||||
updateData,
|
updateData,
|
||||||
@ -1151,7 +1370,6 @@ class DatabaseHelper {
|
|||||||
whereArgs: [item['user_id']],
|
whereArgs: [item['user_id']],
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no record was updated (count == 0), insert a new record.
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
await db.insert(
|
await db.insert(
|
||||||
_usersTable,
|
_usersTable,
|
||||||
@ -1159,7 +1377,6 @@ class DatabaseHelper {
|
|||||||
'user_id': item['user_id'],
|
'user_id': item['user_id'],
|
||||||
'email': item['email'],
|
'email': item['email'],
|
||||||
'user_json': jsonEncode(item),
|
'user_json': jsonEncode(item),
|
||||||
// password_hash will be null for a new user until they log in on this device.
|
|
||||||
},
|
},
|
||||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||||
);
|
);
|
||||||
@ -1167,7 +1384,6 @@ class DatabaseHelper {
|
|||||||
}
|
}
|
||||||
debugPrint("Upserted ${data.length} user items in custom upsert method.");
|
debugPrint("Upserted ${data.length} user items in custom upsert method.");
|
||||||
}
|
}
|
||||||
// --- END: Custom upsert method ---
|
|
||||||
|
|
||||||
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
|
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
|
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
|
||||||
@ -1234,7 +1450,6 @@ class DatabaseHelper {
|
|||||||
Future<void> deleteParameterLimits(List<dynamic> ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids);
|
Future<void> deleteParameterLimits(List<dynamic> ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit');
|
Future<List<Map<String, dynamic>>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit');
|
||||||
|
|
||||||
// --- START: ADDED NEW DB METHODS FOR PARAMETER LIMITS ---
|
|
||||||
Future<void> upsertNpeParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_npeParameterLimitsTable, 'param_autoid', data, 'limit');
|
Future<void> upsertNpeParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_npeParameterLimitsTable, 'param_autoid', data, 'limit');
|
||||||
Future<void> deleteNpeParameterLimits(List<dynamic> ids) => _deleteData(_npeParameterLimitsTable, 'param_autoid', ids);
|
Future<void> deleteNpeParameterLimits(List<dynamic> ids) => _deleteData(_npeParameterLimitsTable, 'param_autoid', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadNpeParameterLimits() => _loadData(_npeParameterLimitsTable, 'limit');
|
Future<List<Map<String, dynamic>>?> loadNpeParameterLimits() => _loadData(_npeParameterLimitsTable, 'limit');
|
||||||
@ -1246,7 +1461,6 @@ class DatabaseHelper {
|
|||||||
Future<void> upsertRiverParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_riverParameterLimitsTable, 'param_autoid', data, 'limit');
|
Future<void> upsertRiverParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_riverParameterLimitsTable, 'param_autoid', data, 'limit');
|
||||||
Future<void> deleteRiverParameterLimits(List<dynamic> ids) => _deleteData(_riverParameterLimitsTable, 'param_autoid', ids);
|
Future<void> deleteRiverParameterLimits(List<dynamic> ids) => _deleteData(_riverParameterLimitsTable, 'param_autoid', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadRiverParameterLimits() => _loadData(_riverParameterLimitsTable, 'limit');
|
Future<List<Map<String, dynamic>>?> loadRiverParameterLimits() => _loadData(_riverParameterLimitsTable, 'limit');
|
||||||
// --- END: ADDED NEW DB METHODS FOR PARAMETER LIMITS ---
|
|
||||||
|
|
||||||
Future<void> upsertApiConfigs(List<Map<String, dynamic>> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config');
|
Future<void> upsertApiConfigs(List<Map<String, dynamic>> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config');
|
||||||
Future<void> deleteApiConfigs(List<dynamic> ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids);
|
Future<void> deleteApiConfigs(List<dynamic> ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids);
|
||||||
|
|||||||
@ -405,8 +405,8 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'statuses': <Map<String, dynamic>>[
|
'statuses': <Map<String, dynamic>>[
|
||||||
...(ftpDataResult['statuses'] as List),
|
...(ftpDataResult['statuses'] as List<dynamic>? ?? []),
|
||||||
...(ftpImageResult['statuses'] as List),
|
...(ftpImageResult['statuses'] as List<dynamic>? ?? []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user