117 lines
3.2 KiB
Dart
117 lines
3.2 KiB
Dart
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<Expense> items;
|
|
|
|
double get total => items.fold<double>(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<ExpensesPage> _state = const AsyncValue.loading();
|
|
JournalScope _scope = JournalScope.current;
|
|
int? _budgetId;
|
|
bool _loadingMore = false;
|
|
|
|
AsyncValue<ExpensesPage> get state => _state;
|
|
JournalScope get scope => _scope;
|
|
bool get isLoadingMore => _loadingMore;
|
|
|
|
List<Expense> 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<ExpenseGroup> get groups {
|
|
final buckets = <DateTime, List<Expense>>{};
|
|
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<void> setScope(JournalScope scope) async {
|
|
if (_scope == scope) return;
|
|
_scope = scope;
|
|
notifyListeners();
|
|
await load();
|
|
}
|
|
|
|
Future<void> 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<void> 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<ExpensesPage> _fetch(int page) {
|
|
return _expenses.page(
|
|
page: page,
|
|
pageSize: pageSize,
|
|
budgetId: _scope == JournalScope.all ? null : _budgetId,
|
|
all: _scope == JournalScope.all,
|
|
);
|
|
}
|
|
}
|