Skip to content

Commit

Permalink
Merge pull request #84 from flow-mn/feat-stats
Browse files Browse the repository at this point in the history
Add stats tab
  • Loading branch information
sadespresso authored Mar 5, 2024
2 parents b8d80d9 + dbe7ba2 commit 74e9fa6
Show file tree
Hide file tree
Showing 56 changed files with 1,662 additions and 118 deletions.
21 changes: 19 additions & 2 deletions assets/l10n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"category.delete": "Delete category",
"category.delete.warning": "Deleting this category will leave {transactionCount} transactions with no category. This action is irreversible!",
"category.skip": "No category",
"category.none": "No category",
"categories": "Categories",
"categories.noCategories": "You don't have any categories",

Expand Down Expand Up @@ -128,8 +129,23 @@
"tabs.home.noTransactions.allTime": "You don't have any transactions",
"tabs.home.noTransactions.last7Days": "No transactions for the last 7 days",
"tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction",
"tabs.home.last7days": "Last 7 days",
"tabs.home.totalBalance": "Total balance",
"tabs.home.flowToday": "Flow today",

"tabs.stats": "Stats",
"tabs.stats.timeRange.select": "Select range",
"tabs.stats.timeRange.changeMode": "More options",
"tabs.stats.timeRange.presets": "Common options",
"tabs.stats.timeRange.thisWeek": "This week",
"tabs.stats.timeRange.thisMonth": "This month",
"tabs.stats.timeRange.thisYear": "This year",
"tabs.stats.timeRange.mode.custom": "Select custom range",
"tabs.stats.timeRange.mode.byWeek": "By week",
"tabs.stats.timeRange.mode.byMonth": "By month",
"tabs.stats.timeRange.mode.byYear": "By year",
"tabs.stats.chart.noData": "No data to show",
"tabs.stats.chart.select.clickToSelect": "Click to select",
"tabs.accounts": "Accounts",
"tabs.accounts.reorder": "Reorder accounts",
"tabs.accounts.reorder.guide": "Long press and drag",
Expand Down Expand Up @@ -177,7 +193,6 @@
"sync.import.emergencyBackup.successful": "Previous data was backed up. You can save the backup file from Backup > Backup history",
"sync.import.start": "Start importing",
"sync.import.success": "Import successful!",
"sync.export.fileDeleted": "File not found",

"sync.export": "Export",
"sync.export.type": "Export ({type})",
Expand All @@ -194,7 +209,9 @@
"sync.export.success": "Export successful!",
"sync.export.success.filePath[0]": "Saved to ",
"sync.export.success.filePath[1]": "",
"sync.export.share": "Flow backup ({type}, {date})",
"sync.export.save": "Save backup",
"sync.export.save.shareTitle": "Flow backup ({type}, {date})",
"sync.export.fileDeleted": "File not found",

"enum.TransactionSubtype": "Type",
"enum.TransactionSubtype#null": "Default",
Expand Down
19 changes: 18 additions & 1 deletion assets/l10n/mn_MN.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"category.delete": "Ангилалыг устгах",
"category.delete.warning": "Энэ ангиллыг устгавал холбоотой {transactionCount} гүйлгээг ангилалгүй болохыг анхаарна уу. Энэ үйлдлийг буцаах боломжгүй юм!",
"category.skip": "Ангилалгүй",
"category.none": "Ангилалгүй",
"categories": "Ангиллууд",
"categories.noCategories": "Танд үүсгэсэн ангилал алга байна",

Expand Down Expand Up @@ -128,8 +129,23 @@
"tabs.home.noTransactions.allTime": "Танд одоогоор гүйлгээ алга байна",
"tabs.home.noTransactions.last7Days": "Сүүлийн долоо хоногт хийгдсэн гүйлгээ алга байна",
"tabs.home.noTransactions.addSome": "Доор байрлах (+) товч дээр дарж гүйлгээ нэмээрэй",
"tabs.home.last7days": "Сүүлийн 7 хоног",
"tabs.home.totalBalance": "Нийт үлдэгдэл",
"tabs.home.flowToday": "Өнөөдөр",

