import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; import '../../../services/alerts/road_alert_models.dart'; import '../../../services/alerts/road_alert_providers.dart'; import '../../../services/alerts/road_alert_service.dart'; import '../../../services/group_rides/group_ride_models.dart'; import '../../../services/group_rides/group_ride_providers.dart'; import '../../../services/location_service.dart'; import 'navigation_maneuver_card.dart'; /// Top alert and notification banner region for the dashboard. /// /// Subscribes strictly to permission, road alert, and group ride providers. /// Does NOT rebuild on high-frequency telemetry samples. class DashboardAlertRegion extends ConsumerWidget { const DashboardAlertRegion({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final hasLocPermissionAsync = ref.watch(locationPermissionGrantedProvider); final hasLocPermission = hasLocPermissionAsync.value ?? true; final alertsAsync = ref.watch(activeAlertsStreamProvider); final activeAlerts = alertsAsync.value ?? []; final closestAlert = activeAlerts.isNotEmpty ? activeAlerts.first : null; final activeGroupRide = ref.watch(activeUserGroupRideProvider); return Column( mainAxisSize: MainAxisSize.min, children: [ if (!hasLocPermission) _buildPermissionBanner(context, ref), const NavigationManeuverCard(), if (closestAlert != null) _buildRoadAlertBanner(context, closestAlert, ref), if (activeGroupRide != null) _buildGroupRideBanner(context, activeGroupRide), ], ); } Widget _buildPermissionBanner(BuildContext context, WidgetRef ref) { return Container( width: double.infinity, color: AppColors.darkAmber, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const Icon(Icons.location_off, color: Colors.white, size: 20), const SizedBox(width: 10), Expanded( child: Text( 'Location permission needed for GPS speedometer.', style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: () async { final locService = ref.read(locationServiceProvider); await locService.requestPermissions(); ref.invalidate(locationPermissionGrantedProvider); }, child: const Text('Enable', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), ], ), ); } Widget _buildRoadAlertBanner(BuildContext context, RoadAlert alert, WidgetRef ref) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSwarm = alert.providerId == 'swarm_alerts' || alert.providerId == 'swarm'; final subtitleColor = isDark ? Colors.white70 : Theme.of(context).colorScheme.onSurfaceVariant; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: isDark ? 0.2 : 0.12), border: Border(bottom: BorderSide(color: alert.category.color, width: 2)), ), child: Row( children: [ Icon(alert.category.icon, color: alert.category.color, size: 26), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( alert.category.displayName.toUpperCase(), overflow: TextOverflow.ellipsis, maxLines: 1, style: TextStyle( fontFamily: AppTypography.fontHeadline, color: alert.category.color, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 0.8, ), ), ), if (isSwarm) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.blueAccent.withValues(alpha: 0.25), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.blueAccent.withValues(alpha: 0.5), width: 0.8), ), child: const Text( 'SWARM', style: TextStyle( fontSize: 9, fontWeight: FontWeight.w900, letterSpacing: 0.5, color: Colors.lightBlueAccent, ), ), ), ], ], ), Text( _formatAlertSubtitle(alert), overflow: TextOverflow.ellipsis, maxLines: 1, style: TextStyle( fontFamily: AppTypography.fontBody, color: subtitleColor, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), const SizedBox(width: 6), if (isSwarm) ...[ IconButton( icon: const Icon(Icons.thumb_up_alt_outlined, color: Colors.greenAccent, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), tooltip: 'Confirm (Still there)', onPressed: () { if (alert.id.isNotEmpty) { ref.read(swarmRoadAlertServiceProvider).confirmAlert(alert.id); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Thanks! Swarm alert confirmed.'), duration: Duration(seconds: 2), ), ); } }, ), IconButton( icon: const Icon(Icons.thumb_down_alt_outlined, color: Colors.orangeAccent, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), tooltip: 'Clear (No longer there)', onPressed: () { if (alert.id.isNotEmpty) { ref.read(swarmRoadAlertServiceProvider).clearAlert(alert.id); ref.read(roadAlertServiceProvider).dismissAlert(alert.id); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Reported clear to Rider Swarm.'), duration: Duration(seconds: 2), ), ); } }, ), ], IconButton( icon: Icon(Icons.close, color: subtitleColor, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), tooltip: 'Dismiss alert', onPressed: () { ref.read(roadAlertServiceProvider).dismissAlert(alert.id); }, ), ], ), ); } Widget _buildGroupRideBanner(BuildContext context, GroupRide ride) { return InkWell( onTap: () => context.push('/group-rides/${ride.id}/live'), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: AppColors.cyberCyan.withValues(alpha: 0.15), border: const Border( bottom: BorderSide(color: AppColors.cyberCyan, width: 1.5), ), ), child: Row( children: [ const Icon(Icons.two_wheeler, color: AppColors.cyberCyan, size: 20), const SizedBox(width: 10), Expanded( child: Row( children: [ Flexible( child: Text( (ride.title.trim().isEmpty || ride.title.trim().toLowerCase() == 'text') ? 'GROUP RIDE ACTIVE' : 'GROUP RIDE ACTIVE: ${ride.title.toUpperCase()}', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: AppTypography.fontHeadline, color: AppColors.cyberCyan, fontWeight: FontWeight.bold, fontSize: 12, letterSpacing: 0.6, ), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.cyberCyan.withValues(alpha: 0.25), borderRadius: BorderRadius.circular(4), ), child: Text( '${ride.participantCount} RIDERS', style: TextStyle( fontFamily: AppTypography.fontTelemetry, color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), ), const SizedBox(width: 8), const Icon(Icons.chevron_right, color: AppColors.cyberCyan, size: 18), ], ), ), ); } String _formatAlertSubtitle(RoadAlert alert) { final distText = _formatAlertDistance(alert.distanceFromUser); final detail = alert.roadName ?? alert.description; if (distText.isEmpty) { return detail.isNotEmpty ? detail : 'Road Alert'; } if (detail.isEmpty) { return distText; } return '$distText • $detail'; } String _formatAlertDistance(double? meters) { if (meters == null) return ''; if (meters < 15.0) { return 'Nearby'; } final miles = meters / 1609.344; if (miles < 0.1) { final feet = ((meters * 3.28084) / 10).round() * 10; return '${feet > 0 ? feet : 50} ft Ahead'; } else { return '${miles.toStringAsFixed(1)} mi Ahead'; } } }