import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../services/notification_service.dart'; import '../../providers/notifications_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/tasks_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../theme/app_surfaces.dart'; class NotificationsScreen extends ConsumerStatefulWidget { const NotificationsScreen({super.key}); @override ConsumerState createState() => _NotificationsScreenState(); } class _NotificationsScreenState extends ConsumerState { bool _showBanner = false; bool _dismissed = false; @override void initState() { super.initState(); _checkChannel(); } Future _checkChannel() async { final muted = await NotificationService.isHighPriorityChannelMuted(); if (!mounted) return; if (muted) { setState(() => _showBanner = true); } } @override Widget build(BuildContext context) { final notificationsAsync = ref.watch(notificationsProvider); final profilesAsync = ref.watch(profilesProvider); final ticketsAsync = ref.watch(ticketsProvider); final tasksAsync = ref.watch(tasksProvider); final profileById = { for (final profile in profilesAsync.valueOrNull ?? []) profile.id: profile, }; final ticketById = { for (final ticket in ticketsAsync.valueOrNull ?? []) ticket.id: ticket, }; final taskById = { for (final task in tasksAsync.valueOrNull ?? []) task.id: task, }; return ResponsiveBody( child: notificationsAsync.when( data: (items) { if (items.isEmpty) { return const Center(child: Text('No notifications yet.')); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.only(top: 16, bottom: 8), child: Text( 'Notifications', style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700), ), ), if (_showBanner && !_dismissed) Padding( padding: const EdgeInsets.only(bottom: 12), child: MaterialBanner( content: const Text( 'Push notifications are currently silenced. Tap here to fix.', ), actions: [ TextButton( onPressed: () { openAppSettings(); }, child: const Text('Open settings'), ), TextButton( onPressed: () { setState(() => _dismissed = true); }, child: const Text('Dismiss'), ), ], ), ), Expanded( child: ListView.separated( padding: const EdgeInsets.only(bottom: 24), itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final item = items[index]; final actorName = item.actorId == null ? 'System' : (profileById[item.actorId]?.fullName ?? item.actorId!); final ticketSubject = item.ticketId == null ? 'Ticket' : (ticketById[item.ticketId]?.subject ?? item.ticketId!); final taskTitle = item.taskId == null ? 'Task' : (taskById[item.taskId]?.title ?? item.taskId!); final subtitle = item.taskId != null ? taskTitle : ticketSubject; final title = _notificationTitle(item.type, actorName); final icon = _notificationIcon(item.type); // Use a slightly more compact card for dense notification lists // — 12px radius, subtle shadow so the list remains readable. return Card( shape: AppSurfaces.of(context).compactShape, shadowColor: AppSurfaces.of(context).compactShadowColor, child: ListTile( leading: Icon(icon), title: Text(title), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(subtitle), const SizedBox(height: 4), if (item.ticketId != null) MonoText('Ticket ${item.ticketId}') else if (item.taskId != null) MonoText('Task ${item.taskId}'), ], ), trailing: item.isUnread ? const Icon( Icons.circle, size: 10, color: Colors.red, ) : null, onTap: () async { final ticketId = item.ticketId; final taskId = item.taskId; if (ticketId != null) { await ref .read(notificationsControllerProvider) .markReadForTicket(ticketId); } else if (taskId != null) { await ref .read(notificationsControllerProvider) .markReadForTask(taskId); } else if (item.isUnread) { await ref .read(notificationsControllerProvider) .markRead(item.id); } if (!context.mounted) return; if (taskId != null) { context.go('/tasks/$taskId'); } else if (ticketId != null) { context.go('/tickets/$ticketId'); } }, ), ); }, ), ), ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center(child: Text('Failed to load notifications: $error')), ), ); } String _notificationTitle(String type, String actorName) { switch (type) { case 'assignment': return '$actorName assigned you'; case 'created': return '$actorName created a new item'; case 'swap_request': return '$actorName requested a shift swap'; case 'swap_update': return '$actorName updated a swap request'; case 'mention': default: return '$actorName mentioned you'; } } IconData _notificationIcon(String type) { switch (type) { case 'assignment': return Icons.assignment_ind_outlined; case 'created': return Icons.campaign_outlined; case 'swap_request': return Icons.swap_horiz; case 'swap_update': return Icons.update; case 'mention': default: return Icons.alternate_email; } } }