"tabs.stats": "Тоо",
"tabs.stats.timeRange.select": "Хугацаа сонгох",
"tabs.stats.timeRange.changeMode": "Өөр сонголтууд",
"tabs.stats.timeRange.presets": "Түгээмэл сонголтууд",
"tabs.stats.timeRange.thisWeek": "Энэ долоо хоног",
"tabs.stats.timeRange.thisMonth": "Энэ сар",
"tabs.stats.timeRange.thisYear": "Энэ жил",
"tabs.stats.timeRange.mode.custom": "Эхлэх дуусах огноогоор",
"tabs.stats.timeRange.mode.byWeek": "Долоо хоногоор",
"tabs.stats.timeRange.mode.byMonth": "Сараар",
"tabs.stats.timeRange.mode.byYear": "Жилээр",
"tabs.stats.chart.noData": "Харуулах өгөгдөл байхгүй байна",
"tabs.stats.chart.select.clickToSelect": "Товшиж сонгоно уу",
"tabs.accounts": "Данснууд",
"tabs.accounts.reorder": "Дараалал өөрчлөх",
"tabs.accounts.reorder.guide": "Удаан дарж чирнэ үү",
Expand Down Expand Up @@ -193,7 +209,8 @@
"sync.export.success": "Амжилттай өгөгдлийг нөөцөллөө!",
"sync.export.success.filePath[0]": "Файлыг ",
"sync.export.success.filePath[1]": "-т хадгаллаа",
"sync.export.share": "Flow нөөц ({type}, {date})",
"sync.export.save": "Нөөц хадгалах",
"sync.export.save.shareTitle": "Flow нөөц ({type}, {date})",
"sync.export.fileDeleted": "Файл олдсонгүй",

"enum.TransactionSubtype": "Төрөл",
Expand Down
2 changes: 1 addition & 1 deletion lib/constants.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/foundation.dart';

const appVersion = "0.2.3+26";
const appVersion = "0.2.8+31";
const debugBuild = true;

bool get flowDebugMode => kDebugMode || debugBuild;
Expand Down
14 changes: 14 additions & 0 deletions lib/data/flow_analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flow/data/money_flow.dart';

class FlowAnalytics<T> {
final DateTime from;
final DateTime to;

final Map<String, MoneyFlow<T>> flow;

const FlowAnalytics({
required this.from,
required this.to,
required this.flow,
});
}
48 changes: 48 additions & 0 deletions lib/data/money_flow.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class MoneyFlow<T> implements Comparable<MoneyFlow> {
final T? associatedData;

double totalExpense;
double totalIncome;

double get flow => totalExpense + totalIncome;

bool get isEmpty => totalExpense.abs() == 0.0 && totalIncome.abs() == 0.0;

MoneyFlow({
this.associatedData,
this.totalExpense = 0.0,
this.totalIncome = 0.0,
});

@override
int compareTo(MoneyFlow other) {
return flow.compareTo(other.flow);
}

void addExpense(double expense) => totalExpense += expense;
void addIncome(double income) => totalIncome += income;
void add(double amount) =>
amount.isNegative ? addExpense(amount) : addIncome(amount);
void addAll(Iterable<double> amounts) => amounts.forEach(add);

operator +(MoneyFlow other) {
return MoneyFlow(
totalExpense: totalExpense + other.totalExpense,
totalIncome: totalIncome + other.totalIncome,
);
}

operator -(MoneyFlow other) {
return MoneyFlow(
totalExpense: totalExpense - other.totalExpense,
totalIncome: totalIncome - other.totalIncome,
);
}

operator -() {
return MoneyFlow(
totalExpense: -totalExpense,
totalIncome: -totalIncome,
);
}
}
2 changes: 2 additions & 0 deletions lib/entity/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class Account implements EntityBase {
}

