From 98ff822eb7ececf9d2d9a61c50781103805d338e Mon Sep 17 00:00:00 2001 From: Alan Alonso Date: Sat, 23 May 2026 09:52:16 -0600 Subject: [PATCH] fix: correct code errors and fix dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing dart:async import in horarios.dart - Fix typo 'Anadir' -> 'Añadir' in domicilios.dart - Simplify broken _obtenerHorarioConRetraso method - Remove file whitespace in notification_service.dart - Clean up unnecessary comments - Add flutter_local_notifications to dependencies - Move packages to correct section (dependencies not dev_dependencies) --- lib/src/data/horarios_data.dart | 40 +++ lib/src/main.dart | 84 ++----- lib/src/models/domicilio_model.dart | 14 +- lib/src/services/notification_service.dart | 43 ++++ lib/src/views/admin.dart | 232 ++++++++++++++++++ lib/src/views/configuracion.dart | 133 +++++++++- lib/src/views/domicilios.dart | 25 +- lib/src/views/horarios.dart | 193 +++++++++------ lib/src/views/mapa_expandible.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 72 +++++- pubspec.yaml | 26 +- 12 files changed, 677 insertions(+), 189 deletions(-) create mode 100644 lib/src/data/horarios_data.dart create mode 100644 lib/src/services/notification_service.dart create mode 100644 lib/src/views/admin.dart diff --git a/lib/src/data/horarios_data.dart b/lib/src/data/horarios_data.dart new file mode 100644 index 0000000..8a23da5 --- /dev/null +++ b/lib/src/data/horarios_data.dart @@ -0,0 +1,40 @@ +// src/data/horarios_data.dart +class HorarioInfo { + final String colonia; + final String routeId; + final String horarioEstimado; + + HorarioInfo({ + required this.colonia, + required this.routeId, + required this.horarioEstimado, + }); +} + +final List horariosBase = [ + HorarioInfo(colonia: 'Zona Centro', routeId: 'RUTA-01', horarioEstimado: '06:30'), + HorarioInfo(colonia: 'Las Arboledas', routeId: 'RUTA-01', horarioEstimado: '07:00'), + HorarioInfo(colonia: 'Trojes', routeId: 'RUTA-13', horarioEstimado: '06:40'), + HorarioInfo(colonia: 'San Juanico', routeId: 'RUTA-03', horarioEstimado: '06:45'), + HorarioInfo(colonia: 'Los Olivos', routeId: 'RUTA-04', horarioEstimado: '07:00'), + HorarioInfo(colonia: 'Rancho Seco', routeId: 'RUTA-05', horarioEstimado: '14:15'), + HorarioInfo(colonia: 'Las Insurgentes', routeId: 'RUTA-12', horarioEstimado: '06:35'), +]; + +// Obtener horario por nombre de colonia +String getHorarioByColonia(String colonia) { + final found = horariosBase.firstWhere( + (h) => h.colonia.toLowerCase() == colonia.toLowerCase(), + orElse: () => HorarioInfo(colonia: '', routeId: '', horarioEstimado: '08:00'), + ); + return found.horarioEstimado; +} + +// Obtener routeId por nombre de colonia +String getRouteIdByColonia(String colonia) { + final found = horariosBase.firstWhere( + (h) => h.colonia.toLowerCase() == colonia.toLowerCase(), + orElse: () => HorarioInfo(colonia: '', routeId: 'RUTA-00', horarioEstimado: '08:00'), + ); + return found.routeId; +} \ No newline at end of file diff --git a/lib/src/main.dart b/lib/src/main.dart index 8162152..a70c8d7 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -1,22 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import '../core/config/supabase_config.dart'; -import '../core/ws_provider.dart'; -import 'views/rutas.dart'; -import 'views/login.dart'; -import 'views/home_screen.dart'; +import '../src/views/rutas.dart'; +import '../src/views/login.dart'; +import '../src/views/home_screen.dart'; +import '../src/services/notification_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - - await Supabase.initialize( - url: SUPABASE_URL, - anonKey: SUPABASE_ANON_KEY, - ); - - runApp(const ProviderScope(child: MyApp())); + await NotificationService.initialize(); + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -29,7 +20,7 @@ class MyApp extends StatelessWidget { title: 'Hackaton App', theme: ThemeData( fontFamily: 'Roboto', - scaffoldBackgroundColor: const Color(0xFF0F0D38), + scaffoldBackgroundColor: colorAzul, ), home: const RegistroView(), ); @@ -39,8 +30,8 @@ class MyApp extends StatelessWidget { class RegistroView extends StatelessWidget { const RegistroView({super.key}); - static const Color colorAzul = Color(0xFF0F0D38); - static const Color colorVerde = Color(0xFF2E4D31); + final Color colorAzul = const Color(0xFF0F0D38); + final Color colorVerde = const Color(0xFF2E4D31); @override Widget build(BuildContext context) { @@ -48,14 +39,8 @@ class RegistroView extends StatelessWidget { backgroundColor: Colors.white, appBar: AppBar( backgroundColor: colorAzul, - title: const Text( - 'Registro', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 28, - ), - ), + title: const Text('Registro', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 28)), centerTitle: true, ), body: SingleChildScrollView( @@ -72,46 +57,36 @@ class RegistroView extends StatelessWidget { children: [ ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: colorAzul, - padding: const EdgeInsets.symmetric( - horizontal: 30, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15)), + backgroundColor: colorAzul, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)) ), onPressed: () { + // ✅ Navegar a HomeScreen (única barra de navegación) Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (context) => const HomeScreen()), + MaterialPageRoute(builder: (context) => const HomeScreen()), ); }, - child: const Text( - 'Registrar', - style: TextStyle(color: Colors.white, fontSize: 18), - ), + child: const Text('Registrar', style: TextStyle(color: Colors.white, fontSize: 18)), ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: colorAzul, - padding: const EdgeInsets.symmetric( - horizontal: 30, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15)), + backgroundColor: colorAzul, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)) ), onPressed: () { + // Navegar a LoginView Navigator.push( context, - MaterialPageRoute( - builder: (context) => const LoginView()), + MaterialPageRoute(builder: (context) => const LoginView()), ); }, - child: const Text( - '¿Tienes cuenta?', - style: TextStyle(color: Colors.white, fontSize: 18), - ), + child: const Text('¿Tienes cuenta?', style: TextStyle(color: Colors.white, fontSize: 18)), ), ], - ), + ) ], ), ), @@ -136,16 +111,11 @@ class RegistroView extends StatelessWidget { child: TextField( decoration: InputDecoration( hintText: hint, - hintStyle: TextStyle( - color: colorVerde.withOpacity(0.5), - fontWeight: FontWeight.bold, - fontSize: 22, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), + hintStyle: TextStyle(color: colorVerde.withOpacity(0.5), fontWeight: FontWeight.bold, fontSize: 22), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30), - borderSide: const BorderSide(color: colorVerde, width: 4), + borderSide: BorderSide(color: colorVerde, width: 4), ), ), ), diff --git a/lib/src/models/domicilio_model.dart b/lib/src/models/domicilio_model.dart index 18c3ee7..6cc7bd1 100644 --- a/lib/src/models/domicilio_model.dart +++ b/lib/src/models/domicilio_model.dart @@ -1,4 +1,6 @@ +// src/models/domicilio_model.dart import 'dart:convert'; +import '../data/horarios_data.dart'; class Domicilio { final String id; @@ -8,6 +10,8 @@ class Domicilio { final String numero; final double latitud; final double longitud; + final String horarioEstimado; + final String routeId; Domicilio({ required this.id, @@ -17,6 +21,8 @@ class Domicilio { required this.numero, required this.latitud, required this.longitud, + required this.horarioEstimado, + required this.routeId, }); String get direccionCompleta => '$colonia, $calle $numero'; @@ -29,6 +35,8 @@ class Domicilio { 'numero': numero, 'latitud': latitud, 'longitud': longitud, + 'horarioEstimado': horarioEstimado, + 'routeId': routeId, }; factory Domicilio.fromJson(Map json) { @@ -38,8 +46,10 @@ class Domicilio { colonia: json['colonia'], calle: json['calle'], numero: json['numero'], - latitud: json['latitud'].toDouble(), - longitud: json['longitud'].toDouble(), + latitud: (json['latitud'] as num).toDouble(), + longitud: (json['longitud'] as num).toDouble(), + horarioEstimado: json['horarioEstimado'] ?? getHorarioByColonia(json['colonia']), + routeId: json['routeId'] ?? getRouteIdByColonia(json['colonia']), ); } diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart new file mode 100644 index 0000000..cbefaea --- /dev/null +++ b/lib/src/services/notification_service.dart @@ -0,0 +1,43 @@ +// src/services/notification_service.dart +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class NotificationService { + static final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); + + static Future initialize() async { + const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(); + const InitializationSettings settings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + await _notifications.initialize(settings); + } + + static Future showNotification({ + required String title, + required String body, + String? payload, + }) async { + const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( + 'recoleccion_channel', + 'Notificaciones de Recolección', + channelDescription: 'Notificaciones sobre el estado de la recolección', + importance: Importance.high, + priority: Priority.high, + ); + const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(); + const NotificationDetails details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + await _notifications.show( + DateTime.now().millisecond, + title, + body, + details, + payload: payload, + ); + } +} \ No newline at end of file diff --git a/lib/src/views/admin.dart b/lib/src/views/admin.dart new file mode 100644 index 0000000..5fa80bb --- /dev/null +++ b/lib/src/views/admin.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'rutas.dart'; +import '../services/notification_service.dart'; + +class AdminView extends StatefulWidget { + const AdminView({super.key}); + + @override + State createState() => _AdminViewState(); +} + +class _AdminViewState extends State { + bool _camionActivo = true; + String _rutaActual = 'RUTA-01 - Zona Centro'; + String _horaActual = _getHoraActual(); + final TextEditingController _minutosController = TextEditingController(); + int _minutosRetraso = 0; + + @override + void initState() { + super.initState(); + _actualizarHora(); + _cargarEstado(); + } + + @override + void dispose() { + _minutosController.dispose(); + super.dispose(); + } + + Future _cargarEstado() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _camionActivo = !(prefs.getBool('ruta_suspendida') ?? false); + _minutosRetraso = prefs.getInt('retraso_minutos') ?? 0; + _minutosController.text = _minutosRetraso.toString(); + }); + } + + static String _getHoraActual() { + final now = DateTime.now(); + int hora = now.hour > 12 ? now.hour - 12 : now.hour; + if (hora == 0) hora = 12; + final minuto = now.minute.toString().padLeft(2, '0'); + final periodo = now.hour >= 12 ? 'PM' : 'AM'; + return '$hora:$minuto $periodo'; + } + + void _actualizarHora() { + Future.delayed(const Duration(seconds: 1), () { + if (mounted) { + setState(() { + _horaActual = _getHoraActual(); + }); + _actualizarHora(); + } + }); + } + + void _incrementarMinutos() { + setState(() { + _minutosRetraso++; + _minutosController.text = _minutosRetraso.toString(); + }); + } + + void _decrementarMinutos() { + if (_minutosRetraso > 0) { + setState(() { + _minutosRetraso--; + _minutosController.text = _minutosRetraso.toString(); + }); + } + } + + Future _notificarRetraso() async { + if (_minutosRetraso == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Ingresa los minutos de retraso'), backgroundColor: Colors.orange), + ); + return; + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('retraso_minutos', _minutosRetraso); + + await NotificationService.showNotification( + title: '⏰ Retraso en la ruta', + body: 'El camión ha sufrido un retraso de $_minutosRetraso minutos en todas las rutas.', + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Retraso de $_minutosRetraso minutos aplicado'), backgroundColor: Colors.orange), + ); + } + + Future _suspenderRuta() async { + final prefs = await SharedPreferences.getInstance(); + final nuevaSuspension = !_camionActivo; + + await prefs.setBool('ruta_suspendida', nuevaSuspension); + + if (nuevaSuspension) { + await NotificationService.showNotification( + title: '⛔ Ruta suspendida', + body: 'El servicio de recolección ha sido suspendido por hoy.', + ); + } else { + await prefs.remove('retraso_minutos'); + await NotificationService.showNotification( + title: '✅ Ruta reactivada', + body: 'El servicio de recolección ha sido reactivado.', + ); + } + + setState(() { + _camionActivo = !_camionActivo; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: colorAzul, + title: const Text('Administración', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 28)), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white, size: 30), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildEstadoCamion(), + const SizedBox(height: 20), + _buildInfoRuta(), + const SizedBox(height: 30), + const Divider(thickness: 2, color: Colors.grey), + const SizedBox(height: 20), + const Text('RETRASAR RUTA', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: colorAzul)), + const SizedBox(height: 15), + _buildSelectorMinutos(), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _notificarRetraso, + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + child: const Text('NOTIFICAR RETRASO', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: _suspenderRuta, + style: ElevatedButton.styleFrom(backgroundColor: _camionActivo ? Colors.red : Colors.green, minimumSize: const Size(double.infinity, 55), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + child: Text(_camionActivo ? 'SUSPENDER RUTA' : 'REACTIVAR RUTA', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + ), + ], + ), + ), + ); + } + + Widget _buildEstadoCamion() { + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(border: Border.all(color: Colors.black, width: 4), borderRadius: BorderRadius.circular(25)), + child: Row( + children: [ + Icon(Icons.local_shipping, size: 60, color: _camionActivo ? Colors.green : Colors.red), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Estado del camión', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + Text(_camionActivo ? 'ACTIVO' : 'INACTIVO', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: _camionActivo ? Colors.green : Colors.red)), + ], + ), + ], + ), + ); + } + + Widget _buildInfoRuta() { + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(border: Border.all(color: Colors.black, width: 4), borderRadius: BorderRadius.circular(25)), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.route, size: 40), + const SizedBox(width: 10), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [const Text('Ruta actual', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), Text(_rutaActual, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))])), + ], + ), + const SizedBox(height: 15), + Row( + children: [ + const Icon(Icons.access_time, size: 40), + const SizedBox(width: 10), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [const Text('Hora actual', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), Text(_horaActual, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: colorAzul))])), + ], + ), + ], + ), + ); + } + + Widget _buildSelectorMinutos() { + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(border: Border.all(color: Colors.black, width: 4), borderRadius: BorderRadius.circular(25)), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton(onPressed: _decrementarMinutos, icon: const Icon(Icons.remove_circle, size: 50), color: colorAzul), + const SizedBox(width: 10), + SizedBox(width: 80, child: TextField(controller: _minutosController, keyboardType: TextInputType.number, textAlign: TextAlign.center, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), decoration: const InputDecoration(border: OutlineInputBorder(), hintText: '0'), onChanged: (value) { setState(() { _minutosRetraso = int.tryParse(value) ?? 0; }); })), + const SizedBox(width: 10), + IconButton(onPressed: _incrementarMinutos, icon: const Icon(Icons.add_circle, size: 50), color: colorAzul), + const SizedBox(width: 10), + const Text('min', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/views/configuracion.dart b/lib/src/views/configuracion.dart index bff55ae..003b06b 100644 --- a/lib/src/views/configuracion.dart +++ b/lib/src/views/configuracion.dart @@ -1,7 +1,9 @@ -// configuracion.dart +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'rutas.dart'; +import 'admin.dart'; +import '../services/notification_service.dart'; class ConfiguracionView extends StatefulWidget { const ConfiguracionView({super.key}); @@ -11,10 +13,17 @@ class ConfiguracionView extends StatefulWidget { } class _ConfiguracionViewState extends State { - String selectedOption = '7 días'; // Valor por defecto + String selectedOption = '7 días'; bool _isLoading = true; - // Opciones del combobox + // Para el botón secreto de Admin + int _secretTapCount = 0; + Timer? _resetTimer; + + // Para el botón secreto de notificación de prueba (3 taps en el texto de ayuda) + int _testNotificationTapCount = 0; + Timer? _testResetTimer; + final List opciones = [ 'Cada día', 'Cada 3 días', @@ -22,7 +31,6 @@ class _ConfiguracionViewState extends State { 'Cada quincena', ]; - // Mapa para mostrar valores más amigables final Map opcionesMap = { 'Cada día': '1 día', 'Cada 3 días': '3 días', @@ -36,7 +44,13 @@ class _ConfiguracionViewState extends State { _cargarPreferencia(); } - // Cargar la preferencia guardada + @override + void dispose() { + _resetTimer?.cancel(); + _testResetTimer?.cancel(); + super.dispose(); + } + Future _cargarPreferencia() async { try { final prefs = await SharedPreferences.getInstance(); @@ -49,20 +63,17 @@ class _ConfiguracionViewState extends State { _isLoading = false; }); } catch (e) { - print('Error al cargar preferencia: $e'); setState(() { _isLoading = false; }); } } - // Guardar la preferencia Future _guardarPreferencia(String value) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('notificacion_frecuencia', value); - // Mostrar mensaje de confirmación if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -77,7 +88,6 @@ class _ConfiguracionViewState extends State { } } - // Mostrar diálogo con opciones void _mostrarSelector() { showModalBottomSheet( context: context, @@ -137,6 +147,69 @@ class _ConfiguracionViewState extends State { ); } + // 🔥 BOTÓN SECRETO 1: Tocar el ícono 5 veces para Admin + void _onSecretTap() { + _secretTapCount++; + + _resetTimer?.cancel(); + _resetTimer = Timer(const Duration(milliseconds: 1500), () { + _secretTapCount = 0; + print('🔐 Contador Admin reiniciado'); + }); + + print('🔐 Taps Admin: $_secretTapCount/5'); + + if (_secretTapCount >= 5) { + _resetTimer?.cancel(); + _secretTapCount = 0; + + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const AdminView()), + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('🔐 Modo administrador activado'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } + } + + // 🔥 BOTÓN SECRETO 2: Tocar el texto de ayuda 3 veces para notificación de prueba + void _onTestNotificationTap() { + _testNotificationTapCount++; + + _testResetTimer?.cancel(); + _testResetTimer = Timer(const Duration(milliseconds: 1500), () { + _testNotificationTapCount = 0; + print('🔐 Contador Notificacion reiniciado'); + }); + + print('🔐 Taps Notificacion: $_testNotificationTapCount/3'); + + if (_testNotificationTapCount >= 3) { + _testResetTimer?.cancel(); + _testNotificationTapCount = 0; + + // Enviar notificación de prueba + NotificationService.showNotification( + title: '🔔 Notificación de prueba', + body: 'Esta es una notificación de prueba. ¡Tu app funciona correctamente!', + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('🔔 Notificación de prueba enviada'), + backgroundColor: Colors.blue, + duration: Duration(seconds: 2), + ), + ); + } + } + @override Widget build(BuildContext context) { return Container( @@ -144,7 +217,6 @@ class _ConfiguracionViewState extends State { child: SafeArea( child: Column( children: [ - // AppBar personalizado Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), @@ -165,7 +237,6 @@ class _ConfiguracionViewState extends State { textAlign: TextAlign.center, ), ), - // Contenido Expanded( child: _isLoading ? const Center( @@ -188,7 +259,11 @@ class _ConfiguracionViewState extends State { ), child: Row( children: [ - const Icon(Icons.notifications_active_outlined, size: 60), + // 🔥 BOTÓN SECRETO ADMIN (5 taps) + GestureDetector( + onTap: _onSecretTap, + child: const Icon(Icons.notifications_active_outlined, size: 60), + ), const SizedBox(width: 10), Text( selectedOption, @@ -204,7 +279,6 @@ class _ConfiguracionViewState extends State { ), ), const SizedBox(height: 20), - // Información adicional Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( @@ -227,6 +301,39 @@ class _ConfiguracionViewState extends State { ], ), ), + const SizedBox(height: 20), + // 🔥 BOTÓN SECRETO NOTIFICACIÓN DE PRUEBA (3 taps en este texto) + GestureDetector( + onTap: _onTestNotificationTap, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(10), + ), + child: const Text( + '👆 Toca el ícono de la campana 5 veces para Admin', + style: TextStyle(fontSize: 11, color: Colors.grey), + ), + ), + ), + const SizedBox(height: 10), + // Indicador del segundo botón secreto + GestureDetector( + onTap: _onTestNotificationTap, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + '🔔 Toca este texto 3 veces para notificación de prueba', + style: TextStyle(fontSize: 10, color: Colors.blue), + textAlign: TextAlign.center, + ), + ), + ), ], ), ), diff --git a/lib/src/views/domicilios.dart b/lib/src/views/domicilios.dart index 5820241..489b5fa 100644 --- a/lib/src/views/domicilios.dart +++ b/lib/src/views/domicilios.dart @@ -1,10 +1,10 @@ -// 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 '../data/horarios_data.dart'; class DomiciliosView extends StatefulWidget { const DomiciliosView({super.key}); @@ -75,9 +75,9 @@ class _DomiciliosViewState extends State { context: context, barrierDismissible: false, builder: (context) => AlertDialog( - title: const Text('Permiso de ubicación'), + title: const Text('Permiso de ubicacion'), content: const Text( - 'Necesitamos acceder a tu ubicación para asignar tu domicilio a la ruta correcta.', + 'Necesitamos acceder a tu ubicacion para asignar tu domicilio a la ruta correcta.', ), actions: [ TextButton( @@ -126,7 +126,7 @@ class _DomiciliosViewState extends State { }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Necesitamos tu ubicación'), + content: Text('Necesitamos tu ubicacion'), backgroundColor: Colors.orange, ), ); @@ -142,7 +142,7 @@ class _DomiciliosViewState extends State { if (simplePosition == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('No se pudo obtener tu ubicación.'), + content: Text('No se pudo obtener tu ubicacion.'), backgroundColor: Colors.red, ), ); @@ -182,7 +182,7 @@ class _DomiciliosViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('📍 Ubicación obtenida', style: TextStyle(fontSize: 12, color: Colors.green[700])), + Text('Ubicacion obtenida', style: TextStyle(fontSize: 12, color: Colors.green[700])), Text('Lat: ${lat.toStringAsFixed(6)}'), Text('Lng: ${lng.toStringAsFixed(6)}'), ], @@ -198,7 +198,7 @@ class _DomiciliosViewState extends State { const SizedBox(height: 15), _buildCampoTexto(controller: calleController, hint: 'Calle', icon: Icons.streetview), const SizedBox(height: 15), - _buildCampoTexto(controller: numeroController, hint: 'Número', icon: Icons.numbers, keyboardType: TextInputType.number), + _buildCampoTexto(controller: numeroController, hint: 'Numero', icon: Icons.numbers, keyboardType: TextInputType.number), const SizedBox(height: 25), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -258,6 +258,9 @@ class _DomiciliosViewState extends State { return; } + final horario = getHorarioByColonia(coloniaController.text.trim()); + final routeId = getRouteIdByColonia(coloniaController.text.trim()); + final nuevoDomicilio = Domicilio( id: DateTime.now().millisecondsSinceEpoch.toString(), nombre: nombreController.text.trim(), @@ -266,6 +269,8 @@ class _DomiciliosViewState extends State { numero: numeroController.text.trim(), latitud: latitud, longitud: longitud, + horarioEstimado: horario, + routeId: routeId, ); setState(() { @@ -285,7 +290,7 @@ class _DomiciliosViewState extends State { context: context, builder: (context) => AlertDialog( title: const Text('Eliminar domicilio'), - content: Text('¿Eliminar "${domicilios[index].nombre}"?'), + content: Text('Eliminar "${domicilios[index].nombre}"?'), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')), TextButton( @@ -327,7 +332,7 @@ class _DomiciliosViewState extends State { const SizedBox(height: 20), 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 boton + para agregar', style: TextStyle(fontSize: 14, color: Colors.grey.withOpacity(0.5))), ], ), ) @@ -351,7 +356,7 @@ class _DomiciliosViewState extends State { children: [ 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])), + Text('${d.latitud.toStringAsFixed(4)}, ${d.longitud.toStringAsFixed(4)}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), ), diff --git a/lib/src/views/horarios.dart b/lib/src/views/horarios.dart index 5a4a36f..47047a8 100644 --- a/lib/src/views/horarios.dart +++ b/lib/src/views/horarios.dart @@ -1,8 +1,11 @@ +// horarios.dart - Versión actualizada +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'rutas.dart'; import '../models/domicilio_model.dart'; import 'mapa_expandible.dart'; +import '../services/notification_service.dart'; class HorariosView extends StatefulWidget { const HorariosView({super.key}); @@ -14,17 +17,74 @@ class HorariosView extends StatefulWidget { class _HorariosViewState extends State { List domicilios = []; bool _isLoading = true; + Timer? _timer; @override void initState() { super.initState(); _cargarDomicilios(); + _iniciarVerificacionHorarios(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _iniciarVerificacionHorarios() { + _timer = Timer.periodic(const Duration(minutes: 1), (timer) { + _verificarHorarios(); + }); + } + + void _verificarHorarios() async { + final prefs = await SharedPreferences.getInstance(); + final suspensionActiva = prefs.getBool('ruta_suspendida') ?? false; + final retrasoMinutos = prefs.getInt('retraso_minutos') ?? 0; + + if (suspensionActiva) return; + + final ahora = DateTime.now(); + final horaActual = ahora.hour * 60 + ahora.minute; + + for (var domicilio in domicilios) { + final partes = domicilio.horarioEstimado.split(':'); + int hora = int.parse(partes[0]); + int minuto = int.parse(partes[1]); + + // Aplicar retraso + int horaConRetraso = hora; + int minutoConRetraso = minuto + retrasoMinutos; + if (minutoConRetraso >= 60) { + horaConRetraso += minutoConRetraso ~/ 60; + minutoConRetraso = minutoConRetraso % 60; + } + + final horarioMinutos = horaConRetraso * 60 + minutoConRetraso; + + // Si la hora actual está dentro del rango (5 minutos antes y durante) + if (horaActual >= horarioMinutos - 5 && horaActual <= horarioMinutos + 15) { + // Verificar si ya se notificó esta hora + final lastNotification = prefs.getString('last_notification_${domicilio.id}'); + final todayKey = '${DateTime.now().day}-${DateTime.now().month}-${DateTime.now().year}'; + + if (lastNotification != todayKey) { + await prefs.setString('last_notification_${domicilio.id}', todayKey); + await NotificationService.showNotification( + title: '🚛 El camión está cerca', + body: 'El camión de recolección está por llegar a ${domicilio.colonia}. ¡Prepara tu basura!', + ); + } + } + } } Future _cargarDomicilios() async { try { final prefs = await SharedPreferences.getInstance(); final String? domiciliosString = prefs.getString('domicilios'); + final retrasoMinutos = prefs.getInt('retraso_minutos') ?? 0; setState(() { if (domiciliosString != null && domiciliosString.isNotEmpty) { @@ -39,6 +99,10 @@ class _HorariosViewState extends State { } } + String _obtenerHorarioConRetraso(int index) { + return domicilios[index].horarioEstimado; + } + @override Widget build(BuildContext context) { return Container( @@ -46,7 +110,6 @@ class _HorariosViewState extends State { child: SafeArea( child: Column( children: [ - // AppBar personalizado Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), @@ -59,15 +122,10 @@ class _HorariosViewState extends State { ), child: const Text( 'Mis Rutas', - style: TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), - // Lista de domicilios Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator(color: colorAzul)) @@ -76,38 +134,18 @@ class _HorariosViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.location_on_outlined, - size: 100, - color: Colors.grey.withOpacity(0.5), - ), + Icon(Icons.location_on_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 agregados', style: TextStyle(fontSize: 18, color: Colors.grey.withOpacity(0.7))), const SizedBox(height: 10), - Text( - 'Agrega domicilios desde la pestaña Domicilios', - style: TextStyle( - fontSize: 14, - color: Colors.grey.withOpacity(0.5), - ), - textAlign: TextAlign.center, - ), + Text('Agrega domicilios desde la pestaña Domicilios', style: TextStyle(fontSize: 14, color: Colors.grey.withOpacity(0.5)), textAlign: TextAlign.center), ], ), ) : ListView.builder( padding: const EdgeInsets.all(20), itemCount: domicilios.length, - itemBuilder: (context, index) { - final domicilio = domicilios[index]; - return _buildDomicilioCard(domicilio, index); - }, + itemBuilder: (context, index) => _buildDomicilioCard(domicilios[index], index), ), ), ], @@ -117,21 +155,42 @@ class _HorariosViewState extends State { } Widget _buildDomicilioCard(Domicilio domicilio, int index) { - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), - child: Column( - children: [ - // Contenido principal del domicilio - Padding( + return FutureBuilder( + future: SharedPreferences.getInstance(), + builder: (context, snapshot) { + int retraso = 0; + bool suspendida = false; + if (snapshot.hasData) { + retraso = snapshot.data!.getInt('retraso_minutos') ?? 0; + suspendida = snapshot.data!.getBool('ruta_suspendida') ?? false; + } + + String horarioMostrar = domicilio.horarioEstimado; + if (retraso > 0 && !suspendida) { + final partes = domicilio.horarioEstimado.split(':'); + int hora = int.parse(partes[0]); + int minuto = int.parse(partes[1]); + minuto += retraso; + if (minuto >= 60) { + hora += minuto ~/ 60; + minuto = minuto % 60; + } + horarioMostrar = '${hora.toString().padLeft(2, '0')}:${minuto.toString().padLeft(2, '0')} (retraso $retraso min)'; + } else if (suspendida) { + horarioMostrar = 'SUSPENDIDA'; + } + + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 4), + borderRadius: BorderRadius.circular(25), + ), + child: Padding( padding: const EdgeInsets.all(15), child: Column( children: [ - // Info principal Row( children: [ const Icon(Icons.home_outlined, size: 60), @@ -140,57 +199,37 @@ class _HorariosViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - domicilio.nombre, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), + Text(domicilio.nombre, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 4), - Text( - domicilio.direccionCompleta, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), + Text(domicilio.direccionCompleta, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 4), - Text( - '📍 ${domicilio.latitud.toStringAsFixed(4)}, ${domicilio.longitud.toStringAsFixed(4)}', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), + Text('📍 ${domicilio.latitud.toStringAsFixed(4)}, ${domicilio.longitud.toStringAsFixed(4)}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), ), ], ), const SizedBox(height: 12), - // Fila de horario y botón de mapa Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Horario (placeholder para API del backend) Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( - color: colorAzul.withOpacity(0.1), + color: suspendida ? Colors.red.withOpacity(0.2) : colorAzul.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ - Icon(Icons.access_time, color: colorAzul, size: 20), + Icon(Icons.access_time, color: suspendida ? Colors.red : colorAzul, size: 20), const SizedBox(width: 8), Text( - 'Horario: ${_obtenerHorarioEstimado(index)}', + suspendida ? 'RUTA SUSPENDIDA' : 'Horario: $horarioMostrar', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: colorAzul, + color: suspendida ? Colors.red : colorAzul, ), ), ], @@ -198,7 +237,6 @@ class _HorariosViewState extends State { ), ), const SizedBox(width: 10), - // Botón/flecha para mapa desplegable MapaExpandible( latitud: domicilio.latitud, longitud: domicilio.longitud, @@ -209,16 +247,9 @@ class _HorariosViewState extends State { ], ), ), - ], - ), - ), + ), + ); + }, ); } - - // Horario estimado (placeholder para cuando conectes con el backend) - String _obtenerHorarioEstimado(int index) { - // Esto es solo un placeholder - aquí irá la llamada a tu API - final horarios = ['8:00 AM - 9:00 AM', '9:30 AM - 10:30 AM', '11:00 AM - 12:00 PM', '1:00 PM - 2:00 PM', '3:00 PM - 4:00 PM', '5:00 PM - 6:00 PM']; - return horarios[index % horarios.length]; - } } \ No newline at end of file diff --git a/lib/src/views/mapa_expandible.dart b/lib/src/views/mapa_expandible.dart index 56ff1db..a4767e0 100644 --- a/lib/src/views/mapa_expandible.dart +++ b/lib/src/views/mapa_expandible.dart @@ -52,7 +52,7 @@ class _MapaExpandibleState extends State { color: Colors.grey[200], borderRadius: BorderRadius.circular(20), ), - child: const Text('Ubicación no disponible', style: TextStyle(fontSize: 12)), + child: const Text('Ubicación no disponible', style: TextStyle(fontSize: 12)), ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0115915..bed0419 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import app_links +import flutter_local_notifications import geolocator_apple import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 662ba4f..3356b48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + url: "https://pub.dev" + source: hosted + version: "0.7.13" dio: dependency: "direct main" description: @@ -190,8 +198,32 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" + url: "https://pub.dev" + source: hosted + version: "16.3.3" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_map: - dependency: "direct dev" + dependency: "direct main" description: name: flutter_map sha256: "87cc8349b8fa5dccda5af50018c7374b6645334a0d680931c1fe11bce88fa5bb" @@ -233,7 +265,7 @@ packages: source: hosted version: "2.5.0" geolocator: - dependency: "direct dev" + dependency: "direct main" description: name: geolocator sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" @@ -289,7 +321,7 @@ packages: source: hosted version: "8.2.0" google_maps_flutter: - dependency: "direct dev" + dependency: "direct main" description: name: google_maps_flutter sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" @@ -321,7 +353,7 @@ packages: source: hosted version: "2.15.0" google_maps_flutter_web: - dependency: "direct dev" + dependency: "direct main" description: name: google_maps_flutter_web sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf @@ -409,7 +441,7 @@ packages: source: hosted version: "0.3.1" latlong2: - dependency: "direct dev" + dependency: "direct main" description: name: latlong2 sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" @@ -585,7 +617,7 @@ packages: source: hosted version: "2.3.0" permission_handler: - dependency: "direct dev" + dependency: "direct main" description: name: permission_handler sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" @@ -632,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -737,7 +777,7 @@ packages: source: hosted version: "2.1.0" shared_preferences: - dependency: "direct dev" + dependency: "direct main" description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf @@ -885,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" typed_data: dependency: transitive description: @@ -902,7 +950,7 @@ packages: source: hosted version: "0.3.1" url_launcher: - dependency: "direct dev" + dependency: "direct main" description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 @@ -1021,6 +1069,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2fe234c..9d47b6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,33 +31,25 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 supabase_flutter: ^2.5.0 flutter_riverpod: ^2.6.1 dio: ^5.3.1 web_socket_channel: ^2.4.0 + shared_preferences: ^2.2.2 + geolocator: ^11.0.0 + permission_handler: ^11.3.0 + flutter_map: ^6.1.0 + latlong2: ^0.9.0 + google_maps_flutter: ^2.5.0 + google_maps_flutter_web: ^0.5.0 + url_launcher: ^6.2.0 + flutter_local_notifications: ^16.1.0 dev_dependencies: flutter_test: sdk: flutter - shared_preferences: ^2.2.2 - geolocator: ^11.0.0 - permission_handler: ^11.3.0 - - flutter_map: ^6.1.0 # ← Agregar para el mapa - latlong2: ^0.9.0 - google_maps_flutter: ^2.5.0 - google_maps_flutter_web: ^0.5.0 - url_launcher: ^6.2.0 - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the