fix river manual image request
This commit is contained in:
parent
37874a1eab
commit
077efa745d
@ -1,63 +1,427 @@
|
|||||||
// lib/screens/river/manual/river_manual_image_request.dart
|
// lib/screens/river/manual/river_manual_image_request.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'dart:io';
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
import '../../../services/api_service.dart';
|
||||||
|
|
||||||
|
class RiverManualImageRequest extends StatelessWidget {
|
||||||
|
const RiverManualImageRequest({super.key});
|
||||||
|
|
||||||
class RiverManualImageRequest extends StatefulWidget {
|
|
||||||
@override
|
@override
|
||||||
State<RiverManualImageRequest> createState() => _RiverManualImageRequestState();
|
Widget build(BuildContext context) {
|
||||||
|
return const RiverImageRequestScreen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RiverManualImageRequestState extends State<RiverManualImageRequest> {
|
|
||||||
XFile? _image;
|
|
||||||
final picker = ImagePicker();
|
|
||||||
final _descriptionController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
class RiverImageRequestScreen extends StatefulWidget {
|
||||||
final pickedFile = await picker.pickImage(source: ImageSource.camera);
|
const RiverImageRequestScreen({super.key});
|
||||||
setState(() => _image = pickedFile);
|
|
||||||
|
@override
|
||||||
|
State<RiverImageRequestScreen> createState() => _RiverImageRequestScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RiverImageRequestScreenState extends State<RiverImageRequestScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _dateController = TextEditingController();
|
||||||
|
|
||||||
|
final String _selectedSamplingType = 'In-Situ Sampling';
|
||||||
|
|
||||||
|
String? _selectedStateName;
|
||||||
|
String? _selectedBasinName;
|
||||||
|
Map<String, dynamic>? _selectedStation;
|
||||||
|
DateTime? _selectedDate;
|
||||||
|
|
||||||
|
List<String> _statesList = [];
|
||||||
|
List<String> _basinsForState = [];
|
||||||
|
List<Map<String, dynamic>> _stationsForBasin = [];
|
||||||
|
|
||||||
|
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.riverManualStations ?? [];
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_selectedStation == null || _selectedDate == null) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Error: Station and date are required.'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stationId = _selectedStation!['station_id'];
|
||||||
|
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await apiService.river.getRiverSamplingImages(
|
||||||
|
stationId: stationId,
|
||||||
|
samplingDate: _selectedDate!,
|
||||||
|
samplingType: _selectedSamplingType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted && result['success'] == true) {
|
||||||
|
// The backend now returns a direct list of full URLs, so we can use it directly.
|
||||||
|
final List<String> fetchedUrls = List<String>.from(result['data'] ?? []);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_imageUrls = fetchedUrls;
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPrint("[Image Request] Successfully received and processed ${_imageUrls.length} image URLs.");
|
||||||
|
|
||||||
|
} else if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(result['message'] ?? 'Failed to fetch images.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $e')));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
final stationCode = _selectedStation?['sampling_station_code'] ?? 'N/A';
|
||||||
|
final stationName = _selectedStation?['sampling_river'] ?? 'N/A';
|
||||||
|
final fullStationIdentifier = '$stationCode - $stationName';
|
||||||
|
|
||||||
|
final result = await apiService.river.sendImageRequestEmail(
|
||||||
|
recipientEmail: toEmail,
|
||||||
|
imageUrls: _selectedImageUrls.toList(),
|
||||||
|
stationName: fullStationIdentifier,
|
||||||
|
samplingDate: _dateController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
if (result['success'] == true) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Success! Email is being sent by the server.'), backgroundColor: Colors.green),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error: ${result['message']}'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('An error occurred: $e'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text("River Manual Image Request")),
|
appBar: AppBar(title: const Text("River Image Request")),
|
||||||
body: Padding(
|
body: Form(
|
||||||
padding: const EdgeInsets.all(24),
|
key: _formKey,
|
||||||
child: Column(
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
Text("Image Search Filters", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
icon: Icon(Icons.camera_alt),
|
const SizedBox(height: 24),
|
||||||
label: Text("Capture Image"),
|
|
||||||
onPressed: _pickImage,
|
// State Dropdown
|
||||||
),
|
DropdownSearch<String>(
|
||||||
SizedBox(height: 16),
|
items: _statesList,
|
||||||
if (_image != null)
|
selectedItem: _selectedStateName,
|
||||||
Image.file(
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
||||||
File(_image!.path),
|
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select State *", border: OutlineInputBorder())),
|
||||||
height: 200,
|
onChanged: (state) {
|
||||||
),
|
setState(() {
|
||||||
SizedBox(height: 16),
|
_selectedStateName = state;
|
||||||
TextField(
|
_selectedBasinName = null;
|
||||||
controller: _descriptionController,
|
_selectedStation = null;
|
||||||
decoration: InputDecoration(labelText: "Description"),
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
maxLines: 3,
|
final allStations = auth.riverManualStations ?? [];
|
||||||
),
|
final basins = state != null ? allStations.where((s) => s['state_name'] == state).map((s) => s['sampling_basin'] as String?).whereType<String>().toSet().toList() : <String>[];
|
||||||
SizedBox(height: 24),
|
basins.sort();
|
||||||
ElevatedButton(
|
_basinsForState = basins;
|
||||||
onPressed: () {
|
_stationsForBasin = [];
|
||||||
// Submit logic here
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text("Image request submitted")),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Text("Submit Request"),
|
validator: (val) => val == null ? "State is required" : null,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Basin Dropdown
|
||||||
|
DropdownSearch<String>(
|
||||||
|
items: _basinsForState,
|
||||||
|
selectedItem: _selectedBasinName,
|
||||||
|
enabled: _selectedStateName != null,
|
||||||
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Basin..."))),
|
||||||
|
dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Basin *", border: OutlineInputBorder())),
|
||||||
|
onChanged: (basin) {
|
||||||
|
setState(() {
|
||||||
|
_selectedBasinName = basin;
|
||||||
|
_selectedStation = null;
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final allStations = auth.riverManualStations ?? [];
|
||||||
|
_stationsForBasin = basin != null ? (allStations.where((s) => s['state_name'] == _selectedStateName && s['sampling_basin'] == basin).toList()..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''))) : [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validator: (val) => _selectedStateName != null && val == null ? "Basin is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Station Dropdown
|
||||||
|
DropdownSearch<Map<String, dynamic>>(
|
||||||
|
items: _stationsForBasin,
|
||||||
|
selectedItem: _selectedStation,
|
||||||
|
enabled: _selectedBasinName != null,
|
||||||
|
itemAsString: (station) => "${station['sampling_station_code']} - ${station['sampling_river']}",
|
||||||
|
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) => _selectedBasinName != null && val == null ? "Station is required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Date Picker
|
||||||
|
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),
|
||||||
|
|
||||||
|
// Search Button
|
||||||
|
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(),
|
||||||
|
|
||||||
|
// Send Email Button
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ class RiverHomePage extends StatelessWidget {
|
|||||||
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/river/manual/in-situ'),
|
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/river/manual/in-situ'),
|
||||||
SidebarItem(icon: Icons.date_range, label: "Triennial Sampling", route: '/river/manual/triennial'),
|
SidebarItem(icon: Icons.date_range, label: "Triennial Sampling", route: '/river/manual/triennial'),
|
||||||
SidebarItem(icon: Icons.article, label: "Data Log", route: '/river/manual/data-log'),
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/river/manual/data-log'),
|
||||||
//SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
|
|||||||
@ -841,6 +841,45 @@ class RiverApiService {
|
|||||||
return _baseService.get(baseUrl, 'river/triennial-stations');
|
return _baseService.get(baseUrl, 'river/triennial-stations');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getRiverSamplingImages({
|
||||||
|
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 = 'river/manual/images-by-station?station_id=$stationId&date=$dateStr';
|
||||||
|
|
||||||
|
debugPrint("ApiService: Calling river image request API endpoint: $endpoint");
|
||||||
|
|
||||||
|
final response = await _baseService.get(baseUrl, endpoint);
|
||||||
|
|
||||||
|
// The backend now returns the data directly, so we just pass the response along.
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'river/images/send-email', // Endpoint for river email requests
|
||||||
|
fields: fields,
|
||||||
|
files: {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitInSituSample({
|
Future<Map<String, dynamic>> submitInSituSample({
|
||||||
required Map<String, String> formData,
|
required Map<String, String> formData,
|
||||||
required Map<String, File?> imageFiles,
|
required Map<String, File?> imageFiles,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user