/// Returns current balance. This is calculated by summing up every single transaction
///
/// TODO should this be cached?
@Transient()
@JsonKey(includeFromJson: false, includeToJson: false)
double get balance {
Expand Down
145 changes: 106 additions & 39 deletions lib/objectbox/actions.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:developer';

import 'package:flow/data/flow_analytics.dart';
import 'package:flow/data/money_flow.dart';
import 'package:flow/data/prefs/frecency_group.dart';
import 'package:flow/entity/account.dart';
import 'package:flow/entity/category.dart';
Expand All @@ -14,6 +16,18 @@ import 'package:moment_dart/moment_dart.dart';
import 'package:uuid/uuid.dart';

extension MainActions on ObjectBox {
double getTotalBalance() {
final Query<Account> accountsQuery = box<Account>()
.query(Account_.excludeFromTotalBalance.equals(false))
.build();

final List<Account> accounts = accountsQuery.find();

return accounts
.map((e) => e.balance)
.fold(0, (previousValue, element) => previousValue + element);
}

List<Account> getAccounts([bool sortByFrecency = true]) {
final List<Account> accounts = box<Account>().getAll();

Expand Down Expand Up @@ -67,6 +81,83 @@ extension MainActions on ObjectBox {

await ObjectBox().box<Account>().putManyAsync(accounts);
}

/// Returns a map of category uuid -> [MoneyFlow]
Future<FlowAnalytics<Category>> flowByCategories({
required DateTime from,
required DateTime to,
bool ignoreTransfers = true,
bool omitZeroes = true,
}) async {
final Condition<Transaction> dateFilter =
Transaction_.transactionDate.betweenDate(from, to);

final Query<Transaction> transactionsQuery =
ObjectBox().box<Transaction>().query(dateFilter).build();

final List<Transaction> transactions = await transactionsQuery.findAsync();

transactionsQuery.close();

final Map<String, MoneyFlow<Category>> flow = {};

for (final transaction in transactions) {
if (ignoreTransfers && transaction.isTransfer) continue;

final String categoryUuid =
transaction.category.target?.uuid ?? Uuid.NAMESPACE_NIL;

flow[categoryUuid] ??=
MoneyFlow(associatedData: transaction.category.target);
flow[categoryUuid]!.add(transaction.amount);
}

if (omitZeroes) {
flow.removeWhere((key, value) => value.isEmpty);
}

return FlowAnalytics(flow: flow, from: from, to: to);
}

/// Returns a map of category uuid -> [MoneyFlow]
Future<FlowAnalytics<Account>> flowByAccounts({
required DateTime from,
required DateTime to,
bool ignoreTransfers = true,
bool omitZeroes = true,
}) async {
final Condition<Transaction> dateFilter =
Transaction_.transactionDate.betweenDate(from, to);

final Query<Transaction> transactionsQuery =
ObjectBox().box<Transaction>().query(dateFilter).build();

final List<Transaction> transactions = await transactionsQuery.findAsync();

transactionsQuery.close();

final Map<String, MoneyFlow<Account>> flow = {};

for (final transaction in transactions) {
if (ignoreTransfers && transaction.isTransfer) continue;

final String accountUuid =
transaction.account.target?.uuid ?? Uuid.NAMESPACE_NIL;

flow[accountUuid] ??=
MoneyFlow(associatedData: transaction.account.target);
flow[accountUuid]!.add(transaction.amount);
}

assert(!flow.containsKey(Uuid.NAMESPACE_NIL),
"There is no way you've managed to make a transaction without an account");

if (omitZeroes) {
flow.removeWhere((key, value) => value.isEmpty);
}

return FlowAnalytics(from: from, to: to, flow: flow);
}
}

extension TransactionActions on Transaction {
Expand Down Expand Up @@ -127,51 +218,27 @@ extension TransactionActions on Transaction {
}

extension TransactionListActions on Iterable<Transaction> {
Iterable<Transaction> get nonTransactions =>
Iterable<Transaction> get nonTransfers =>
where((transaction) => !transaction.isTransfer);

double get incomeSum => where((transaction) => transaction.amount >= 0)
.map((transaction) => transaction.amount)
.fold(0, (value, element) => value + element);
double get expenseSum => where((transaction) => transaction.amount < 0)
.map((transaction) => transaction.amount)
.fold(0, (value, element) => value + element);
double get sum =>
map((transaction) => transaction.amount).fold(0, (a, b) => a + b);
Iterable<Transaction> get transfers =>
where((transaction) => transaction.isTransfer);
Iterable<Transaction> get expenses =>
where((transaction) => transaction.amount.isNegative);
Iterable<Transaction> get incomes =>
where((transaction) => transaction.amount > 0);

double get incomeSum =>
incomes.fold(0, (value, element) => value + element.amount);
double get expenseSum =>
expenses.fold(0, (value, element) => value + element.amount);
double get sum => fold(0, (value, element) => value + element.amount);

Map<DateTime, List<Transaction>> groupByDate() {
final Map<DateTime, List<Transaction>> value = {};

int? lastTransferIndex;
Transaction? lastTransferFrom;

for (final (index, transaction) in indexed) {
for (final transaction in this) {
final date = transaction.transactionDate.toLocal().startOfDay();

if (LocalPreferences().combineTransferTransactions.get() &&
transaction.isTransfer) {
if (lastTransferIndex == null) {
lastTransferIndex = index;
lastTransferFrom = transaction;
continue;
}

value[date] ??= [];
value[date]!.add(
lastTransferFrom!
..title ??= "transaction.transfer.fromToTitle".tr(
{
"from": lastTransferFrom.account.target!.name,
"to": transaction.account.target!.name
},
),
);

lastTransferIndex = null;
lastTransferFrom = null;
continue;
}

value[date] ??= [];
value[date]!.add(transaction);
}
Expand All @@ -186,7 +253,7 @@ extension AccountActions on Account {
/// This is probably better of as Singleton somewhere with memoization as
/// account names won't get changed that frequently (I hope)
///
/// TODO
/// TODO refactor this to be more efficient
static String nameByUuid(String uuid) {
final query =
ObjectBox().box<Account>().query(Account_.uuid.equals(uuid)).build();
Expand Down
6 changes: 4 additions & 2 deletions lib/objectbox/objectbox-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
},
{
"id": "2:649350347514211469",
"lastPropertyId": "6:7989789340130049283",
"lastPropertyId": "8:2832069050067036408",
"name": "Category",
"properties": [
{
Expand Down Expand Up @@ -283,7 +283,9 @@
8163231449257835161,
8413544260372569748,
5446772239174299489,
3056128952161562633
3056128952161562633,
2675470948342446870,
2832069050067036408
],
"retiredRelationUids": [],
"version": 1
Expand Down
Loading

0 comments on commit 74e9fa6

Please sign in to comment.