// lib/features/eta/eta_screen.dart // Vista principal del ciudadano: ETA sin mapa ni coordenadas. // Se refresca en initState y al recibir push FCM (vía NotificationService). import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'eta_model.dart'; import 'eta_provider.dart'; import '../notifications/notification_service.dart'; import '../../shared/widgets/prevention_banner.dart'; import '../../shared/widgets/progress_steps.dart'; class EtaScreen extends ConsumerStatefulWidget { const EtaScreen({super.key}); @override ConsumerState createState() => _EtaScreenState(); } class _EtaScreenState extends ConsumerState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Escuchar pushes en foreground: refrescar ETA al recibir cualquier // evento FCM de RUTA_PROXIMITY o ROUTE_START. NotificationService.onFcmMessage.addListener(_onPush); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); NotificationService.onFcmMessage.removeListener(_onPush); super.dispose(); } /// Refresca cuando la app vuelve al foreground. @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { ref.read(etaProvider.notifier).refresh(); } } void _onPush() { ref.read(etaProvider.notifier).refresh(); } @override Widget build(BuildContext context) { final etaAsync = ref.watch(etaProvider); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( title: const Text('Mi recolección'), actions: [ IconButton( icon: const Icon(Icons.refresh_rounded), tooltip: 'Actualizar', onPressed: () => ref.read(etaProvider.notifier).refresh(), ), ], ), body: etaAsync.when( loading: () => const _EtaLoading(), error: (e, _) => _EtaError(message: e.toString()), data: (eta) => _EtaContent(eta: eta), ), ); } } // ────────────────────────────────────────── // Contenido principal cuando hay datos // ────────────────────────────────────────── class _EtaContent extends StatelessWidget { final EtaResponse eta; const _EtaContent({required this.eta}); @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () => ProviderScope.containerOf(context).read(etaProvider.notifier).refresh(), child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [ _EtaHeroCard(eta: eta), const SizedBox(height: 12), const PreventionBanner(), const SizedBox(height: 12), ProgressSteps(stepIndex: eta.stepIndex), const SizedBox(height: 12), const _FcmStatusBadge(), const SizedBox(height: 24), ], ), ); } } // ────────────────────────────────────────── // Hero card con ventana horaria y mensaje // ────────────────────────────────────────── class _EtaHeroCard extends StatelessWidget { final EtaResponse eta; const _EtaHeroCard({required this.eta}); Color _bgColor(BuildContext context) { final cs = Theme.of(context).colorScheme; if (eta.isCompleted) return cs.surfaceContainerHighest; if (eta.isNearby) return const Color(0xFFFFF8E1); // amber-50 equivalente return const Color(0xFFE1F5EE); // teal-50 } Color _accentColor(BuildContext context) { if (eta.isCompleted) return Theme.of(context).colorScheme.outline; if (eta.isNearby) return const Color(0xFFBA7517); // amber-400 return const Color(0xFF1D9E75); // teal-400 } @override Widget build(BuildContext context) { final accent = _accentColor(context); final textTheme = Theme.of(context).textTheme; return AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: _bgColor(context), borderRadius: BorderRadius.circular(16), border: Border.all(color: accent.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Status pill _StatusPill(eta: eta, accent: accent), const SizedBox(height: 10), // Ventana horaria o estado Text( eta.ventanaHoraria ?? _windowLabel(eta.status), style: textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, color: accent, height: 1.1, ), ), const SizedBox(height: 6), Text( eta.mensaje, style: textTheme.bodyMedium?.copyWith( color: accent.withOpacity(0.85), height: 1.45, ), ), ], ), ); } String _windowLabel(RouteStatus s) { switch (s) { case RouteStatus.completada: return 'Servicio finalizado'; case RouteStatus.diferida: return 'Servicio diferido'; case RouteStatus.reasignada: return 'Ruta reasignada'; default: return 'En camino'; } } } class _StatusPill extends StatelessWidget { final EtaResponse eta; final Color accent; const _StatusPill({required this.eta, required this.accent}); @override Widget build(BuildContext context) { final label = eta.isNearby ? 'Cerca de tu domicilio' : eta.isCompleted ? 'Servicio completado' : 'En camino a tu sector'; return Row( mainAxisSize: MainAxisSize.min, children: [ if (!eta.isCompleted) _PulsingDot(color: accent), if (!eta.isCompleted) const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: accent.withOpacity(0.15), borderRadius: BorderRadius.circular(100), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: accent, ), ), ), ], ); } } class _PulsingDot extends StatefulWidget { final Color color; const _PulsingDot({required this.color}); @override State<_PulsingDot> createState() => _PulsingDotState(); } class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { late AnimationController _ctrl; late Animation _anim; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(reverse: true); _anim = Tween(begin: 1.0, end: 0.3).animate(_ctrl); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _anim, builder: (_, __) => Opacity( opacity: _anim.value, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: widget.color, shape: BoxShape.circle, ), ), ), ); } } // ────────────────────────────────────────── // Badge de suscripción FCM // ────────────────────────────────────────── class _FcmStatusBadge extends ConsumerWidget { const _FcmStatusBadge(); @override Widget build(BuildContext context, WidgetRef ref) { final routeId = ref.watch(activeRouteIdProvider); return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(10), border: Border.all( color: Theme.of(context).colorScheme.outlineVariant, ), ), child: Row( children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Color(0xFF1D9E75), shape: BoxShape.circle, ), ), const SizedBox(width: 10), Expanded( child: Text.rich( TextSpan( children: [ const TextSpan( text: 'Notificaciones activas ', style: TextStyle(fontWeight: FontWeight.w500), ), TextSpan( text: routeId != null ? 'para topic_$routeId' : '— suscribiendo...', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), style: const TextStyle(fontSize: 12), ), ), ], ), ); } } // Expone el routeId activo (se puebla desde el provider de sesión/domicilio) final activeRouteIdProvider = StateProvider((ref) => null); // ────────────────────────────────────────── // Estados de carga y error // ────────────────────────────────────────── class _EtaLoading extends StatelessWidget { const _EtaLoading(); @override Widget build(BuildContext context) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator.adaptive(), SizedBox(height: 12), Text('Consultando estado del servicio...'), ], ), ); } } class _EtaError extends StatelessWidget { final String message; const _EtaError({required this.message}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.wifi_off_rounded, size: 48, color: Colors.grey), const SizedBox(height: 12), const Text( 'No se pudo obtener el estado', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 6), Text( message, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center, ), const SizedBox(height: 16), FilledButton.icon( onPressed: () => ProviderScope.containerOf(context) .read(etaProvider.notifier) .refresh(), icon: const Icon(Icons.refresh_rounded), label: const Text('Reintentar'), ), ], ), ), ); } }