Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upcoming transactions #122

Merged
merged 3 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions assets/l10n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"account": "Account",
"account.name": "Account name",
"account.balance": "Balance",
"account.balance.upcomingDescription": "Upcoming transactions don't affect current balance",
"account.excludeFromTotalBalance": "Exclude from balance",
"account.excludeFromTotalBalance.description": "If you check this option, this account's balance will not be included in the total balance. Useful for savings or non-personal accounts.",
"account.updateBalance": "Update balance",
Expand All @@ -71,6 +72,7 @@
"account.edit": "Edit account",
"account.edit.selectCurrency": "Select a currency",
"account.transactions": "Transactions",
"account.transactions.title": "\"{account}\" transactions",
"account.delete": "Delete account",
"account.delete.warning": "Deleting this account will also delete {transactionCount} transactions associated. This action is irreversible!",
"account.noAccounts": "You don't have any accounts!",
Expand All @@ -93,6 +95,9 @@
"transaction.transfer.to.title": "To {account}",
"transaction.transfer.fromToTitle": "From {from} to {to}",

"transactions.all": "All transactions",
"transactions.upcoming": "Upcoming transactions",

"category": "Category",
"category.name": "Category name",
"category.new": "Add a category",
Expand Down Expand Up @@ -133,6 +138,8 @@
"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.upcomingTransactions": "Upcoming ({count})",
"tabs.home.upcomingTransactions.seeAll": "See all",
"tabs.home.transactionsCount": "{count} transactions",
"tabs.home.last7days": "Last 7 days",
"tabs.home.totalBalance": "Total balance",
Expand Down
7 changes: 7 additions & 0 deletions assets/l10n/mn_MN.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"account": "Данс",
"account.name": "Дансны нэр",
"account.balance": "Үлдэгдэл",
"account.balance.upcomingDescription": "Төлөвлөсөн гүйлгээнүүд одоогийн дансны үлдэгдэлд нөлөөлөхгүй",
"account.excludeFromTotalBalance": "Нийт үлдэгдэлд тооцохгүй",
"account.excludeFromTotalBalance.description": "Энэ сонголтыг идэвхжүүлбэл энэ дансны үлдэгдэл нийт үлдэгдэлд бодогдохгүй. Хадгаламж, хувийн бус данс гэх мэт зүйлсд тохиромжтой.",
"account.updateBalance": "Үлдэгдэл өөрчлөх",
Expand All @@ -71,6 +72,7 @@
"account.edit": "Данс засварлах",
"account.edit.selectCurrency": "Валют сонгох",
"account.transactions": "Гүйлгээнүүд",
"account.transactions.title": "\"{account}\"-н гүйлгээнүүд",
"account.delete": "Дансыг устгах",
"account.delete.warning": "Энэ дансыг устгавал холбоотой {transactionCount} гүйлгээг хамт устгах болно. Энэ үйлдлийг буцаах боломжгүй юм!",
"account.noAccounts": "Танд үүсгэсэн данс алга байна!",
Expand All @@ -93,6 +95,9 @@
"transaction.transfer.to.title": "{account}-руу",
"transaction.transfer.fromToTitle": "{from}-с {to} руу",

"transactions.all": "Бүх гүйлгээнүүд",
"transactions.upcoming": "Төлөвлөсөн гүйлгээнүүд",

"category": "Ангилал",
"category.name": "Нэр",
"category.new": "Ангилал үүсгэх",
Expand Down Expand Up @@ -133,6 +138,8 @@
"tabs.home.noTransactions.allTime": "Танд одоогоор гүйлгээ алга байна",
"tabs.home.noTransactions.last7Days": "Сүүлийн долоо хоногт хийгдсэн гүйлгээ алга байна",
"tabs.home.noTransactions.addSome": "Доор байрлах (+) товч дээр дарж гүйлгээ нэмээрэй",
"tabs.home.upcomingTransactions": "Төлөвлөсөн ({count})",
"tabs.home.upcomingTransactions.seeAll": "Бүгд",
"tabs.home.transactionsCount": "{count} гүйлгээ",
"tabs.home.last7days": "Сүүлийн 7 хоног",
"tabs.home.totalBalance": "Нийт үлдэгдэл",
Expand Down
11 changes: 7 additions & 4 deletions lib/entity/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flow/entity/_base.dart';
import 'package:flow/entity/transaction.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:moment_dart/moment_dart.dart';
import 'package:objectbox/objectbox.dart';
import 'package:uuid/uuid.dart';

