import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/auth/auth_provider.dart'; import '../../shared/models/diary_entry.dart'; import '../../shared/models/menu.dart'; import '../../shared/models/shopping_item.dart'; import 'menu_service.dart'; // ── Service provider ────────────────────────────────────────── final menuServiceProvider = Provider((ref) { return MenuService(ref.read(apiClientProvider)); }); // ── Current week (state) ────────────────────────────────────── /// The ISO week string for the currently displayed week, e.g. "2026-W08". final currentWeekProvider = StateProvider((ref) { final now = DateTime.now().toUtc(); final (y, w) = _isoWeek(now); return '$y-W${w.toString().padLeft(2, '0')}'; }); (int year, int week) _isoWeek(DateTime dt) { // Shift to Thursday to get ISO week year. final thu = dt.add(Duration(days: 4 - (dt.weekday == 7 ? 0 : dt.weekday))); final jan1 = DateTime.utc(thu.year, 1, 1); final week = ((thu.difference(jan1).inDays) / 7).ceil(); return (thu.year, week); } /// Returns the ISO 8601 week string for [date], e.g. "2026-W12". String isoWeekString(DateTime date) { final (year, week) = _isoWeek(date.toUtc()); return '$year-W${week.toString().padLeft(2, '0')}'; } // ── Menu notifier ───────────────────────────────────────────── class MenuNotifier extends StateNotifier> { final MenuService _service; final String _week; MenuNotifier(this._service, this._week) : super(const AsyncValue.loading()) { load(); } Future load() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => _service.getMenu(week: _week)); } Future generate() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => _service.generateMenu(week: _week)); } Future updateItem(String itemId, String recipeId) async { await _service.updateMenuItem(itemId, recipeId); await load(); } Future deleteItem(String itemId) async { await _service.deleteMenuItem(itemId); await load(); } } final menuProvider = StateNotifierProvider.family, String>( (ref, week) => MenuNotifier(ref.read(menuServiceProvider), week), ); // ── Shopping list notifier ──────────────────────────────────── class ShoppingListNotifier extends StateNotifier>> { final MenuService _service; final String _week; ShoppingListNotifier(this._service, this._week) : super(const AsyncValue.loading()) { load(); } Future load() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => _service.getShoppingList(week: _week)); } Future regenerate() async { state = const AsyncValue.loading(); state = await AsyncValue.guard( () => _service.generateShoppingList(week: _week)); } Future toggle(int index, bool checked) async { // Optimistic update. state = state.whenData((items) { final list = List.from(items); if (index < list.length) { list[index] = list[index].copyWith(checked: checked); } return list; }); try { await _service.toggleShoppingItem(index, checked, week: _week); } catch (_) { // Revert on failure. await load(); } } } final shoppingListProvider = StateNotifierProvider.family< ShoppingListNotifier, AsyncValue>, String>( (ref, week) => ShoppingListNotifier(ref.read(menuServiceProvider), week), ); // ── Diary notifier ──────────────────────────────────────────── class DiaryNotifier extends StateNotifier>> { final MenuService _service; final String _date; DiaryNotifier(this._service, this._date) : super(const AsyncValue.loading()) { load(); } Future load() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => _service.getDiary(_date)); } Future add(Map body) async { await _service.createDiaryEntry(body); await load(); } Future remove(String id) async { final prev = state; state = state.whenData((l) => l.where((e) => e.id != id).toList()); try { await _service.deleteDiaryEntry(id); } catch (_) { state = prev; } } } final diaryProvider = StateNotifierProvider.family>, String>( (ref, date) => DiaryNotifier(ref.read(menuServiceProvider), date), ); // ── Plan menu helpers ───────────────────────────────────────── /// The latest future date (among cached menus for the current and next two weeks) /// that already has at least one planned meal slot. Returns null when no future /// meals are planned. final lastPlannedDateProvider = Provider((ref) { final today = DateTime.now(); DateTime? latestDate; for (var offsetDays = 0; offsetDays <= 14; offsetDays += 7) { final weekDate = today.add(Duration(days: offsetDays)); final plan = ref.watch(menuProvider(isoWeekString(weekDate))).valueOrNull; if (plan == null) continue; for (final day in plan.days) { final dayDate = DateTime.parse(day.date); if (dayDate.isAfter(today) && day.meals.isNotEmpty) { if (latestDate == null || dayDate.isAfter(latestDate)) { latestDate = dayDate; } } } } return latestDate; }); /// Service for planning meals across specific dates. /// Calls the API and then invalidates the affected week providers so that /// [plannedMealsProvider] picks up the new slots automatically. class PlanMenuService { final Ref _ref; const PlanMenuService(this._ref); Future generateForDates({ required List dates, required List mealTypes, }) async { final menuService = _ref.read(menuServiceProvider); final plans = await menuService.generateForDates( dates: dates, mealTypes: mealTypes, ); for (final plan in plans) { _ref.invalidate(menuProvider(isoWeekString(DateTime.parse(plan.weekStart)))); } } } final planMenuServiceProvider = Provider((ref) { return PlanMenuService(ref); });