Files
supplements/lib/providers/supplement_provider.dart

587 lines
19 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
import '../services/database_helper.dart';
import '../services/database_sync_service.dart';
import '../services/notification_service.dart';
class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
final DatabaseHelper _databaseHelper = DatabaseHelper.instance;
final NotificationService _notificationService = NotificationService();
List<Supplement> _supplements = [];
List<Map<String, dynamic>> _todayIntakes = [];
List<Map<String, dynamic>> _monthlyIntakes = [];
bool _isLoading = false;
Timer? _persistentReminderTimer;
Timer? _dateChangeTimer;
DateTime _lastDateCheck = DateTime.now();
// Callback for triggering sync when data changes
VoidCallback? _onDataChanged;
List<Supplement> get supplements => _supplements;
List<Map<String, dynamic>> get todayIntakes => _todayIntakes;
List<Map<String, dynamic>> get monthlyIntakes => _monthlyIntakes;
bool get isLoading => _isLoading;
/// Set callback for triggering sync when data changes
void setOnDataChangedCallback(VoidCallback? callback) {
_onDataChanged = callback;
}
/// Trigger sync if callback is set
void _triggerSyncIfEnabled() {
_onDataChanged?.call();
}
Future<void> initialize() async {
// Add this provider as an observer for app lifecycle changes
WidgetsBinding.instance.addObserver(this);
await _notificationService.initialize();
// Set up the callback for handling supplement intake from notifications
print('SupplementsLog: 📱 Setting up notification callback...');
_notificationService.setTakeSupplementCallback((supplementId, supplementName, units, unitType) {
print('SupplementsLog: 📱 === NOTIFICATION CALLBACK TRIGGERED ===');
print('SupplementsLog: 📱 Supplement ID: $supplementId');
print('SupplementsLog: 📱 Supplement Name: $supplementName');
print('SupplementsLog: 📱 Units: $units');
print('SupplementsLog: 📱 Unit Type: $unitType');
// Record the intake when user taps "Take" on notification
recordIntake(supplementId, 0.0, unitsTaken: units);
print('SupplementsLog: 📱 Intake recorded successfully');
print('SupplementsLog: 📱 === CALLBACK COMPLETE ===');
if (kDebugMode) {
print('SupplementsLog: 📱 Recorded intake from notification: $supplementName ($units $unitType)');
}
});
print('SupplementsLog: 📱 Notification callback setup complete');
// Request permissions with error handling
try {
await _notificationService.requestPermissions();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error requesting notification permissions: $e');
}
// Continue without notifications rather than crashing
}
await loadSupplements();
await loadTodayIntakes();
// Reschedule notifications for all active supplements to ensure persistence
await _rescheduleAllNotifications();
// Start periodic checking for persistent reminders (every 5 minutes)
_startPersistentReminderCheck();
// Start date change monitoring to reset daily intake status
_startDateChangeMonitoring();
}
void _startPersistentReminderCheck() {
// Cancel any existing timer
_persistentReminderTimer?.cancel();
// Check every 5 minutes for persistent reminders
_persistentReminderTimer = Timer.periodic(const Duration(minutes: 5), (timer) async {
try {
// This will be called from settings provider context, so we need to import it
await _checkPersistentReminders();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error checking persistent reminders: $e');
}
}
});
// Also check immediately
_checkPersistentReminders();
}
void _startDateChangeMonitoring() {
// Cancel any existing timer
_dateChangeTimer?.cancel();
// Check every minute if the date has changed
_dateChangeTimer = Timer.periodic(const Duration(minutes: 1), (timer) async {
final now = DateTime.now();
final currentDate = DateTime(now.year, now.month, now.day);
final lastCheckDate = DateTime(_lastDateCheck.year, _lastDateCheck.month, _lastDateCheck.day);
if (currentDate != lastCheckDate) {
if (kDebugMode) {
print('SupplementsLog: Date changed detected: ${lastCheckDate} -> ${currentDate}');
print('SupplementsLog: Refreshing today\'s intakes for new day...');
}
// Date has changed, refresh today's intakes
_lastDateCheck = now;
await loadTodayIntakes();
if (kDebugMode) {
print('SupplementsLog: Today\'s intakes refreshed for new day');
}
}
});
}
Future<void> _checkPersistentReminders() async {
// This method will be enhanced to accept settings from the UI layer
// For now, we'll check with default settings
// In practice, the UI should call checkPersistentRemindersWithSettings
if (kDebugMode) {
print('SupplementsLog: 📱 Checking persistent reminders with default settings');
}
}
// Method to be called from UI with actual settings
Future<void> checkPersistentRemindersWithSettings({
required bool persistentReminders,
required int reminderRetryInterval,
required int maxRetryAttempts,
}) async {
print('SupplementsLog: 📱 🔄 MANUAL CHECK: Persistent reminders called from UI');
await _notificationService.checkPersistentReminders(
persistentReminders,
reminderRetryInterval,
maxRetryAttempts,
);
}
// Add a manual trigger method for testing
Future<void> triggerRetryCheck() async {
print('SupplementsLog: 📱 🚨 MANUAL TRIGGER: Forcing retry check...');
await checkPersistentRemindersWithSettings(
persistentReminders: true,
reminderRetryInterval: 5, // Force 5 minute interval for testing
maxRetryAttempts: 3,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_persistentReminderTimer?.cancel();
_dateChangeTimer?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
// App came back to foreground, check if date changed
if (kDebugMode) {
print('SupplementsLog: App resumed, checking for date change...');
}
forceCheckDateChange();
}
}
Future<void> _rescheduleAllNotifications() async {
if (kDebugMode) {
print('SupplementsLog: 📱 Rescheduling notifications for all active supplements...');
}
for (final supplement in _supplements) {
if (supplement.reminderTimes.isNotEmpty) {
try {
await _notificationService.scheduleSupplementReminders(supplement);
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: 📱 Error rescheduling notifications for ${supplement.name}: $e');
}
}
}
}
if (kDebugMode) {
print('SupplementsLog: 📱 Finished rescheduling notifications');
}
}
Future<void> loadSupplements() async {
_isLoading = true;
notifyListeners();
try {
print('SupplementsLog: Loading supplements from database...');
_supplements = await _databaseHelper.getAllSupplements();
print('SupplementsLog: Loaded ${_supplements.length} supplements');
for (var supplement in _supplements) {
print('SupplementsLog: Supplement: ${supplement.name}');
}
} catch (e) {
print('SupplementsLog: Error loading supplements: $e');
if (kDebugMode) {
print('SupplementsLog: Error loading supplements: $e');
}
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> addSupplement(Supplement supplement) async {
try {
print('SupplementsLog: Adding supplement: ${supplement.name}');
final id = await _databaseHelper.insertSupplement(supplement);
print('SupplementsLog: Supplement inserted with ID: $id');
final newSupplement = supplement.copyWith(id: id);
// Schedule notifications (skip if there's an error)
try {
await _notificationService.scheduleSupplementReminders(newSupplement);
print('SupplementsLog: Notifications scheduled');
} catch (notificationError) {
print('SupplementsLog: Warning: Could not schedule notifications: $notificationError');
}
await loadSupplements();
print('SupplementsLog: Supplements reloaded, count: ${_supplements.length}');
// Trigger sync after adding supplement
_triggerSyncIfEnabled();
} catch (e) {
print('SupplementsLog: Error adding supplement: $e');
if (kDebugMode) {
print('SupplementsLog: Error adding supplement: $e');
}
rethrow;
}
}
Future<void> updateSupplement(Supplement supplement) async {
try {
await _databaseHelper.updateSupplement(supplement);
// Reschedule notifications
await _notificationService.scheduleSupplementReminders(supplement);
await loadSupplements();
// Trigger sync after updating supplement
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error updating supplement: $e');
}
}
}
Future<void> duplicateSupplement(int supplementId) async {
try {
final originalSupplement = await _databaseHelper.getSupplement(supplementId);
if (originalSupplement != null) {
final newSupplement = originalSupplement.copyWith(
setNullId: true, // This will be a new entry
newSyncId: true, // Generate a new syncId
name: '${originalSupplement.name} (Copy)',
createdAt: DateTime.now(),
lastModified: DateTime.now(),
syncStatus: RecordSyncStatus.pending,
isDeleted: false,
);
await addSupplement(newSupplement);
}
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error duplicating supplement: $e');
}
}
}
Future<void> deleteSupplement(int id) async {
try {
await _databaseHelper.deleteSupplement(id);
// Cancel notifications
await _notificationService.cancelSupplementReminders(id);
await loadSupplements();
// Trigger sync after deleting supplement
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error deleting supplement: $e');
}
}
}
Future<void> recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes, DateTime? takenAt}) async {
try {
final intake = SupplementIntake(
supplementId: supplementId,
takenAt: takenAt ?? DateTime.now(),
dosageTaken: dosage,
unitsTaken: unitsTaken ?? 1.0,
notes: notes,
);
await _databaseHelper.insertIntake(intake);
await loadTodayIntakes();
// Trigger sync after recording intake
_triggerSyncIfEnabled();
// Show confirmation notification
final supplement = _supplements.firstWhere((s) => s.id == supplementId);
final unitsText = unitsTaken != null && unitsTaken != 1 ? '${unitsTaken.toStringAsFixed(unitsTaken % 1 == 0 ? 0 : 1)} ${supplement.unitType}' : '';
await _notificationService.showInstantNotification(
'Supplement Taken',
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} (${supplement.ingredientsDisplay})',
);
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error recording intake: $e');
}
}
}
Future<void> loadTodayIntakes() async {
try {
final today = DateTime.now();
if (kDebugMode) {
print('SupplementsLog: Loading intakes for date: ${today.year}-${today.month}-${today.day}');
}
_todayIntakes = await _databaseHelper.getIntakesWithSupplementsForDate(today);
if (kDebugMode) {
print('SupplementsLog: Loaded ${_todayIntakes.length} intakes for today');
for (var intake in _todayIntakes) {
print('SupplementsLog: - Supplement ID: ${intake['supplement_id']}, taken at: ${intake['takenAt']}');
}
}
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error loading today\'s intakes: $e');
}
}
}
Future<void> loadMonthlyIntakes(int year, int month) async {
try {
_monthlyIntakes = await _databaseHelper.getIntakesWithSupplementsForMonth(year, month);
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error loading monthly intakes: $e');
}
}
}
Future<List<Map<String, dynamic>>> getIntakesForDate(DateTime date) async {
try {
return await _databaseHelper.getIntakesWithSupplementsForDate(date);
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error loading intakes for date: $e');
}
return [];
}
}
Future<void> deleteIntake(int intakeId) async {
try {
await _databaseHelper.deleteIntake(intakeId);
await loadTodayIntakes();
// Also refresh monthly intakes if they're loaded
if (_monthlyIntakes.isNotEmpty) {
await loadMonthlyIntakes(DateTime.now().year, DateTime.now().month);
}
notifyListeners();
// Trigger sync after deleting intake
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error deleting intake: $e');
}
}
}
Future<void> permanentlyDeleteIntake(int intakeId) async {
try {
await _databaseHelper.permanentlyDeleteIntake(intakeId);
await loadTodayIntakes();
// Also refresh monthly intakes if they're loaded
if (_monthlyIntakes.isNotEmpty) {
await loadMonthlyIntakes(DateTime.now().year, DateTime.now().month);
}
notifyListeners();
// Trigger sync after permanently deleting intake
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error permanently deleting intake: $e');
}
}
}
bool hasBeenTakenToday(int supplementId) {
return _todayIntakes.any((intake) => intake['supplement_id'] == supplementId);
}
int getTodayIntakeCount(int supplementId) {
return _todayIntakes.where((intake) => intake['supplement_id'] == supplementId).length;
}
Map<String, double> get dailyIngredientIntake {
final Map<String, double> ingredientIntake = {};
for (final intake in _todayIntakes) {
final supplement = _supplements.firstWhere((s) => s.id == intake['supplement_id']);
final unitsTaken = intake['unitsTaken'] as double;
for (final ingredient in supplement.ingredients) {
final currentAmount = ingredientIntake[ingredient.name] ?? 0;
ingredientIntake[ingredient.name] = currentAmount + (ingredient.amount * unitsTaken);
}
}
return ingredientIntake;
}
// Method to manually refresh daily status (useful for testing or manual refresh)
Future<void> refreshDailyStatus() async {
if (kDebugMode) {
print('SupplementsLog: Manually refreshing daily status...');
}
_lastDateCheck = DateTime.now();
await loadTodayIntakes();
}
// Method to force check for date change (useful for testing)
Future<void> forceCheckDateChange() async {
final now = DateTime.now();
final currentDate = DateTime(now.year, now.month, now.day);
final lastCheckDate = DateTime(_lastDateCheck.year, _lastDateCheck.month, _lastDateCheck.day);
if (kDebugMode) {
print('SupplementsLog: Force checking date change...');
print('SupplementsLog: Current date: $currentDate');
print('SupplementsLog: Last check date: $lastCheckDate');
}
if (currentDate != lastCheckDate) {
if (kDebugMode) {
print('SupplementsLog: Date change detected, refreshing intakes...');
}
_lastDateCheck = now;
await loadTodayIntakes();
} else {
if (kDebugMode) {
print('SupplementsLog: No date change detected');
}
}
}
// Archive functionality
List<Supplement> _archivedSupplements = [];
List<Supplement> get archivedSupplements => _archivedSupplements;
Future<void> loadArchivedSupplements() async {
try {
_archivedSupplements = await _databaseHelper.getArchivedSupplements();
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error loading archived supplements: $e');
}
}
}
Future<void> archiveSupplement(int supplementId) async {
try {
await _databaseHelper.archiveSupplement(supplementId);
await loadSupplements(); // Refresh active supplements
await loadArchivedSupplements(); // Refresh archived supplements
// Trigger sync after archiving supplement
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error archiving supplement: $e');
}
}
}
Future<void> unarchiveSupplement(int supplementId) async {
try {
await _databaseHelper.unarchiveSupplement(supplementId);
await loadSupplements(); // Refresh active supplements
await loadArchivedSupplements(); // Refresh archived supplements
// Trigger sync after unarchiving supplement
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error unarchiving supplement: $e');
}
}
}
Future<void> deleteArchivedSupplement(int supplementId) async {
try {
await _databaseHelper.permanentlyDeleteSupplement(supplementId);
await loadArchivedSupplements(); // Refresh archived supplements
// Trigger sync after permanently deleting archived supplement
_triggerSyncIfEnabled();
} catch (e) {
if (kDebugMode) {
print('SupplementsLog: Error permanently deleting archived supplement: $e');
}
}
}
// Debug methods for notification testing
Future<void> testNotifications() async {
await _notificationService.testNotification();
}
Future<void> testScheduledNotification() async {
await _notificationService.testScheduledNotification();
}
Future<void> testNotificationActions() async {
await _notificationService.testNotificationWithActions();
}
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
return await _notificationService.getPendingNotifications();
}
// Get pending notifications with retry information from database
Future<List<Map<String, dynamic>>> getTrackedNotifications() async {
return await DatabaseHelper.instance.getPendingNotifications();
}
// Debug method to test notification persistence
Future<void> rescheduleAllNotifications() async {
await _rescheduleAllNotifications();
}
// Debug method to cancel all notifications
Future<void> cancelAllNotifications() async {
await _notificationService.cancelAllReminders();
}
}