Skip to content

Commit

Permalink
feat: Bulk edit/delete transactions (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
enrique-lozano authored May 3, 2024
1 parent e25645d commit ebadb20
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 89 deletions.
278 changes: 222 additions & 56 deletions lib/app/transactions/transactions.page.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
// ignore_for_file: unnecessary_string_interpolations, prefer_single_quotes

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:monekin/app/layout/tabs.dart';
import 'package:monekin/app/transactions/form/transaction_form.page.dart';
import 'package:monekin/app/transactions/widgets/bulk_edit_transaction_modal.dart';
import 'package:monekin/app/transactions/widgets/transaction_list.dart';
import 'package:monekin/core/database/services/transaction/transaction_service.dart';
import 'package:monekin/core/models/transaction/transaction.dart';
import 'package:monekin/core/presentation/app_colors.dart';
import 'package:monekin/core/presentation/widgets/confirm_dialog.dart';
import 'package:monekin/core/presentation/widgets/filter_row_indicator.dart';
import 'package:monekin/core/presentation/widgets/monekin_popup_menu_button.dart';
import 'package:monekin/core/presentation/widgets/no_results.dart';
import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart';
import 'package:monekin/core/presentation/widgets/skeleton.dart';
import 'package:monekin/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart';
import 'package:monekin/core/presentation/widgets/transaction_filter/transaction_filters.dart';
import 'package:monekin/core/routes/route_utils.dart';
import 'package:monekin/core/utils/list_tile_action_item.dart';
import 'package:monekin/i18n/translations.g.dart';

class TransactionsPage extends StatefulWidget {
Expand All @@ -29,6 +39,8 @@ class _TransactionsPageState extends State<TransactionsPage> {
FocusNode searchFocusNode = FocusNode();
String? searchValue;

List<MoneyTransaction> selectedTransactions = [];

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -75,64 +87,67 @@ class _TransactionsPageState extends State<TransactionsPage> {
Navigator.pop(context);
},
child: Scaffold(
appBar: AppBar(
leading: searchActive
? IconButton(
onPressed: () {
setState(() {
searchActive = false;
searchValue = null;
});
},
icon: const Icon(Icons.close))
: null,
title: searchActive
? TextField(
focusNode: searchFocusNode,
decoration: InputDecoration(
hintText: t.transaction.list.searcher_placeholder,
border: const UnderlineInputBorder()),
onChanged: (value) {
setState(() {
searchValue = value;
});
},
)
: Text(t.transaction.display(n: 10)),
actions: [
if (!searchActive)
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
searchActive = true;
});

searchFocusNode.requestFocus();
},
),
IconButton(
onPressed: () async {
final modalRes = await openFilterSheetModal(
context,
FilterSheetModal(preselectedFilter: filters),
);

if (modalRes != null) {
setState(() {
filters = modalRes.copyWith(
includeParentCategoriesInSearch: true);
});
}
},
icon: const Icon(Icons.filter_alt_outlined)),
]),
appBar: selectedTransactions.isNotEmpty
? selectedTransactionsAppbar()
: AppBar(
leading: searchActive
? IconButton(
onPressed: () {
setState(() {
searchActive = false;
searchValue = null;
});
},
icon: const Icon(Icons.close))
: null,
title: searchActive
? TextField(
focusNode: searchFocusNode,
decoration: InputDecoration(
hintText: t.transaction.list.searcher_placeholder,
border: const UnderlineInputBorder()),
onChanged: (value) {
setState(() {
searchValue = value;
});
},
)
: Text(t.transaction.display(n: 10)),
actions: [
if (!searchActive)
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
searchActive = true;
});

