import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../../core/theme/app_theme.dart'; import 'colonias_data.dart'; import '../../core/widgets/app_widgets.dart'; import '../../core/network/api_client.dart'; import '../notifications/notification_service.dart'; import '../../shared/widgets/prevention_banner.dart'; import '../../shared/widgets/progress_steps.dart'; // ───────────────────────────────────────────────────────────────────────────── // Modelo de resultado ETA // ───────────────────────────────────────────────────────────────────────────── class _EtaResult { final String mensaje; final String status; final String direccion; final String colonia; final bool hasAddress; final double? lat; final double? lng; const _EtaResult({ required this.mensaje, required this.status, required this.direccion, required this.colonia, required this.hasAddress, this.lat, this.lng, }); const _EtaResult.noAddress() : mensaje = '', status = '', direccion = '', colonia = '', hasAddress = false, lat = null, lng = null; // ── Utilidades derivadas ─────────────────────────────────────────────────── bool get isCompleted => status == 'completada'; bool get isNearby => mensaje.contains('15 minutos') || mensaje.contains('Está atendiendo'); double get progreso { if (isNearby) return 0.85; if (isCompleted) return 1.0; return 0.35; } /// Índice para el widget ProgressSteps (0 = inicio, 1 = en ruta, 2 = cerca, /// 3 = atendiendo, 4 = completado). Ajusta los valores según tu enum real. int get stepIndex { if (isCompleted) return 4; if (isNearby) return 3; if (status == 'en_ruta') return 2; return 1; } String get etiquetaEstado { if (isCompleted) return 'Finalizado'; if (status == 'en_ruta') return 'En ruta'; return 'Pendiente'; } } // ───────────────────────────────────────────────────────────────────────────── // Provider de ETA // ───────────────────────────────────────────────────────────────────────────── class _EtaNotifier extends AsyncNotifier<_EtaResult> { @override Future<_EtaResult> build() => _fetch(); Future refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(_fetch); } Future<_EtaResult> _fetch() async { final dio = ref.read(apiClientProvider); final addressesResp = await dio.get('/addresses'); final raw = addressesResp.data; List items = const []; if (raw is List) { items = raw; } else if (raw is Map && raw['data'] is List) { items = raw['data'] as List; } else if (raw is Map && raw['addresses'] is List) { items = raw['addresses'] as List; } if (items.isEmpty) return const _EtaResult.noAddress(); final addressId = items.first['id'] as String; final etaResp = await dio.get( '/eta', queryParameters: {'address_id': addressId}, ); final data = etaResp.data as Map; return _EtaResult( mensaje: data['mensaje'] as String? ?? '', status: data['status'] as String? ?? '', direccion: items.first['calle'] as String? ?? '', colonia: items.first['colonia'] as String? ?? '', lat: (items.first['lat'] as num?)?.toDouble(), lng: (items.first['lng'] as num?)?.toDouble(), hasAddress: true, ); } } final etaProvider = AsyncNotifierProvider<_EtaNotifier, _EtaResult>( _EtaNotifier.new, ); // Expone el routeId activo (se puebla desde el provider de sesión/domicilio) class ActiveRouteIdNotifier extends Notifier { @override String? build() => null; } final activeRouteIdProvider = NotifierProvider( ActiveRouteIdNotifier.new, ); // ───────────────────────────────────────────────────────────────────────────── // Pantalla principal // ───────────────────────────────────────────────────────────────────────────── class CitizenHomeScreen extends ConsumerStatefulWidget { const CitizenHomeScreen({super.key}); @override ConsumerState createState() => _CitizenHomeScreenState(); } class _CitizenHomeScreenState extends ConsumerState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Refresca al recibir push FCM (RUTA_PROXIMITY, ROUTE_START, etc.) NotificationService.onFcmMessage.addListener(_onPush); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); NotificationService.onFcmMessage.removeListener(_onPush); super.dispose(); } @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: AppTheme.background, 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( error: e.toString(), onRetry: () => ref.read(etaProvider.notifier).refresh(), ), data: (result) => result.hasAddress ? _EtaContent(result: result) : _NoAddressState(onAdd: () => context.go('/addresses/new')), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Contenido principal // ───────────────────────────────────────────────────────────────────────────── class _EtaContent extends StatelessWidget { final _EtaResult result; const _EtaContent({required this.result}); @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: [ // ── 1. Hero card ──────────────────────────────────────────────── _EtaHeroCard(result: result), const SizedBox(height: 16), // ── 2. Domicilio registrado ───────────────────────────────────── AppInfoRow( icon: Icons.home_outlined, label: 'Col. ${result.colonia}', value: result.direccion.isEmpty ? 'Mi domicilio' : result.direccion, trailing: AppStatusBadge.green('Activo'), ), const SizedBox(height: 12), // ── 2.5. Mapa de ubicación ───────────────────────────────── _MapaUbicacion( colonia: result.colonia, lat: result.lat, lng: result.lng, ), const SizedBox(height: 12), // ── 3. Pasos de progreso (justo debajo del domicilio) ─────────── ProgressSteps(stepIndex: result.stepIndex), const SizedBox(height: 12), // ── 4. Banner de prevención ───────────────────────────────────── const PreventionBanner(), const SizedBox(height: 12), // ── 5. Badge de suscripción FCM ───────────────────────────────── const _FcmStatusBadge(), const SizedBox(height: 16), // ── 6. Horario semanal ────────────────────────────────────────── AppSectionTitle(title: 'Horario del camión'), _HorarioCard(), const SizedBox(height: 24), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Mapa de ubicación del domicilio (no interactivo) // ───────────────────────────────────────────────────────────────────────────── class _MapaUbicacion extends StatelessWidget { final String colonia; final double? lat; final double? lng; const _MapaUbicacion({required this.colonia, this.lat, this.lng}); @override Widget build(BuildContext context) { // Usar coordenadas del usuario si están disponibles, sino usar centro de colonia final center = kColoniaCenter(colonia); final pin = (lat != null && lng != null) ? LatLng(lat!, lng!) : center; return Container( height: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppTheme.radiusLg), border: Border.all(color: AppTheme.border, width: 1), ), clipBehavior: Clip.hardEdge, child: FlutterMap( options: MapOptions( initialCenter: pin, initialZoom: 16.0, interactionOptions: const InteractionOptions( flags: InteractiveFlag.none, ), ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.onlineshack.recolecta', ), MarkerLayer( markers: [ Marker( point: pin, width: 36, height: 36, child: const Icon( Icons.home_rounded, color: AppTheme.primary, size: 36, ), ), ], ), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Hero card: estado + ventana horaria + barra de progreso // ───────────────────────────────────────────────────────────────────────────── class _EtaHeroCard extends StatelessWidget { final _EtaResult result; const _EtaHeroCard({required this.result}); Color _bgColor(BuildContext context) { final cs = Theme.of(context).colorScheme; if (result.isCompleted) return cs.surfaceContainerHighest; if (result.isNearby) return const Color(0xFFFFF8E1); // amber-50 return const Color(0xFFE1F5EE); // teal-50 } Color _accentColor(BuildContext context) { if (result.isCompleted) return Theme.of(context).colorScheme.outline; if (result.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(20), decoration: BoxDecoration( color: _bgColor(context), borderRadius: BorderRadius.circular(AppTheme.radiusLg), border: Border.all(color: accent.withOpacity(0.3)), boxShadow: AppTheme.softShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cabecera: icono + etiqueta + punto vivo Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: accent, borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.delete_outline_rounded, color: Colors.white, size: 24, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Camión recolector', style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, color: accent, ), ), const SizedBox(height: 2), _StatusPill(result: result, accent: accent), ], ), ), _LiveDot(active: result.status == 'en_ruta'), ], ), const SizedBox(height: 16), // Ventana horaria o mensaje de estado Text( result.mensaje.isNotEmpty ? result.mensaje : _windowLabel(result.status), style: textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, color: accent, height: 1.2, ), ), const SizedBox(height: 16), // Barra de progreso ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: result.progreso, backgroundColor: accent.withOpacity(0.2), valueColor: AlwaysStoppedAnimation(accent), minHeight: 8, ), ), const SizedBox(height: 6), Row( children: [ Text( 'Inicio de ruta', style: TextStyle(fontSize: 10, color: accent.withOpacity(0.7)), ), const Spacer(), Text( 'Tu casa', style: TextStyle(fontSize: 10, color: accent.withOpacity(0.7)), ), ], ), ], ), ); } String _windowLabel(String s) { switch (s) { case 'completada': return 'Servicio finalizado'; case 'diferida': return 'Servicio diferido'; case 'reasignada': return 'Ruta reasignada'; default: return 'En camino'; } } } // ───────────────────────────────────────────────────────────────────────────── // Pill de estado con punto pulsante // ───────────────────────────────────────────────────────────────────────────── class _StatusPill extends StatelessWidget { final _EtaResult result; final Color accent; const _StatusPill({required this.result, required this.accent}); @override Widget build(BuildContext context) { final label = result.isNearby ? 'Cerca de tu domicilio' : result.isCompleted ? 'Servicio completado' : 'En camino a tu sector'; return Row( mainAxisSize: MainAxisSize.min, children: [ if (!result.isCompleted) _PulsingDot(color: accent), if (!result.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, ), ), ), ], ); } } // ───────────────────────────────────────────────────────────────────────────── // Punto pulsante (animación de opacidad) // ───────────────────────────────────────────────────────────────────────────── 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 final AnimationController _ctrl; late final 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, ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Punto vivo "EN VIVO" (escala + opacidad) // ───────────────────────────────────────────────────────────────────────────── class _LiveDot extends StatefulWidget { final bool active; const _LiveDot({required this.active}); @override State<_LiveDot> createState() => _LiveDotState(); } class _LiveDotState extends State<_LiveDot> with SingleTickerProviderStateMixin { late final AnimationController _anim; @override void initState() { super.initState(); _anim = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), )..repeat(reverse: true); } @override void dispose() { _anim.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!widget.active) return const SizedBox.shrink(); return AnimatedBuilder( animation: _anim, builder: (_, __) => Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: AppTheme.primary.withValues(alpha: 0.5 + _anim.value * 0.5), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // 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), ), ), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Horario semanal // ───────────────────────────────────────────────────────────────────────────── class _HorarioCard extends StatelessWidget { static const _dias = [ _HorarioDia(dia: 'Lunes', hora: '8:00 – 10:00 a.m.', activo: true), _HorarioDia(dia: 'Martes', hora: '8:00 – 10:00 a.m.', activo: true), _HorarioDia(dia: 'Miércoles', hora: 'Sin servicio', activo: false), _HorarioDia(dia: 'Jueves', hora: '8:00 – 10:00 a.m.', activo: true), _HorarioDia(dia: 'Viernes', hora: '8:00 – 10:00 a.m.', activo: true), _HorarioDia(dia: 'Sábado', hora: '9:00 – 11:00 a.m.', activo: true), _HorarioDia(dia: 'Domingo', hora: 'Sin servicio', activo: false), ]; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.radiusLg), border: Border.all(color: AppTheme.border, width: 0.5), boxShadow: AppTheme.softShadow, ), child: Column( children: _dias.map((d) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( children: [ Text( d.dia, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: d.activo ? AppTheme.textPrimary : AppTheme.textSecondary, ), ), const Spacer(), Text( d.hora, style: TextStyle( fontSize: 13, color: d.activo ? AppTheme.primary : AppTheme.textSecondary, ), ), ], ), ); }).toList(), ), ); } } class _HorarioDia { final String dia; final String hora; final bool activo; const _HorarioDia({ required this.dia, required this.hora, required this.activo, }); } // ───────────────────────────────────────────────────────────────────────────── // Sin domicilio registrado // ───────────────────────────────────────────────────────────────────────────── class _NoAddressState extends StatelessWidget { final VoidCallback onAdd; const _NoAddressState({required this.onAdd}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, decoration: const BoxDecoration( color: AppTheme.primaryLight, shape: BoxShape.circle, ), child: const Icon( Icons.home_outlined, color: AppTheme.primary, size: 40, ), ), const SizedBox(height: 20), const Text( 'Sin domicilio registrado', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), const Text( 'Registra tu domicilio para\nrecibir el ETA de tu ruta.', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, height: 1.5, ), ), const SizedBox(height: 24), SizedBox( width: 200, child: ElevatedButton( onPressed: onAdd, child: const Text('Agregar domicilio'), ), ), ], ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Cargando // ───────────────────────────────────────────────────────────────────────────── class _EtaLoading extends StatelessWidget { const _EtaLoading(); @override Widget build(BuildContext context) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: AppTheme.primary), SizedBox(height: 16), Text( 'Consultando estado del servicio...', style: TextStyle(color: AppTheme.textSecondary, fontSize: 14), ), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Error // ───────────────────────────────────────────────────────────────────────────── class _EtaError extends StatelessWidget { final String error; final VoidCallback onRetry; const _EtaError({required this.error, required this.onRetry}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.wifi_off_rounded, color: AppTheme.textSecondary, size: 48, ), const SizedBox(height: 16), const Text( 'No se pudo obtener el estado', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), Text( error, textAlign: TextAlign.center, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), const SizedBox(height: 20), SizedBox( width: 160, child: FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh_rounded), label: const Text('Reintentar'), ), ), ], ), ), ); } }