import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../app/icons/app_icons.dart'; import '../../../app/theme/app_colors.dart'; import '../../../data/services/notification_analytics_service.dart'; import '../../../data/services/notification_inbox_service.dart'; import '../../../data/services/notification_service.dart'; class NotificationCenterScreen extends StatefulWidget { const NotificationCenterScreen({super.key}); @override State createState() => _NotificationCenterScreenState(); } class _NotificationCenterScreenState extends State with TickerProviderStateMixin { late final TabController _tabController; late Future> _alarmsFuture; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); unawaited(NotificationInboxService.instance.removeByType('prayer')); _alarmsFuture = NotificationService.instance.pendingAlerts(); NotificationAnalyticsService.instance.track( 'notif_inbox_opened', dimensions: const {'screen': 'notification_center'}, ); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _refreshAlarms() async { setState(() { _alarmsFuture = NotificationService.instance.pendingAlerts(); }); await _alarmsFuture; } Future _markAllRead() async { await NotificationInboxService.instance.markAllRead(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Semua pesan sudah ditandai terbaca.')), ); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final inboxListenable = NotificationInboxService.instance.listenable(); return Scaffold( appBar: AppBar( leading: IconButton( onPressed: () => context.pop(), icon: const AppIcon(glyph: AppIcons.backArrow), ), title: const Text('Pemberitahuan'), centerTitle: false, actions: [ ListenableBuilder( listenable: _tabController.animation!, builder: (context, _) { final tabIndex = _tabController.index; if (tabIndex == 0) { return IconButton( onPressed: _refreshAlarms, icon: Icon( Icons.refresh_rounded, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ); } return ValueListenableBuilder( valueListenable: inboxListenable, builder: (context, _, __) { final unread = NotificationInboxService.instance.unreadCount(); if (unread <= 0) return const SizedBox.shrink(); return TextButton( onPressed: _markAllRead, child: const Text('Tandai semua'), ); }, ); }, ), const SizedBox(width: 6), ], bottom: TabBar( controller: _tabController, indicatorColor: AppColors.primary, labelColor: AppColors.primary, unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, tabs: [ FutureBuilder>( future: _alarmsFuture, builder: (context, snapshot) { final count = snapshot.data?.length ?? 0; return Tab(text: count > 0 ? 'Alarm ($count)' : 'Alarm'); }, ), ValueListenableBuilder( valueListenable: inboxListenable, builder: (context, _, __) { final unread = NotificationInboxService.instance.unreadCount(); return Tab(text: unread > 0 ? 'Pesan ($unread)' : 'Pesan'); }, ), ], ), ), body: SafeArea( top: false, child: TabBarView( controller: _tabController, children: [ _AlarmTab(future: _alarmsFuture, onRefresh: _refreshAlarms), _InboxTab(), ], ), ), ); } } class _AlarmTab extends StatefulWidget { const _AlarmTab({ required this.future, required this.onRefresh, }); final Future> future; final Future Function() onRefresh; @override State<_AlarmTab> createState() => _AlarmTabState(); } class _AlarmTabState extends State<_AlarmTab> { String _alarmFilter = 'upcoming'; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return FutureBuilder>( future: widget.future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } final alarms = snapshot.data ?? const []; final now = DateTime.now(); final upcoming = alarms .where((alarm) => alarm.scheduledAt == null || !alarm.scheduledAt!.isBefore(now)) .toList(); final passed = alarms .where((alarm) => alarm.scheduledAt != null && alarm.scheduledAt!.isBefore(now)) .toList(); final visible = _alarmFilter == 'past' ? passed : upcoming; if (alarms.isEmpty) { return RefreshIndicator( onRefresh: widget.onRefresh, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _buildAlarmFilters( isDark: isDark, upcomingCount: 0, passedCount: 0, ), const SizedBox(height: 20), AppIcon( glyph: AppIcons.notification, size: 40, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), const SizedBox(height: 12), Text( 'Belum ada alarm aktif', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'Alarm adzan dan iqamah akan muncul di sini saat sudah dijadwalkan.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ); } return RefreshIndicator( onRefresh: widget.onRefresh, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _buildAlarmFilters( isDark: isDark, upcomingCount: upcoming.length, passedCount: passed.length, ), const SizedBox(height: 12), if (visible.isEmpty) Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 24, ), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDarkElevated : AppColors.surfaceLight, borderRadius: BorderRadius.circular(14), border: Border.all( color: isDark ? AppColors.primary.withValues(alpha: 0.14) : AppColors.cream, ), ), child: Text( _alarmFilter == 'upcoming' ? 'Tidak ada alarm akan datang.' : 'Belum ada alarm sudah lewat.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ), for (final alarm in visible) ...[ _buildAlarmItem(context, isDark: isDark, alarm: alarm), const SizedBox(height: 10), ], ], ), ); }, ); } Widget _buildAlarmItem( BuildContext context, { required bool isDark, required NotificationPendingAlert alarm, }) { final chipColor = _chipColor(alarm.type); final when = _formatAlarmTime(alarm.scheduledAt); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDarkElevated : AppColors.surfaceLight, borderRadius: BorderRadius.circular(14), border: Border.all( color: isDark ? AppColors.primary.withValues(alpha: 0.16) : AppColors.cream, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: chipColor.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), ), child: Center( child: AppIcon( glyph: AppIcons.notification, size: 18, color: chipColor, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( alarm.title, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), if (alarm.body.isNotEmpty) Text( alarm.body, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), const SizedBox(height: 8), Row( children: [ _TypeBadge( label: _alarmLabel(alarm.type), color: chipColor, ), const SizedBox(width: 8), Text( when, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ], ), ), ], ), ); } Widget _buildAlarmFilters({ required bool isDark, required int upcomingCount, required int passedCount, }) { return Wrap( spacing: 8, runSpacing: 8, children: [ _FilterChip( label: upcomingCount > 0 ? 'Akan Datang ($upcomingCount)' : 'Akan Datang', selected: _alarmFilter == 'upcoming', isDark: isDark, onTap: () => setState(() => _alarmFilter = 'upcoming'), ), _FilterChip( label: passedCount > 0 ? 'Sudah Lewat ($passedCount)' : 'Sudah Lewat', selected: _alarmFilter == 'past', isDark: isDark, onTap: () => setState(() => _alarmFilter = 'past'), ), ], ); } Color _chipColor(String type) { switch (type) { case 'adhan': return AppColors.primary; case 'iqamah': return const Color(0xFF7B61FF); case 'checklist': return const Color(0xFF2D98DA); case 'system': return const Color(0xFFE17055); default: return AppColors.sage; } } String _alarmLabel(String type) { switch (type) { case 'adhan': return 'Adzan'; case 'iqamah': return 'Iqamah'; case 'checklist': return 'Checklist'; case 'system': return 'Sistem'; default: return 'Alarm'; } } String _formatAlarmTime(DateTime? value) { if (value == null) return 'Waktu tidak diketahui'; try { return DateFormat('EEE, d MMM • HH:mm', 'id_ID').format(value); } catch (_) { return DateFormat('d/MM • HH:mm').format(value); } } } class _InboxTab extends StatefulWidget { @override State<_InboxTab> createState() => _InboxTabState(); } class _InboxTabState extends State<_InboxTab> { String _filter = 'all'; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final inbox = NotificationInboxService.instance; return ValueListenableBuilder( valueListenable: inbox.listenable(), builder: (context, _, __) { final items = inbox.allItems(filter: _filter); if (items.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _buildFilters(isDark), const SizedBox(height: 20), AppIcon( glyph: AppIcons.notification, size: 40, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), const SizedBox(height: 12), Text( 'Belum ada pesan', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'Pesan sistem dan ringkasan ibadah akan muncul di sini.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ); } return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _buildFilters(isDark), const SizedBox(height: 12), ...items.map((item) { final accent = _inboxAccent(item.type); return Padding( padding: const EdgeInsets.only(bottom: 10), child: Dismissible( key: ValueKey(item.id), background: _swipeBackground( isDark: isDark, icon: item.isRead ? Icons.mark_email_unread : Icons.done, label: item.isRead ? 'Belum dibaca' : 'Tandai dibaca', alignment: Alignment.centerLeft, color: AppColors.primary, ), secondaryBackground: _swipeBackground( isDark: isDark, icon: Icons.delete_outline, label: 'Hapus', alignment: Alignment.centerRight, color: AppColors.errorLight, ), confirmDismiss: (direction) async { if (direction == DismissDirection.startToEnd) { if (item.isRead) { await inbox.markUnread(item.id); } else { await inbox.markRead(item.id); } return false; } await inbox.remove(item.id); return true; }, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: () async { if (!item.isRead) { await inbox.markRead(item.id); } await NotificationAnalyticsService.instance.track( 'notif_inbox_opened', dimensions: { 'event_type': item.type, 'deeplink': item.deeplink ?? '', }, ); if (!context.mounted) return; final deeplink = item.deeplink; if (deeplink != null && deeplink.isNotEmpty) { if (deeplink.startsWith('/')) { context.go(deeplink); } else { context.push(deeplink); } } }, child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDarkElevated : AppColors.surfaceLight, borderRadius: BorderRadius.circular(14), border: Border.all( color: !item.isRead ? accent.withValues(alpha: isDark ? 0.4 : 0.32) : (isDark ? AppColors.primary.withValues(alpha: 0.14) : AppColors.cream), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: accent.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), ), child: Center( child: AppIcon( glyph: _inboxGlyph(item.type), size: 18, color: accent, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( item.title, style: Theme.of(context) .textTheme .titleSmall ?.copyWith( fontWeight: FontWeight.w700), ), ), if (item.isPinned) const Padding( padding: EdgeInsets.only(right: 6), child: Icon( Icons.push_pin_rounded, size: 15, color: AppColors.navActiveGoldDeep, ), ), if (!item.isRead) Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.primary, shape: BoxShape.circle, ), ), ], ), const SizedBox(height: 6), Text( item.body, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), const SizedBox(height: 8), Row( children: [ _TypeBadge( label: _inboxLabel(item.type), color: accent, ), const SizedBox(width: 8), Text( _formatInboxTime(item.createdAt), style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ], ), ), IconButton( onPressed: () => inbox.togglePinned(item.id), icon: Icon( item.isPinned ? Icons.push_pin_rounded : Icons.push_pin_outlined, size: 18, color: item.isPinned ? AppColors.navActiveGoldDeep : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), ), ], ), ), ), ), ); }), ], ); }, ); } Widget _buildFilters(bool isDark) { return Wrap( spacing: 8, runSpacing: 8, children: [ _FilterChip( label: 'Semua', selected: _filter == 'all', isDark: isDark, onTap: () => setState(() => _filter = 'all'), ), _FilterChip( label: 'Belum Dibaca', selected: _filter == 'unread', isDark: isDark, onTap: () => setState(() => _filter = 'unread'), ), _FilterChip( label: 'Sistem', selected: _filter == 'system', isDark: isDark, onTap: () => setState(() => _filter = 'system'), ), ], ); } Widget _swipeBackground({ required bool isDark, required IconData icon, required String label, required Alignment alignment, required Color color, }) { return Container( alignment: alignment, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: color.withValues(alpha: isDark ? 0.18 : 0.12), borderRadius: BorderRadius.circular(14), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18, color: color), const SizedBox(width: 6), Text( label, style: TextStyle( color: color, fontWeight: FontWeight.w700, fontSize: 12, ), ), ], ), ); } AppIconGlyph _inboxGlyph(String type) { switch (type) { case 'system': return AppIcons.settings; case 'summary': case 'streak_risk': return AppIcons.laporan; case 'prayer': return AppIcons.notification; case 'content': return AppIcons.notification; default: return AppIcons.notification; } } Color _inboxAccent(String type) { switch (type) { case 'system': return const Color(0xFFE17055); case 'summary': return const Color(0xFF7B61FF); case 'prayer': return AppColors.primary; case 'content': return const Color(0xFF00CEC9); default: return AppColors.sage; } } String _inboxLabel(String type) { switch (type) { case 'system': return 'Sistem'; case 'summary': return 'Ringkasan'; case 'streak_risk': return 'Pengingat'; case 'prayer': return 'Sholat'; case 'content': return 'Konten'; default: return 'Pesan'; } } String _formatInboxTime(DateTime value) { final now = DateTime.now(); final isToday = now.year == value.year && now.month == value.month && now.day == value.day; try { if (isToday) { return DateFormat('HH:mm', 'id_ID').format(value); } return DateFormat('d MMM • HH:mm', 'id_ID').format(value); } catch (_) { if (isToday) { return DateFormat('HH:mm').format(value); } return DateFormat('d/MM • HH:mm').format(value); } } } class _FilterChip extends StatelessWidget { const _FilterChip({ required this.label, required this.selected, required this.isDark, required this.onTap, }); final String label; final bool selected; final bool isDark; final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(999), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: selected ? AppColors.primary.withValues(alpha: isDark ? 0.22 : 0.16) : (isDark ? AppColors.surfaceDarkElevated : AppColors.surfaceLightElevated), borderRadius: BorderRadius.circular(999), border: Border.all( color: selected ? AppColors.primary : (isDark ? AppColors.primary.withValues(alpha: 0.2) : AppColors.cream), ), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: selected ? AppColors.primary : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), ), ), ); } } class _TypeBadge extends StatelessWidget { const _TypeBadge({ required this.label, required this.color, }); final String label; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(10), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: color, ), ), ); } }