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/app_page_header.dart'; import '../../widgets/app_state_view.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../theme/app_surfaces.dart'; import '../../theme/m3_motion.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: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const AppPageHeader( title: 'Notifications', subtitle: 'Updates and mentions across tasks and tickets', ), 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: notificationsAsync.when( data: (items) { if (items.isEmpty) { return const AppEmptyView( icon: Icons.notifications_none_outlined, title: 'No notifications yet', subtitle: "You'll see updates here when something needs your attention.", ); } return 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.announcementId != null ? 'Announcement' : item.taskId != null ? taskTitle : ticketSubject; final title = _notificationTitle(item.type, actorName); final icon = _notificationIcon(item.type); Future handleTap() 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 (item.announcementId != null) { context.go('/announcements'); } else if (taskId != null) { context.go('/tasks/$taskId'); } else if (ticketId != null) { context.go('/tickets/$ticketId'); } } return M3FadeSlideIn( delay: Duration( milliseconds: index.clamp(0, 6) * 50, ), child: M3PressScale( onTap: handleTap, child: Card( shape: AppSurfaces.of(context).compactShape, 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 ? Icon( Icons.circle, size: 10, color: Theme.of(context).colorScheme.error, ) : null, ), ), ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => AppErrorView( error: error, onRetry: () => ref.invalidate(notificationsProvider), ), ), ), ], ), ); } 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 'announcement': return '$actorName posted an announcement'; case 'announcement_comment': return '$actorName commented on an announcement'; case 'it_job_reminder': return 'IT Job submission reminder'; 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 'announcement': return Icons.campaign; case 'announcement_comment': return Icons.comment_outlined; case 'it_job_reminder': return Icons.print; case 'mention': default: return Icons.alternate_email; } } }