import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; import '../services/notification_service.dart'; import '../services/database_helper.dart'; import '../providers/settings_provider.dart'; import '../providers/supplement_provider.dart'; class PendingNotificationsScreen extends StatefulWidget { const PendingNotificationsScreen({super.key}); @override State createState() => _PendingNotificationsScreenState(); } class _PendingNotificationsScreenState extends State { List _pendingNotifications = []; List> _trackedNotifications = []; bool _isLoading = true; @override void initState() { super.initState(); _loadNotifications(); } Future _loadNotifications() async { setState(() { _isLoading = true; }); try { // Get settings for retry interval calculation final settingsProvider = Provider.of(context, listen: false); final reminderRetryInterval = settingsProvider.reminderRetryInterval; final maxRetryAttempts = settingsProvider.maxRetryAttempts; // Get system pending notifications final notificationService = NotificationService(); final systemPending = await notificationService.getPendingNotifications(); // Get tracked notifications from database (including retries) final trackedNotifications = await DatabaseHelper.instance.getPendingNotifications(); // Create a more intelligent matching system final allNotifications = >[]; final matchedSystemIds = {}; // First, try to match tracked notifications with system notifications for (final trackedNotification in trackedNotifications) { final scheduledTime = DateTime.parse(trackedNotification['scheduledTime']).toLocal(); final lastRetryTime = trackedNotification['lastRetryTime'] != null ? DateTime.parse(trackedNotification['lastRetryTime']).toLocal() : null; final retryCount = trackedNotification['retryCount'] ?? 0; final isRetrying = trackedNotification['status'] == 'retrying'; final notificationId = trackedNotification['notificationId'] as int; // Try to find matching system notification(s) final matchingSystemNotifications = systemPending.where((systemNotification) { return _isMatchingNotification(systemNotification, trackedNotification, retryCount); }).toList(); // Calculate next retry time if this is a retry notification DateTime? nextRetryTime; bool hasReachedMaxRetries = retryCount >= maxRetryAttempts; if (isRetrying && !hasReachedMaxRetries) { if (lastRetryTime != null) { // Next retry is based on last retry time + interval nextRetryTime = lastRetryTime.add(Duration(minutes: reminderRetryInterval)); } else { // First retry is based on original scheduled time + interval nextRetryTime = scheduledTime.add(Duration(minutes: reminderRetryInterval)); } } // Create the notification entry final notificationEntry = { 'id': notificationId, 'title': 'Time for ${trackedNotification['supplementName'] ?? 'Supplement'}', 'body': 'Take your supplement', 'scheduledTime': scheduledTime, 'nextRetryTime': nextRetryTime, 'lastRetryTime': lastRetryTime, 'type': matchingSystemNotifications.isNotEmpty ? 'matched' : 'tracked_only', 'status': trackedNotification['status'], 'isRetry': isRetrying, 'retryCount': retryCount, 'hasReachedMaxRetries': hasReachedMaxRetries, 'maxRetryAttempts': maxRetryAttempts, 'supplementName': trackedNotification['supplementName'], 'supplementId': trackedNotification['supplementId'], 'systemNotificationCount': matchingSystemNotifications.length, }; allNotifications.add(notificationEntry); // Mark these system notifications as matched for (final systemNotification in matchingSystemNotifications) { matchedSystemIds.add(systemNotification.id); } } // Add unmatched system notifications for (final systemNotification in systemPending) { if (!matchedSystemIds.contains(systemNotification.id)) { allNotifications.add({ 'id': systemNotification.id, 'title': systemNotification.title ?? 'System Notification', 'body': systemNotification.body ?? '', 'scheduledTime': DateTime.now(), // PendingNotificationRequest doesn't have scheduledTime 'type': 'system_only', 'isRetry': false, 'retryCount': 0, 'hasReachedMaxRetries': false, 'maxRetryAttempts': maxRetryAttempts, }); } } // Sort by scheduled time (soonest first) allNotifications.sort((a, b) { final timeA = a['scheduledTime'] as DateTime; final timeB = b['scheduledTime'] as DateTime; return timeA.compareTo(timeB); }); setState(() { _pendingNotifications = systemPending; _trackedNotifications = trackedNotifications; _isLoading = false; }); } catch (e) { print('SupplementsLog: Error loading notifications: $e'); setState(() { _isLoading = false; }); } } Future _showCleanupDialog() async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Cleanup Old Notifications'), content: const Text( 'This will help clean up old or duplicate notifications:\n\n' '• Clear stale system notifications\n' '• Remove very old tracked notifications (>24h overdue)\n' '• Reschedule fresh notifications for active supplements\n\n' 'Choose an option:', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop('cancel'), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(context).pop('clear_old'), child: const Text('Clear Old Only'), ), TextButton( onPressed: () => Navigator.of(context).pop('clear_all'), child: const Text('Clear All & Reschedule'), ), ], ); }, ); if (result != null && result != 'cancel') { await _performCleanup(result); } } Future _performCleanup(String action) async { try { // Show loading indicator if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Cleaning up notifications...'), duration: Duration(seconds: 2), ), ); } final notificationService = NotificationService(); if (action == 'clear_all') { // Clear all notifications and reschedule fresh ones await notificationService.cancelAllReminders(); await DatabaseHelper.instance.cleanupOldNotificationTracking(); // Reschedule notifications for all active supplements final supplementProvider = Provider.of(context, listen: false); await supplementProvider.rescheduleAllNotifications(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('All notifications cleared and rescheduled!'), backgroundColor: Colors.green, ), ); } } else if (action == 'clear_old') { // Clear only very old notifications (>24 hours overdue) await _clearOldNotifications(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Old notifications cleared!'), backgroundColor: Colors.orange, ), ); } } // Refresh the list await _loadNotifications(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error during cleanup: $e'), backgroundColor: Colors.red, ), ); } } } Future _clearOldNotifications() async { final now = DateTime.now(); final cutoff = now.subtract(const Duration(hours: 24)); // Clear very old tracked notifications final db = await DatabaseHelper.instance.database; await db.delete( 'notification_tracking', where: 'scheduledTime < ? AND status IN (?, ?)', whereArgs: [cutoff.toIso8601String(), 'pending', 'retrying'], ); // Note: We can't selectively clear system notifications as we don't have their scheduled times // This is a limitation of the flutter_local_notifications plugin } bool _isMatchingNotification(PendingNotificationRequest systemNotification, Map trackedNotification, int retryCount) { final trackedId = trackedNotification['notificationId'] as int; final supplementId = trackedNotification['supplementId'] as int; // Check for exact ID match (original notification) if (systemNotification.id == trackedId) { return true; } // Check for retry notification IDs (200000 + original_id * 10 + retry_attempt) for (int attempt = 1; attempt <= retryCount; attempt++) { final retryId = 200000 + (trackedId * 10) + attempt; if (systemNotification.id == retryId) { return true; } } // Check for snooze notification IDs (supplementId * 1000 + minutes) // Common snooze intervals: 5, 10, 15, 30 minutes final snoozeIds = [5, 10, 15, 30].map((minutes) => supplementId * 1000 + minutes); if (snoozeIds.contains(systemNotification.id)) { return true; } // Check if it's within the supplement's notification ID range (supplementId * 100 + reminderIndex) final baseId = supplementId * 100; if (systemNotification.id >= baseId && systemNotification.id < baseId + 10) { return true; } return false; } Color _getTypeColor(String type) { switch (type) { case 'matched': return Colors.green; case 'tracked_only': return Colors.orange; case 'system_only': return Colors.blue; default: return Colors.grey; } } Color _getTypeDarkColor(String type) { switch (type) { case 'matched': return Colors.green.shade700; case 'tracked_only': return Colors.orange.shade700; case 'system_only': return Colors.blue.shade700; default: return Colors.grey.shade700; } } String _getTypeLabel(String type, Map notification) { final systemCount = notification['systemNotificationCount'] as int? ?? 0; switch (type) { case 'matched': return systemCount > 1 ? 'Matched' : 'Synced'; case 'tracked_only': return 'Tracking Only'; case 'system_only': return 'System Only'; default: return 'Unknown'; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Pending Notifications'), actions: [ IconButton( icon: const Icon(Icons.cleaning_services), onPressed: _showCleanupDialog, tooltip: 'Cleanup old notifications', ), IconButton( icon: const Icon(Icons.refresh), onPressed: _loadNotifications, tooltip: 'Refresh', ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _loadNotifications, child: _buildNotificationsList(), ), ); } Widget _buildNotificationsList() { if (_pendingNotifications.isEmpty && _trackedNotifications.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.notifications_off, size: 64, color: Colors.grey, ), SizedBox(height: 16), Text( 'No pending notifications', style: TextStyle( fontSize: 18, color: Colors.grey, ), ), SizedBox(height: 8), Text( 'All caught up!', style: TextStyle( color: Colors.grey, ), ), ], ), ); } // Get settings for retry interval calculation final settingsProvider = Provider.of(context, listen: false); final reminderRetryInterval = settingsProvider.reminderRetryInterval; final maxRetryAttempts = settingsProvider.maxRetryAttempts; // Create a more intelligent matching system final allNotifications = >[]; final matchedSystemIds = {}; // First, try to match tracked notifications with system notifications for (final trackedNotification in _trackedNotifications) { final scheduledTime = DateTime.parse(trackedNotification['scheduledTime']).toLocal(); final lastRetryTime = trackedNotification['lastRetryTime'] != null ? DateTime.parse(trackedNotification['lastRetryTime']).toLocal() : null; final retryCount = trackedNotification['retryCount'] ?? 0; final isRetrying = trackedNotification['status'] == 'retrying'; final notificationId = trackedNotification['notificationId'] as int; // Try to find matching system notification(s) final matchingSystemNotifications = _pendingNotifications.where((systemNotification) { return _isMatchingNotification(systemNotification, trackedNotification, retryCount); }).toList(); // Calculate next retry time if this is a retry notification DateTime? nextRetryTime; bool hasReachedMaxRetries = retryCount >= maxRetryAttempts; if (isRetrying && !hasReachedMaxRetries) { if (lastRetryTime != null) { // Next retry is based on last retry time + interval nextRetryTime = lastRetryTime.add(Duration(minutes: reminderRetryInterval)); } else { // First retry is based on original scheduled time + interval nextRetryTime = scheduledTime.add(Duration(minutes: reminderRetryInterval)); } } // Create the notification entry final notificationEntry = { 'id': notificationId, 'title': 'Time for ${trackedNotification['supplementName'] ?? 'Supplement'}', 'body': 'Take your supplement', 'scheduledTime': scheduledTime, 'nextRetryTime': nextRetryTime, 'lastRetryTime': lastRetryTime, 'type': matchingSystemNotifications.isNotEmpty ? 'matched' : 'tracked_only', 'status': trackedNotification['status'], 'isRetry': isRetrying, 'retryCount': retryCount, 'hasReachedMaxRetries': hasReachedMaxRetries, 'maxRetryAttempts': maxRetryAttempts, 'supplementName': trackedNotification['supplementName'], 'supplementId': trackedNotification['supplementId'], 'systemNotificationCount': matchingSystemNotifications.length, }; allNotifications.add(notificationEntry); // Mark these system notifications as matched for (final systemNotification in matchingSystemNotifications) { matchedSystemIds.add(systemNotification.id); } } // Add unmatched system notifications for (final systemNotification in _pendingNotifications) { if (!matchedSystemIds.contains(systemNotification.id)) { allNotifications.add({ 'id': systemNotification.id, 'title': systemNotification.title ?? 'System Notification', 'body': systemNotification.body ?? '', 'scheduledTime': DateTime.now(), // PendingNotificationRequest doesn't have scheduledTime 'type': 'system_only', 'isRetry': false, 'retryCount': 0, 'hasReachedMaxRetries': false, 'maxRetryAttempts': maxRetryAttempts, }); } } // Sort by scheduled time (soonest first) allNotifications.sort((a, b) { final timeA = a['scheduledTime'] as DateTime; final timeB = b['scheduledTime'] as DateTime; return timeA.compareTo(timeB); }); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: allNotifications.length, itemBuilder: (context, index) { final notification = allNotifications[index]; return _buildNotificationCard(notification); }, ); } Widget _buildNotificationCard(Map notification) { final scheduledTime = notification['scheduledTime'] as DateTime; final nextRetryTime = notification['nextRetryTime'] as DateTime?; final lastRetryTime = notification['lastRetryTime'] as DateTime?; final now = DateTime.now(); final isOverdue = scheduledTime.isBefore(now); final isRetry = notification['isRetry'] as bool; final retryCount = notification['retryCount'] as int; final hasReachedMaxRetries = notification['hasReachedMaxRetries'] as bool? ?? false; final maxRetryAttempts = notification['maxRetryAttempts'] as int? ?? 3; final type = notification['type'] as String; // Determine which time to show and calculate time text DateTime displayTime = scheduledTime; String timePrefix = ''; bool showAsNextRetry = false; String timeText = ''; if (isRetry && nextRetryTime != null && !hasReachedMaxRetries) { // For retry notifications that haven't reached max retries, show next retry time final timeDifference = nextRetryTime.difference(now); if (timeDifference.inSeconds.abs() < 30) { // If within 30 seconds, show "due now" displayTime = nextRetryTime; timePrefix = 'Next retry: '; showAsNextRetry = true; timeText = 'due now'; } else if (nextRetryTime.isAfter(now)) { displayTime = nextRetryTime; timePrefix = 'Next retry: '; showAsNextRetry = true; } else { // If next retry time has passed, show it's overdue for retry displayTime = nextRetryTime; timePrefix = 'Retry overdue: '; showAsNextRetry = true; } } else if (isRetry && hasReachedMaxRetries) { // For notifications that have reached max retries, don't show next retry time showAsNextRetry = false; } // Calculate time text if not already set if (timeText.isEmpty) { final isDisplayTimeOverdue = displayTime.isBefore(now); final timeUntil = displayTime.difference(now); if (isDisplayTimeOverdue) { final overdue = now.difference(displayTime); if (overdue.inDays > 0) { timeText = 'Overdue by ${overdue.inDays}d ${overdue.inHours % 24}h'; } else if (overdue.inHours > 0) { timeText = 'Overdue by ${overdue.inHours}h ${overdue.inMinutes % 60}m'; } else if (overdue.inMinutes > 0) { timeText = 'Overdue by ${overdue.inMinutes}m'; } else { timeText = 'Overdue by ${overdue.inSeconds}s'; } } else { if (timeUntil.inDays > 0) { timeText = 'In ${timeUntil.inDays}d ${timeUntil.inHours % 24}h'; } else if (timeUntil.inHours > 0) { timeText = 'In ${timeUntil.inHours}h ${timeUntil.inMinutes % 60}m'; } else if (timeUntil.inMinutes > 0) { timeText = 'In ${timeUntil.inMinutes}m'; } else { timeText = 'In ${timeUntil.inSeconds}s'; } } } // Determine if display time is overdue (needed for icon colors later) final isDisplayTimeOverdue = displayTime.isBefore(now); return Card( margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( notification['title'] ?? 'Notification', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), if (notification['body'] != null) ...[ const SizedBox(height: 4), Text( notification['body'], style: Theme.of(context).textTheme.bodyMedium, ), ], ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (isRetry) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: hasReachedMaxRetries ? Colors.red.withOpacity(0.2) : Colors.orange.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: hasReachedMaxRetries ? Colors.red.withOpacity(0.5) : Colors.orange.withOpacity(0.5), ), ), child: Text( hasReachedMaxRetries ? 'Max retries ($retryCount/$maxRetryAttempts)' : 'Retry #$retryCount', style: TextStyle( fontSize: 12, color: hasReachedMaxRetries ? Colors.red.shade700 : Colors.orange.shade700, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 4), ], Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _getTypeColor(type).withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: _getTypeColor(type).withOpacity(0.5), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _getTypeLabel(type, notification), style: TextStyle( fontSize: 12, color: _getTypeDarkColor(type), fontWeight: FontWeight.bold, ), ), if (type == 'matched' && (notification['systemNotificationCount'] as int? ?? 0) > 1) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(8), ), child: Text( '${notification['systemNotificationCount']}', style: TextStyle( fontSize: 10, color: _getTypeDarkColor(type), fontWeight: FontWeight.bold, ), ), ), ], ], ), ), ], ), ], ), const SizedBox(height: 12), // Show original scheduled time Row( children: [ Icon( isOverdue ? Icons.schedule_outlined : Icons.access_time, size: 16, color: isOverdue ? Colors.red : Colors.grey, ), const SizedBox(width: 8), Text( 'Scheduled: ${_formatTime(scheduledTime)}', style: TextStyle( color: isOverdue ? Colors.red : Colors.grey.shade700, fontWeight: isOverdue ? FontWeight.bold : FontWeight.normal, ), ), if (isOverdue) ...[ const SizedBox(width: 8), Text( '(${_formatOverdueTime(scheduledTime)})', style: TextStyle( color: Colors.red, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ], ), // Show next retry time if applicable if (showAsNextRetry && nextRetryTime != null) ...[ const SizedBox(height: 6), Row( children: [ Icon( isDisplayTimeOverdue ? Icons.warning_outlined : Icons.refresh, size: 16, color: isDisplayTimeOverdue ? Colors.red : Colors.orange, ), const SizedBox(width: 8), Text( '${timePrefix}${_formatTime(displayTime)} • $timeText', style: TextStyle( color: isDisplayTimeOverdue ? Colors.red : Colors.orange.shade700, fontWeight: FontWeight.bold, fontSize: 13, ), ), ], ), ], // Show max retries reached message if (isRetry && hasReachedMaxRetries) ...[ const SizedBox(height: 6), Row( children: [ const Icon( Icons.block, size: 16, color: Colors.red, ), const SizedBox(width: 8), Expanded( child: Text( 'Maximum retry attempts reached. No more reminders will be sent.', style: TextStyle( color: Colors.red.shade700, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), ], // Show last retry time if available if (isRetry && lastRetryTime != null) ...[ const SizedBox(height: 6), Row( children: [ const Icon( Icons.history, size: 16, color: Colors.grey, ), const SizedBox(width: 8), Text( 'Last retry: ${_formatTime(lastRetryTime)}', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, fontStyle: FontStyle.italic, ), ), ], ), ], if (type == 'tracked' && notification['supplementName'] != null) ...[ const SizedBox(height: 8), Row( children: [ const Icon( Icons.medication, size: 16, color: Colors.grey, ), const SizedBox(width: 8), Text( notification['supplementName'], style: TextStyle( color: Colors.grey.shade700, fontStyle: FontStyle.italic, ), ), ], ), ], ], ), ), ); } String _formatTime(DateTime dateTime) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final tomorrow = today.add(const Duration(days: 1)); final notificationDate = DateTime(dateTime.year, dateTime.month, dateTime.day); String timeStr = '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; if (notificationDate == today) { return 'Today $timeStr'; } else if (notificationDate == tomorrow) { return 'Tomorrow $timeStr'; } else { return '${dateTime.day}/${dateTime.month} $timeStr'; } } String _formatOverdueTime(DateTime scheduledTime) { final now = DateTime.now(); final overdue = now.difference(scheduledTime); if (overdue.inDays > 0) { return '${overdue.inDays}d ${overdue.inHours % 24}h overdue'; } else if (overdue.inHours > 0) { return '${overdue.inHours}h ${overdue.inMinutes % 60}m overdue'; } else { return '${overdue.inMinutes}m overdue'; } } }