Add new "Brokerage" account type, and add more convenience for getting total assets.

This commit is contained in:
Andrew Lalis 2024-02-08 09:06:07 -05:00
parent d43c61d6ee
commit 5a339cbee6
9 changed files with 58 additions and 36 deletions

View File

@ -4,7 +4,6 @@ import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountRepository; import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.MoneyValue;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.component.AccountTile; import com.andrewlalis.perfin.view.component.AccountTile;
import javafx.application.Platform; import javafx.application.Platform;
@ -57,15 +56,9 @@ public class AccountsViewController implements RouteSelectionListener {
.toList() .toList()
)); ));
}); });
// Compute grand totals! profile.dataSource().getCombinedAccountBalances()
Thread.ofVirtual().start(() -> { .thenApply(CurrencyUtil::formatMoneyValues)
var totals = profile.dataSource().getCombinedAccountBalances(); .thenAccept(s -> Platform.runLater(() -> totalLabel.setText("Totals: " + s)));
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()));
});
}); });
} }

View File

@ -86,6 +86,7 @@ public class EditAccountController implements RouteSelectionListener {
accountTypeChoiceBox.getItems().add(AccountType.CHECKING); accountTypeChoiceBox.getItems().add(AccountType.CHECKING);
accountTypeChoiceBox.getItems().add(AccountType.SAVINGS); accountTypeChoiceBox.getItems().add(AccountType.SAVINGS);
accountTypeChoiceBox.getItems().add(AccountType.CREDIT_CARD); accountTypeChoiceBox.getItems().add(AccountType.CREDIT_CARD);
accountTypeChoiceBox.getItems().add(AccountType.BROKERAGE);
accountTypeChoiceBox.getSelectionModel().select(AccountType.CHECKING); accountTypeChoiceBox.getSelectionModel().select(AccountType.CHECKING);
initialBalanceContent.visibleProperty().bind(creatingNewAccount); initialBalanceContent.visibleProperty().bind(creatingNewAccount);

View File

@ -106,19 +106,27 @@ public interface DataSource {
return cf; return cf;
} }
default Map<Currency, BigDecimal> getCombinedAccountBalances() { /**
try (var accountRepo = getAccountRepository()) { * Gets a list of combined total assets for each currency that's tracked,
List<Account> accounts = accountRepo.findAll(PageRequest.unpaged()).items(); * ordered with highest assets first.
* @return A future that resolves to the list of amounts for each currency.
*/
default CompletableFuture<List<MoneyValue>> getCombinedAccountBalances() {
return mapRepoAsync(AccountRepository.class, repo -> {
List<Account> accounts = repo.findAll(PageRequest.unpaged()).items();
Map<Currency, BigDecimal> totals = new HashMap<>(); Map<Currency, BigDecimal> totals = new HashMap<>();
for (var account : accounts) { for (var account : accounts) {
BigDecimal currencyTotal = totals.computeIfAbsent(account.getCurrency(), c -> BigDecimal.ZERO); 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(); if (account.getType() == AccountType.CREDIT_CARD) accountBalance = accountBalance.negate();
totals.put(account.getCurrency(), currencyTotal.add(accountBalance)); totals.put(account.getCurrency(), currencyTotal.add(accountBalance));
} }
return totals; List<MoneyValue> values = new ArrayList<>(totals.size());
} catch (Exception e) { for (var entry : totals.entrySet()) {
throw new RuntimeException(e); values.add(new MoneyValue(entry.getValue(), entry.getKey()));
} }
values.sort((m1, m2) -> m2.amount().compareTo(m1.amount()));
return values;
});
} }
} }

View File

