tasq/lib/screens/notifications/notifications_screen.dart

236 lines
8.5 KiB
Dart

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';
class NotificationsScreen extends ConsumerStatefulWidget {
const NotificationsScreen({super.key});
@override
ConsumerState<NotificationsScreen> createState() =>
_NotificationsScreenState();
}
class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
bool _showBanner = false;
bool _dismissed = false;
@override
void initState() {
super.initState();
_checkChannel();
}
Future<void> _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);
return 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,
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 (item.announcementId != null) {
context.go('/announcements');
} else if (taskId != null) {
context.go('/tasks/$taskId');
} else if (ticketId != null) {
context.go('/tickets/$ticketId');
}
},
),
);
},
);
},
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;
}
}
}