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.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)));
});
}

View File

@ -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);

View File

@ -106,19 +106,27 @@ public interface DataSource {
return cf;
}
default Map<Currency, BigDecimal> getCombinedAccountBalances() {
try (var accountRepo = getAccountRepository()) {
List<Account> 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<List<MoneyValue>> getCombinedAccountBalances() {
return mapRepoAsync(AccountRepository.class, repo -> {
List<Account> accounts = repo.findAll(PageRequest.unpaged()).items();
Map<Currency, BigDecimal> 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<MoneyValue> 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;
});
}
}

View File

@ -216,8 +216,8 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
if (account == null) return;
List<String> 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);

View File

@ -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<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 {
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);
};
}
}

View File

@ -28,7 +28,8 @@ public class AccountTile extends BorderPane {
public static final Map<AccountType, String> 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) {

View File

@ -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) {

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-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 {