@ -216,8 +216,8 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
if (account == null) return; if (account == null) return;
List<String> updateMessages = new ArrayList<>(); List<String> updateMessages = new ArrayList<>();
if (account.getType() != type) { if (account.getType() != type) {
DbUtil.updateOne(conn, "UPDATE account SET account_type = ? WHERE id = ?", type, accountId); 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().toString(), type.toString())); updateMessages.add(String.format("Updated account type from %s to %s.", account.getType(), type));
} }
if (!account.getAccountNumber().equals(accountNumber)) { if (!account.getAccountNumber().equals(accountNumber)) {
DbUtil.updateOne(conn, "UPDATE account SET account_number = ? WHERE id = ?", accountNumber, accountId); DbUtil.updateOne(conn, "UPDATE account SET account_number = ? WHERE id = ?", accountNumber, accountId);

View File

@ -5,6 +5,7 @@ import com.andrewlalis.perfin.model.MoneyValue;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.List;
import java.util.Locale; import java.util.Locale;
public class CurrencyUtil { public class CurrencyUtil {
@ -26,4 +27,14 @@ public class CurrencyUtil {
BigDecimal displayValue = money.amount().setScale(money.currency().getDefaultFractionDigits(), RoundingMode.HALF_UP); BigDecimal displayValue = money.amount().setScale(money.currency().getDefaultFractionDigits(), RoundingMode.HALF_UP);
return displayValue.toString(); return displayValue.toString();
} }
public static String formatMoneyValues(List<MoneyValue> 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();
}
} }

View File

@ -6,7 +6,8 @@ package com.andrewlalis.perfin.model;
public enum AccountType { public enum AccountType {
CHECKING("Checking", true), CHECKING("Checking", true),
SAVINGS("Savings", true), SAVINGS("Savings", true),
CREDIT_CARD("Credit Card", false); CREDIT_CARD("Credit Card", false),
BROKERAGE("Brokerage", true);
private final String name; private final String name;
private final boolean debitsPositive; private final boolean debitsPositive;
@ -24,14 +25,4 @@ public enum AccountType {
public String toString() { public String toString() {
return name; 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);
};
}
} }

View File

@ -28,7 +28,8 @@ public class AccountTile extends BorderPane {
public static final Map<AccountType, String> ACCOUNT_TYPE_COLORS = Map.of( public static final Map<AccountType, String> ACCOUNT_TYPE_COLORS = Map.of(
AccountType.CHECKING, "-fx-theme-account-type-checking", AccountType.CHECKING, "-fx-theme-account-type-checking",
AccountType.SAVINGS, "-fx-theme-account-type-savings", 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) { public AccountTile(Account account) {

View File

@ -11,10 +11,7 @@ import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -25,6 +22,7 @@ import static com.andrewlalis.perfin.PerfinApp.router;
*/ */
public class AccountsModule extends DashboardModule { public class AccountsModule extends DashboardModule {
private final VBox accountsVBox = new VBox(); private final VBox accountsVBox = new VBox();
private final Label totalAssetsLabel = new Label();
public AccountsModule(Pane parent) { public AccountsModule(Pane parent) {
super(parent); super(parent);
@ -48,6 +46,16 @@ public class AccountsModule extends DashboardModule {
refreshButton refreshButton
)); ));
this.getChildren().add(scrollPane); 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 @Override
@ -61,6 +69,14 @@ public class AccountsModule extends DashboardModule {
accountsVBox.getChildren().clear(); accountsVBox.getChildren().clear();
accountsVBox.getChildren().addAll(nodes); 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) { private static Node buildMiniAccountTile(Account account) {

View File

@ -17,6 +17,7 @@ rather than with your own CSS.
-fx-theme-account-type-checking: rgb(3, 127, 252); -fx-theme-account-type-checking: rgb(3, 127, 252);
-fx-theme-account-type-savings: rgb(57, 158, 74); -fx-theme-account-type-savings: rgb(57, 158, 74);
-fx-theme-account-type-credit-card: rgb(207, 8, 68); -fx-theme-account-type-credit-card: rgb(207, 8, 68);
-fx-theme-account-type-brokerage: rgb(130, 17, 242);
} }
.root { .root {