// lib/features/notifications/notifications_screen.dart // Historial de notificaciones FCM recibidas. // Los items se almacenan en memoria (no en BD) — solo mensajes del topic propio. import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'notification_service.dart'; import '../eta/eta_screen.dart'; // activeRouteIdProvider // ────────────────────────────────────────── // Modelo local de item de notificación // ────────────────────────────────────────── enum FcmEventType { routeStart, truckProximity, routeCompleted, reassignment, unknown } FcmEventType _eventTypeFromMessage(RemoteMessage msg) { final type = msg.data['event'] as String?; switch (type) { case 'ROUTE_START': return FcmEventType.routeStart; case 'TRUCK_PROXIMITY': return FcmEventType.truckProximity; case 'ROUTE_COMPLETED': return FcmEventType.routeCompleted; case 'reasignacion': case 'retraso': return FcmEventType.reassignment; default: return FcmEventType.unknown; } } class NotificationItem { final String title; final String body; final FcmEventType type; final DateTime receivedAt; const NotificationItem({ required this.title, required this.body, required this.type, required this.receivedAt, }); } // ────────────────────────────────────────── // Provider: lista de notificaciones en memoria // ────────────────────────────────────────── final notificationsListProvider = NotifierProvider>( NotificationsNotifier.new, ); class NotificationsNotifier extends Notifier> { @override List build() { // Escuchar mensajes FCM en foreground NotificationService.onFcmMessage.addListener(_onMessage); ref.onDispose( () => NotificationService.onFcmMessage.removeListener(_onMessage), ); return []; } void _onMessage() { final msg = NotificationService.onFcmMessage.lastMessage; if (msg == null) return; final item = NotificationItem( title: msg.notification?.title ?? 'Recolección', body: msg.notification?.body ?? '', type: _eventTypeFromMessage(msg), receivedAt: DateTime.now(), ); state = [item, ...state]; } void clearAll() => state = []; } // ────────────────────────────────────────── // Pantalla de notificaciones // ────────────────────────────────────────── class NotificationsScreen extends ConsumerWidget { const NotificationsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final items = ref.watch(notificationsListProvider); final routeId = ref.watch(activeRouteIdProvider); return Scaffold( appBar: AppBar( title: const Text('Notificaciones'), actions: [ if (items.isNotEmpty) TextButton( onPressed: () => ref.read(notificationsListProvider.notifier).clearAll(), child: const Text('Limpiar'), ), ], ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ // Badge de suscripción FCM _FcmTopicBadge(routeId: routeId), const SizedBox(height: 12), // Aviso de privacidad _PrivacyNote(), const SizedBox(height: 16), if (items.isEmpty) const _EmptyState() else ...[ const _SectionLabel(label: 'Recientes'), ...items.map((item) => _NotificationCard(item: item)), ], ], ), ); } } // ────────────────────────────────────────── // Widgets auxiliares // ────────────────────────────────────────── class _FcmTopicBadge extends StatelessWidget { final String? routeId; const _FcmTopicBadge({required this.routeId}); @override Widget build(BuildContext context) { 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: 'Suscrito a ', style: TextStyle(fontSize: 12), ), TextSpan( text: routeId != null ? 'topic_$routeId' : 'topic pendiente', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), const TextSpan( text: ' · Solo recibes eventos de tu ruta', style: TextStyle(fontSize: 12), ), ]), ), ), ], ), ); } } class _PrivacyNote extends StatelessWidget { @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFAEEDA), // amber-50 borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFAC775)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.info_outline_rounded, size: 18, color: Color(0xFFBA7517)), const SizedBox(width: 8), Expanded( child: Text( 'Los mensajes no revelan la ubicación del camión. Solo se muestra el tiempo estimado de llegada.', style: const TextStyle(fontSize: 12, color: Color(0xFF633806)), maxLines: 3, ), ), ], ), ); } } class _SectionLabel extends StatelessWidget { final String label; const _SectionLabel({required this.label}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( label.toUpperCase(), style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.8, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ); } } class _NotificationCard extends StatelessWidget { final NotificationItem item; const _NotificationCard({required this.item}); IconData get _icon { switch (item.type) { case FcmEventType.routeStart: return Icons.arrow_forward_rounded; case FcmEventType.truckProximity: return Icons.local_shipping_rounded; case FcmEventType.routeCompleted: return Icons.check_circle_outline_rounded; case FcmEventType.reassignment: return Icons.swap_horiz_rounded; default: return Icons.notifications_outlined; } } Color _accentColor() { switch (item.type) { case FcmEventType.routeStart: return const Color(0xFF1D9E75); case FcmEventType.truckProximity: return const Color(0xFFBA7517); case FcmEventType.routeCompleted: return Colors.grey; case FcmEventType.reassignment: return const Color(0xFF378ADD); default: return Colors.grey; } } String _relativeTime() { final diff = DateTime.now().difference(item.receivedAt); if (diff.inMinutes < 1) return 'Ahora mismo'; if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes} min'; if (diff.inHours < 24) return 'Hace ${diff.inHours} h'; return 'Ayer'; } @override Widget build(BuildContext context) { final accent = _accentColor(); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10), border: Border( left: BorderSide(color: accent, width: 3), top: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), right: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), bottom: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: accent.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(_icon, size: 16, color: accent), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.title, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( item.body, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.4, ), ), const SizedBox(height: 4), Text( _relativeTime(), style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), ], ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 48), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.notifications_none_rounded, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(height: 12), Text( 'Sin notificaciones aún', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( 'Recibirás un aviso cuando el camión esté cerca.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ), ); } }