mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-12-07 21:52:35 +00:00
824 lines
30 KiB
Dart
824 lines
30 KiB
Dart
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<PendingNotificationsScreen> createState() => _PendingNotificationsScreenState();
|
|
}
|
|
|
|
class _PendingNotificationsScreenState extends State<PendingNotificationsScreen> {
|
|
List<PendingNotificationRequest> _pendingNotifications = [];
|
|
List<Map<String, dynamic>> _trackedNotifications = [];
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadNotifications();
|
|
}
|
|
|
|
Future<void> _loadNotifications() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
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
|
|
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 = <Map<String, dynamic>>[];
|
|
final matchedSystemIds = <int>{};
|
|
|
|
// 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<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
|
|
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<SettingsProvider>(context, listen: false);
|
|
final reminderRetryInterval = settingsProvider.reminderRetryInterval;
|
|
final maxRetryAttempts = settingsProvider.maxRetryAttempts;
|
|
|
|
// Create a more intelligent matching system
|
|
final allNotifications = <Map<String, dynamic>>[];
|
|
final matchedSystemIds = <int>{};
|
|
|
|
// 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<String, dynamic> 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';
|
|
}
|
|
}
|
|
}
|