import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:supplements/providers/supplement_provider.dart'; import 'package:supplements/services/notification_debug_store.dart'; import 'package:supplements/services/simple_notification_service.dart'; class DebugNotificationsScreen extends StatefulWidget { const DebugNotificationsScreen({super.key}); @override State createState() => _DebugNotificationsScreenState(); } class _DebugNotificationsScreenState extends State { bool _loading = true; List _pending = const []; List _logEntries = const []; final Map _supplementNameCache = {}; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _loading = true; }); // Fetch pending from plugin final pending = await SimpleNotificationService.instance.getPendingNotifications(); // Fetch log from local store final logs = await NotificationDebugStore.instance.getAll(); // Optionally resolve supplement names for single payloads await _resolveSupplementNames(pending, logs); if (!mounted) return; setState(() { _pending = pending; _logEntries = logs.reversed.toList(); // newest first _loading = false; }); } Future _resolveSupplementNames( List pending, List logs, ) async { final ctx = context; if (!mounted) return; final provider = Provider.of(ctx, listen: false); // Use existing list; attempt load if empty if (provider.supplements.isEmpty) { try { await provider.loadSupplements(); } catch (_) { // ignore } } // Collect potential IDs final Set ids = {}; for (final p in pending) { final id = _extractSingleIdFromPayload(p.payload); if (id != null) ids.add(id); } for (final e in logs) { if (e.singleId != null) ids.add(e.singleId!); } // Build cache for (final id in ids) { try { final s = provider.supplements.firstWhere((el) => el.id == id); _supplementNameCache[id] = s.name; } catch (_) { // leave missing } } } int? _extractSingleIdFromPayload(String? payload) { if (payload == null || payload.isEmpty) return null; try { final map = jsonDecode(payload); if (map is Map && map['type'] == 'single') { final v = map['id']; if (v is int) return v; } } catch (_) { // ignore } return null; } String? _extractGroupTimeFromPayload(String? payload) { if (payload == null || payload.isEmpty) return null; try { final map = jsonDecode(payload); if (map is Map && map['type'] == 'group') { final v = map['time']; if (v is String) return v; } } catch (_) { // ignore } return null; } String _extractKindFromPayload(String? payload) { if (payload == null || payload.isEmpty) return 'unknown'; try { final map = jsonDecode(payload); if (map is Map) { final meta = map['meta']; if (meta is Map && meta['kind'] is String) return meta['kind'] as String; } } catch (_) {} return 'unknown'; } DateTime? _estimateScheduledAtFromPayload(String? payload) { // For snooze with meta.createdAt/delayMin we can compute when. if (payload == null || payload.isEmpty) return null; try { final map = jsonDecode(payload); if (map is Map) { final meta = map['meta']; if (meta is Map) { final kind = meta['kind']; if (kind == 'snooze') { final createdAt = meta['createdAt']; final delayMin = meta['delayMin']; if (createdAt is int && delayMin is int) { return DateTime.fromMillisecondsSinceEpoch(createdAt) .add(Duration(minutes: delayMin)); } } } // For daily group we can compute next HH:mm if (map['type'] == 'group' && map['time'] is String) { final timeKey = map['time'] as String; final parts = timeKey.split(':'); if (parts.length == 2) { final hour = int.tryParse(parts[0]) ?? 0; final minute = int.tryParse(parts[1]) ?? 0; final now = DateTime.now(); DateTime sched = DateTime(now.year, now.month, now.day, hour, minute); if (!sched.isAfter(now)) { sched = sched.add(const Duration(days: 1)); } return sched; } } } } catch (_) {} return null; } Future _cancelId(int id) async { await SimpleNotificationService.instance.cancelById(id); await _load(); if (!mounted) return; ShadSonner.of(context).show( ShadToast( title: Text('Canceled notification $id'), ), ); } Future _cancelAll() async { await SimpleNotificationService.instance.cancelAll(); await _load(); if (!mounted) return; ShadSonner.of(context).show( const ShadToast( title: Text('Canceled all notifications'), ), ); } Future _clearLog() async { await NotificationDebugStore.instance.clear(); await _load(); if (!mounted) return; ShadSonner.of(context).show( const ShadToast( title: Text('Cleared debug log'), ), ); } void _copyToClipboard(String text) { Clipboard.setData(ClipboardData(text: text)); ShadSonner.of(context).show( const ShadToast( title: Text('Copied to clipboard'), ), ); } @override Widget build(BuildContext context) { final pendingIds = _pending.map((e) => e.id).toSet(); return Scaffold( appBar: AppBar( title: const Text('Debug Notifications'), actions: [ IconButton( tooltip: 'Refresh', onPressed: _load, icon: const Icon(Icons.refresh), ), IconButton( tooltip: 'Cancel All', onPressed: _cancelAll, icon: const Icon(Icons.cancel_schedule_send), ), IconButton( tooltip: 'Clear Log', onPressed: _clearLog, icon: const Icon(Icons.delete_sweep), ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(12), children: [ _buildPendingSection(), const SizedBox(height: 16), _buildTestSnoozeSection(), // Add the test snooze section const SizedBox(height: 16), _buildLogSection(pendingIds), ], ), ); } Widget _buildPendingSection() { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Pending (${_pending.length})', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), if (_pending.isEmpty) const Text('No pending notifications'), for (final p in _pending) _buildPendingTile(p), ], ), ), ); } Widget _buildTestSnoozeSection() { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Test Snooze', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ElevatedButton( onPressed: () async { // Trigger a test snooze notification with snooze actions await SimpleNotificationService.instance.showInstant( title: 'Test Snooze Notification', body: 'This is a test notification for snooze.', payload: jsonEncode({ "type": "single", "id": 1, // Use a dummy ID for testing "meta": {"kind": "daily"} // Simulate a daily notification }), includeSnoozeActions: true, // Include snooze actions isSingle: true, // This is a single notification ); if (!mounted) return; ShadSonner.of(context).show( const ShadToast( title: Text('Test snooze notification sent!'), ), ); }, child: const Text('Send Test Snooze Notification'), ), ], ), ), ); } Widget _buildPendingTile(PendingNotificationRequest p) { final kind = _extractKindFromPayload(p.payload); final singleId = _extractSingleIdFromPayload(p.payload); final groupTime = _extractGroupTimeFromPayload(p.payload); final schedAt = _estimateScheduledAtFromPayload(p.payload); final forStr = singleId != null ? (_supplementNameCache[singleId] != null ? '${_supplementNameCache[singleId]} (id=$singleId)' : 'Supplement id=$singleId') : (groupTime != null ? 'Time $groupTime' : 'unknown'); // Build a clearer subtitle with scheduled time prominently displayed String subtitle = 'Kind: $kind • For: $forStr'; if (schedAt != null) { final now = DateTime.now(); final diff = schedAt.difference(now); String whenStr; if (diff.isNegative) { whenStr = 'Overdue (${schedAt.toLocal().toString().substring(0, 16)})'; } else if (diff.inDays > 0) { whenStr = 'In ${diff.inDays}d ${diff.inHours % 24}h (${schedAt.toLocal().toString().substring(0, 16)})'; } else if (diff.inHours > 0) { whenStr = 'In ${diff.inHours}h ${diff.inMinutes % 60}m (${schedAt.toLocal().toString().substring(11, 16)})'; } else if (diff.inMinutes > 0) { whenStr = 'In ${diff.inMinutes}m (${schedAt.toLocal().toString().substring(11, 16)})'; } else { whenStr = 'Very soon (${schedAt.toLocal().toString().substring(11, 16)})'; } subtitle = '$subtitle\nšŸ•’ $whenStr'; } // Removed the "else" block that displayed "Schedule time unknown" return ListTile( dense: true, title: Text('ID ${p.id} — ${p.title ?? "(no title)"}'), subtitle: Text(subtitle), trailing: Wrap( spacing: 4, children: [ IconButton( tooltip: 'Copy payload', icon: const Icon(Icons.copy), onPressed: () => _copyToClipboard(p.payload ?? ''), ), IconButton( tooltip: 'Cancel', icon: const Icon(Icons.cancel), onPressed: () => _cancelId(p.id), ), ], ), ); } Widget _buildLogSection(Set pendingIds) { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Schedule Log (${_logEntries.length})', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), if (_logEntries.isEmpty) const Text('No log entries'), for (final e in _logEntries) _buildLogTile(e, pendingIds), ], ), ), ); } Widget _buildLogTile(NotificationLogEntry e, Set pendingIds) { final inQueue = pendingIds.contains(e.id); String forStr = 'unknown'; if (e.type == 'single') { if (e.singleId != null) { final name = _supplementNameCache[e.singleId!]; forStr = name != null ? '$name (id=${e.singleId})' : 'id=${e.singleId}'; } } else if (e.type == 'group') { if (e.timeKey != null) { forStr = 'Time ${e.timeKey}'; } } final scheduledAt = DateTime.fromMillisecondsSinceEpoch(e.whenEpochMs).toLocal(); final createdAt = DateTime.fromMillisecondsSinceEpoch(e.createdAtEpochMs).toLocal(); // Format times more clearly final scheduledStr = scheduledAt.toString().substring(0, 16); final createdStr = createdAt.toString().substring(0, 16); // Show status and timing info final statusStr = inQueue ? '🟔 Pending' : 'āœ… Completed/Canceled'; return ListTile( dense: true, leading: Icon(inQueue ? Icons.pending : Icons.check, color: inQueue ? Colors.amber : Colors.green), title: Text('[${e.kind}] ${e.title} (ID ${e.id})'), subtitle: Text('$statusStr • Type: ${e.type} • For: $forStr\nšŸ•’ Scheduled: $scheduledStr\nšŸ“ Created: $createdStr'), trailing: Wrap( spacing: 4, children: [ IconButton( tooltip: 'Copy payload', icon: const Icon(Icons.copy), onPressed: () => _copyToClipboard(e.payload), ), if (inQueue) IconButton( tooltip: 'Cancel', icon: const Icon(Icons.cancel), onPressed: () => _cancelId(e.id), ), ], ), ); } }