feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 04:06:43 +03:00
commit c956b94983
1076 changed files with 50876 additions and 0 deletions
@@ -0,0 +1,137 @@
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/budget.dart';
import 'package:please_pay_me/data/repositories/repositories.dart';
/// Source of truth for budgets: the overview, the budget list and the expense
/// form all read the selected envelope from here.
class BudgetsController extends ChangeNotifier {
BudgetsController({
required BudgetRepository budgets,
required ExpenseRepository expenses,
}) : _budgets = budgets,
_expenses = expenses;
final BudgetRepository _budgets;
final ExpenseRepository _expenses;
AsyncValue<List<BudgetStatus>> _state = const AsyncValue.loading();
bool _mutating = false;
AsyncValue<List<BudgetStatus>> get state => _state;
/// True while a write is in flight — used to disable buttons.
bool get isMutating => _mutating;
List<BudgetStatus> get items => _state.valueOrNull ?? const [];
/// Currently selected envelope; `null` when the user has no budgets at all.
BudgetStatus? get selected {
final all = items;
if (all.isEmpty) return null;
for (final status in all) {
if (status.selected) return status;
}
return all.first;
}
bool get hasBudgets => items.isNotEmpty;
Future<void> load({bool silent = false}) async {
if (!silent) {
_state = const AsyncValue.loading();
notifyListeners();
}
try {
_state = AsyncValue.data(await _budgets.list());
} on ApiException catch (error) {
_state = AsyncValue.error(error.message);
}
notifyListeners();
}
Future<String?> select(int budgetId) {
return _mutate(() => _budgets.select(budgetId));
}
Future<String?> setActive(int budgetId, {required bool isActive}) {
return _mutate(() => _budgets.setActive(budgetId, isActive: isActive));
}
Future<String?> create({
required String name,
required double totalAmount,
required DateTime endDate,
DateTime? startDate,
}) {
return _mutate(
() => _budgets.create(
name: name,
totalAmount: totalAmount,
endDate: endDate,
startDate: startDate,
),
);
}
Future<String?> update({
required int budgetId,
String? name,
double? totalAmount,
DateTime? endDate,
DateTime? startDate,
bool resetExpenses = false,
}) {
return _mutate(
() => _budgets.update(
budgetId: budgetId,
name: name,
totalAmount: totalAmount,
endDate: endDate,
startDate: startDate,
resetExpenses: resetExpenses,
),
);
}
Future<String?> delete(int budgetId) => _mutate(() => _budgets.delete(budgetId));
Future<String?> addExpense({
required double amount,
String? note,
DateTime? spentAt,
int? budgetId,
}) {
return _mutate(
() => _expenses.create(
amount: amount,
note: note,
spentAt: spentAt,
budgetId: budgetId ?? selected?.budget.id,
),
);
}
Future<String?> undoLastExpense() {
return _mutate(() => _expenses.undoLast(budgetId: selected?.budget.id));
}
/// Runs a write, reloads the list and returns an error message or `null`.
Future<String?> _mutate(Future<void> Function() action) async {
_mutating = true;
notifyListeners();
try {
await action();
await load(silent: true);
return null;
} on ApiException catch (error) {
return error.message;
} finally {
_mutating = false;
notifyListeners();
}
}
}