// ================================================================ // lib/screens/home_screen.dart // Pantalla Principal — Visualización de ETA y Mensajería Preventiva // ================================================================ // // PROPÓSITO: // Mostrar de forma CLARA y VISUAL el estado del camión de // recolección y el mensaje preventivo correspondiente. // // FLUJO: // 1. Recibe usuario_id desde LoginScreen (Navigator arguments) // 2. Llama a ApiService.obtenerETA() en initState // 3. Muestra el mensaje preventivo con diseño de alto impacto // 4. Se refresca cada 60 segundos para simular actualización real // 5. Registra el FCM token si Firebase está disponible // // DECISIÓN DE DISEÑO: // El texto del mensaje preventivo es ENORME y ocupa el centro // de la pantalla. En una app real de alertas críticas, la // claridad visual es más importante que la estética. // ================================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Descomenta cuando Firebase esté configurado: // import 'package:firebase_messaging/firebase_messaging.dart'; import '../services/api_service.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State with TickerProviderStateMixin { // ---------------------------------------------------------------- // ESTADO LOCAL // ---------------------------------------------------------------- int? _usuarioId; ETAInfo? _etaInfo; bool _cargando = true; String? _error; // Timer para auto-refresh cada 60 segundos Timer? _refreshTimer; // Controlador de animación para el pulso del círculo de ETA late AnimationController _pulseController; late Animation _pulseAnimation; final ApiService _apiService = ApiService(); // ---------------------------------------------------------------- // LIFECYCLE // ---------------------------------------------------------------- @override void initState() { super.initState(); // Configurar animación de pulso (escala entre 1.0 y 1.05) // Da vida a la UI y atrae atención al ETA — importante para demos _pulseController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(reverse: true); _pulseAnimation = Tween(begin: 1.0, end: 1.05).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); // didChangeDependencies se llama después de initState y tiene // acceso al context (necesario para Navigator.arguments). // Por eso la carga de datos inicial va en didChangeDependencies. } @override void didChangeDependencies() { super.didChangeDependencies(); // Obtener el usuario_id pasado desde LoginScreen // Solo lo hacemos una vez (cuando _usuarioId aún es null) if (_usuarioId == null) { final args = ModalRoute.of(context)?.settings.arguments; if (args is int) { _usuarioId = args; _cargarETA(); _iniciarAutoRefresh(); // _registrarFCMToken(); // Activar cuando Firebase esté listo } else { // Fallback: leer de shared_preferences si no viene por argumento _cargarUsuarioDeStorage(); } } } @override void dispose() { _pulseController.dispose(); _refreshTimer?.cancel(); // MUY IMPORTANTE: cancelar timer para evitar leaks super.dispose(); } // ---------------------------------------------------------------- // CARGAR USUARIO ID DESDE STORAGE (fallback) // ---------------------------------------------------------------- Future _cargarUsuarioDeStorage() async { final prefs = await SharedPreferences.getInstance(); final id = prefs.getInt('usuario_id'); if (id != null) { setState(() => _usuarioId = id); _cargarETA(); _iniciarAutoRefresh(); } else { // No hay sesión, volver al login if (mounted) { Navigator.pushReplacementNamed(context, '/'); } } } // ---------------------------------------------------------------- // CARGAR ETA DESDE EL BACKEND // // Centralizado aquí para poder llamarlo tanto en init como // en el auto-refresh y en el botón de recarga manual. // ---------------------------------------------------------------- Future _cargarETA() async { if (_usuarioId == null) return; // Solo mostrar spinner en la carga inicial, no en refresh silencioso if (_etaInfo == null) { setState(() { _cargando = true; _error = null; }); } try { final eta = await _apiService.obtenerETA(_usuarioId!); if (mounted) { setState(() { _etaInfo = eta; _cargando = false; _error = null; }); } } catch (e) { if (mounted) { setState(() { _cargando = false; _error = 'No se pudo conectar al servidor.\nVerifica que el backend esté corriendo.'; }); } } } // ---------------------------------------------------------------- // AUTO-REFRESH CADA 60 SEGUNDOS // // Simula que el ETA se actualiza en tiempo real sin que el // usuario tenga que hacer pull-to-refresh manualmente. // En producción: usar WebSockets o Server-Sent Events. // ---------------------------------------------------------------- void _iniciarAutoRefresh() { _refreshTimer = Timer.periodic( const Duration(seconds: 60), (_) => _cargarETA(), ); } // ---------------------------------------------------------------- // REGISTRAR FCM TOKEN EN EL BACKEND // // Obtiene el token único de este dispositivo de Firebase y lo // manda al backend para poder recibir notificaciones push. // DESCOMENTA cuando tengas Firebase configurado. // ---------------------------------------------------------------- // Future _registrarFCMToken() async { // try { // final messaging = FirebaseMessaging.instance; // // // Pedir permisos de notificación al usuario (iOS requiere esto) // final settings = await messaging.requestPermission( // alert: true, // sound: true, // badge: true, // ); // // if (settings.authorizationStatus == AuthorizationStatus.authorized) { // final token = await messaging.getToken(); // if (token != null && _usuarioId != null) { // await _apiService.registrarFcmToken(_usuarioId!, token); // debugPrint('✅ FCM Token registrado: ${token.substring(0, 20)}...'); // } // } // // // Escuchar notificaciones cuando la app está en FOREGROUND // FirebaseMessaging.onMessage.listen((RemoteMessage message) { // if (message.notification != null && mounted) { // ScaffoldMessenger.of(context).showSnackBar( // SnackBar( // content: Text('🚛 ${message.notification!.body}'), // backgroundColor: Colors.green.shade700, // duration: const Duration(seconds: 5), // ), // ); // // Refrescar ETA al recibir notificación // _cargarETA(); // } // }); // } catch (e) { // debugPrint('Error registrando FCM token: $e'); // } // } // ---------------------------------------------------------------- // CERRAR SESIÓN // ---------------------------------------------------------------- Future _cerrarSesion() async { _refreshTimer?.cancel(); final prefs = await SharedPreferences.getInstance(); await prefs.clear(); if (mounted) { Navigator.pushReplacementNamed(context, '/'); } } // ================================================================ // HELPERS DE UI // ================================================================ // Determina el color del fondo según el ETA (urgencia visual) Color _colorSegunETA(int etaMinutos) { if (etaMinutos <= 5) return const Color(0xFFB71C1C); // Rojo: ¡URGENTE! if (etaMinutos <= 15) return const Color(0xFFF57F17); // Naranja: Pronto if (etaMinutos <= 30) return const Color(0xFF1B5E20); // Verde: Con tiempo return const Color(0xFF1A237E); // Azul: Tranquilo } // Emoji indicador de urgencia String _emojiSegunETA(int etaMinutos) { if (etaMinutos <= 5) return '🔴'; if (etaMinutos <= 15) return '🟡'; if (etaMinutos <= 30) return '🟢'; return '🔵'; } // ================================================================ // BUILD PRINCIPAL // ================================================================ @override Widget build(BuildContext context) { return Scaffold( body: AnimatedContainer( duration: const Duration(milliseconds: 800), // El fondo cambia de color según la urgencia del ETA decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: _etaInfo != null ? [ _colorSegunETA(_etaInfo!.etaMinutos), _colorSegunETA(_etaInfo!.etaMinutos).withOpacity(0.7), ] : [const Color(0xFF2E7D32), const Color(0xFF1B5E20)], ), ), child: SafeArea( child: _buildContenido(), ), ), ); } Widget _buildContenido() { // Estado: Cargando if (_cargando) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.white), SizedBox(height: 16), Text( 'Consultando estado del camión...', style: TextStyle(color: Colors.white70, fontSize: 16), ), ], ), ); } // Estado: Error if (_error != null) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.wifi_off_rounded, size: 80, color: Colors.white54), const SizedBox(height: 16), Text( _error!, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 18), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: _cargarETA, icon: const Icon(Icons.refresh), label: const Text('Reintentar'), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.red.shade700, ), ), ], ), ), ); } // Estado: Con datos - UI principal return _buildUIConDatos(); } // ---------------------------------------------------------------- // UI PRINCIPAL CON DATOS DE ETA // // DECISIÓN DE DISEÑO: El mensaje preventivo ocupa 60% de la // pantalla porque es lo más importante. El usuario debe verlo // de un vistazo, sin lentes y desde lejos. // ---------------------------------------------------------------- Widget _buildUIConDatos() { if (_etaInfo == null) return const SizedBox.shrink(); final eta = _etaInfo!; return Column( children: [ // -------------------------------------------------------- // HEADER: Barra superior con colonia y botón de logout // -------------------------------------------------------- Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Colonia del usuario Row( children: [ const Icon(Icons.location_on, color: Colors.white70, size: 18), const SizedBox(width: 4), Text( eta.colonia, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16, ), ), ], ), // Botón de logout IconButton( onPressed: _cerrarSesion, icon: const Icon(Icons.logout, color: Colors.white70), tooltip: 'Cerrar sesión', ), ], ), ), const Spacer(flex: 1), // -------------------------------------------------------- // CENTRO: ETA Visual (el corazón de la pantalla) // ScaleTransition aplica la animación de pulso al círculo // -------------------------------------------------------- ScaleTransition( scale: _pulseAnimation, child: Container( width: 220, height: 220, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.15), border: Border.all(color: Colors.white, width: 3), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _emojiSegunETA(eta.etaMinutos), style: const TextStyle(fontSize: 48), ), const SizedBox(height: 4), // Número de minutos — el dato más importante Text( '${eta.etaMinutos}', style: const TextStyle( fontSize: 64, fontWeight: FontWeight.w900, color: Colors.white, height: 1, ), ), const Text( 'minutos', style: TextStyle( fontSize: 18, color: Colors.white70, fontWeight: FontWeight.w300, ), ), ], ), ), ), const SizedBox(height: 24), // ETA en texto descriptivo Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( eta.etaTexto, textAlign: TextAlign.center, style: const TextStyle( fontSize: 22, color: Colors.white, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 40), // -------------------------------------------------------- // MENSAJE PREVENTIVO — El núcleo del producto // // Este es el mensaje que el usuario DEBE leer. Enorme, // contrastado, en un card destacado. Sin distracciones. // -------------------------------------------------------- Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Text( // ¡ESTE ES EL MENSAJE PRINCIPAL DEL SISTEMA! // Viene del backend (campo mensaje_preventivo) // Ejemplos: // "⏰ Prepárate, el camión llega pronto. No saques tu basura aún." // "🚛 ¡El camión está muy cerca! Saca tu basura AHORA." eta.mensajePreventivo, textAlign: TextAlign.center, style: TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: _colorSegunETA(eta.etaMinutos), height: 1.3, ), ), ), ), const Spacer(flex: 2), // -------------------------------------------------------- // FOOTER: Botón de refresh manual + última actualización // -------------------------------------------------------- Padding( padding: const EdgeInsets.only(bottom: 32), child: Column( children: [ // Indicador de auto-refresh const Text( '🔄 Se actualiza automáticamente cada minuto', style: TextStyle(color: Colors.white54, fontSize: 12), ), const SizedBox(height: 12), // Botón de refresh manual para demos / jueces impacientes OutlinedButton.icon( onPressed: _cargarETA, icon: const Icon(Icons.refresh, color: Colors.white), label: const Text( 'Actualizar ahora', style: TextStyle(color: Colors.white), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.white54), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), ), ], ), ), ], ); } }