From d0dd8a8c32e34ae84df089aef396a894c636734a Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Sun, 8 Jan 2023 16:52:58 +0100 Subject: [PATCH] #724 - match columns from csv with BudgetMaster attributes (date, title and amount) + show status for each row --- .../TransactionImportController.java | 40 +++++- .../csvImport/CsvColumnSettings.java | 5 + .../csvImport/CsvTransaction.java | 70 +++++++++++ .../csvImport/CsvTransactionStatus.java | 8 ++ .../resources/languages/base_de.properties | 6 +- .../resources/languages/base_en.properties | 6 +- .../static/css/transactionImport.css | 11 ++ .../transactions/transactionImport.ftl | 116 +++++++++++++++++- 8 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvColumnSettings.java create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransaction.java create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransactionStatus.java diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java index 914ea33e6..c5dc437c4 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java @@ -2,9 +2,7 @@ package de.deadlocker8.budgetmaster.transactions; import de.deadlocker8.budgetmaster.controller.BaseController; import de.deadlocker8.budgetmaster.services.HelpersService; -import de.deadlocker8.budgetmaster.transactions.csvImport.CsvImport; -import de.deadlocker8.budgetmaster.transactions.csvImport.CsvParser; -import de.deadlocker8.budgetmaster.transactions.csvImport.CsvRow; +import de.deadlocker8.budgetmaster.transactions.csvImport.*; import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -19,7 +17,9 @@ import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.WebRequest; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; +import java.util.Random; @Controller @RequestMapping(Mappings.TRANSACTION_IMPORT) @@ -41,6 +41,7 @@ public class TransactionImportController extends BaseController { public static final String CSV_IMPORT = "csvImport"; public static final String CSV_ROWS = "csvRows"; + public static final String CSV_TRANSACTIONS = "csvTransactions"; public static final String ERROR_UPLOAD = "errorUpload"; public static final String ERROR_UPLOAD_FILE = "errorUploadFile"; } @@ -119,6 +120,38 @@ public class TransactionImportController extends BaseController return ReturnValues.REDIRECT_IMPORT; } + @PostMapping("/columnSettings") + public String columnSettings(WebRequest request, + @ModelAttribute("CsvColumnSettings") CsvColumnSettings csvColumnSettings, + BindingResult bindingResult) + { + if(bindingResult.hasErrors()) + { + request.setAttribute(RequestAttributeNames.ERROR_UPLOAD, bindingResult, RequestAttributes.SCOPE_SESSION); + return ReturnValues.REDIRECT_IMPORT; + } + + final Object attribute = request.getAttribute(RequestAttributeNames.CSV_ROWS, RequestAttributes.SCOPE_SESSION); + if(attribute == null) + { + return ReturnValues.REDIRECT_CANCEL; + } + + final List<CsvRow> csvRows = (List<CsvRow>) attribute; + final List<CsvTransaction> csvTransactions = new ArrayList<>(); + for(CsvRow csvRow : csvRows) + { + final String date = csvRow.getColumns().get(csvColumnSettings.columnDate() - 1); + final String name = csvRow.getColumns().get(csvColumnSettings.columnName() - 1); + final String amount = csvRow.getColumns().get(csvColumnSettings.columnAmount() - 1); + csvTransactions.add(new CsvTransaction(date, name, amount, CsvTransactionStatus.PENDING)); + } + + request.setAttribute(RequestAttributeNames.CSV_TRANSACTIONS, csvTransactions, RequestAttributes.SCOPE_SESSION); + + return ReturnValues.REDIRECT_IMPORT; + } + @GetMapping("/cancel") public String cancel(WebRequest request) { @@ -130,6 +163,7 @@ public class TransactionImportController extends BaseController { request.removeAttribute(RequestAttributeNames.CSV_IMPORT, RequestAttributes.SCOPE_SESSION); request.removeAttribute(RequestAttributeNames.CSV_ROWS, RequestAttributes.SCOPE_SESSION); + request.removeAttribute(RequestAttributeNames.CSV_TRANSACTIONS, RequestAttributes.SCOPE_SESSION); request.removeAttribute(RequestAttributeNames.ERROR_UPLOAD, RequestAttributes.SCOPE_SESSION); request.removeAttribute(RequestAttributeNames.ERROR_UPLOAD_FILE, RequestAttributes.SCOPE_SESSION); } diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvColumnSettings.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvColumnSettings.java new file mode 100644 index 000000000..341b50ff2 --- /dev/null +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvColumnSettings.java @@ -0,0 +1,5 @@ +package de.deadlocker8.budgetmaster.transactions.csvImport; + +public record CsvColumnSettings(int columnDate, int columnName, int columnAmount) +{ +} diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransaction.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransaction.java new file mode 100644 index 000000000..0b083d3e6 --- /dev/null +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransaction.java @@ -0,0 +1,70 @@ +package de.deadlocker8.budgetmaster.transactions.csvImport; + +import java.util.Objects; + +public final class CsvTransaction +{ + private final String date; + private final String name; + private final String amount; + private CsvTransactionStatus status; + + public CsvTransaction(String date, String name, String amount, CsvTransactionStatus status) + { + this.date = date; + this.name = name; + this.amount = amount; + this.status = status; + } + + public String getDate() + { + return date; + } + + public String getName() + { + return name; + } + + public String getAmount() + { + return amount; + } + + public CsvTransactionStatus getStatus() + { + return status; + } + + public void setStatus(CsvTransactionStatus status) + { + this.status = status; + } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + CsvTransaction that = (CsvTransaction) o; + return Objects.equals(date, that.date) && Objects.equals(name, that.name) && Objects.equals(amount, that.amount) && status == that.status; + } + + @Override + public int hashCode() + { + return Objects.hash(date, name, amount, status); + } + + @Override + public String toString() + { + return "CsvTransaction{" + + "date='" + date + '\'' + + ", name='" + name + '\'' + + ", amount='" + amount + '\'' + + ", status=" + status + + '}'; + } +} diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransactionStatus.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransactionStatus.java new file mode 100644 index 000000000..65974a741 --- /dev/null +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvTransactionStatus.java @@ -0,0 +1,8 @@ +package de.deadlocker8.budgetmaster.transactions.csvImport; + +public enum CsvTransactionStatus +{ + PENDING, + IMPORTED, + SKIPPED; +} diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties index ccaf34e97..899eafbe5 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_de.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties @@ -370,11 +370,15 @@ transaction.new.label.repeating=Wiederholung transaction.new.label.repeating.all=Alle transactions.recurring.headline=Aktive wiederholende Buchungen transactions.recurring.placeholder=Keine aktiven wiederholenden Buchungen -transactions.import.overview=Übersicht +transactions.import.matchColumns=Spalten zuordnen transactions.import.column=Spalte transactions.import.separator=Trennzeichen transactions.import.encoding=Kodierung transactions.import.numberOfLinesToSkip=Zeilen überspringen +transactions.import.status=Status +transactions.import.status.pending=ausstehend +transactions.import.status.imported=importiert +transactions.import.status.skipped=übersprungen repeating.button.add=Wiederholung hinzufügen repeating.button.remove=Wiederholung entfernen diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties index 7d1ec8045..dcc9acccb 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_en.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties @@ -369,11 +369,15 @@ transaction.new.label.repeating=Repeating transaction.new.label.repeating.all=Every transactions.recurring.headline=Active Recurring Transactions transactions.recurring.placeholder=No active recurring transactions -transactions.import.overview=Overview +transactions.import.matchColumns=Match columns transactions.import.column=Column transactions.import.separator=Separator transactions.import.encoding=Encoding transactions.import.numberOfLinesToSkip=Skip lines +transactions.import.status=Status +transactions.import.status.pending=pending +transactions.import.status.imported=imported +transactions.import.status.skipped=skipped repeating.button.add=Add repetition repeating.button.remove=Remove repetition diff --git a/BudgetMasterServer/src/main/resources/static/css/transactionImport.css b/BudgetMasterServer/src/main/resources/static/css/transactionImport.css index f7fe2480c..58da3cc99 100644 --- a/BudgetMasterServer/src/main/resources/static/css/transactionImport.css +++ b/BudgetMasterServer/src/main/resources/static/css/transactionImport.css @@ -1,3 +1,14 @@ #transaction-import-overview { overflow: auto; +} + +.transaction-import-text-with-icon { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 1rem; +} + +.transaction-import-text-with-icon i { + margin-right: 1.3rem; } \ No newline at end of file diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl index e96ccd019..bae4fa80b 100644 --- a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl +++ b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl @@ -23,7 +23,7 @@ <@header.content> <div class="container"> - <#if !error?? && csvImport.getFileName()??> + <#if csvRows??> <div class="row center-align"> <div class="col s12 m12 l8 offset-l2 headline-small text-green truncate"> <i class="fas fa-file-csv"></i> ${csvImport.getFileName()} @@ -40,7 +40,15 @@ </#if> </div> - <#if csvRows??> + <#if csvTransactions??> + <@renderCsvTransactions/> + <#elseif csvRows?? > + <div class="container"> + <div class="section center-align"> + <div class="headline-small">${locale.getString("transactions.import.matchColumns")}</div> + </div> + </div> + <@columnSettings/> <@renderCsvRows/> </#if> </@header.content> @@ -92,13 +100,68 @@ </form> </#macro> -<#macro renderCsvRows> +<#macro columnSettings> <div class="container"> - <div class="section center-align"> - <div class="headline-small">${locale.getString("transactions.import.overview")}</div> - </div> + <form id="form-csv-column-settings" name="CsvColumnSettings" method="POST" action="<@s.url '/transactionImport/columnSettings'/>"> + <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> + <div class="row"> + <div class="col s6 m4 offset-m2 l3 offset-l3 bold"> + BudgetMaster + </div> + <div class="col s6 m4 l3 bold"> + CSV + </div> + </div> + + <div class="row"> + <div class="col s6 m4 offset-m2 l3 offset-l3"> + <div class="transaction-import-text-with-icon"> + <i class="material-icons">event</i> + ${locale.getString("transaction.new.label.date")} + </div> + </div> + <div class="input-field col s6 m4 l3 no-margin-top no-margin-bottom"> + <input id="columnDate" type="number" min="1" max="${csvRows?size}" name="columnDate" <@validation.validation "columnDate"/> value="<#if csvColumnSettings??>${csvColumnSettings.columnDate()}</#if>"> + <label class="input-label" for="columnDate">${locale.getString("transactions.import.column")}</label> + </div> + </div> + <div class="row"> + <div class="col s6 m4 offset-m2 l3 offset-l3"> + <div class="transaction-import-text-with-icon"> + <i class="material-icons">edit</i> + ${locale.getString("transaction.new.label.name")} + </div> + </div> + <div class="input-field col s6 m4 l3 no-margin-top no-margin-bottom"> + <input id="columnName" type="number" min="1" max="${csvRows?size}" name="columnName" <@validation.validation "columnName"/> value="<#if csvColumnSettings??>${csvColumnSettings.columnName()}</#if>"> + <label class="input-label" for="columnName">${locale.getString("transactions.import.column")}</label> + </div> + </div> + <div class="row"> + <div class="col s6 m4 offset-m2 l3 offset-l3"> + <div class="transaction-import-text-with-icon"> + <i class="material-icons">euro</i> + ${locale.getString("transaction.new.label.amount")} + </div> + </div> + <div class="input-field col s6 m4 l3 no-margin-top no-margin-bottom"> + <input id="columnAmount" type="number" min="1" max="${csvRows?size}" name="columnAmount" <@validation.validation "columnAmount"/> value="<#if csvColumnSettings??>${csvColumnSettings.columnAmount()}</#if>"> + <label class="input-label" for="columnAmount">${locale.getString("transactions.import.column")}</label> + </div> + </div> + + <br> + + <div class="row"> + <div class="col s12 center-align"> + <@header.buttonSubmit name='action' icon='save' localizationKey='save' id='button-confirm-csv-column-settings' classes='text-white'/> + </div> + </div> + </form> </div> +</#macro> +<#macro renderCsvRows> <div class="container" id="transaction-import-overview"> <table class="bordered centered"> <tr> @@ -119,4 +182,45 @@ </#list> </table> </div> +</#macro> + +<#macro renderCsvTransactions> + <div class="container" id="transaction-import-list"> + <table class="bordered centered"> + <tr> + <td class="bold">${locale.getString("transactions.import.status")}</td> + <td class="bold">${locale.getString("transaction.new.label.date")}</td> + <td class="bold">${locale.getString("transaction.new.label.name")}</td> + <td class="bold">${locale.getString("transaction.new.label.amount")}</td> + </tr> + + <#list csvTransactions as csvTransaction> + <tr> + <td><@statusBanner csvTransaction.getStatus()/></td> + <td>${csvTransaction.getDate()}</td> + <td>${csvTransaction.getName()}</td> + <td>${csvTransaction.getAmount()}</td> + </tr> + </#list> + </table> + </div> +</#macro> + +<#macro statusBanner status> + <#if status.name() == "PENDING"> + <#assign bannerClasses="background-blue text-white"> + <#assign bannerText=locale.getString("transactions.import.status.pending")> + <#elseif status.name() == "IMPORTED"> + <#assign bannerClasses="background-green text-white"> + <#assign bannerText=locale.getString("transactions.import.status.imported")> + <#elseif status.name() == "SKIPPED"> + <#if settings.isUseDarkTheme()> + <#assign bannerClasses="background-grey text-black"> + <#else> + <#assign bannerClasses="background-grey text-white"> + </#if> + <#assign bannerText=locale.getString("transactions.import.status.skipped")> + </#if> + + <div class="banner ${bannerClasses}">${bannerText}</div> </#macro> \ No newline at end of file -- GitLab