From 5a339cbee6a63b6d7996f29c8e11fcf37445dfc3 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 8 Feb 2024 09:06:07 -0500 Subject: [PATCH] Add new "Brokerage" account type, and add more convenience for getting total assets. --- .../control/AccountsViewController.java | 13 +++------- .../perfin/control/EditAccountController.java | 1 + .../andrewlalis/perfin/data/DataSource.java | 24 ++++++++++++------- .../data/impl/JdbcAccountRepository.java | 4 ++-- .../perfin/data/util/CurrencyUtil.java | 11 +++++++++ .../andrewlalis/perfin/model/AccountType.java | 13 ++-------- .../perfin/view/component/AccountTile.java | 3 ++- .../view/component/module/AccountsModule.java | 24 +++++++++++++++---- src/main/resources/style/base.css | 1 + 9 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java index 5071b96..a1544d5 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java @@ -4,7 +4,6 @@ import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.data.AccountRepository; import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.model.Account; -import com.andrewlalis.perfin.model.MoneyValue; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.component.AccountTile; import javafx.application.Platform; @@ -57,15 +56,9 @@ public class AccountsViewController implements RouteSelectionListener { .toList() )); }); - // Compute grand totals! - Thread.ofVirtual().start(() -> { - var totals = profile.dataSource().getCombinedAccountBalances(); - StringBuilder sb = new StringBuilder("Totals: "); - for (var entry : totals.entrySet()) { - sb.append(CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(entry.getValue(), entry.getKey()))); - } - Platform.runLater(() -> totalLabel.setText(sb.toString().strip())); - }); + profile.dataSource().getCombinedAccountBalances() + .thenApply(CurrencyUtil::formatMoneyValues) + .thenAccept(s -> Platform.runLater(() -> totalLabel.setText("Totals: " + s))); }); } diff --git a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java index caf9a7b..7d11807 100644 --- a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java +++ b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java @@ -86,6 +86,7 @@ public class EditAccountController implements RouteSelectionListener { accountTypeChoiceBox.getItems().add(AccountType.CHECKING); accountTypeChoiceBox.getItems().add(AccountType.SAVINGS); accountTypeChoiceBox.getItems().add(AccountType.CREDIT_CARD); + accountTypeChoiceBox.getItems().add(AccountType.BROKERAGE); accountTypeChoiceBox.getSelectionModel().select(AccountType.CHECKING); initialBalanceContent.visibleProperty().bind(creatingNewAccount); diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java index 1ef61f0..1f21073 100644 --- a/src/main/java/com/andrewlalis/perfin/data/DataSource.java +++ b/src/main/java/com/andrewlalis/perfin/data/DataSource.java @@ -106,19 +106,27 @@ public interface DataSource { return cf; } - default Map getCombinedAccountBalances() { - try (var accountRepo = getAccountRepository()) { - List accounts = accountRepo.findAll(PageRequest.unpaged()).items(); + /** + * Gets a list of combined total assets for each currency that's tracked, + * ordered with highest assets first. + * @return A future that resolves to the list of amounts for each currency. + */ + default CompletableFuture> getCombinedAccountBalances() { + return mapRepoAsync(AccountRepository.class, repo -> { + List accounts = repo.findAll(PageRequest.unpaged()).items(); Map totals = new HashMap<>(); for (var account : accounts) { BigDecimal currencyTotal = totals.computeIfAbsent(account.getCurrency(), c -> BigDecimal.ZERO); - BigDecimal accountBalance = accountRepo.deriveCurrentBalance(account.id); + BigDecimal accountBalance = repo.deriveCurrentBalance(account.id); if (account.getType() == AccountType.CREDIT_CARD) accountBalance = accountBalance.negate(); totals.put(account.getCurrency(), currencyTotal.add(accountBalance)); } - return totals; - } catch (Exception e) { - throw new RuntimeException(e); - } + List values = new ArrayList<>(totals.size()); + for (var entry : totals.entrySet()) { + values.add(new MoneyValue(entry.getValue(), entry.getKey())); + } + values.sort((m1, m2) -> m2.amount().compareTo(m1.amount())); + return values; + }); } } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java index 04a8594..6dec67e 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java @@ -216,8 +216,8 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements if (account == null) return; List updateMessages = new ArrayList<>(); if (account.getType() != type) { - DbUtil.updateOne(conn, "UPDATE account SET account_type = ? WHERE id = ?", type, accountId); - updateMessages.add(String.format("Updated account type from %s to %s.", account.getType().toString(), type.toString())); + DbUtil.updateOne(conn, "UPDATE account SET account_type = ? WHERE id = ?", type.name(), accountId); + updateMessages.add(String.format("Updated account type from %s to %s.", account.getType(), type)); } if (!account.getAccountNumber().equals(accountNumber)) { DbUtil.updateOne(conn, "UPDATE account SET account_number = ? WHERE id = ?", accountNumber, accountId); diff --git a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java index 44a1fde..e4b06d2 100644 --- a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java +++ b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java @@ -5,6 +5,7 @@ import com.andrewlalis.perfin.model.MoneyValue; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; +import java.util.List; import java.util.Locale; public class CurrencyUtil { @@ -26,4 +27,14 @@ public class CurrencyUtil { BigDecimal displayValue = money.amount().setScale(money.currency().getDefaultFractionDigits(), RoundingMode.HALF_UP); return displayValue.toString(); } + + public static String formatMoneyValues(List values) { + StringBuilder sb = new StringBuilder(); + final int len = values.size(); + for (int i = 0; i < len; i++) { + sb.append(formatMoneyWithCurrencyPrefix(values.get(i))); + if (i < len - 1) sb.append(", "); + } + return sb.toString(); + } } diff --git a/src/main/java/com/andrewlalis/perfin/model/AccountType.java b/src/main/java/com/andrewlalis/perfin/model/AccountType.java index ef4a127..41601e0 100644 --- a/src/main/java/com/andrewlalis/perfin/model/AccountType.java +++ b/src/main/java/com/andrewlalis/perfin/model/AccountType.java @@ -6,7 +6,8 @@ package com.andrewlalis.perfin.model; public enum AccountType { CHECKING("Checking", true), SAVINGS("Savings", true), - CREDIT_CARD("Credit Card", false); + CREDIT_CARD("Credit Card", false), + BROKERAGE("Brokerage", true); private final String name; private final boolean debitsPositive; @@ -24,14 +25,4 @@ public enum AccountType { public String toString() { return name; } - - public static AccountType parse(String s) { - s = s.strip().toUpperCase(); - return switch (s) { - case "CHECKING" -> CHECKING; - case "SAVINGS" -> SAVINGS; - case "CREDIT CARD", "CREDITCARD" -> CREDIT_CARD; - default -> throw new IllegalArgumentException("Invalid AccountType string: " + s); - }; - } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java index ec6ab1d..a21690a 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java @@ -28,7 +28,8 @@ public class AccountTile extends BorderPane { public static final Map ACCOUNT_TYPE_COLORS = Map.of( AccountType.CHECKING, "-fx-theme-account-type-checking", AccountType.SAVINGS, "-fx-theme-account-type-savings", - AccountType.CREDIT_CARD, "-fx-theme-account-type-credit-card" + AccountType.CREDIT_CARD, "-fx-theme-account-type-credit-card", + AccountType.BROKERAGE, "-fx-theme-account-type-brokerage" ); public AccountTile(Account account) { diff --git a/src/main/java/com/andrewlalis/perfin/view/component/module/AccountsModule.java b/src/main/java/com/andrewlalis/perfin/view/component/module/AccountsModule.java index 387ae92..54d020c 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/module/AccountsModule.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/module/AccountsModule.java @@ -11,10 +11,7 @@ import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import java.math.BigDecimal; @@ -25,6 +22,7 @@ import static com.andrewlalis.perfin.PerfinApp.router; */ public class AccountsModule extends DashboardModule { private final VBox accountsVBox = new VBox(); + private final Label totalAssetsLabel = new Label(); public AccountsModule(Pane parent) { super(parent); @@ -48,6 +46,16 @@ public class AccountsModule extends DashboardModule { refreshButton )); this.getChildren().add(scrollPane); + + HBox footer = new HBox(); + footer.getStyleClass().addAll("std-padding", "std-spacing"); + + totalAssetsLabel.getStyleClass().addAll("mono-font"); + HBox totalAssetsBox = new HBox(new Label("Total Tracked Assets: "), totalAssetsLabel); + totalAssetsBox.getStyleClass().addAll("std-spacing"); + footer.getChildren().add(totalAssetsBox); + + this.getChildren().add(footer); } @Override @@ -61,6 +69,14 @@ public class AccountsModule extends DashboardModule { accountsVBox.getChildren().clear(); accountsVBox.getChildren().addAll(nodes); })); + totalAssetsLabel.setText("Computing..."); + totalAssetsLabel.setDisable(true); + Profile.getCurrent().dataSource().getCombinedAccountBalances() + .thenApply(CurrencyUtil::formatMoneyValues) + .thenAccept(s -> Platform.runLater(() -> { + totalAssetsLabel.setText(s); + totalAssetsLabel.setDisable(false); + })); } private static Node buildMiniAccountTile(Account account) { diff --git a/src/main/resources/style/base.css b/src/main/resources/style/base.css index 0bee10e..27bf547 100644 --- a/src/main/resources/style/base.css +++ b/src/main/resources/style/base.css @@ -17,6 +17,7 @@ rather than with your own CSS. -fx-theme-account-type-checking: rgb(3, 127, 252); -fx-theme-account-type-savings: rgb(57, 158, 74); -fx-theme-account-type-credit-card: rgb(207, 8, 68); + -fx-theme-account-type-brokerage: rgb(130, 17, 242); } .root {