Add new "Brokerage" account type, and add more convenience for getting total assets.
This commit is contained in:
parent
d43c61d6ee
commit
5a339cbee6
|
@ -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()));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue