feat: gps coords added

This commit is contained in:
imlildud
2026-05-22 23:31:09 -06:00
parent 2d56d6de0d
commit 36c04582f3
2 changed files with 205 additions and 261 deletions

View File

@@ -1,73 +1,87 @@
// src/services/geolocation_service.dart
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async'; // ← IMPORTANTE: Para TimeoutException
class GeolocationService {
// Verificar y solicitar permisos de ubicación
static Future<bool> requestPermission() async {
final status = await Permission.location.request();
return status.isGranted;
// Verificar servicios de ubicación
static Future<bool> isLocationServiceEnabled() async {
return await Geolocator.isLocationServiceEnabled();
}
// Obtener ubicación actual
// Verificar permiso
static Future<LocationPermission> checkPermission() async {
return await Geolocator.checkPermission();
}
// Solicitar permiso
static Future<LocationPermission> requestPermission() async {
return await Geolocator.requestPermission();
}
// Verificar si hay permiso concedido
static Future<bool> hasPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
return permission == LocationPermission.always ||
permission == LocationPermission.whileInUse;
}
// Obtener ubicación actual con timeout
static Future<Position?> getCurrentLocation() async {
try {
// Verificar si los servicios de ubicación están activados
// Verificar servicios
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
print('❌ Servicios de ubicación desactivados');
return null;
}
print('✅ Servicios de ubicación activados');
// Verificar permisos
LocationPermission permission = await Geolocator.checkPermission();
print('📌 Permiso actual: $permission');
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
print('📌 Permiso solicitado: $permission');
if (permission == LocationPermission.denied) {
print('❌ Permiso denegado');
return null;
}
}
if (permission == LocationPermission.deniedForever) {
print('❌ Permiso denegado permanentemente');
return null;
}
// Obtener ubicación
print('📍 Obteniendo ubicación...');
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
desiredAccuracy: LocationAccuracy.best,
);
print('✅ Ubicación obtenida: ${position.latitude}, ${position.longitude}');
return position;
} catch (e) {
print('Error getting location: $e');
print('Error general: $e');
return null;
}
}
// Mostrar diálogo de permisos
static Future<bool> showPermissionDialog(BuildContext context) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Permiso de ubicación'),
content: const Text(
'Necesitamos acceder a tu ubicación para asignar tu domicilio a la ruta correcta de recolección.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Aceptar', style: TextStyle(color: colorAzul)),
),
],
),
);
if (result == true) {
return await requestPermission();
// Obtener ubicación con reintentos (sin timeout que da problemas)
static Future<Position?> getCurrentLocationWithRetry({int maxRetries = 3}) async {
for (int i = 0; i < maxRetries; i++) {
print('🔄 Intento ${i + 1} de $maxRetries');
final position = await getCurrentLocation();
if (position != null) {
return position;
}
return false;
if (i < maxRetries - 1) {
await Future.delayed(const Duration(seconds: 2));
}
}
print('❌ No se pudo obtener ubicación después de $maxRetries intentos');
return null;
}
}

View File

