mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
feat: Enhance pending notifications handling with intelligent matching and cleanup options
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
import '../services/database_helper.dart';
|
import '../services/database_helper.dart';
|
||||||
|
import '../providers/settings_provider.dart';
|
||||||
|
import '../providers/supplement_provider.dart';
|
||||||
|
|
||||||
class PendingNotificationsScreen extends StatefulWidget {
|
class PendingNotificationsScreen extends StatefulWidget {
|
||||||
const PendingNotificationsScreen({super.key});
|
const PendingNotificationsScreen({super.key});
|
||||||
@@ -27,6 +30,11 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get settings for retry interval calculation
|
||||||
|
final settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
|
||||||
|
final reminderRetryInterval = settingsProvider.reminderRetryInterval;
|
||||||
|
final maxRetryAttempts = settingsProvider.maxRetryAttempts;
|
||||||
|
|
||||||
// Get system pending notifications
|
// Get system pending notifications
|
||||||
final notificationService = NotificationService();
|
final notificationService = NotificationService();
|
||||||
final systemPending = await notificationService.getPendingNotifications();
|
final systemPending = await notificationService.getPendingNotifications();
|
||||||
@@ -34,37 +42,81 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
// Get tracked notifications from database (including retries)
|
// Get tracked notifications from database (including retries)
|
||||||
final trackedNotifications = await DatabaseHelper.instance.getPendingNotifications();
|
final trackedNotifications = await DatabaseHelper.instance.getPendingNotifications();
|
||||||
|
|
||||||
// Combine and sort by scheduled time (soonest first)
|
// Create a more intelligent matching system
|
||||||
final allNotifications = <Map<String, dynamic>>[];
|
final allNotifications = <Map<String, dynamic>>[];
|
||||||
|
final matchedSystemIds = <int>{};
|
||||||
|
|
||||||
// Add system notifications
|
// First, try to match tracked notifications with system notifications
|
||||||
for (final notification in systemPending) {
|
for (final trackedNotification in trackedNotifications) {
|
||||||
allNotifications.add({
|
final scheduledTime = DateTime.parse(trackedNotification['scheduledTime']).toLocal();
|
||||||
'id': notification.id,
|
final lastRetryTime = trackedNotification['lastRetryTime'] != null
|
||||||
'title': notification.title ?? 'Notification',
|
? DateTime.parse(trackedNotification['lastRetryTime']).toLocal()
|
||||||
'body': notification.body ?? '',
|
: null;
|
||||||
'scheduledTime': DateTime.now(), // PendingNotificationRequest doesn't have scheduledTime
|
final retryCount = trackedNotification['retryCount'] ?? 0;
|
||||||
'type': 'system',
|
final isRetrying = trackedNotification['status'] == 'retrying';
|
||||||
'isRetry': false,
|
final notificationId = trackedNotification['notificationId'] as int;
|
||||||
'retryCount': 0,
|
|
||||||
});
|
// Try to find matching system notification(s)
|
||||||
}
|
final matchingSystemNotifications = systemPending.where((systemNotification) {
|
||||||
|
return _isMatchingNotification(systemNotification, trackedNotification, retryCount);
|
||||||
// Add tracked notifications from database
|
}).toList();
|
||||||
for (final notification in trackedNotifications) {
|
|
||||||
final scheduledTime = DateTime.parse(notification['scheduledTime']).toLocal();
|
// Calculate next retry time if this is a retry notification
|
||||||
allNotifications.add({
|
DateTime? nextRetryTime;
|
||||||
'id': notification['notificationId'],
|
bool hasReachedMaxRetries = retryCount >= maxRetryAttempts;
|
||||||
'title': 'Time for ${notification['supplementName'] ?? 'Supplement'}',
|
|
||||||
|
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',
|
'body': 'Take your supplement',
|
||||||
'scheduledTime': scheduledTime,
|
'scheduledTime': scheduledTime,
|
||||||
'type': 'tracked',
|
'nextRetryTime': nextRetryTime,
|
||||||
'status': notification['status'],
|
'lastRetryTime': lastRetryTime,
|
||||||
'isRetry': notification['status'] == 'retrying',
|
'type': matchingSystemNotifications.isNotEmpty ? 'matched' : 'tracked_only',
|
||||||
'retryCount': notification['retryCount'] ?? 0,
|
'status': trackedNotification['status'],
|
||||||
'supplementName': notification['supplementName'],
|
'isRetry': isRetrying,
|
||||||
'supplementId': notification['supplementId'],
|
'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)
|
// Sort by scheduled time (soonest first)
|
||||||
@@ -87,12 +139,202 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showCleanupDialog() async {
|
||||||
|
final result = await showDialog<String>(
|
||||||
|
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<void> _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<SupplementProvider>(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<void> _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<String, dynamic> 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<String, dynamic> 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Pending Notifications'),
|
title: const Text('Pending Notifications'),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.cleaning_services),
|
||||||
|
onPressed: _showCleanupDialog,
|
||||||
|
tooltip: 'Cleanup old notifications',
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: _loadNotifications,
|
onPressed: _loadNotifications,
|
||||||
@@ -140,37 +382,86 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine and sort all notifications
|
// Get settings for retry interval calculation
|
||||||
|
final settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
|
||||||
|
final reminderRetryInterval = settingsProvider.reminderRetryInterval;
|
||||||
|
final maxRetryAttempts = settingsProvider.maxRetryAttempts;
|
||||||
|
|
||||||
|
// Create a more intelligent matching system
|
||||||
final allNotifications = <Map<String, dynamic>>[];
|
final allNotifications = <Map<String, dynamic>>[];
|
||||||
|
final matchedSystemIds = <int>{};
|
||||||
|
|
||||||
// Add system notifications
|
// First, try to match tracked notifications with system notifications
|
||||||
for (final notification in _pendingNotifications) {
|
for (final trackedNotification in _trackedNotifications) {
|
||||||
allNotifications.add({
|
final scheduledTime = DateTime.parse(trackedNotification['scheduledTime']).toLocal();
|
||||||
'id': notification.id,
|
final lastRetryTime = trackedNotification['lastRetryTime'] != null
|
||||||
'title': notification.title ?? 'Notification',
|
? DateTime.parse(trackedNotification['lastRetryTime']).toLocal()
|
||||||
'body': notification.body ?? '',
|
: null;
|
||||||
'scheduledTime': DateTime.now(), // PendingNotificationRequest doesn't have scheduledTime
|
final retryCount = trackedNotification['retryCount'] ?? 0;
|
||||||
'type': 'system',
|
final isRetrying = trackedNotification['status'] == 'retrying';
|
||||||
'isRetry': false,
|
final notificationId = trackedNotification['notificationId'] as int;
|
||||||
'retryCount': 0,
|
|
||||||
});
|
// Try to find matching system notification(s)
|
||||||
}
|
final matchingSystemNotifications = _pendingNotifications.where((systemNotification) {
|
||||||
|
return _isMatchingNotification(systemNotification, trackedNotification, retryCount);
|
||||||
// Add tracked notifications from database
|
}).toList();
|
||||||
for (final notification in _trackedNotifications) {
|
|
||||||
final scheduledTime = DateTime.parse(notification['scheduledTime']).toLocal();
|
// Calculate next retry time if this is a retry notification
|
||||||
allNotifications.add({
|
DateTime? nextRetryTime;
|
||||||
'id': notification['notificationId'],
|
bool hasReachedMaxRetries = retryCount >= maxRetryAttempts;
|
||||||
'title': 'Time for ${notification['supplementName'] ?? 'Supplement'}',
|
|
||||||
|
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',
|
'body': 'Take your supplement',
|
||||||
'scheduledTime': scheduledTime,
|
'scheduledTime': scheduledTime,
|
||||||
'type': 'tracked',
|
'nextRetryTime': nextRetryTime,
|
||||||
'status': notification['status'],
|
'lastRetryTime': lastRetryTime,
|
||||||
'isRetry': notification['status'] == 'retrying',
|
'type': matchingSystemNotifications.isNotEmpty ? 'matched' : 'tracked_only',
|
||||||
'retryCount': notification['retryCount'] ?? 0,
|
'status': trackedNotification['status'],
|
||||||
'supplementName': notification['supplementName'],
|
'isRetry': isRetrying,
|
||||||
'supplementId': notification['supplementId'],
|
'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)
|
// Sort by scheduled time (soonest first)
|
||||||
@@ -192,33 +483,77 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
|
|
||||||
Widget _buildNotificationCard(Map<String, dynamic> notification) {
|
Widget _buildNotificationCard(Map<String, dynamic> notification) {
|
||||||
final scheduledTime = notification['scheduledTime'] as DateTime;
|
final scheduledTime = notification['scheduledTime'] as DateTime;
|
||||||
|
final nextRetryTime = notification['nextRetryTime'] as DateTime?;
|
||||||
|
final lastRetryTime = notification['lastRetryTime'] as DateTime?;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final isOverdue = scheduledTime.isBefore(now);
|
final isOverdue = scheduledTime.isBefore(now);
|
||||||
final isRetry = notification['isRetry'] as bool;
|
final isRetry = notification['isRetry'] as bool;
|
||||||
final retryCount = notification['retryCount'] as int;
|
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;
|
final type = notification['type'] as String;
|
||||||
|
|
||||||
final timeUntil = scheduledTime.difference(now);
|
// Determine which time to show and calculate time text
|
||||||
String timeText;
|
DateTime displayTime = scheduledTime;
|
||||||
|
String timePrefix = '';
|
||||||
|
bool showAsNextRetry = false;
|
||||||
|
String timeText = '';
|
||||||
|
|
||||||
if (isOverdue) {
|
if (isRetry && nextRetryTime != null && !hasReachedMaxRetries) {
|
||||||
final overdue = now.difference(scheduledTime);
|
// For retry notifications that haven't reached max retries, show next retry time
|
||||||
if (overdue.inDays > 0) {
|
final timeDifference = nextRetryTime.difference(now);
|
||||||
timeText = 'Overdue by ${overdue.inDays}d ${overdue.inHours % 24}h';
|
if (timeDifference.inSeconds.abs() < 30) {
|
||||||
} else if (overdue.inHours > 0) {
|
// If within 30 seconds, show "due now"
|
||||||
timeText = 'Overdue by ${overdue.inHours}h ${overdue.inMinutes % 60}m';
|
displayTime = nextRetryTime;
|
||||||
|
timePrefix = 'Next retry: ';
|
||||||
|
showAsNextRetry = true;
|
||||||
|
timeText = 'due now';
|
||||||
|
} else if (nextRetryTime.isAfter(now)) {
|
||||||
|
displayTime = nextRetryTime;
|
||||||
|
timePrefix = 'Next retry: ';
|
||||||
|
showAsNextRetry = true;
|
||||||
} else {
|
} else {
|
||||||
timeText = 'Overdue by ${overdue.inMinutes}m';
|
// If next retry time has passed, show it's overdue for retry
|
||||||
|
displayTime = nextRetryTime;
|
||||||
|
timePrefix = 'Retry overdue: ';
|
||||||
|
showAsNextRetry = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (isRetry && hasReachedMaxRetries) {
|
||||||
if (timeUntil.inDays > 0) {
|
// For notifications that have reached max retries, don't show next retry time
|
||||||
timeText = 'In ${timeUntil.inDays}d ${timeUntil.inHours % 24}h';
|
showAsNextRetry = false;
|
||||||
} else if (timeUntil.inHours > 0) {
|
}
|
||||||
timeText = 'In ${timeUntil.inHours}h ${timeUntil.inMinutes % 60}m';
|
|
||||||
|
// 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 {
|
} else {
|
||||||
timeText = 'In ${timeUntil.inMinutes}m';
|
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(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -259,15 +594,25 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange.withOpacity(0.2),
|
color: hasReachedMaxRetries
|
||||||
|
? Colors.red.withOpacity(0.2)
|
||||||
|
: Colors.orange.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.orange.withOpacity(0.5)),
|
border: Border.all(
|
||||||
|
color: hasReachedMaxRetries
|
||||||
|
? Colors.red.withOpacity(0.5)
|
||||||
|
: Colors.orange.withOpacity(0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Retry #$retryCount',
|
hasReachedMaxRetries
|
||||||
|
? 'Max retries ($retryCount/$maxRetryAttempts)'
|
||||||
|
: 'Retry #$retryCount',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.orange.shade700,
|
color: hasReachedMaxRetries
|
||||||
|
? Colors.red.shade700
|
||||||
|
: Colors.orange.shade700,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -280,25 +625,42 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: type == 'system'
|
color: _getTypeColor(type).withOpacity(0.2),
|
||||||
? Colors.blue.withOpacity(0.2)
|
|
||||||
: Colors.green.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: type == 'system'
|
color: _getTypeColor(type).withOpacity(0.5),
|
||||||
? Colors.blue.withOpacity(0.5)
|
|
||||||
: Colors.green.withOpacity(0.5),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
type == 'system' ? 'System' : 'Tracked',
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 12,
|
Text(
|
||||||
color: type == 'system'
|
_getTypeLabel(type, notification),
|
||||||
? Colors.blue.shade700
|
style: TextStyle(
|
||||||
: Colors.green.shade700,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -306,6 +668,7 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
// Show original scheduled time
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
@@ -315,14 +678,93 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'${_formatTime(scheduledTime)} • $timeText',
|
'Scheduled: ${_formatTime(scheduledTime)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isOverdue ? Colors.red : Colors.grey.shade700,
|
color: isOverdue ? Colors.red : Colors.grey.shade700,
|
||||||
fontWeight: isOverdue ? FontWeight.bold : FontWeight.normal,
|
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) ...[
|
if (type == 'tracked' && notification['supplementName'] != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
@@ -365,4 +807,17 @@ class _PendingNotificationsScreenState extends State<PendingNotificationsScreen>
|
|||||||
return '${dateTime.day}/${dateTime.month} $timeStr';
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user