mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
556 lines
16 KiB
Dart
556 lines
16 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 'package:provider/provider.dart';
|
|
import 'package:supplements/logging.dart';
|
|
|
|
import '../models/supplement.dart';
|
|
import '../models/supplement_intake.dart';
|
|
import '../providers/settings_provider.dart';
|
|
import '../services/database_helper.dart';
|
|
import '../services/database_sync_service.dart';
|
|
import '../services/simple_notification_service.dart';
|
|
|
|
class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
|
final DatabaseHelper _databaseHelper = DatabaseHelper.instance;
|
|
final SimpleNotificationService _notificationService = SimpleNotificationService.instance;
|
|
bool _initialized = false;
|
|
|
|
List<Supplement> _supplements = [];
|
|
List<Map<String, dynamic>> _todayIntakes = [];
|
|
List<Map<String, dynamic>> _monthlyIntakes = [];
|
|
bool _isLoading = false;
|
|
|
|
Timer? _dateChangeTimer;
|
|
DateTime _lastDateCheck = DateTime.now();
|
|
|
|
// Callback for triggering sync when data changes
|
|
VoidCallback? _onDataChanged;
|
|
|
|
// Context for accessing other providers
|
|
BuildContext? _context;
|
|
|
|
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([BuildContext? context]) async {
|
|
if (_initialized) {
|
|
return;
|
|
}
|
|
_initialized = true;
|
|
_context = context;
|
|
|
|
// Add this provider as an observer for app lifecycle changes
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
await _notificationService.initialize();
|
|
|
|
// Request permissions with error handling
|
|
try {
|
|
await _notificationService.requestPermissions();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('Error requesting notification permissions: $e');
|
|
}
|
|
// Continue without notifications rather than crashing
|
|
}
|
|
|
|
await loadSupplements();
|
|
await loadTodayIntakes();
|
|
|
|
// Schedule notifications for all active supplements
|
|
await _rescheduleAllNotifications();
|
|
|
|
// Start date change monitoring to reset daily intake status
|
|
_startDateChangeMonitoring();
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
printLog('Date changed detected: $lastCheckDate -> $currentDate');
|
|
printLog('Refreshing today\'s intakes for new day...');
|
|
}
|
|
|
|
// Date has changed, refresh today's intakes
|
|
_lastDateCheck = now;
|
|
await loadTodayIntakes();
|
|
|
|
if (kDebugMode) {
|
|
printLog('Today\'s intakes refreshed for new day');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
_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) {
|
|
printLog('App resumed, checking for date change...');
|
|
}
|
|
forceCheckDateChange();
|
|
}
|
|
}
|
|
|
|
Future<void> _rescheduleAllNotifications() async {
|
|
if (kDebugMode) {
|
|
printLog('📱 Rescheduling notifications for all active supplements...');
|
|
}
|
|
|
|
try {
|
|
SettingsProvider? settingsProvider;
|
|
if (_context != null && _context!.mounted) {
|
|
try {
|
|
settingsProvider = Provider.of<SettingsProvider>(_context!, listen: false);
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('📱 Could not access SettingsProvider: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
await _notificationService.scheduleDailyGroupedRemindersSafe(
|
|
_supplements,
|
|
settingsProvider: settingsProvider,
|
|
);
|
|
await _notificationService.getPendingNotifications();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('📱 Error scheduling grouped notifications: $e');
|
|
}
|
|
}
|
|
|
|
if (kDebugMode) {
|
|
printLog('📱 Finished rescheduling notifications');
|
|
}
|
|
}
|
|
|
|
Future<void> loadSupplements() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
printLog('Loading supplements from database...');
|
|
_supplements = await _databaseHelper.getAllSupplements();
|
|
printLog('Loaded ${_supplements.length} supplements');
|
|
for (var supplement in _supplements) {
|
|
printLog('Supplement: ${supplement.name}');
|
|
}
|
|
} catch (e) {
|
|
printLog('Error loading supplements: $e');
|
|
if (kDebugMode) {
|
|
printLog('Error loading supplements: $e');
|
|
}
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> addSupplement(Supplement supplement) async {
|
|
try {
|
|
printLog('Adding supplement: ${supplement.name}');
|
|
final id = await _databaseHelper.insertSupplement(supplement);
|
|
printLog('Supplement inserted with ID: $id');
|
|
|
|
await loadSupplements();
|
|
printLog('Supplements reloaded, count: ${_supplements.length}');
|
|
await _rescheduleAllNotifications();
|
|
|
|
// Trigger sync after adding supplement
|
|
_triggerSyncIfEnabled();
|
|
} catch (e) {
|
|
printLog('Error adding supplement: $e');
|
|
if (kDebugMode) {
|
|
printLog('Error adding supplement: $e');
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<void> updateSupplement(Supplement supplement) async {
|
|
try {
|
|
await _databaseHelper.updateSupplement(supplement);
|
|
|
|
await loadSupplements();
|
|
await _rescheduleAllNotifications();
|
|
|
|
// Trigger sync after updating supplement
|
|
_triggerSyncIfEnabled();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('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) {
|
|
printLog('Error duplicating supplement: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> deleteSupplement(int id) async {
|
|
try {
|
|
await _databaseHelper.deleteSupplement(id);
|
|
|
|
await loadSupplements();
|
|
await _rescheduleAllNotifications();
|
|
|
|
// Trigger sync after deleting supplement
|
|
_triggerSyncIfEnabled();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('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.showInstant(
|
|
title: 'Supplement Taken',
|
|
body: 'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} (${supplement.ingredientsDisplay})',
|
|
);
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('Error recording intake: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> loadTodayIntakes() async {
|
|
try {
|
|
final today = DateTime.now();
|
|
if (kDebugMode) {
|
|
printLog('Loading intakes for date: ${today.year}-${today.month}-${today.day}');
|
|
}
|
|
|
|
_todayIntakes = await _databaseHelper.getIntakesWithSupplementsForDate(today);
|
|
|
|
if (kDebugMode) {
|
|
printLog('Loaded ${_todayIntakes.length} intakes for today');
|
|
for (var intake in _todayIntakes) {
|
|
printLog(' - Supplement ID: ${intake['supplement_id']}, taken at: ${intake['takenAt']}');
|
|
}
|
|
}
|
|
|
|
notifyListeners();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('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) {
|
|
printLog('Error loading monthly intakes: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getIntakesForDate(DateTime date) async {
|
|
try {
|
|
return await _databaseHelper.getIntakesWithSupplementsForDate(date);
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
printLog('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) {
|
|
printLog('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) {
|
|
printLog('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;
|
|
}
|
|
|
|
/// Get the most recent intake for a supplement today
|
|
Map<String, dynamic>? getMostRecentIntake(int supplementId) {
|
|
final supplementIntakes = _todayIntakes
|
|
.where((intake) => intake['supplement_id'] == supplementId)
|
|
.toList();
|
|
|
|
if (supplementIntakes.isEmpty) return null;
|
|
|
|
// Sort by takenAt time (most recent first)
|
|
supplementIntakes.sort((a, b) {
|
|
final aTime = DateTime.parse(a['takenAt']);
|
|
final bTime = DateTime.parse(b['takenAt']);
|
|
return bTime.compareTo(aTime); // Descending order
|
|
});
|
|
|
|
return supplementIntakes.first;
|
|
}
|
|
|
|
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) {
|
|
printLog('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) {
|
|
printLog('Force checking date change...');
|
|
printLog('Current date: $currentDate');
|
|
printLog('Last check date: $lastCheckDate');
|
|
}
|
|
|
|
if (currentDate != lastCheckDate) {
|
|
if (kDebugMode) {
|
|
printLog('Date change detected, refreshing intakes...');
|
|
}
|
|
_lastDateCheck = now;
|
|
await loadTodayIntakes();
|
|
} else {
|
|
if (kDebugMode) {
|
|
printLog('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) {
|
|
printLog('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) {
|
|
printLog('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) {
|
|
printLog('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) {
|
|
printLog('Error permanently deleting archived supplement: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug methods for notification testing
|
|
Future<void> testNotifications() async {
|
|
await _notificationService.showInstant(
|
|
title: 'Test Notification',
|
|
body: 'This is a test notification to verify the system is working.',
|
|
);
|
|
}
|
|
|
|
Future<void> testScheduledNotification() async {
|
|
await _notificationService.showInstant(
|
|
title: 'Test Scheduled Notification',
|
|
body: 'This is a simple test notification.',
|
|
);
|
|
}
|
|
|
|
Future<void> testNotificationActions() async {
|
|
await _notificationService.showInstant(
|
|
title: 'Test Action Notification',
|
|
body: 'Actions are not available in the simple notification service.',
|
|
);
|
|
}
|
|
|
|
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
|
|
// Not supported in simple service; return empty list for compatibility.
|
|
return [];
|
|
}
|
|
|
|
|
|
|
|
// Debug method to test notification persistence
|
|
Future<void> rescheduleAllNotifications() async {
|
|
await _rescheduleAllNotifications();
|
|
}
|
|
|
|
// Debug method to cancel all notifications
|
|
Future<void> cancelAllNotifications() async {
|
|
await _notificationService.cancelAll();
|
|
}
|
|
}
|