mobile-flutter/covas_mobile/lib/pages/ListItemMenu.dart
2025-01-20 21:42:14 +01:00

773 lines
26 KiB
Dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv
import 'dart:convert';
import 'dart:io';
import 'ItemMenu.dart';
import '../classes/events.dart';
import '../classes/MyDrawer.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'dart:math';
import 'package:geolocator/geolocator.dart';
import '../variable/globals.dart' as globals;
import 'package:permission_handler/permission_handler.dart';
import "Camera.dart";
import 'package:camera/camera.dart';
void main() {
initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const ListItemMenu(),
debugShowCheckedModeBanner: false,
);
}
}
class ListItemMenu extends StatefulWidget {
const ListItemMenu({super.key});
@override
State<ListItemMenu> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<ListItemMenu> {
Future<List<Events>> postsFuture = getPosts();
List<Events> filteredPosts = [];
String geographicalZone = '';
String itemName = '';
String itemTags = '';
String query = '';
List<Map<String, dynamic>> suggestionsGeo = [];
List<Map<String, dynamic>> suggestionsItem = [];
List<Map<String, dynamic>> suggestionsTags = [];
TextEditingController inputGeo = TextEditingController();
TextEditingController startDatepicker = TextEditingController();
TextEditingController endDatepicker = TextEditingController();
TextEditingController inputItem = TextEditingController();
TextEditingController inputTags = TextEditingController();
bool showDateFields = false; // State to toggle date fields
bool showArrow = true;
bool showInputSearch = true;
bool showInputGeo = true;
bool showInputTag = true;
// Fetching events from API
static Future<List<Events>> getPosts() async {
PermissionStatus status = await Permission.location.status;
final List<Events> body = [];
var url = Uri.parse("${globals.api}/events");
if (status.isGranted) {
print("Location permission granted");
// Get the current position with high accuracy
const LocationSettings locationSettings = LocationSettings(
accuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 5),
);
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: locationSettings);
} on LocationServiceDisabledException {
// Handle location services disabled
print('Location services are disabled.');
position = await Geolocator.getLastKnownPosition();
if (position == null) {
print('No last known position available.');
}
} catch (e) {
// Handle other errors
print('Failed to get location: $e');
position = await Geolocator.getLastKnownPosition();
if (position == null) {
print('No last known position available.');
}
}
SharedPreferences prefs = await SharedPreferences.getInstance();
if (position != null) {
// Calculate the boundaries
double radiusInKm = prefs.getDouble("kilometer") ?? 50;
double latDistance = radiusInKm / 111.0;
double lonDistance =
radiusInKm / (111.0 * cos(position.latitude * pi / 180));
double minLat = position.latitude - latDistance;
double maxLat = position.latitude + latDistance;
double minLon = position.longitude - lonDistance;
double maxLon = position.longitude + lonDistance;
DateTime currentDatetime = DateTime.now();
url = Uri.parse("${globals.api}/events/search"
"?min_lat=$minLat&max_lat=$maxLat"
"&min_lon=$minLon&max_lon=$maxLon&current_datetime=${currentDatetime.toString()}");
}
var accessToken = prefs.getString("access_token") ?? "";
if (accessToken.isNotEmpty) {
final response = await http.get(url, headers: {
"Content-Type": "application/json",
HttpHeaders.cookieHeader: "access_token=${accessToken}"
});
final List body = json.decode(utf8.decode(response.bodyBytes));
return body.map((e) => Events.fromJson(e)).toList();
}
}
return body;
}
String formatDate(String date) {
var splitedDate = date.split("-");
var day = splitedDate[0];
var month = splitedDate[1];
var year = splitedDate[2];
return "${year}-${month}-${day}";
}
@override
void initState() {
super.initState();
// Initialize data fetch when the page loads
_getCurrentLocation();
}
// Get the device's current location
Future<void> _getCurrentLocation() async {
PermissionStatus status = await Permission.location.status;
if (status.isGranted) {
print("Location permission granted");
// Get the current position with high accuracy
const LocationSettings locationSettings = LocationSettings(
accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 5));
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: locationSettings);
} on LocationServiceDisabledException {
// Handle location services disabled
print('Location services are disabled.');
position = await Geolocator.getLastKnownPosition();
if (position == null) {
print('No last known position available.');
}
} catch (e) {
// Handle other errors
print('Failed to get location: $e');
position = await Geolocator.getLastKnownPosition();
if (position == null) {
print('No last known position available.');
}
}
// Reverse geocode: Get city and country from latitude and longitude using Mapbox Search API
if (position != null) {
_getCityAndCountry(position!.latitude, position!.longitude);
}
}
}
// Method to get city and country from latitude and longitude using Mapbox API
Future<void> _getCityAndCountry(double latitude, double longitude) async {
await dotenv.load(fileName: ".env"); // Load .env file
final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? '';
final url = Uri.parse(
'https://api.mapbox.com/geocoding/v5/mapbox.places/$longitude,$latitude.json?access_token=$mapboxAccessToken',
);
try {
// Send GET request to Mapbox API
final response = await http.get(url);
// If the request is successful (HTTP status 200)
print("status mapbox : ${response.statusCode}");
if (response.statusCode == 200) {
// Parse the response body
final data = json.decode(response.body);
// Extract the city and country from the response
final features = data['features'];
if (features.isNotEmpty) {
String city = _getCityFromFeatures(features);
String country = _getCountryFromFeatures(features);
print("city : ${city} ${country}");
if (city.isNotEmpty && country.isNotEmpty) {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setDouble("city_lat", latitude);
prefs.setDouble("city_long", longitude);
fetchPostsByLocation();
setState(() {
inputGeo.text = "${city}, ${country}";
});
} else {
fetchPostsByLocation();
}
} else {
fetchPostsByLocation();
}
} else {
fetchPostsByLocation();
throw Exception('Failed to load location data');
}
} catch (e) {
fetchPostsByLocation();
print("Error getting city and country: $e");
}
}
// Helper function to extract the city from the Mapbox features array
String _getCityFromFeatures(List<dynamic> features) {
for (var feature in features) {
if (feature['place_type'] != null &&
feature['place_type'].contains('place')) {
return feature['text'] ?? '';
}
}
return '';
}
// Helper function to extract the country from the Mapbox features array
String _getCountryFromFeatures(List<dynamic> features) {
for (var feature in features) {
if (feature['place_type'] != null &&
feature['place_type'].contains('country')) {
return feature['text'] ?? '';
}
}
return '';
}
Future<void> searchSuggestionsGeo(String input) async {
await dotenv.load(fileName: ".env"); // Load .env file
final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? '';
final url =
'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
suggestionsGeo = (data['features'] as List)
.map((feature) => {
'place_name': feature['place_name'],
'geometry': feature[
'geometry'], // Include geometry for latitude/longitude
})
.toList();
if (suggestionsGeo.isNotEmpty) {
showArrow = false;
showInputSearch = false;
showInputTag = false;
}
});
} else {
throw Exception('Failed to load suggestions');
}
}
Future<Uri> getUrlForEvents() async {
final prefs = await SharedPreferences.getInstance();
final latitude = prefs.getDouble("city_lat") ?? 0.0;
final longitude = prefs.getDouble("city_long") ?? 0.0;
final radiusInKm = 50;
String endpoint = "events";
String queryParameters = "";
if (latitude != 0.0 && longitude != 0.0) {
final latDistance = radiusInKm / 111.0;
final lonDistance = radiusInKm / (111.0 * cos(latitude * pi / 180));
final minLat = latitude - latDistance;
final maxLat = latitude + latDistance;
final minLon = longitude - lonDistance;
final maxLon = longitude + lonDistance;
endpoint = "events/search";
queryParameters =
"min_lat=$minLat&max_lat=$maxLat&min_lon=$minLon&max_lon=$maxLon";
}
final currentDate = DateTime.now();
String dateParameter = "current_datetime=${currentDate.toIso8601String()}";
if (startDatepicker.text.isNotEmpty || endDatepicker.text.isNotEmpty) {
endpoint = "events/search";
if (startDatepicker.text.isNotEmpty) {
final startDate = DateTime.parse(formatDate(startDatepicker.text));
dateParameter = "start_date=${startDate.toIso8601String()}";
}
if (endDatepicker.text.isNotEmpty) {
final endDate = DateTime.parse(formatDate(endDatepicker.text));
dateParameter += "&end_date=${endDate.toIso8601String()}";
}
}
if (inputItem.text.isNotEmpty) {
queryParameters += "&item=${inputItem.text}";
}
if (inputTags.text.isNotEmpty) {
queryParameters += "&tags=${inputTags.text}";
}
if (queryParameters.isNotEmpty) {
queryParameters = "$queryParameters&$dateParameter";
} else {
queryParameters = dateParameter;
}
return Uri.parse("${globals.api}/$endpoint?$queryParameters");
}
Future<void> searchSuggestionsByItem(String input) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token") ?? "";
if (accessToken.isNotEmpty) {
var url = await getUrlForEvents();
final response = await http.get(url, headers: {
"Content-Type": "application/json",
HttpHeaders.cookieHeader: "access_token=$accessToken"
});
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
setState(() {
suggestionsItem = (data as List)
.map((feature) => {'name': feature['name']})
.toList();
if (suggestionsItem.isNotEmpty) {
showDateFields = false;
showArrow = false;
}
});
print("status code : ${response.statusCode}");
}
}
}
Future<void> searchSuggestionsByTag(String input) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token") ?? "";
if (accessToken.isNotEmpty) {
var url = Uri.parse("${globals.api}/tags?name=${input}");
final response = await http.get(url, headers: {
"Content-Type": "application/json",
HttpHeaders.cookieHeader: "access_token=$accessToken"
});
print("status code tags : ${response.statusCode}");
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
print("tags ${data}");
setState(() {
suggestionsTags = (data as List)
.map((feature) => {'name': feature['name']})
.toList();
print("suggesttion tag : ${suggestionsTags}");
if (suggestionsTags.isNotEmpty) {
showInputGeo = false;
showInputSearch = false;
showArrow = false;
}
});
}
}
}
Future<void> fetchPostsByLocation() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token") ?? "";
if (accessToken.isNotEmpty) {
var url = await getUrlForEvents();
final response = await http.get(url, headers: {
"Content-Type": "application/json",
HttpHeaders.cookieHeader: "access_token=$accessToken"
});
print("status code : ${response.statusCode}");
if (response.statusCode == 200) {
final List<dynamic> body = json.decode(utf8.decode(response.bodyBytes));
print("results fetch : ${body}");
// Update state after getting the response
setState(() {
if (body.isNotEmpty) {
// If we have results, map them to Events
filteredPosts = body
.map((e) => Events.fromJson(e as Map<String, dynamic>))
.toList();
} else {
// If no results, clear filteredPosts
filteredPosts.clear();
}
});
} else {
throw Exception('Failed to load posts');
}
}
}
onTapFunctionDatePicker(
{required BuildContext context, String position = ""}) async {
DateTime dateEvent = DateTime.now();
if (startDatepicker.text.isNotEmpty) {
dateEvent = DateTime.parse(formatDate(startDatepicker.text));
}
DateTime? pickedDate = await showDatePicker(
context: context, firstDate: dateEvent, lastDate: DateTime(2104));
if (pickedDate == null) return;
if (position == "start") {
startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate);
} else if (position == "end") {
endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate);
}
fetchPostsByLocation();
}
Padding _buildDateField(String position) {
TextEditingController datePicker = startDatepicker;
String hintText = "Date de début";
if (position == "end") {
datePicker = endDatepicker;
hintText = "Date de fin";
}
return Padding(
padding: const EdgeInsets.all(8.0),
//padding: EdgeInsets.symmetric(horizontal: 15),
child: TextFormField(
controller: datePicker,
readOnly: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
suffixIcon: datePicker.text.isEmpty
? null
: IconButton(
icon: const Icon(Icons.clear),
onPressed: () async {
setState(() {
datePicker.text = '';
});
fetchPostsByLocation();
},
),
hintText: hintText),
onTap: () =>
onTapFunctionDatePicker(context: context, position: position)),
);
}
Widget _buildSearchField({
required TextEditingController controller,
required String labelText,
required Function(String) onChanged,
required Function() onClear,
required List<Map<String, dynamic>> suggestions,
required Function(Map<String, dynamic>) onSuggestionTap,
}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
controller: controller,
decoration: InputDecoration(
labelText: labelText,
border: OutlineInputBorder(),
suffixIcon: controller.text.isEmpty
? null
: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => onClear(),
),
),
onChanged: onChanged,
),
if (suggestions.isNotEmpty)
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(8),
),
child: ListView.builder(
shrinkWrap: true,
itemCount: suggestions.length,
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
title: Text(
suggestion['name'] ?? suggestion['place_name'] ?? ''),
onTap: () => onSuggestionTap(suggestion),
);
},
),
),
],
),
);
}
Future<void> popCamera() async {
await availableCameras().then((value) => Navigator.push(context,
MaterialPageRoute(builder: (_) => Camera(camera: value.first))));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Item list menu"),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
drawer: MyDrawer(),
body: Column(
children: [
if (showInputSearch)
_buildSearchField(
controller: inputItem,
labelText: 'Search by item',
onChanged: (value) {
if (value.isNotEmpty) {
setState(() {
itemName = value;
searchSuggestionsByItem(value);
});
} else {
setState(() {
inputItem.clear();
itemName = '';
suggestionsItem.clear();
showDateFields = true;
showArrow = true;
});
fetchPostsByLocation();
}
},
onClear: () {
setState(() {
inputItem.clear();
itemName = '';
suggestionsItem.clear();
showDateFields = true;
showArrow = true;
});
fetchPostsByLocation();
},
suggestions: suggestionsItem,
onSuggestionTap: (suggestion) async {
setState(() {
itemName = suggestion['name'];
inputItem.text = itemName;
suggestionsItem.clear();
showDateFields = true;
showArrow = true;
});
await fetchPostsByLocation();
},
),
if ((showDateFields) && (showInputTag))
_buildSearchField(
controller: inputTags,
labelText: 'Search by tags',
onChanged: (value) {
if (value.isNotEmpty) {
setState(() {
itemTags = value;
searchSuggestionsByTag(value);
});
} else {
setState(() {
inputTags.clear();
showArrow = true;
showInputSearch = true;
showInputGeo = true;
itemTags = '';
});
fetchPostsByLocation();
}
},
onClear: () {
setState(() {
inputTags.clear();
});
fetchPostsByLocation();
},
suggestions: suggestionsTags,
onSuggestionTap: (suggestion) async {
setState(() {
itemTags = suggestion['name'];
inputTags.text = itemTags;
suggestionsTags.clear();
showArrow = true;
showInputSearch = true;
showInputGeo = true;
});
await fetchPostsByLocation();
},
),
if ((showDateFields) && (showArrow))
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: _buildDateField("start")),
Flexible(child: _buildDateField("end"))
]),
if ((showDateFields) && (showInputGeo))
_buildSearchField(
controller: inputGeo,
labelText: 'Search by geographical zone',
onChanged: (value) async {
if (value.isNotEmpty) {
setState(() {
geographicalZone = value;
searchSuggestionsGeo(value);
});
} else {
final prefs = await SharedPreferences.getInstance();
prefs.remove("city_lat");
prefs.remove("city_long");
setState(() {
inputGeo.clear();
geographicalZone = '';
suggestionsGeo.clear();
showArrow = true;
showInputSearch = true;
showInputTag = true;
});
fetchPostsByLocation();
}
},
onClear: () async {
final prefs = await SharedPreferences.getInstance();
prefs.remove("city_lat");
prefs.remove("city_long");
setState(() {
inputGeo.clear();
geographicalZone = '';
suggestionsGeo.clear();
showArrow = true;
showInputSearch = true;
showInputTag = true;
});
fetchPostsByLocation();
},
suggestions: suggestionsGeo,
onSuggestionTap: (suggestion) async {
final latitude = suggestion['geometry']['coordinates'][1];
final longitude = suggestion['geometry']['coordinates'][0];
setState(() {
geographicalZone = suggestion['place_name'];
inputGeo.text = geographicalZone;
suggestionsGeo.clear();
showArrow = true;
showInputSearch = true;
showInputTag = true;
});
final prefs = await SharedPreferences.getInstance();
prefs.setDouble("city_lat", latitude);
prefs.setDouble("city_long", longitude);
await fetchPostsByLocation();
},
),
if (showArrow)
IconButton(
onPressed: () {
setState(() {
showDateFields = !showDateFields; // Toggle visibility
});
},
icon: Icon(
showDateFields
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: Colors.blue,
),
tooltip: showDateFields ? 'Show Date Fields' : 'Hide Date Fields',
),
Expanded(
child: FutureBuilder<List<Events>>(
future: postsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasData) {
final posts = snapshot.data!;
final displayedPosts =
filteredPosts.isEmpty ? posts : filteredPosts;
return buildPosts(displayedPosts);
} else {
return const Text("No data available");
}
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: popCamera,
backgroundColor: Colors.blue,
tooltip: 'Recherche',
child: const Icon(Icons.photo_camera, color: Colors.white),
),
);
}
// Function to display fetched data on screen
Widget buildPosts(List<Events> posts) {
print("posts : ${posts}");
print("filteredposts : ${filteredPosts}");
final displayedPosts = filteredPosts;
print("results ${displayedPosts}");
// If filteredPosts is empty, show a message saying no data is available
if (displayedPosts.isEmpty) {
return const Center(
child: Text('No events available for this location.',
style: TextStyle(fontSize: 18, color: Colors.grey)),
);
}
return ListView.separated(
itemCount: displayedPosts.length,
itemBuilder: (context, index) {
final post = displayedPosts[index];
final startDate = DateTime.parse(post.startDate!);
final date = DateFormat.yMd().format(startDate);
final time = DateFormat.Hm().format(startDate);
return ListTile(
title: Text('${post.name!}'),
subtitle: Text('${post.place!}\n${date} ${time}'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)),
);
},
);
},
separatorBuilder: (context, index) {
return Divider();
});
}
}