@@ -1,9 +1,12 @@
// domicilios.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:geolocator/geolocator.dart';
import 'rutas.dart';
import '../models/domicilio_model.dart';
import '../services/geolocation_service.dart';
import 'nav_bar.dart';
import 'main_screen.dart';
class DomiciliosView extends StatefulWidget {
const DomiciliosView({super.key});
@@ -15,12 +18,8 @@ class DomiciliosView extends StatefulWidget {
class _DomiciliosViewState extends State<DomiciliosView> {
List<Domicilio> domicilios = [];
bool _isLoading = true;
// Ubicación actual
Position? _currentPosition;
bool _isLoadingLocation = false;
// Controladores para el formulario
final TextEditingController nombreController = TextEditingController();
final TextEditingController coloniaController = TextEditingController();
final TextEditingController calleController = TextEditingController();
@@ -69,51 +68,126 @@ class _DomiciliosViewState extends State<DomiciliosView> {
}
}
// Obtener ubicación actual y mostrar diálogo
Future<bool> _showLocationPermissionDialog() async {
if (await GeolocationService.hasPermission()) {
return true;
}
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Permiso de ubicación'),
content: const Text(
'Necesitamos acceder a tu ubicación para asignar tu domicilio a la ruta correcta.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Aceptar', style: TextStyle(color: colorAzul)),
),
],
),
);
if (result == true) {
LocationPermission newPermission = await GeolocationService.requestPermission();
return newPermission == LocationPermission.always ||
newPermission == LocationPermission.whileInUse;
}
return false;
}
// ⬇️⬇️⬇️ ESTE ES EL MÉTODO QUE DEBES TENER ⬇️⬇️⬇️
Future<void> _obtenerUbicacionYAgregar() async {
print('🔵 Iniciando obtención de ubicación');
setState(() {
_isLoadingLocation = true;
});
// Solicitar permisos
final hasPermission = await GeolocationService.showPermissionDialog(context);
// Verificar GPS
final serviceEnabled = await GeolocationService.isLocationServiceEnabled();
print('📡 GPS activado: $serviceEnabled');
if (!serviceEnabled) {
setState(() {
_isLoadingLocation = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Activa el GPS para agregar un domicilio'),
backgroundColor: Colors.orange,
duration: Duration(seconds: 3),
),
);
return;
}
// Verificar permisos
final hasPermission = await _showLocationPermissionDialog();
print('🔐 Permiso concedido: $hasPermission');
if (!hasPermission) {
setState(() {
_isLoadingLocation = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Necesitamos tu ubicación para agregar un domicilio'),
content: Text('Necesitamos tu ubicación para continuar'),
backgroundColor: Colors.orange,
duration: Duration(seconds: 3),
),
);
return;
}
// Obtener ubicación
final position = await GeolocationService.getCurrentLocation();
// Mostrar mensaje de "obteniendo ubicación"
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
),
SizedBox(width: 10),
Text('Obteniendo tu ubicación...'),
],
),
backgroundColor: colorAzul,
duration: Duration(seconds: 5),
),
);
// Obtener ubicación con reintentos
final position = await GeolocationService.getCurrentLocationWithRetry(maxRetries: 3);
setState(() {
_isLoadingLocation = false;
_currentPosition = position;
});
if (position == null) {
print('❌ No se pudo obtener ubicación después de reintentos');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No se pudo obtener tu ubicación. Activa el GPS.'),
content: Text('No se pudo obtener tu ubicación. Revisa tu GPS.'),
backgroundColor: Colors.red,
duration: Duration(seconds: 4),
),
);
return;
}
// Mostrar diálogo con la ubicación obtenida
print('✅ Ubicación obtenida exitosamente: ${position.latitude}, ${position.longitude}');
_mostrarDialogoAgregarConUbicacion(position);
}
void _mostrarDialogoAgregarConUbicacion(Position position) {
// Limpiar controladores
nombreController.clear();
coloniaController.clear();
calleController.clear();
@@ -130,23 +204,16 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Título
const Text(
'Añadir domicilio',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorAzul,
),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: colorAzul),
),
const SizedBox(height: 10),
// Mostrar ubicación obtenida
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.withOpacity(0.3)),
),
child: Row(
children: [
@@ -156,22 +223,10 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📍 Ubicación obtenida',
style: TextStyle(
fontSize: 12,
color: Colors.green[700],
fontWeight: FontWeight.bold,
),
),
Text(
'Lat: ${position.latitude.toStringAsFixed(6)}',
style: const TextStyle(fontSize: 11),
),
Text(
'Lng: ${position.longitude.toStringAsFixed(6)}',
style: const TextStyle(fontSize: 11),
),
Text('📍 Ubicación obtenida',
style: TextStyle(fontSize: 12, color: Colors.green[700])),
Text('Lat: ${position.latitude.toStringAsFixed(6)}'),
Text('Lng: ${position.longitude.toStringAsFixed(6)}'),
],
),
),
@@ -179,36 +234,14 @@ class _DomiciliosViewState extends State<DomiciliosView> {
),
),
const SizedBox(height: 15),
// Campo: Nombre del domicilio
_buildCampoTexto(
controller: nombreController,
hint: 'Nombre del domicilio',
icon: Icons.home_outlined,
),
_buildCampoTexto(controller: nombreController, hint: 'Nombre del domicilio', icon: Icons.home_outlined),
const SizedBox(height: 15),
// Campo: Colonia
_buildCampoTexto(
controller: coloniaController,
hint: 'Colonia',
icon: Icons.location_city_outlined,
),
_buildCampoTexto(controller: coloniaController, hint: 'Colonia', icon: Icons.location_city_outlined),
const SizedBox(height: 15),
// Campo: Calle
_buildCampoTexto(
controller: calleController,
hint: 'Calle',
icon: Icons.streetview,
),
_buildCampoTexto(controller: calleController, hint: 'Calle', icon: Icons.streetview),
const SizedBox(height: 15),
// Campo: Número
_buildCampoTexto(
controller: numeroController,
hint: 'Número',
icon: Icons.numbers,
keyboardType: TextInputType.number,
),
_buildCampoTexto(controller: numeroController, hint: 'Número', icon: Icons.numbers, keyboardType: TextInputType.number),
const SizedBox(height: 25),
// Botones
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@@ -216,18 +249,10 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorAzul, width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: () {
Navigator.pop(context);
},
child: const Text(
'Cancelar',
style: TextStyle(fontSize: 16, color: colorAzul),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: () => Navigator.pop(context),
child: const Text('Cancelar', style: TextStyle(color: colorAzul)),
),
),
const SizedBox(width: 15),
@@ -235,21 +260,10 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: colorAzul,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: () {
_agregarDomicilio(
latitud: position.latitude,
longitud: position.longitude,
);
},
child: const Text(
'Agregar',
style: TextStyle(fontSize: 16, color: Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: () => _agregarDomicilio(latitud: position.latitude, longitud: position.longitude),
child: const Text('Agregar', style: TextStyle(color: Colors.white)),
),
),
],
@@ -274,10 +288,7 @@ class _DomiciliosViewState extends State<DomiciliosView> {
decoration: InputDecoration(
hintText: hint,
prefixIcon: Icon(icon, color: colorAzul),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: colorAzul),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: colorAzul.withOpacity(0.5)),
@@ -290,28 +301,23 @@ class _DomiciliosViewState extends State<DomiciliosView> {
);
}
void _agregarDomicilio({required double latitud, required double longitud}) {
// Validar campos
if (nombreController.text.isEmpty ||
coloniaController.text.isEmpty ||
calleController.text.isEmpty ||
numeroController.text.isEmpty) {
void _agregarDomicilio({required double latitud, required double longitud}) async {
if (nombreController.text.trim().isEmpty ||
coloniaController.text.trim().isEmpty ||
calleController.text.trim().isEmpty ||
numeroController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Por favor, llena todos los campos'),
backgroundColor: Colors.red,
),
const SnackBar(content: Text('Llena todos los campos'), backgroundColor: Colors.red),
);
return;
}
// Crear nuevo domicilio con lat/lng
final nuevoDomicilio = Domicilio(
id: DateTime.now().millisecondsSinceEpoch.toString(),
nombre: nombreController.text,
colonia: coloniaController.text,
calle: calleController.text,
numero: numeroController.text,
nombre: nombreController.text.trim(),
colonia: coloniaController.text.trim(),
calle: calleController.text.trim(),
numero: numeroController.text.trim(),
latitud: latitud,
longitud: longitud,
);
@@ -320,58 +326,49 @@ class _DomiciliosViewState extends State<DomiciliosView> {
domicilios.add(nuevoDomicilio);
});
_guardarDomicilios();
await _guardarDomicilios();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Domicilio "${nombreController.text}" agregado'),
backgroundColor: colorAzul,
duration: const Duration(seconds: 2),
),
SnackBar(content: Text('Domicilio agregado'), backgroundColor: colorAzul, duration: const Duration(seconds: 2)),
);
}
void _eliminarDomicilio(int index) async {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
builder: (context) => AlertDialog(
title: const Text('Eliminar domicilio'),
content: Text('¿Deseas eliminar "${domicilios[index].nombre}"?'),
content: Text('¿Eliminar "${domicilios[index].nombre}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancelar'),
),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')),
TextButton(
onPressed: () async {
setState(() {
domicilios.removeAt(index);
});
setState(() => domicilios.removeAt(index));
await _guardarDomicilios();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Domicilio eliminado'),
backgroundColor: Colors.red,
duration: Duration(seconds: 2),
),
);
},
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
),
],
),
);
},
}
void _navegarA(int index) {
if (index == 1) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Container(
@@ -379,20 +376,9 @@ class _DomiciliosViewState extends State<DomiciliosView> {
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
decoration: const BoxDecoration(
color: colorAzul,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: const Text(
'Domicilios',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
),
child: const Text('Domicilios', style: TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
),
Expanded(
child: _isLoading
@@ -402,27 +388,11 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.home_outlined,
size: 100,
color: Colors.grey.withOpacity(0.5),
),
Icon(Icons.home_outlined, size: 100, color: Colors.grey.withOpacity(0.5)),
const SizedBox(height: 20),
Text(
'No hay domicilios agregados',
style: TextStyle(
fontSize: 18,
color: Colors.grey.withOpacity(0.7),
),
),
Text('No hay domicilios', style: TextStyle(fontSize: 18, color: Colors.grey.withOpacity(0.7))),
const SizedBox(height: 10),
Text(
'Toca el botón + para agregar',
style: TextStyle(
fontSize: 14,
color: Colors.grey.withOpacity(0.5),
),
),
Text('Toca el botón + para agregar', style: TextStyle(fontSize: 14, color: Colors.grey.withOpacity(0.5))),
],
),
)
@@ -430,7 +400,7 @@ class _DomiciliosViewState extends State<DomiciliosView> {
padding: const EdgeInsets.all(20),
itemCount: domicilios.length,
itemBuilder: (context, index) {
final domicilio = domicilios[index];
final d = domicilios[index];
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Container(
@@ -447,36 +417,13 @@ class _DomiciliosViewState extends State<DomiciliosView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
domicilio.nombre,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
Text(
domicilio.direccionCompleta,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
// Mostrar lat/lng (para debug)
Text(
'📍 ${domicilio.latitud.toStringAsFixed(4)}, ${domicilio.longitud.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Text(d.nombre, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
Text(d.direccionCompleta, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text('📍 ${d.latitud.toStringAsFixed(4)}, ${d.longitud.toStringAsFixed(4)}', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
),
IconButton(
onPressed: () => _eliminarDomicilio(index),
icon: const Icon(Icons.delete_outline, size: 40),
color: Colors.red,
),
IconButton(onPressed: () => _eliminarDomicilio(index), icon: const Icon(Icons.delete_outline, size: 40), color: Colors.red),
],
),
),
@@ -487,33 +434,16 @@ class _DomiciliosViewState extends State<DomiciliosView> {
Padding(
padding: const EdgeInsets.all(20),
child: _isLoadingLocation
? Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
color: colorAzul,
borderRadius: BorderRadius.circular(20),
),
child: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
)
? Container(width: double.infinity, height: 100, decoration: BoxDecoration(color: colorAzul, borderRadius: BorderRadius.circular(20)), child: const Center(child: CircularProgressIndicator(color: Colors.white)))
: GestureDetector(
onTap: _obtenerUbicacionYAgregar,
child: Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
color: colorAzul,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.add, color: Colors.white, size: 80),
),
child: Container(width: double.infinity, height: 100, decoration: BoxDecoration(color: colorAzul, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.add, color: Colors.white, size: 80)),
),
),
],
),
),
bottomNavigationBar: CustomNavBar(currentIndex: 0, onTap: _navegarA),
);
}
}