Expand Down Expand Up @@ -55,10 +56,12 @@ class Account implements EntityBase {
@Transient()
@JsonKey(includeFromJson: false, includeToJson: false)
double get balance {
return transactions.fold<double>(
0,
(previousValue, element) => previousValue + element.amount,
);
return transactions
.where((element) => element.transactionDate.isPast)
.fold<double>(
0,
(previousValue, element) => previousValue + element.amount,
);
}

Account({
Expand Down
10 changes: 8 additions & 2 deletions lib/objectbox/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,17 @@ extension TransactionListActions on Iterable<Transaction> {
expenses.fold(0, (value, element) => value + element.amount);
double get sum => fold(0, (value, element) => value + element.amount);

Map<DateTime, List<Transaction>> groupByDate() {
/// If [mergeFutureTransactions] is set to true, transactions in future
/// relative to [anchor] will be grouped into the same group
Map<DateTime, List<Transaction>> groupByDate({
DateTime? anchor,
}) {
anchor ??= DateTime.now();

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

for (final transaction in this) {
final date = transaction.transactionDate.toLocal().startOfDay();
final DateTime date = transaction.transactionDate.toLocal().startOfDay();

value[date] ??= [];
value[date]!.add(transaction);
Expand Down
22 changes: 22 additions & 0 deletions lib/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import 'package:flow/routes/utils/crop_square_image_page.dart';
import 'package:flow/sync/export/mode.dart';
import 'package:flow/sync/import/import_v1.dart';
import 'package:flow/utils/utils.dart';
import 'package:flow/widgets/general/info_text.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';

final router = GoRouter(
Expand All @@ -52,6 +54,26 @@ final router = GoRouter(
transactionId: int.tryParse(state.pathParameters["id"]!) ?? -1,
),
),
GoRoute(
path: '/transactions',
builder: (context, state) => TransactionsPage.all(
title: "transactions.all".t(context),
),
),
GoRoute(
path: '/transactions/upcoming',
builder: (context, state) => TransactionsPage.upcoming(
title: "transactions.upcoming".t(context),
header: InfoText(
singleLine: true,
child: Text(
"account.balance.upcomingDescription".t(context),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
GoRoute(
path: '/account/new',
builder: (context, state) => const AccountPage.create(),
Expand Down
42 changes: 34 additions & 8 deletions lib/routes/home/home_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'package:flow/entity/transaction.dart';
import 'package:flow/objectbox.dart';
import 'package:flow/objectbox/actions.dart';
import 'package:flow/objectbox/objectbox.g.dart';
import 'package:flow/widgets/general/wavy_divider.dart';
import 'package:flow/widgets/home/home/no_transactions.dart';
import 'package:flow/widgets/home/greetings_bar.dart';
import 'package:flow/widgets/grouped_transaction_list.dart';
import 'package:flow/widgets/home/home/upcoming_transactions_list.dart';
import 'package:flow/widgets/home/transactions_date_header.dart';
import 'package:flutter/material.dart';
import 'package:moment_dart/moment_dart.dart';
Expand Down Expand Up @@ -47,10 +49,7 @@ class _HomeTabState extends State<HomeTab> with AutomaticKeepAliveClientMixin {
return StreamBuilder<Query<Transaction>>(
stream: qb().watch(triggerImmediately: true),
builder: (context, snapshot) {
final transactions = snapshot.data
?.find()
.where((element) => element.transactionDate <= Moment.now())
.toList();
final transactions = snapshot.data?.find();

return Column(
children: [
Expand Down Expand Up @@ -80,18 +79,45 @@ class _HomeTabState extends State<HomeTab> with AutomaticKeepAliveClientMixin {
}

Widget buildGroupedList(
BuildContext context, List<Transaction> transactions) {
final Map<DateTime, List<Transaction>> grouped = transactions.groupByDate();
BuildContext context,
List<Transaction> transactions,
) {
final Map<DateTime, List<Transaction>> grouped = transactions
.where((element) => element.transactionDate.isPast)
.groupByDate();

final List<Widget> headers = grouped.keys
.map((date) =>
TransactionListDateHeader(transactions: grouped[date]!, date: date))
.map(
(date) => TransactionListDateHeader(
transactions: grouped[date]!,
date: date,
),
)
.toList();

final List<Transaction> upcoming = transactions
.where((element) => element.transactionDate.isFuture)
.toList();

final Widget? header = upcoming.isEmpty
? null
: Column(
children: [
UpcomingTransactionsList(
transactions: upcoming,
shouldCombineTransferIfNeeded: true,
),
const SizedBox(height: 16.0),
const WavyDivider(),
],
);

return GroupedTransactionList(
controller: widget.scrollController,
transactions: grouped.values.toList(),
shouldCombineTransferIfNeeded: true,
headers: headers,
header: header,
listPadding: const EdgeInsets.only(
top: 0,
bottom: 80.0,
Expand Down
9 changes: 6 additions & 3 deletions lib/routes/transaction_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -481,10 +481,10 @@ class _TransactionPageState extends State<TransactionPage> {
}

void selectTransactionDate() async {
final result = await showDatePicker(
final DateTime? result = await showDatePicker(
context: context,
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
lastDate: DateTime.now().add(const Duration(days: 365)),
lastDate: DateTime(9999, 12, 31),
initialDate: _transactionDate,
);

Expand All @@ -494,7 +494,7 @@ class _TransactionPageState extends State<TransactionPage> {

if (!mounted || result == null) return;

final timeResult = await showTimePicker(
final TimeOfDay? timeResult = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_transactionDate),
);
Expand Down Expand Up @@ -582,12 +582,15 @@ class _TransactionPageState extends State<TransactionPage> {
_selectedAccount!.transferTo(
targetAccount: _selectedAccountTransferTo!,
amount: _amount.abs(),
transactionDate: _transactionDate,
title: formattedTitle,
);
} else {
_selectedAccount!.createAndSaveTransaction(
amount: _amount,
title: formattedTitle,
category: _selectedCategory,
transactionDate: _transactionDate,
);
}

Expand Down
58 changes: 55 additions & 3 deletions lib/routes/transactions_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ class TransactionsPage extends StatefulWidget {
final QueryBuilder<Transaction> query;
final String? title;

const TransactionsPage({super.key, required this.query, this.title});
final Widget? header;

factory TransactionsPage.account(
{Key? key, required int accountId, String? title}) {
const TransactionsPage({
super.key,
required this.query,
this.title,
this.header,
});

factory TransactionsPage.account({
Key? key,
required int accountId,
String? title,
Widget? header,
}) {
final QueryBuilder<Transaction> queryBuilder = ObjectBox()
.box<Transaction>()
.query(Transaction_.account.equals(accountId))
Expand All @@ -24,6 +35,46 @@ class TransactionsPage extends StatefulWidget {
query: queryBuilder,
key: key,
title: title,
header: header,
);
}

factory TransactionsPage.all({
Key? key,
String? title,
Widget? header,
}) {
final QueryBuilder<Transaction> queryBuilder = ObjectBox()
.box<Transaction>()
.query()
.order(Transaction_.transactionDate, flags: Order.descending);

return TransactionsPage(
query: queryBuilder,
key: key,
title: title,
header: header,
);
}

factory TransactionsPage.upcoming({
Key? key,
DateTime? anchor,
String? title,
Widget? header,
}) {
anchor ??= DateTime.now();

final QueryBuilder<Transaction> queryBuilder = ObjectBox()
.box<Transaction>()
.query(Transaction_.transactionDate.greaterThanDate(anchor))
.order(Transaction_.transactionDate, flags: Order.descending);

return TransactionsPage(
query: queryBuilder,
key: key,
title: title,
header: header,
);
}

Expand Down Expand Up @@ -55,6 +106,7 @@ class _TransactionsPageState extends State<TransactionsPage> {
return GroupedTransactionList(
transactions: grouped.values.toList(),
headers: headers,
header: widget.header,
);
},
),
Expand Down
25 changes: 25 additions & 0 deletions lib/utils/extensions/iterables.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,29 @@ extension Iterables<E> on Iterable<E> {
}
return null;
}

/// Returns a list with items alternating from [this] and [other].
///
/// Both iterables must have the same length.
///
/// Example:
/// ```dart
/// List<Object> list1 = [1, 2, 3];
/// List<Object> list2 = ['a', 'b', 'c'];
/// list1.alternate(list2); // [1, 'a', 2, 'b', 3, 'c']
/// ```
List<E> alternate(Iterable<E> other) {
if (length != other.length) {
throw ArgumentError('Both iterables must have the same length');
}

List<E> result = [];

for (int i = 0; i < length; i++) {
result.add(elementAt(i));
result.add(other.elementAt(i));
}

return result;
}
}
2 changes: 1 addition & 1 deletion lib/widgets/account_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class AccountCard extends StatelessWidget {
),
CupertinoContextMenuAction(
onPressed: () => context.push(
"/account/${account.id}/transactions?title=${account.name}"),
"/account/${account.id}/transactions?title=${"account.transactions.title".t(context, account.name)}"),
isDefaultAction: true,
trailingIcon: CupertinoIcons.square_list,
child: Text("account.transactions".t(context)),
Expand Down
Loading