fix: LOC — location filter never applied to event API calls
Root cause: SearchScreen popped with a plain city label string; the
pincode was available in search results but discarded. home_screen only
saved the display label to prefs and never updated the 'pincode' key,
so every API call always sent {pincode:'all'} regardless of selection.
GPS path had the same issue — lat/lng were obtained but thrown away
after reverse-geocoding; only the label was passed back.
Fix:
- SearchScreen now pops with Map<String,dynamic> {label, pincode,
lat?, lng?} instead of a plain String
- Pincode results return their pincode; GPS returns actual coordinates;
popular city chips look up the first matching pincode from the
Kerala pincodes DB (fallback 'all' if not found)
- home_screen._openLocationSearch() saves pincode + lat/lng to prefs
and updates _pincode/_userLat/_userLng in state
- home_screen._loadUserDataAndEvents() prefers getEventsByLocation
(haversine) when GPS coords are saved, falls back to getEventsByPincode
- EventsService gains getEventsByLocation(lat, lng) which sends
latitude/longitude/radius_km to the existing Django haversine endpoint
and auto-expands radius 10→25→50→100 km until ≥ 6 events found
This commit is contained in:
@@ -108,6 +108,33 @@ class EventsService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get events by GPS coordinates using haversine distance filtering.
|
||||||
|
/// Automatically expands radius (10 → 25 → 50 → 100 km) until ≥ 6 events found.
|
||||||
|
Future<List<EventModel>> getEventsByLocation(double lat, double lng, {double initialRadiusKm = 10}) async {
|
||||||
|
const radii = [10.0, 25.0, 50.0, 100.0];
|
||||||
|
for (final radius in radii) {
|
||||||
|
if (radius < initialRadiusKm) continue;
|
||||||
|
final body = {
|
||||||
|
'latitude': lat,
|
||||||
|
'longitude': lng,
|
||||||
|
'radius_km': radius,
|
||||||
|
'page': 1,
|
||||||
|
'page_size': 50,
|
||||||
|
'per_type': 5,
|
||||||
|
};
|
||||||
|
final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false);
|
||||||
|
final list = <EventModel>[];
|
||||||
|
final events = res['events'] ?? res['data'] ?? [];
|
||||||
|
if (events is List) {
|
||||||
|
for (final e in events) {
|
||||||
|
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (list.length >= 6 || radius >= 100) return list;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||||
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
||||||
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
String _username = '';
|
String _username = '';
|
||||||
String _location = '';
|
String _location = '';
|
||||||
String _pincode = 'all';
|
String _pincode = 'all';
|
||||||
|
double? _userLat;
|
||||||
|
double? _userLng;
|
||||||
|
|
||||||
final EventsService _eventsService = EventsService();
|
final EventsService _eventsService = EventsService();
|
||||||
|
|
||||||
@@ -104,12 +106,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
_location = storedLocation;
|
_location = storedLocation;
|
||||||
}
|
}
|
||||||
_pincode = prefs.getString('pincode') ?? 'all';
|
_pincode = prefs.getString('pincode') ?? 'all';
|
||||||
|
_userLat = prefs.getDouble('user_lat');
|
||||||
|
_userLng = prefs.getDouble('user_lng');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch types and events in parallel for faster loading
|
// Fetch types and events in parallel for faster loading.
|
||||||
|
// Prefer haversine (lat/lng) when GPS coords are available; fall back to pincode.
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
_events_service_getEventTypesSafe(),
|
_events_service_getEventTypesSafe(),
|
||||||
_events_service_getEventsSafe(_pincode),
|
(_userLat != null && _userLng != null)
|
||||||
|
? _events_service_getEventsByLocationSafe(_userLat!, _userLng!)
|
||||||
|
: _events_service_getEventsSafe(_pincode),
|
||||||
]);
|
]);
|
||||||
final types = results[0] as List<EventTypeModel>;
|
final types = results[0] as List<EventTypeModel>;
|
||||||
final events = results[1] as List<EventModel>;
|
final events = results[1] as List<EventModel>;
|
||||||
@@ -171,6 +178,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<EventModel>> _events_service_getEventsByLocationSafe(double lat, double lng) async {
|
||||||
|
try {
|
||||||
|
return await _eventsService.getEventsByLocation(lat, lng);
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback to all events if location-based fetch fails
|
||||||
|
return _events_service_getEventsSafe('all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
await _loadUserDataAndEvents();
|
await _loadUserDataAndEvents();
|
||||||
}
|
}
|
||||||
@@ -277,12 +293,31 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
transitionDuration: const Duration(milliseconds: 220),
|
transitionDuration: const Duration(milliseconds: 220),
|
||||||
));
|
));
|
||||||
|
|
||||||
if (selected != null && selected is String) {
|
if (selected != null && selected is Map) {
|
||||||
|
final label = (selected['label'] as String?) ?? 'Current Location';
|
||||||
|
final pincode = (selected['pincode'] as String?) ?? 'all';
|
||||||
|
final lat = selected['lat'] as double?;
|
||||||
|
final lng = selected['lng'] as double?;
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('location', selected);
|
await prefs.setString('location', label);
|
||||||
setState(() {
|
await prefs.setString('pincode', pincode);
|
||||||
_location = selected;
|
if (lat != null && lng != null) {
|
||||||
});
|
await prefs.setDouble('user_lat', lat);
|
||||||
|
await prefs.setDouble('user_lng', lng);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('user_lat');
|
||||||
|
await prefs.remove('user_lng');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_location = label;
|
||||||
|
_pincode = pincode;
|
||||||
|
_userLat = lat;
|
||||||
|
_userLng = lng;
|
||||||
|
});
|
||||||
|
}
|
||||||
await _refresh();
|
await _refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,8 +106,24 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pop with a structured result so home_screen can update both the display
|
||||||
|
/// label AND the pincode / GPS coordinates used for API filtering.
|
||||||
|
void _selectWithPincode(String label, {String? pincode}) {
|
||||||
|
Navigator.of(context).pop(<String, dynamic>{
|
||||||
|
'label': label,
|
||||||
|
'pincode': pincode ?? 'all',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _selectAndClose(String location) {
|
void _selectAndClose(String location) {
|
||||||
Navigator.of(context).pop(location);
|
// Legacy path for plain strings — wrap in structured map.
|
||||||
|
// Tries to look up a pincode from the database for the given city name.
|
||||||
|
final match = _locationDb.cast<_LocationItem?>().firstWhere(
|
||||||
|
(loc) => loc!.city.toLowerCase() == location.toLowerCase() ||
|
||||||
|
loc.displayTitle.toLowerCase() == location.toLowerCase(),
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
_selectWithPincode(location, pincode: match?.pincode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _useCurrentLocation() async {
|
Future<void> _useCurrentLocation() async {
|
||||||
@@ -122,13 +138,14 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) {
|
if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
|
||||||
Navigator.of(context).pop('Current Location');
|
Navigator.of(context).pop(<String, dynamic>{'label': 'Current Location', 'pincode': 'all'});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
|
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
|
||||||
|
|
||||||
|
String label = 'Current Location';
|
||||||
try {
|
try {
|
||||||
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
|
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
|
||||||
if (placemarks.isNotEmpty) {
|
if (placemarks.isNotEmpty) {
|
||||||
@@ -137,17 +154,24 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
||||||
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
||||||
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
||||||
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
|
if (parts.isNotEmpty) label = parts.join(', ');
|
||||||
if (mounted) Navigator.of(context).pop(label);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (mounted) Navigator.of(context).pop('Current Location');
|
if (mounted) {
|
||||||
|
// Return lat/lng so home_screen can use haversine filtering
|
||||||
|
Navigator.of(context).pop(<String, dynamic>{
|
||||||
|
'label': label,
|
||||||
|
'pincode': 'all',
|
||||||
|
'lat': pos.latitude,
|
||||||
|
'lng': pos.longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
|
||||||
Navigator.of(context).pop('Current Location');
|
Navigator.of(context).pop(<String, dynamic>{'label': 'Current Location', 'pincode': 'all'});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loadingLocation = false);
|
if (mounted) setState(() => _loadingLocation = false);
|
||||||
@@ -320,7 +344,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
subtitle: loc.pincode != null && loc.pincode!.isNotEmpty
|
subtitle: loc.pincode != null && loc.pincode!.isNotEmpty
|
||||||
? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13))
|
? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13))
|
||||||
: null,
|
: null,
|
||||||
onTap: () => _selectAndClose(loc.returnValue),
|
onTap: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user