import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:habo/constants.dart'; import 'package:habo/habits/habit.dart'; import 'package:habo/model/habit_data.dart'; import 'package:habo/model/category.dart'; import 'package:habo/repositories/habit_repository.dart'; import 'package:habo/repositories/event_repository.dart'; import 'package:habo/repositories/category_repository.dart'; import 'package:habo/services/backup_service.dart'; import 'package:habo/services/notification_service.dart'; import 'package:habo/services/ui_feedback_service.dart'; class HabitsManager extends ChangeNotifier { final HabitRepository _habitRepository; final EventRepository _eventRepository; final CategoryRepository _categoryRepository; final BackupService? _backupService; final NotificationService? _notificationService; final UIFeedbackService? _uiFeedbackService; final List _allHabits = []; final List _toDelete = []; final List _categories = []; List get allHabits => _allHabits; List get toDelete => _toDelete; List get categories => _categories; List get activeHabits => _allHabits.where((h) => !h.habitData.archived).toList(); List get archivedHabits => _allHabits.where((h) => h.habitData.archived).toList(); HabitsManager({ required HabitRepository habitRepository, required EventRepository eventRepository, required CategoryRepository categoryRepository, BackupService? backupService, NotificationService? notificationService, UIFeedbackService? uiFeedbackService, }) : _habitRepository = habitRepository, _eventRepository = eventRepository, _categoryRepository = categoryRepository, _backupService = backupService, _notificationService = notificationService, _uiFeedbackService = uiFeedbackService; // ─── Initialization ──────────────────────────────────────────────────────── Future initModel() async { await loadHabits(); await loadCategories(); } Future loadHabits() async { _allHabits.clear(); final habits = await _habitRepository.getAllHabits(); for (final habit in habits) { try { final events = await _eventRepository.getEventsMapForHabit( habit.habitData.id ?? 0, ); habit.habitData.events = events; } catch (_) { // If event loading fails, keep empty events } _allHabits.add(habit); } _allHabits.sort((a, b) => a.habitData.position.compareTo(b.habitData.position)); notifyListeners(); } Future loadCategories() async { _categories.clear(); try { final cats = await _categoryRepository.getAllCategories(); _categories.addAll(cats); } catch (_) { // If category loading fails, keep empty } notifyListeners(); } // ─── CRUD Operations ─────────────────────────────────────────────────────── void addHabit( String title, bool twoDayRule, String cue, String routine, String reward, bool showReward, bool advanced, bool notification, TimeOfDay notTime, String sanction, bool showSanction, String accountant, { HabitType habitType = HabitType.boolean, double targetValue = 100.0, double partialValue = 10.0, String unit = '', }) { final habitData = HabitData( position: _allHabits.length, title: title, twoDayRule: twoDayRule, cue: cue, routine: routine, reward: reward, showReward: showReward, advanced: advanced, notification: notification, notTime: notTime, sanction: sanction, showSanction: showSanction, accountant: accountant, habitType: habitType, targetValue: targetValue, partialValue: partialValue, unit: unit, events: SplayTreeMap(), ); final habit = Habit(habitData: habitData); _habitRepository.createHabit(habit); _allHabits.add(habit); _notificationService?.resetNotifications(activeHabits); notifyListeners(); } void editHabit(HabitData data) { final index = _allHabits.indexWhere((h) => h.habitData.id == data.id); if (index != -1) { _allHabits[index].habitData ..title = data.title ..twoDayRule = data.twoDayRule ..cue = data.cue ..routine = data.routine ..reward = data.reward ..showReward = data.showReward ..advanced = data.advanced ..notification = data.notification ..notTime = data.notTime ..sanction = data.sanction ..showSanction = data.showSanction ..accountant = data.accountant ..habitType = data.habitType ..targetValue = data.targetValue ..partialValue = data.partialValue ..unit = data.unit ..archived = data.archived; _habitRepository.updateHabit(_allHabits[index]); _notificationService?.resetNotifications(activeHabits); notifyListeners(); } } void deleteHabit(int id) { final habit = findHabitById(id); if (habit != null) { _toDelete.add(habit); _allHabits.remove(habit); _habitRepository.deleteHabit(id); _notificationService?.removeNotifications(id); _uiFeedbackService?.showMessageWithAction( message: 'Habit deleted.', actionLabel: 'Undo', onActionPressed: () => undoDelete(), backgroundColor: Colors.red, ); updateOrder(); notifyListeners(); } } void undoDelete() { if (_toDelete.isNotEmpty) { final habit = _toDelete.removeLast(); _allHabits.add(habit); updateOrder(); notifyListeners(); } } // ─── Archive Operations ──────────────────────────────────────────────────── void archiveHabit(int id) { final habit = findHabitById(id); if (habit != null) { habit.habitData.archived = true; _habitRepository.updateHabit(habit); _notificationService?.disableHabitNotification(id); _uiFeedbackService?.showMessageWithAction( message: 'Habit archived', actionLabel: 'Undo', onActionPressed: () => unarchiveHabit(id), backgroundColor: Colors.orange, ); notifyListeners(); } } void unarchiveHabit(int id) { final habit = findHabitById(id); if (habit != null) { habit.habitData.archived = false; _habitRepository.updateHabit(habit); if (habit.habitData.notification) { _notificationService?.setHabitNotification( id, habit.habitData.notTime, 'Habo', habit.habitData.title, ); } _uiFeedbackService?.showSuccess('Habit unarchived'); notifyListeners(); } } // ─── Reorder ─────────────────────────────────────────────────────────────── void reorderList(int oldIndex, int newIndex) { if (oldIndex < newIndex) newIndex--; final item = _allHabits.removeAt(oldIndex); _allHabits.insert(newIndex, item); updateOrder(); _habitRepository.updateHabitsOrder(_allHabits); notifyListeners(); } void updateOrder() { for (int i = 0; i < _allHabits.length; i++) { _allHabits[i].habitData.position = i; } // In-memory position update only - persistence handled separately } // ─── Event Operations ────────────────────────────────────────────────────── void addEvent(int id, DateTime date, List event) { final key = DateTime(date.year, date.month, date.day); // Update in-memory if habit exists final habit = findHabitById(id); if (habit != null) { habit.habitData.events[key] = event; _updateLastStreak(habit.habitData); } // Always persist to repository try { _eventRepository.insertEvent(id, key, event); } catch (_) {} notifyListeners(); } void deleteEvent(int id, DateTime date) { final key = DateTime(date.year, date.month, date.day); // Update in-memory if habit exists final habit = findHabitById(id); if (habit != null) { habit.habitData.events.remove(key); _updateLastStreak(habit.habitData); } // Always persist to repository try { _eventRepository.deleteEvent(id, key); } catch (_) {} notifyListeners(); } void _updateLastStreak(HabitData data) { final events = data.events; if (events.isEmpty) { data.streak = 0; data.streakVisible = false; data.orangeStreak = false; return; } if (data.twoDayRule) { _updateLastStreakTwoDay(data); } else { _updateLastStreakNormal(data); } } void _updateLastStreakNormal(HabitData data) { final events = data.events; if (events.isEmpty) { data.streak = 0; data.streakVisible = false; data.orangeStreak = false; return; } int inStreak = 0; final dates = events.keys.toList().reversed.toList(); for (int i = 0; i < dates.length; i++) { final date = dates[i]; final event = events[date]!; final dayType = event[0] is DayType ? event[0] as DayType : DayType.values[event[0] as int]; if (dayType == DayType.clear) continue; if (i > 0) { final prevDate = dates[i - 1]; final diff = prevDate.difference(date).inDays; if (diff > 1) break; } if (dayType == DayType.check) { inStreak++; } else if (dayType == DayType.progress && event.length >= 4) { final progressValue = event[2] as double? ?? 0.0; final target = event[3] as double? ?? data.targetValue; if (progressValue >= target) inStreak++; } else if (dayType == DayType.fail || dayType == DayType.skip) { break; } } data.streak = inStreak; data.streakVisible = inStreak >= 2; data.orangeStreak = false; } void _updateLastStreakTwoDay(HabitData data) { final events = data.events; if (events.isEmpty) { data.streak = 0; data.streakVisible = false; data.orangeStreak = false; return; } int inStreak = 0; bool usingTwoDayRule = false; final dates = events.keys.toList().reversed.toList(); for (int i = 0; i < dates.length; i++) { final date = dates[i]; final event = events[date]!; final dayType = event[0] is DayType ? event[0] as DayType : DayType.values[event[0] as int]; if (dayType == DayType.clear) continue; if (i > 0) { final prevDate = dates[i - 1]; final diff = prevDate.difference(date).inDays; if (diff > 1) break; } if (dayType == DayType.check) { inStreak++; usingTwoDayRule = false; } else if (dayType == DayType.progress && event.length >= 4) { final progressValue = event[2] as double? ?? 0.0; final target = event[3] as double? ?? data.targetValue; if (progressValue >= target) { inStreak++; usingTwoDayRule = false; } } else if (dayType == DayType.fail) { if (usingTwoDayRule) { break; } usingTwoDayRule = true; } else if (dayType == DayType.skip) { if (usingTwoDayRule) break; // Skip doesn't affect streak } } data.streak = inStreak; data.streakVisible = inStreak >= 2; data.orangeStreak = usingTwoDayRule; } // ─── Utility ─────────────────────────────────────────────────────────────── Habit? findHabitById(int id) { try { return _allHabits.firstWhere((h) => h.habitData.id == id); } catch (_) { return null; } } String getNameOfHabit(int id) { final habit = findHabitById(id); return habit?.habitData.title ?? ''; } // ─── Backup & Widget ─────────────────────────────────────────────────────── Future createBackup() async { await _backupService?.createDatabaseBackup(); } Future loadBackup(String path) async { await _backupService?.loadBackup(path); await loadHabits(); } void resetNotifications([List? habits]) { _notificationService?.resetNotifications(habits ?? activeHabits); } void updateHomeWidget() { // Stub } // ─── Category ────────────────────────────────────────────────────────────── Future addCategory(Category category) async { await _categoryRepository.createCategory(category); await loadCategories(); } Future updateCategory(Category category) async { await _categoryRepository.updateCategory(category); await loadCategories(); } Future deleteCategory(int id) async { await _categoryRepository.deleteCategory(id); await loadCategories(); } }