diff --git a/lib/main.dart b/lib/main.dart index 314195b..7634c8a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'src/views/rutas.dart'; import 'src/views/login.dart'; import 'src/views/home_screen.dart'; +import 'src/services/notification_service.dart'; // ← AGREGAR -void main() { +void main() async { // ← CAMBIAR a async + WidgetsFlutterBinding.ensureInitialized(); + await NotificationService.initialize(); // ← AGREGAR await runApp(const MyApp()); } 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/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 index d028dcf..2ff2fba 100644 --- a/lib/src/views/admin.dart +++ b/lib/src/views/admin.dart @@ -1,6 +1,8 @@ // src/views/admin.dart 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}); @@ -10,22 +12,17 @@ class AdminView extends StatefulWidget { } class _AdminViewState extends State { - // Estado del camión bool _camionActivo = true; String _rutaActual = 'RUTA-01 - Zona Centro'; String _horaActual = _getHoraActual(); - - // Controlador para minutos de retraso final TextEditingController _minutosController = TextEditingController(); int _minutosRetraso = 0; - String _horaEstimadaOriginal = '8:30 AM'; - String _horaEstimadaActual = '8:30 AM'; - @override void initState() { super.initState(); _actualizarHora(); + _cargarEstado(); } @override @@ -34,6 +31,15 @@ class _AdminViewState extends State { 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; @@ -70,7 +76,7 @@ class _AdminViewState extends State { } } - void _notificarRetraso() { + Future _notificarRetraso() async { if (_minutosRetraso == 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Ingresa los minutos de retraso'), backgroundColor: Colors.orange), @@ -78,37 +84,50 @@ class _AdminViewState extends State { 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 notificado'), - backgroundColor: Colors.orange, - ), + SnackBar(content: Text('Retraso de $_minutosRetraso minutos aplicado'), backgroundColor: Colors.orange), ); } - void _suspenderRuta() { + 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; }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(_camionActivo ? 'Ruta reactivada' : 'Ruta suspendida'), - backgroundColor: _camionActivo ? Colors.green : Colors.red, - ), - ); } @override Widget build(BuildContext context) { - return Scaffold( // ← CAMBIADO: ahora usa Scaffold + return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: colorAzul, - title: const Text( - 'Administración', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 28), - ), + 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), @@ -119,53 +138,26 @@ class _AdminViewState extends State { padding: const EdgeInsets.all(20), child: Column( children: [ - // Estado del camión _buildEstadoCamion(), const SizedBox(height: 20), - - // Información de ruta _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 Text('RETRASAR RUTA', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: colorAzul)), const SizedBox(height: 15), - - // Selector de minutos _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), - ), + 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), - - // Botón Suspender Ruta 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), - ), + 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)), ), ], ), @@ -176,31 +168,17 @@ class _AdminViewState extends State { Widget _buildEstadoCamion() { return Container( padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), + 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, - ), + 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, - ), - ), + Text(_camionActivo ? 'ACTIVO' : 'INACTIVO', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: _camionActivo ? Colors.green : Colors.red)), ], ), ], @@ -211,25 +189,14 @@ class _AdminViewState extends State { Widget _buildInfoRuta() { return Container( padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), + 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)), - ], - ), - ), + 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), @@ -237,15 +204,7 @@ class _AdminViewState extends State { 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)), - ], - ), - ), + 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))])), ], ), ], @@ -256,47 +215,15 @@ class _AdminViewState extends State { Widget _buildSelectorMinutos() { return Container( padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), + 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, - ), + 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(() { - if (value.isEmpty) { - _minutosRetraso = 0; - } else { - _minutosRetraso = int.tryParse(value) ?? 0; - } - }); - }, - ), - ), + 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, - ), + 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)), ], diff --git a/lib/src/views/configuracion.dart b/lib/src/views/configuracion.dart index 49ca8a9..003b06b 100644 --- a/lib/src/views/configuracion.dart +++ b/lib/src/views/configuracion.dart @@ -3,6 +3,7 @@ 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}); @@ -15,10 +16,14 @@ class _ConfiguracionViewState extends State { String selectedOption = '7 días'; bool _isLoading = true; - // Para el botón secreto + // 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', @@ -42,6 +47,7 @@ class _ConfiguracionViewState extends State { @override void dispose() { _resetTimer?.cancel(); + _testResetTimer?.cancel(); super.dispose(); } @@ -141,26 +147,22 @@ class _ConfiguracionViewState extends State { ); } - // 🔥 BOTÓN SECRETO - Tocar el ícono 5 veces + // 🔥 BOTÓN SECRETO 1: Tocar el ícono 5 veces para Admin void _onSecretTap() { _secretTapCount++; - // Cancelar el reset anterior si existe _resetTimer?.cancel(); - - // Resetear después de 1.5 segundos si no completa los 5 taps _resetTimer = Timer(const Duration(milliseconds: 1500), () { _secretTapCount = 0; - print('🔐 Contador reiniciado'); + print('🔐 Contador Admin reiniciado'); }); - print('🔐 Taps secretos: $_secretTapCount/5'); + print('🔐 Taps Admin: $_secretTapCount/5'); if (_secretTapCount >= 5) { _resetTimer?.cancel(); _secretTapCount = 0; - // Navegar a Admin Navigator.push( context, MaterialPageRoute(builder: (context) => const AdminView()), @@ -176,6 +178,38 @@ class _ConfiguracionViewState extends State { } } + // 🔥 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( @@ -225,7 +259,7 @@ class _ConfiguracionViewState extends State { ), child: Row( children: [ - // 🔥 BOTÓN SECRETO - Solo el ícono responde a los taps + // 🔥 BOTÓN SECRETO ADMIN (5 taps) GestureDetector( onTap: _onSecretTap, child: const Icon(Icons.notifications_active_outlined, size: 60), @@ -268,15 +302,36 @@ class _ConfiguracionViewState extends State { ), ), const SizedBox(height: 20), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(10), + // 🔥 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), + ), ), - child: const Text( - '👆 Toca el ícono de la campana 5 veces rápido 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..85b077d 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, ), ); @@ -169,7 +169,7 @@ class _DomiciliosViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Añadir domicilio', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: colorAzul)), + const Text('Anadir domicilio', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: colorAzul)), const SizedBox(height: 10), Container( padding: const EdgeInsets.all(12), @@ -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..36f52ee 100644 --- a/lib/src/views/horarios.dart +++ b/lib/src/views/horarios.dart @@ -1,8 +1,10 @@ +// horarios.dart - Versión actualizada 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 +16,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 +98,13 @@ class _HorariosViewState extends State { } } + String _obtenerHorarioConRetraso(int index) { + final prefs = SharedPreferences.getInstance(); + final retraso = prefs is int ? prefs : 0; + // Simplificado - en producción usar Future + return domicilios[index].horarioEstimado; + } + @override Widget build(BuildContext context) { return Container( @@ -46,7 +112,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 +124,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 +136,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 +157,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 +201,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 +239,6 @@ class _HorariosViewState extends State { ), ), const SizedBox(width: 10), - // Botón/flecha para mapa desplegable MapaExpandible( latitud: domicilio.latitud, longitud: domicilio.longitud, @@ -209,16 +249,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)), ); }