From 61cd310fb01b2372c9d833a0e5bb67d21ed1054b Mon Sep 17 00:00:00 2001 From: MK13 Date: Fri, 5 Mar 2021 20:15:05 +0100 Subject: [PATCH] #18 Period overview: improvements --- .../src/main/antlr4/de/financer/fql/FQL.g4 | 56 ++++++++-------- .../java/de/financer/fql/FieldMapping.java | 45 ++++++++----- .../fql/field_handler/AccountTypeHandler.java | 32 +++++++++ .../fql/field_handler/PeriodIdHandler.java | 24 +++++++ .../financer/controller/PeriodController.java | 67 +++++++++++++++---- .../main/resources/i18n/message.properties | 8 ++- .../resources/i18n/message_de_DE.properties | 8 ++- .../src/main/resources/static/changelog.txt | 2 + .../src/main/resources/static/css/main.css | 8 +++ .../templates/period/periodOverview.html | 62 ++++++++++++----- .../transaction/searchTransactions.html | 5 +- 11 files changed, 243 insertions(+), 74 deletions(-) create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/PeriodIdHandler.java diff --git a/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 index dbaeb44..4af34fb 100644 --- a/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 +++ b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 @@ -23,7 +23,7 @@ orderByExpression regularExpression : (stringExpression | intExpression | booleanExpression | dateExpression) ; stringExpression - : field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ; + : field=IDENTIFIER operator=STRING_OPERATOR value=(STRING_VALUE | ACCOUNT_TYPE_VALUE) ; intExpression : field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ; @@ -96,34 +96,34 @@ fragment K : ('K' | 'k') ; fragment SPACE : ' ' ; // Keywords -BETWEEN : B E T W E E N ; -AND : A N D ; -OR : O R ; -ORDER_BY : O R D E R SPACE B Y ; -DESC : D E S C ; -ASC : A S C ; -TRUE : T R U E ; -FALSE : F A L S E ; -LIKE : L I K E ; -IN : I N ; -COMMA : ',' ; +BETWEEN : B E T W E E N ; +AND : A N D ; +OR : O R ; +ORDER_BY : O R D E R SPACE B Y ; +DESC : D E S C ; +ASC : A S C ; +TRUE : T R U E ; +FALSE : F A L S E ; +LIKE : L I K E ; +IN : I N ; +COMMA : ',' ; // Constant values -CURRENT : C U R R E N T ; -LAST : L A S T ; -YEAR : Y E A R ; -GRAND_TOTAL : G R A N D '_' T O T A L ; -CURRENT_YEAR : CURRENT '_' YEAR ; -LAST_YEAR : LAST '_' YEAR ; +CURRENT : C U R R E N T ; +LAST : L A S T ; +YEAR : Y E A R ; +GRAND_TOTAL : G R A N D '_' T O T A L ; +CURRENT_YEAR : CURRENT '_' YEAR ; +LAST_YEAR : LAST '_' YEAR ; -STRING_OPERATOR : '=' ; -INT_OPERATOR : '>' | '<' | '>=' | '<=' | '!=' ; -L_PAREN : '(' ; -R_PAREN : ')' ; -IDENTIFIER : [a-zA-Z]+ ; -INT_VALUE : [0-9]+ ; -STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$ ]+ '\'' ; -DATE_VALUE : [0-9][0-9][0-9][0-9][-][0-9][0-9][-][0-9][0-9] ; // ANTLR does not support regex quantifiers +STRING_OPERATOR : '=' ; +INT_OPERATOR : '>' | '<' | '>=' | '<=' | '!=' ; +L_PAREN : '(' ; +R_PAREN : ')' ; +IDENTIFIER : [a-zA-Z]+ ; +INT_VALUE : [0-9]+ ; +STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$ ]+ '\'' ; +DATE_VALUE : [0-9][0-9][0-9][0-9][-][0-9][0-9][-][0-9][0-9] ; // ANTLR does not support regex quantifiers -NEWLINE : ('\r'? '\n' | '\r')+ ; -WHITESPACE : ' ' -> skip; \ No newline at end of file +NEWLINE : ('\r'? '\n' | '\r')+ ; +WHITESPACE : ' ' -> skip; \ No newline at end of file diff --git a/financer-server/src/main/java/de/financer/fql/FieldMapping.java b/financer-server/src/main/java/de/financer/fql/FieldMapping.java index f80a76f..995253e 100644 --- a/financer-server/src/main/java/de/financer/fql/FieldMapping.java +++ b/financer-server/src/main/java/de/financer/fql/FieldMapping.java @@ -7,24 +7,38 @@ import de.financer.model.*; import java.util.Arrays; public enum FieldMapping { - AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class, JoinKey - .of(Transaction.class), null, IntHandler.class), + AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class, + JoinKey.of(Transaction.class), null, IntHandler.class), - PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class, JoinKey.of(Period.class), null, PeriodConstHandler.class), + PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class, + JoinKey.of(Period.class), null, PeriodConstHandler.class), - FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class, JoinKey - .of(Account.class, "FROM"), null, StringHandler.class), + /** + * ONLY FOR INTERNAL USAGE - NOT EXPOSED TO USER + */ + PERIOD_ID("periodId", Period_.ID, Period.class, PeriodJoinHandler.class, + JoinKey.of(Period.class), null, PeriodIdHandler.class), - TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class, JoinKey - .of(Account.class, "TO"), null, StringHandler.class), + FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class, + JoinKey.of(Account.class, "FROM"), null, StringHandler.class), - FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class, - AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class), + TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class, + JoinKey.of(Account.class, "TO"), null, StringHandler.class), - TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class, - AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class), + FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class, AccountGroupJoinHandler.class, + JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class), - DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, DateHandler.class), + TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class, AccountGroupJoinHandler.class, + JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class), + + FROM_ACCOUNT_TYPE("fromAccountType", Account_.TYPE, Account.class, FromAccountJoinHandler.class, + JoinKey.of(Account.class, "FROM"), null, AccountTypeHandler.class), + + TO_ACCOUNT_TYPE("toAccountType", Account_.TYPE, Account.class, ToAccountJoinHandler.class, + JoinKey.of(Account.class, "TO"), null, AccountTypeHandler.class), + + DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class, + JoinKey.of(Transaction.class), null, DateHandler.class), RECURRING("recurring", Transaction_.RECURRING_TRANSACTION, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class), @@ -32,10 +46,11 @@ public enum FieldMapping { TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, BooleanHandler.class), - HAS_FILE("hasFile", File_.ID, File.class, FileJoinHandler.class, JoinKey.of(File.class), null, NotNullSyntheticHandler.class), + HAS_FILE("hasFile", File_.ID, File.class, FileJoinHandler.class, + JoinKey.of(File.class), null, NotNullSyntheticHandler.class), - DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), - null, StringHandler.class); + DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class, + JoinKey.of(Transaction.class), null, StringHandler.class); private final String fieldName; diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java new file mode 100644 index 0000000..696ef95 --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java @@ -0,0 +1,32 @@ +package de.financer.fql.field_handler; + +import de.financer.fql.FQLException; +import de.financer.fql.FieldMapping; +import de.financer.fql.join_handler.JoinKey; +import de.financer.model.AccountType; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Predicate; +import java.util.Map; + +public class AccountTypeHandler implements FieldHandler { + @Override + public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) { + return criteriaBuilder + .equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toAccountType(FieldHandlerUtils.removeQuotes(value))); + } + + private static AccountType toAccountType(String value) { + if (value == null) { + throw new FQLException("NULL cannot be solved to AccountType!"); + } + + try { + return AccountType.valueOf(value.toUpperCase()); + } + catch (IllegalArgumentException e) { + throw new FQLException(e); + } + } +} diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/PeriodIdHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/PeriodIdHandler.java new file mode 100644 index 0000000..179533e --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/field_handler/PeriodIdHandler.java @@ -0,0 +1,24 @@ +package de.financer.fql.field_handler; + +import de.financer.fql.FQLException; +import de.financer.fql.FieldMapping; +import de.financer.fql.join_handler.JoinKey; +import org.apache.commons.lang3.math.NumberUtils; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Predicate; +import java.util.Map; + +public class PeriodIdHandler implements FieldHandler { + @Override + public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) { + final String unquotedValue = FieldHandlerUtils.removeQuotes(value); + + if (!NumberUtils.isCreatable(unquotedValue)) { + throw new FQLException(String.format("Invalid value %s for field periodId - only numbers allowed!", unquotedValue)); + } + + return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), NumberUtils.toLong(unquotedValue)); + } +} diff --git a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java index e004c52..2242a57 100644 --- a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java +++ b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java @@ -58,31 +58,72 @@ public class PeriodController { return "period/periodOverview"; } - @GetMapping("/showTransactions") - public String showTransactions(Model model, Long periodId) { + private String showInternal(Model model, Long periodId, String fql, boolean showSum) { + final SearchTransactionsForm form = new SearchTransactionsForm(); + + form.setFql(fql); + try { - final Iterable response = - new SearchTransactionsTemplate() - .exchangeGet(financerConfig, null, null, periodId, null, - Order.TRANSACTIONS_BY_DATE_DESC, null, false); + final UriComponentsBuilder transactionBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_SEARCH_BY_FQL)); - final List transactions = IterableUtils.toList(response); + transactionBuilder.queryParam("fql", form.getFql()); + final Iterable trxs = FinancerRestTemplate.exchangeGet(transactionBuilder, + new ParameterizedTypeReference>() { + }); - model.addAttribute("transactions", transactions); - model.addAttribute("transactionCount", IterableUtils.size(transactions)); - } - catch(FinancerRestException e) { + model.addAttribute("transactions", trxs); + model.addAttribute("transactionCount", IterableUtils.size(trxs)); + + if (showSum) { + Long trxSum = IterableUtils.toList(trxs).stream().mapToLong(t -> t.getAmount()).sum(); + + model.addAttribute("transactionSum", trxSum); + } + } catch (FinancerRestException e) { model.addAttribute("errorMessage", e.getResponseReason().name()); model.addAttribute("transactionCount", 0); } - model.addAttribute("form", new SearchTransactionsForm()); - model.addAttribute("showSum", false); + model.addAttribute("form", form); + model.addAttribute("showSum", showSum); + model.addAttribute("returnTo", "/periodOverview"); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); ControllerUtils.addDarkMode(model, this.financerConfig); return "transaction/searchTransactions"; } + + @GetMapping("/showAllTransactions") + public String showAllTransactions(Model model, Long periodId) { + final String fql = String.format("periodId = '%s' ORDER BY date DESC", periodId); + + return showInternal(model, periodId, fql, false); + } + + @GetMapping("/showIncomeTransactions") + public String showIncomeTransactions(Model model, Long periodId) { + final String fql = String + .format("periodId = '%s' AND (fromAccountType = 'INCOME' OR (fromAccountType = 'LIABILITY' AND (toAccountType = 'BANK' OR toAccountType = 'CASH')) OR (fromAccountType = 'START' AND (toAccountType = 'BANK' OR toAccountType = 'CASH'))) ORDER BY date DESC", periodId); + + return showInternal(model, periodId, fql, true); + } + + @GetMapping("/showExpenseTransactions") + public String showExpenseTransactions(Model model, Long periodId) { + final String fql = String + .format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'EXPENSE' ORDER BY date DESC", periodId); + + return showInternal(model, periodId, fql, true); + } + + @GetMapping("/showLiabilityTransactions") + public String showLiabilityTransactions(Model model, Long periodId) { + final String fql = String + .format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'LIABILITY' ORDER BY date DESC", periodId); + + return showInternal(model, periodId, fql, true); + } } diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties index e220c96..3e7167a 100644 --- a/financer-web-client/src/main/resources/i18n/message.properties +++ b/financer-web-client/src/main/resources/i18n/message.properties @@ -138,6 +138,8 @@ financer.search-transactions.show-query-options.fromAccount=fromAccount\: the ke financer.search-transactions.show-query-options.toAccount=toAccount\: the key of the to account financer.search-transactions.show-query-options.fromAccountGroup=fromAccountGroup\: the name of the account group of the from account financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\: the name of the account group of the to account +financer.search-transactions.show-query-options.fromAccountType=fromAccountType\: the type of the from account +financer.search-transactions.show-query-options.toAccountType=toAccountType\: the type of the to account financer.search-transactions.show-query-options.date=date\: the date of the transaction in the format yyyy-mm-dd financer.search-transactions.show-query-options.recurring=recurring\: whether the transaction has been created from a recurring transaction financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: whether the transaction is relevant for tax declaration @@ -185,7 +187,11 @@ financer.period-overview.table-header.total=Total financer.period-overview.table-header.assets=Assets financer.period-overview.table-header.transactions=Transaction count financer.period-overview.table-header.actions=Actions -financer.period-overview.table.actions.showTransactions=Show transactions +financer.period-overview.table.actions.showAllTransactions=All transactions +financer.period-overview.table.actions.showIncomeTransactions=Income transactions +financer.period-overview.table.actions.showExpenseTransactions=Expense transactions +financer.period-overview.table.actions.showLiabilityTransactions=Liability transactions +financer.period-overview.show-actions=Show... financer.interval-type.DAILY=Daily financer.interval-type.WEEKLY=Weekly diff --git a/financer-web-client/src/main/resources/i18n/message_de_DE.properties b/financer-web-client/src/main/resources/i18n/message_de_DE.properties index 573b99d..759a376 100644 --- a/financer-web-client/src/main/resources/i18n/message_de_DE.properties +++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties @@ -138,6 +138,8 @@ financer.search-transactions.show-query-options.fromAccount=fromAccount\: der Sc financer.search-transactions.show-query-options.toAccount=toAccount\: der Schl\u00FCssel des An Kontos financer.search-transactions.show-query-options.fromAccountGroup=fromAccountGroup\: der Name der Kontogruppe des Von Kontos financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\: der Name der Kontogruppe des An Kontos +financer.search-transactions.show-query-options.fromAccountType=fromAccountType\: der Typ des Von Kontos +financer.search-transactions.show-query-options.toAccountType=toAccountType\: der Typ des An Kontos financer.search-transactions.show-query-options.date=date\: das Datum der Buchung im Format jjjj-mm-tt financer.search-transactions.show-query-options.recurring=recurring\: ob die Buchung durch eine wiederkehrende Buchung erzeugt wurde financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: ob die Buchung als steuerrelevant markiert wurde @@ -185,7 +187,11 @@ financer.period-overview.table-header.total=Insgesamt financer.period-overview.table-header.assets=Umlaufverm\u00F6gen financer.period-overview.table-header.transactions=Anzahl Buchungen financer.period-overview.table-header.actions=Aktionen -financer.period-overview.table.actions.showTransactions=Zeige Buchungen +financer.period-overview.table.actions.showAllTransactions=Alle Buchungen +financer.period-overview.table.actions.showIncomeTransactions=Einkommenbuchungen +financer.period-overview.table.actions.showExpenseTransactions=Ausgabebuchungen +financer.period-overview.table.actions.showLiabilityTransactions=Verbindlichkeitenbuchungen +financer.period-overview.show-actions=Anzeigen... financer.interval-type.DAILY=T\u00E4glich financer.interval-type.WEEKLY=W\u00F6chentlich diff --git a/financer-web-client/src/main/resources/static/changelog.txt b/financer-web-client/src/main/resources/static/changelog.txt index df067cf..227de70 100644 --- a/financer-web-client/src/main/resources/static/changelog.txt +++ b/financer-web-client/src/main/resources/static/changelog.txt @@ -1,5 +1,7 @@ v45 -> v46: - #17 Add actions to the period overview +- #18 Improve actions in the period overview +- Introduce new FQL fields toAccountType and fromAccountType v44 -> v45: - #5 Having no periods breaks the "expenses per period" graph on the account overview page diff --git a/financer-web-client/src/main/resources/static/css/main.css b/financer-web-client/src/main/resources/static/css/main.css index 15653fb..885cff3 100644 --- a/financer-web-client/src/main/resources/static/css/main.css +++ b/financer-web-client/src/main/resources/static/css/main.css @@ -46,6 +46,14 @@ color: var(--neutral-color); } +.expense-period-period-overview { + padding-left: 1em; +} + +.expense-year-period-period-overview { + font-weight: bolder; +} + #period-overview-asset-container { display: inline; } diff --git a/financer-web-client/src/main/resources/templates/period/periodOverview.html b/financer-web-client/src/main/resources/templates/period/periodOverview.html index 00540a5..6c60885 100644 --- a/financer-web-client/src/main/resources/templates/period/periodOverview.html +++ b/financer-web-client/src/main/resources/templates/period/periodOverview.html @@ -12,7 +12,7 @@

- + - +
@@ -28,30 +28,62 @@
- - - - - - + + +
+ +
+
+ + + + + th:classappend="${(periodOverview.total > periodOverview.incomeSum ? 'overspend' : '') + + (periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR ? ' expense-year-period-period-overview' : '')}"/>
- +
- + -
- -
+
+ + +
diff --git a/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html b/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html index 6070b6a..3d5a0e7 100644 --- a/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html +++ b/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html @@ -12,7 +12,8 @@

- + +