diff --git a/lib/app/transactions/transactions.page.dart b/lib/app/transactions/transactions.page.dart index 4cab59ed..117f6465 100644 --- a/lib/app/transactions/transactions.page.dart +++ b/lib/app/transactions/transactions.page.dart @@ -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 { @@ -29,6 +39,8 @@ class _TransactionsPageState extends State { FocusNode searchFocusNode = FocusNode(); String? searchValue; + List selectedTransactions = []; + @override void initState() { super.initState(); @@ -75,64 +87,67 @@ class _TransactionsPageState extends State { 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( @@ -154,6 +169,9 @@ class _TransactionsPageState extends State { builder: (context, snapshot) { final res = snapshot.data; + const smallerTextStyle = + TextStyle(fontSize: 14, fontWeight: FontWeight.w300); + return Card( elevation: 2, //color: AppColors.of(context).primary, @@ -170,9 +188,47 @@ class _TransactionsPageState extends State { 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), @@ -190,6 +246,17 @@ class _TransactionsPageState extends State { 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 @@ -204,4 +271,103 @@ class _TransactionsPageState extends State { ), ); } + + 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(() {}); + } } diff --git a/lib/app/transactions/widgets/bulk_edit_transaction_modal.dart b/lib/app/transactions/widgets/bulk_edit_transaction_modal.dart new file mode 100644 index 00000000..8492a13a --- /dev/null +++ b/lib/app/transactions/widgets/bulk_edit_transaction_modal.dart @@ -0,0 +1,116 @@ +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:monekin/app/categories/categories_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/widgets/dates/outlinedButtonStacked.dart'; +import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/core/utils/date_time_picker.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +class BulkEditTransactionModal extends StatelessWidget { + const BulkEditTransactionModal({ + super.key, + required this.transactionsToEdit, + required this.onSuccess, + }); + + final List transactionsToEdit; + + final void Function() onSuccess; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return ModalContainer( + title: t.transaction.edit_multiple, + subtitle: t.transaction.list.selected_long(n: transactionsToEdit.length), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + OutlinedButtonStacked( + text: t.transaction.list.bulk_edit.dates, + onTap: () { + openDateTimePicker(context, showTimePickerAfterDate: true).then( + (date) { + if (date == null) { + return; + } + + performUpdates( + context, + futures: transactionsToEdit.map( + (e) => TransactionService.instance + .insertOrUpdateTransaction(e.copyWith(date: date)), + ), + ); + }, + ); + }, + alignLeft: true, + alignBeside: true, + fontSize: 18, + padding: const EdgeInsets.all(16), + iconData: Icons.calendar_month, + ), + const SizedBox(height: 8), + OutlinedButtonStacked( + text: t.transaction.list.bulk_edit.categories, + onTap: () { + showCategoryListModal( + context, + const CategoriesList( + mode: CategoriesListMode.modalSelectSubcategory, + ), + ).then( + (modalRes) { + if (modalRes != null && modalRes.isNotEmpty) { + performUpdates( + context, + futures: transactionsToEdit.map( + (e) => TransactionService.instance + .insertOrUpdateTransaction(e.copyWith( + categoryID: Value(modalRes.first.id))), + ), + ); + } + }, + ); + }, + alignLeft: true, + alignBeside: true, + fontSize: 18, + padding: const EdgeInsets.all(16), + iconData: Icons.category_rounded, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + void performUpdates( + BuildContext context, { + required Iterable> futures, + }) { + Navigator.pop(context); + + Future.wait(futures).then((value) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(transactionsToEdit.length <= 1 + ? t.transaction.edit_success + : t.transaction + .edit_multiple_success(x: transactionsToEdit.length)), + )); + + onSuccess(); + }).catchError((err) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(err.toString()), + )); + }); + } +} diff --git a/lib/app/transactions/widgets/transaction_list.dart b/lib/app/transactions/widgets/transaction_list.dart index a8388941..7035f224 100644 --- a/lib/app/transactions/widgets/transaction_list.dart +++ b/lib/app/transactions/widgets/transaction_list.dart @@ -26,6 +26,10 @@ class TransactionListComponent extends StatefulWidget { ), required this.onEmptyList, required this.heroTagBuilder, + this.onLongPress, + this.onTap, + this.selectedTransactions = const [], + this.onTransactionsLoaded, }); final TransactionFilters filters; @@ -48,6 +52,20 @@ class TransactionListComponent extends StatefulWidget { final Object? Function(MoneyTransaction tr)? heroTagBuilder; + /// Action to trigger when a transaction tile is long pressed. If `null`, + /// the tile will display a modal with some quick actions for + /// this transaction + final void Function(MoneyTransaction tr)? onLongPress; + + /// Action to trigger when a transaction tile is pressed. If `null`, + /// the tile will redirect to the `transaction-details-page` + final void Function(MoneyTransaction tr)? onTap; + + final void Function({List allTransactions})? + onTransactionsLoaded; + + final List selectedTransactions; + @override State createState() => _TransactionListComponentState(); @@ -129,6 +147,10 @@ class _TransactionListComponentState extends State { final transactions = snapshot.data!; + if (widget.onTransactionsLoaded != null) { + widget.onTransactionsLoaded!(allTransactions: transactions); + } + if (transactions.isEmpty) { return widget.onEmptyList; } @@ -159,6 +181,14 @@ class _TransactionListComponentState extends State { showDate: !widget.showGroupDivider, showTime: widget.showGroupDivider, heroTag: heroTag, + onTap: widget.onTap == null + ? null + : (() => widget.onTap!(transaction)), + onLongPress: widget.onLongPress == null + ? null + : (() => widget.onLongPress!(transaction)), + isSelected: widget.selectedTransactions + .any((element) => element.id == transaction.id), ); }, separatorBuilder: (context, index) { diff --git a/lib/app/transactions/widgets/transaction_list_tile.dart b/lib/app/transactions/widgets/transaction_list_tile.dart index 2859239b..42253cda 100644 --- a/lib/app/transactions/widgets/transaction_list_tile.dart +++ b/lib/app/transactions/widgets/transaction_list_tile.dart @@ -21,6 +21,9 @@ class TransactionListTile extends StatelessWidget { this.showTime = true, this.periodicityInfo, required this.heroTag, + this.onLongPress, + this.onTap, + this.isSelected = false, }); final MoneyTransaction transaction; @@ -32,6 +35,17 @@ class TransactionListTile extends StatelessWidget { final Object? heroTag; + /// Action to trigger when the tile is long pressed. If `null`, + /// the tile will display a modal with some quick actions for + /// this transaction + final void Function()? onLongPress; + + /// Action to trigger when the tile is pressed. If `null`, + /// the tile will redirect to the `transaction-details-page` + final void Function()? onTap; + + final bool isSelected; + showTransactionActions(BuildContext context, MoneyTransaction transaction) { showModalBottomSheet( context: context, @@ -209,19 +223,35 @@ class TransactionListTile extends StatelessWidget { ), leading: Hero( tag: heroTag ?? UniqueKey(), - child: transaction.getDisplayIcon(context, size: 28, padding: 6), + child: isSelected + ? Stack( + alignment: Alignment.center, + children: [ + const Icon(Icons.circle, size: 28 + 12), + Icon( + Icons.check, + size: 24, + color: AppColors.of(context).background, + ), + ], + ) + : transaction.getDisplayIcon(context, size: 28, padding: 6), ), - onTap: () { - RouteUtils.pushRoute( - context, - TransactionDetailsPage( - transaction: transaction, - heroTag: heroTag, - prevPage: prevPage, - ), - ); - }, - onLongPress: () => showTransactionActions(context, transaction), + selected: isSelected, + selectedTileColor: AppColors.of(context).primary.withOpacity(0.15), + onTap: onTap ?? + () { + RouteUtils.pushRoute( + context, + TransactionDetailsPage( + transaction: transaction, + heroTag: heroTag, + prevPage: prevPage, + ), + ); + }, + onLongPress: + onLongPress ?? () => showTransactionActions(context, transaction), ); } } diff --git a/lib/core/models/transaction/transaction.dart b/lib/core/models/transaction/transaction.dart index 7a09c033..03f21e81 100644 --- a/lib/core/models/transaction/transaction.dart +++ b/lib/core/models/transaction/transaction.dart @@ -96,6 +96,17 @@ class MoneyTransaction extends TransactionInDB { ? TransactionType.expense : TransactionType.income; + /// Get the balance (positive or negative) that this transaction cause to the user accounts + double getCurrentBalanceInPreferredCurrency() { + if (type == TransactionType.transfer) { + return (currentValueInDestinyInPreferredCurrency ?? + currentValueInPreferredCurrency) - + currentValueInPreferredCurrency; + } + + return currentValueInPreferredCurrency; + } + IconDisplayer getDisplayIcon( BuildContext context, { double size = 22, diff --git a/lib/core/presentation/widgets/monekin_popup_menu_button.dart b/lib/core/presentation/widgets/monekin_popup_menu_button.dart index 9cab4ede..b448ab92 100644 --- a/lib/core/presentation/widgets/monekin_popup_menu_button.dart +++ b/lib/core/presentation/widgets/monekin_popup_menu_button.dart @@ -14,23 +14,29 @@ class MonekinPopupMenuButton extends StatelessWidget { final actionItem = actionItems[index]; return PopupMenuItem( - value: index, - child: ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon( - actionItem.icon, - color: actionItem.role != null - ? actionItem.getColorBasedOnRole(context) - : null, - ), - minLeadingWidth: 26, - title: Text(actionItem.label, - style: TextStyle( - color: actionItem.role != null - ? actionItem.getColorBasedOnRole(context) - : null, - )), - )); + value: index, + enabled: actionItems[index].onClick != null, + child: ListTile( + contentPadding: EdgeInsets.zero, + enabled: actionItems[index].onClick != null, + mouseCursor: actionItems[index].onClick != null + ? SystemMouseCursors.click + : null, + leading: Icon( + actionItem.icon, + color: actionItem.role != null + ? actionItem.getColorBasedOnRole(context) + : null, + ), + minLeadingWidth: 26, + title: Text(actionItem.label, + style: TextStyle( + color: actionItem.role != null + ? actionItem.getColorBasedOnRole(context) + : null, + )), + ), + ); }); }, onSelected: (int value) { diff --git a/lib/i18n/strings_en.json b/lib/i18n/strings_en.json index abe79eb9..140b15a0 100644 --- a/lib/i18n/strings_en.json +++ b/lib/i18n/strings_en.json @@ -253,6 +253,8 @@ "new.success": "Transaction created successfully", "edit": "Edit transaction", "edit.success": "Transaction edited successfully", + "edit.multiple": "Edit transactions", + "edit.multiple.success": "{{ x }} transactions edited successfully", "duplicate": "Clone transaction", "duplicate.short": "Clone", "duplicate.warning-message": "A transaction identical to this will be created with the same date, do you want to continue?", @@ -260,6 +262,9 @@ "delete": "Delete transaction", "delete.warning-message": "This action is irreversible. The current balance of your accounts and all your statistics will be recalculated", "delete.success": "Transaction deleted correctly", + "delete.multiple": "Delete transactions", + "delete.multiple.warning-message": "This action is irreversible and will remove {{ x }} transactions. The current balance of your accounts and all your statistics will be recalculated", + "delete.multiple.success": "{{x}} transactions deleted correctly", "details": "Movement details", "NEXT-PAYMENTS": { "accept": "Accept", @@ -278,7 +283,16 @@ "empty": "No transactions found to display here. Add a transaction by clicking the '+' button at the bottom", "searcher.placeholder": "Search by category, description...", "searcher.no-results": "No transactions found matching the search criteria", - "loading": "Loading more transactions..." + "loading": "Loading more transactions...", + "selected.short": { "one": "{{n}} selected", "other": "{{n}} selected" }, + "selected.long": { + "one": "{{n}} transaction selected", + "other": "{{n}} transactions selected" + }, + "BULK_EDIT": { + "dates": "Edit dates", + "categories": "Edit categories" + } }, "FILTERS": { "from_value": "From amount", diff --git a/lib/i18n/strings_es.json b/lib/i18n/strings_es.json index 493c4507..25434329 100644 --- a/lib/i18n/strings_es.json +++ b/lib/i18n/strings_es.json @@ -257,6 +257,8 @@ "new.success": "Transacción creada correctamente", "edit": "Editar transacción", "edit.success": "Transacción editada correctamente", + "edit.multiple": "Editar transacciones", + "edit.multiple.success": "{{ x }} transacciones editadas correctamente", "duplicate": "Clonar transacción", "duplicate.short": "Clonar", "duplicate.warning-message": "Se creará una transacción identica a esta con su misma fecha, ¿deseas continuar?", @@ -264,6 +266,9 @@ "delete": "Eliminar transacción", "delete.warning-message": "Esta acción es irreversible. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas", "delete.success": "Transacción eliminada correctamente", + "delete.multiple": "Eliminar transacciones", + "delete.multiple.warning-message": "Esta acción es irreversible y borrará definitivamente {{ x }} transacciones. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas", + "delete.multiple.success": "{{ x }} transacciones eliminadas correctamente", "details": "Detalles del movimiento", "NEXT-PAYMENTS": { "skip": "Saltar", @@ -282,7 +287,19 @@ "empty": "No se han encontrado transacciones que mostrar aquí. Añade una transacción pulsando el botón '+' de la parte inferior", "searcher.placeholder": "Busca por categoría, descripción...", "searcher.no-results": "No se han encontrado transacciones que coincidan con los criterios de busqueda", - "loading": "Cargando más transacciones..." + "loading": "Cargando más transacciones...", + "selected.short": { + "one": "{{n}} seleccionada", + "other": "{{n}} seleccionadas" + }, + "selected.long": { + "one": "{{n}} transacción seleccionada", + "other": "{{n}} transacciones seleccionadas" + }, + "BULK_EDIT": { + "dates": "Editar fechas", + "categories": "Editar categorías" + } }, "FILTERS": { "from_value": "Desde monto", diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index 604ccf93..0c1b3418 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1035 (517 per locale) +/// Strings: 1057 (528 per locale) /// -/// Built on 2024-05-01 at 14:05 UTC +/// Built on 2024-05-02 at 14:20 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -333,6 +333,8 @@ class _TranslationsTransactionEn { String get new_success => 'Transaction created successfully'; String get edit => 'Edit transaction'; String get edit_success => 'Transaction edited successfully'; + String get edit_multiple => 'Edit transactions'; + String edit_multiple_success({required Object x}) => '${x} transactions edited successfully'; String get duplicate => 'Clone transaction'; String get duplicate_short => 'Clone'; String get duplicate_warning_message => 'A transaction identical to this will be created with the same date, do you want to continue?'; @@ -340,6 +342,9 @@ class _TranslationsTransactionEn { String get delete => 'Delete transaction'; String get delete_warning_message => 'This action is irreversible. The current balance of your accounts and all your statistics will be recalculated'; String get delete_success => 'Transaction deleted correctly'; + String get delete_multiple => 'Delete transactions'; + String delete_multiple_warning_message({required Object x}) => 'This action is irreversible and will remove ${x} transactions. The current balance of your accounts and all your statistics will be recalculated'; + String delete_multiple_success({required Object x}) => '${x} transactions deleted correctly'; String get details => 'Movement details'; late final _TranslationsTransactionNextPaymentsEn next_payments = _TranslationsTransactionNextPaymentsEn._(_root); late final _TranslationsTransactionListEn list = _TranslationsTransactionListEn._(_root); @@ -780,6 +785,15 @@ class _TranslationsTransactionListEn { String get searcher_placeholder => 'Search by category, description...'; String get searcher_no_results => 'No transactions found matching the search criteria'; String get loading => 'Loading more transactions...'; + String selected_short({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: '${n} selected', + other: '${n} selected', + ); + String selected_long({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: '${n} transaction selected', + other: '${n} transactions selected', + ); + late final _TranslationsTransactionListBulkEditEn bulk_edit = _TranslationsTransactionListBulkEditEn._(_root); } // Path: transaction.filters @@ -1269,6 +1283,17 @@ class _TranslationsFinancialHealthSavingsPercentageTextEn { String get very_bad => 'Wow, you haven\'t managed to save anything during this period.'; } +// Path: transaction.list.bulk_edit +class _TranslationsTransactionListBulkEditEn { + _TranslationsTransactionListBulkEditEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get dates => 'Edit dates'; + String get categories => 'Edit categories'; +} + // Path: transaction.form.validators class _TranslationsTransactionFormValidatorsEn { _TranslationsTransactionFormValidatorsEn._(this._root); @@ -1568,6 +1593,8 @@ class _TranslationsTransactionEs implements _TranslationsTransactionEn { @override String get new_success => 'Transacción creada correctamente'; @override String get edit => 'Editar transacción'; @override String get edit_success => 'Transacción editada correctamente'; + @override String get edit_multiple => 'Editar transacciones'; + @override String edit_multiple_success({required Object x}) => '${x} transacciones editadas correctamente'; @override String get duplicate => 'Clonar transacción'; @override String get duplicate_short => 'Clonar'; @override String get duplicate_warning_message => 'Se creará una transacción identica a esta con su misma fecha, ¿deseas continuar?'; @@ -1575,6 +1602,9 @@ class _TranslationsTransactionEs implements _TranslationsTransactionEn { @override String get delete => 'Eliminar transacción'; @override String get delete_warning_message => 'Esta acción es irreversible. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas'; @override String get delete_success => 'Transacción eliminada correctamente'; + @override String get delete_multiple => 'Eliminar transacciones'; + @override String delete_multiple_warning_message({required Object x}) => 'Esta acción es irreversible y borrará definitivamente ${x} transacciones. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas'; + @override String delete_multiple_success({required Object x}) => '${x} transacciones eliminadas correctamente'; @override String get details => 'Detalles del movimiento'; @override late final _TranslationsTransactionNextPaymentsEs next_payments = _TranslationsTransactionNextPaymentsEs._(_root); @override late final _TranslationsTransactionListEs list = _TranslationsTransactionListEs._(_root); @@ -2015,6 +2045,15 @@ class _TranslationsTransactionListEs implements _TranslationsTransactionListEn { @override String get searcher_placeholder => 'Busca por categoría, descripción...'; @override String get searcher_no_results => 'No se han encontrado transacciones que coincidan con los criterios de busqueda'; @override String get loading => 'Cargando más transacciones...'; + @override String selected_short({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, + one: '${n} seleccionada', + other: '${n} seleccionadas', + ); + @override String selected_long({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, + one: '${n} transacción seleccionada', + other: '${n} transacciones seleccionadas', + ); + @override late final _TranslationsTransactionListBulkEditEs bulk_edit = _TranslationsTransactionListBulkEditEs._(_root); } // Path: transaction.filters @@ -2505,6 +2544,17 @@ class _TranslationsFinancialHealthSavingsPercentageTextEs implements _Translatio @override String get very_bad => 'Vaya, no has conseguido ahorrar nada durante este periodo.'; } +// Path: transaction.list.bulk_edit +class _TranslationsTransactionListBulkEditEs implements _TranslationsTransactionListBulkEditEn { + _TranslationsTransactionListBulkEditEs._(this._root); + + @override final _TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get dates => 'Editar fechas'; + @override String get categories => 'Editar categorías'; +} + // Path: transaction.form.validators class _TranslationsTransactionFormValidatorsEs implements _TranslationsTransactionFormValidatorsEn { _TranslationsTransactionFormValidatorsEs._(this._root); @@ -2858,6 +2908,8 @@ extension on Translations { case 'transaction.new_success': return 'Transaction created successfully'; case 'transaction.edit': return 'Edit transaction'; case 'transaction.edit_success': return 'Transaction edited successfully'; + case 'transaction.edit_multiple': return 'Edit transactions'; + case 'transaction.edit_multiple_success': return ({required Object x}) => '${x} transactions edited successfully'; case 'transaction.duplicate': return 'Clone transaction'; case 'transaction.duplicate_short': return 'Clone'; case 'transaction.duplicate_warning_message': return 'A transaction identical to this will be created with the same date, do you want to continue?'; @@ -2865,6 +2917,9 @@ extension on Translations { case 'transaction.delete': return 'Delete transaction'; case 'transaction.delete_warning_message': return 'This action is irreversible. The current balance of your accounts and all your statistics will be recalculated'; case 'transaction.delete_success': return 'Transaction deleted correctly'; + case 'transaction.delete_multiple': return 'Delete transactions'; + case 'transaction.delete_multiple_warning_message': return ({required Object x}) => 'This action is irreversible and will remove ${x} transactions. The current balance of your accounts and all your statistics will be recalculated'; + case 'transaction.delete_multiple_success': return ({required Object x}) => '${x} transactions deleted correctly'; case 'transaction.details': return 'Movement details'; case 'transaction.next_payments.accept': return 'Accept'; case 'transaction.next_payments.skip': return 'Skip'; @@ -2881,6 +2936,16 @@ extension on Translations { case 'transaction.list.searcher_placeholder': return 'Search by category, description...'; case 'transaction.list.searcher_no_results': return 'No transactions found matching the search criteria'; case 'transaction.list.loading': return 'Loading more transactions...'; + case 'transaction.list.selected_short': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: '${n} selected', + other: '${n} selected', + ); + case 'transaction.list.selected_long': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: '${n} transaction selected', + other: '${n} transactions selected', + ); + case 'transaction.list.bulk_edit.dates': return 'Edit dates'; + case 'transaction.list.bulk_edit.categories': return 'Edit categories'; case 'transaction.filters.from_value': return 'From amount'; case 'transaction.filters.to_value': return 'Up to amount'; case 'transaction.filters.from_value_def': return ({required Object x}) => 'From ${x}'; @@ -3452,6 +3517,8 @@ extension on _TranslationsEs { case 'transaction.new_success': return 'Transacción creada correctamente'; case 'transaction.edit': return 'Editar transacción'; case 'transaction.edit_success': return 'Transacción editada correctamente'; + case 'transaction.edit_multiple': return 'Editar transacciones'; + case 'transaction.edit_multiple_success': return ({required Object x}) => '${x} transacciones editadas correctamente'; case 'transaction.duplicate': return 'Clonar transacción'; case 'transaction.duplicate_short': return 'Clonar'; case 'transaction.duplicate_warning_message': return 'Se creará una transacción identica a esta con su misma fecha, ¿deseas continuar?'; @@ -3459,6 +3526,9 @@ extension on _TranslationsEs { case 'transaction.delete': return 'Eliminar transacción'; case 'transaction.delete_warning_message': return 'Esta acción es irreversible. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas'; case 'transaction.delete_success': return 'Transacción eliminada correctamente'; + case 'transaction.delete_multiple': return 'Eliminar transacciones'; + case 'transaction.delete_multiple_warning_message': return ({required Object x}) => 'Esta acción es irreversible y borrará definitivamente ${x} transacciones. El balance actual de tus cuentas y todas tus estadisticas serán recalculadas'; + case 'transaction.delete_multiple_success': return ({required Object x}) => '${x} transacciones eliminadas correctamente'; case 'transaction.details': return 'Detalles del movimiento'; case 'transaction.next_payments.skip': return 'Saltar'; case 'transaction.next_payments.skip_success': return 'Transacción saltada con exito'; @@ -3475,6 +3545,16 @@ extension on _TranslationsEs { case 'transaction.list.searcher_placeholder': return 'Busca por categoría, descripción...'; case 'transaction.list.searcher_no_results': return 'No se han encontrado transacciones que coincidan con los criterios de busqueda'; case 'transaction.list.loading': return 'Cargando más transacciones...'; + case 'transaction.list.selected_short': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, + one: '${n} seleccionada', + other: '${n} seleccionadas', + ); + case 'transaction.list.selected_long': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('es'))(n, + one: '${n} transacción seleccionada', + other: '${n} transacciones seleccionadas', + ); + case 'transaction.list.bulk_edit.dates': return 'Editar fechas'; + case 'transaction.list.bulk_edit.categories': return 'Editar categorías'; case 'transaction.filters.from_value': return 'Desde monto'; case 'transaction.filters.to_value': return 'Hasta monto'; case 'transaction.filters.from_value_def': return ({required Object x}) => 'Desde ${x}';