import 'dart:convert'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; /// Why location cannot be captured (used to show the right alert). enum LocationCheckResult { ready, serviceDisabled, // device GPS/location is switched off permissionDenied, // user denied — can ask again permissionPermanentlyDenied, // "Don't ask again" was checked } /// GPS coordinates + reverse-geocoded details captured at survey submission. class SurveyLocation { final double lat; final double lng; final String address; final String zone; // suburb / neighbourhood final String city; // detected city / town final String state; // detected state final String pincode; // postal code from reverse-geocode const SurveyLocation({ required this.lat, required this.lng, required this.address, required this.zone, this.city = '', this.state = '', this.pincode = '', }); factory SurveyLocation.empty() => const SurveyLocation(lat: 0.0, lng: 0.0, address: '', zone: ''); bool get hasFix => lat != 0.0 || lng != 0.0; } class LocationService { /// Checks service + permission WITHOUT triggering a system prompt. /// Use this to decide whether to show an in-app alert before capturing. static Future checkStatus() async { if (!await Geolocator.isLocationServiceEnabled()) { return LocationCheckResult.serviceDisabled; } final perm = await Geolocator.checkPermission(); if (perm == LocationPermission.deniedForever) { return LocationCheckResult.permissionPermanentlyDenied; } if (perm == LocationPermission.denied) { return LocationCheckResult.permissionDenied; } return LocationCheckResult.ready; } /// Requests permission (one-shot OS prompt) then captures GPS + address. /// Never throws — returns [SurveyLocation.empty] on any failure. static Future capture() async { try { if (!await Geolocator.isLocationServiceEnabled()) { return SurveyLocation.empty(); } var perm = await Geolocator.checkPermission(); if (perm == LocationPermission.denied) { perm = await Geolocator.requestPermission(); } if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) { return SurveyLocation.empty(); } final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ).timeout(const Duration(seconds: 10)); final geo = await _reverseGeocode(position.latitude, position.longitude); return SurveyLocation( lat: position.latitude, lng: position.longitude, address: geo['display_name'] ?? '', zone: geo['zone'] ?? '', city: geo['city'] ?? '', state: geo['state'] ?? '', pincode: geo['pincode'] ?? '', ); } catch (_) { return SurveyLocation.empty(); } } /// Opens the device's Location / GPS settings page. static Future openLocationSettings() => Geolocator.openLocationSettings(); /// Opens this app's permission settings page (for permanently-denied case). static Future openAppSettings() => Geolocator.openAppSettings(); static Future> _reverseGeocode( double lat, double lng) async { try { final uri = Uri.parse( 'https://nominatim.openstreetmap.org/reverse' '?lat=$lat&lon=$lng&format=json&addressdetails=1', ); final res = await http .get(uri, headers: {'User-Agent': 'DoorMile-CRM/1.0'}) .timeout(const Duration(seconds: 6)); if (res.statusCode != 200) return {}; final body = jsonDecode(res.body) as Map; final addr = (body['address'] as Map?) ?? {}; final zone = (addr['suburb'] ?? addr['neighbourhood'] ?? addr['quarter'] ?? addr['village'] ?? '') as String; final city = (addr['city'] ?? addr['town'] ?? addr['municipality'] ?? addr['county'] ?? addr['state_district'] ?? '') as String; final state = (addr['state'] ?? '') as String; final pincode = (addr['postcode'] ?? '') as String; return { 'display_name': (body['display_name'] as String?) ?? '', 'zone': zone, 'city': city, 'state': state, 'pincode': pincode, }; } catch (_) { return {}; } } }