searchFocusNode.requestFocus();
},
),
IconButton(
onPressed: () async {
final modalRes = await openFilterSheetModal(
context,
FilterSheetModal(preselectedFilter: filters),
);

if (modalRes != null) {
setState(() {
filters = modalRes.copyWith(
includeParentCategoriesInSearch: true);
});
}
},
icon: const Icon(Icons.filter_alt_outlined)),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add_rounded),
label: Text(t.transaction.create),
onPressed: () => RouteUtils.pushRoute(
context,
TransactionFormPage(),
const TransactionFormPage(),
),
),
body: Column(
Expand All @@ -154,6 +169,9 @@ class _TransactionsPageState extends State<TransactionsPage> {
builder: (context, snapshot) {
final res = snapshot.data;

const smallerTextStyle =
TextStyle(fontSize: 14, fontWeight: FontWeight.w300);

return Card(
elevation: 2,
//color: AppColors.of(context).primary,
Expand All @@ -170,9 +188,47 @@ class _TransactionsPageState extends State<TransactionsPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (res != null) ...[
Text(
'${res.numberOfRes} ${t.transaction.display(n: res.numberOfRes).toLowerCase()}'),
CurrencyDisplayer(amountToConvert: res.valueSum)
Text.rich(
TextSpan(
text: selectedTransactions.isNotEmpty
? ('${selectedTransactions.length.toStringAsFixed(0)}')
: '',
children: [
TextSpan(
text:
'${selectedTransactions.isNotEmpty ? ' / ' : ''}${res.numberOfRes} ',
style: selectedTransactions.isNotEmpty
? smallerTextStyle
: null),
TextSpan(
text: t.transaction
.display(n: res.numberOfRes)
.toLowerCase(),
),
]),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (selectedTransactions.isNotEmpty) ...[
CurrencyDisplayer(
amountToConvert: selectedTransactions
.map((e) => e
.getCurrentBalanceInPreferredCurrency())
.sum,
showDecimals: false,
),
const Text("/ ", style: smallerTextStyle)
],
CurrencyDisplayer(
amountToConvert: res.valueSum,
showDecimals: selectedTransactions.isEmpty,
integerStyle: selectedTransactions.isEmpty
? const TextStyle(inherit: true)
: smallerTextStyle,
),
],
)
],
if (res == null) ...[
const Skeleton(width: 38, height: 18),
Expand All @@ -190,6 +246,17 @@ class _TransactionsPageState extends State<TransactionsPage> {
heroTagBuilder: (tr) => 'transactions-page__tr-icon-${tr.id}',
filters: filters.copyWith(searchValue: searchValue),
prevPage: const TabsPage(),
selectedTransactions: selectedTransactions,
onLongPress: (tr) {
if (selectedTransactions.isNotEmpty) {
return;
}

setState(() {
selectedTransactions = [tr];
});
},
onTap: selectedTransactions.isEmpty ? null : toggleTransaction,
onEmptyList: NoResults(
title: filters.hasFilter ? null : t.general.empty_warn,
description: filters.hasFilter
Expand All @@ -204,4 +271,103 @@ class _TransactionsPageState extends State<TransactionsPage> {
),
);
}

AppBar selectedTransactionsAppbar() {
return AppBar(
backgroundColor: AppColors.of(context).primary,
foregroundColor: AppColors.of(context).onPrimary,
leading: IconButton(
onPressed: () {
setState(() {
selectedTransactions = [];
});
},
icon: const Icon(Icons.close),
),
title: Text(
t.transaction.list.selected_short(n: selectedTransactions.length),
),
actions: [
MonekinPopupMenuButton(actionItems: [
ListTileActionItem(
label: t.general.edit,
icon: Icons.edit_rounded,
onClick: () {
showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (context) {
return BulkEditTransactionModal(
transactionsToEdit: selectedTransactions,
onSuccess: () {
selectedTransactions = [];
setState(() {});
},
);
},
);
},
),
ListTileActionItem(
label: t.general.delete,
icon: Icons.delete_rounded,
onClick: () {
confirmDialog(
context,
dialogTitle: selectedTransactions.length <= 1
? t.transaction.delete
: t.transaction.delete_multiple,
confirmationText: t.general.confirm,
showCancelButton: true,
icon: Icons.delete_rounded,
contentParagraphs: [
Text(selectedTransactions.length <= 1
? t.transaction.delete_warning_message
: t.transaction.delete_multiple_warning_message(
x: selectedTransactions.length))
],
).then((value) {
if (value != true) {
return;
}

final futures = selectedTransactions.map(
(e) => TransactionService.instance.deleteTransaction(e.id));

Future.wait(futures).then((value) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(selectedTransactions.length <= 1
? t.transaction.delete_success
: t.transaction.delete_multiple_success(
x: selectedTransactions.length)),
));

setState(() {
selectedTransactions = [];
});
}).catchError((err) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(err.toString()),
));
});
});
},
role: ListTileActionRole.delete,
)
])
],
);
}

void toggleTransaction(MoneyTransaction tr) {
HapticFeedback.lightImpact();

if (selectedTransactions.any((element) => element.id == tr.id)) {
selectedTransactions.removeWhere((element) => element.id == tr.id);
} else {
selectedTransactions.add(tr);
}

setState(() {});
}
}
Loading

0 comments on commit ebadb20

Please sign in to comment.