import 'package:flutter/foundation.dart'; import 'package:please_pay_me/core/state/async_value.dart'; import 'package:please_pay_me/data/api/api_client.dart'; import 'package:please_pay_me/data/models/expense.dart'; import 'package:please_pay_me/data/repositories/repositories.dart'; enum JournalScope { current('Текущий'), all('Все бюджеты'); const JournalScope(this.label); final String label; } class ExpenseGroup { const ExpenseGroup({required this.day, required this.items}); final DateTime day; final List items; double get total => items.fold(0, (sum, expense) => sum + expense.amount); } /// Paginated journal of operations with day grouping. class JournalController extends ChangeNotifier { JournalController({required ExpenseRepository expenses, this.pageSize = 20}) : _expenses = expenses; final ExpenseRepository _expenses; final int pageSize; AsyncValue _state = const AsyncValue.loading(); JournalScope _scope = JournalScope.current; int? _budgetId; bool _loadingMore = false; AsyncValue get state => _state; JournalScope get scope => _scope; bool get isLoadingMore => _loadingMore; List get items => _state.valueOrNull?.items ?? const []; bool get hasMore => _state.valueOrNull?.hasMore ?? false; double get totalSum => _state.valueOrNull?.totalSum ?? 0; /// Operations bucketed by day, newest first — the journal renders one /// inset-grouped section per bucket. List get groups { final buckets = >{}; for (final expense in items) { final day = DateTime(expense.spentAt.year, expense.spentAt.month, expense.spentAt.day); buckets.putIfAbsent(day, () => []).add(expense); } final days = buckets.keys.toList()..sort((a, b) => b.compareTo(a)); return [for (final day in days) ExpenseGroup(day: day, items: buckets[day]!)]; } void bindBudget(int? budgetId) { if (_budgetId == budgetId) return; _budgetId = budgetId; load(silent: true); } Future setScope(JournalScope scope) async { if (_scope == scope) return; _scope = scope; notifyListeners(); await load(); } Future load({bool silent = false}) async { if (!silent) { _state = const AsyncValue.loading(); notifyListeners(); } try { _state = AsyncValue.data(await _fetch(1)); } on ApiException catch (error) { _state = AsyncValue.error(error.message); } notifyListeners(); } Future loadMore() async { final current = _state.valueOrNull; if (current == null || !current.hasMore || _loadingMore) return; _loadingMore = true; notifyListeners(); try { final next = await _fetch(current.page + 1); _state = AsyncValue.data( next.copyWithItems([...current.items, ...next.items]), ); } on ApiException catch (error) { _state = AsyncValue.error(error.message); } finally { _loadingMore = false; notifyListeners(); } } Future _fetch(int page) { return _expenses.page( page: page, pageSize: pageSize, budgetId: _scope == JournalScope.all ? null : _budgetId, all: _scope == JournalScope.all, ); } }