Added updates to use and show asset value of brokerage accounts.

This commit is contained in:
Andrew Lalis 2024-07-10 17:05:02 -04:00
parent ec6bc83353
commit f23d2c85a9
12 changed files with 189 additions and 59 deletions

View File

@ -4,9 +4,7 @@ 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.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.*;
import com.andrewlalis.perfin.model.MoneyValue;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.BindingUtil;
import com.andrewlalis.perfin.view.component.AccountHistoryView; import com.andrewlalis.perfin.view.component.AccountHistoryView;
import com.andrewlalis.perfin.view.component.PropertiesPane; import com.andrewlalis.perfin.view.component.PropertiesPane;
@ -14,7 +12,10 @@ import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator; import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.BooleanExpression; import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.*; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -23,7 +24,10 @@ import javafx.scene.control.Label;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import java.time.*; import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
@ -31,6 +35,7 @@ public class AccountViewController implements RouteSelectionListener {
private final ObjectProperty<Account> accountProperty = new SimpleObjectProperty<>(null); private final ObjectProperty<Account> accountProperty = new SimpleObjectProperty<>(null);
private final ObservableValue<Boolean> accountArchived = accountProperty.map(a -> a != null && a.isArchived()); private final ObservableValue<Boolean> accountArchived = accountProperty.map(a -> a != null && a.isArchived());
private final StringProperty balanceTextProperty = new SimpleStringProperty(null); private final StringProperty balanceTextProperty = new SimpleStringProperty(null);
private final StringProperty assetValueTextProperty = new SimpleStringProperty(null);
@FXML public Label titleLabel; @FXML public Label titleLabel;
@FXML public Label accountNameLabel; @FXML public Label accountNameLabel;
@ -38,6 +43,8 @@ public class AccountViewController implements RouteSelectionListener {
@FXML public Label accountCurrencyLabel; @FXML public Label accountCurrencyLabel;
@FXML public Label accountCreatedAtLabel; @FXML public Label accountCreatedAtLabel;
@FXML public Label accountBalanceLabel; @FXML public Label accountBalanceLabel;
@FXML public PropertiesPane assetValuePane;
@FXML public Label latestAssetsValueLabel;
@FXML public PropertiesPane descriptionPane; @FXML public PropertiesPane descriptionPane;
@FXML public Text accountDescriptionText; @FXML public Text accountDescriptionText;
@ -58,14 +65,23 @@ public class AccountViewController implements RouteSelectionListener {
var hasDescription = accountProperty.map(a -> a.getDescription() != null); var hasDescription = accountProperty.map(a -> a.getDescription() != null);
BindingUtil.bindManagedAndVisible(descriptionPane, hasDescription); BindingUtil.bindManagedAndVisible(descriptionPane, hasDescription);
accountBalanceLabel.textProperty().bind(balanceTextProperty); accountBalanceLabel.textProperty().bind(balanceTextProperty);
var isBrokerageAccount = accountProperty.map(a -> a.getType() == AccountType.BROKERAGE);
BindingUtil.bindManagedAndVisible(assetValuePane, isBrokerageAccount);
latestAssetsValueLabel.textProperty().bind(assetValueTextProperty);
actionsBox.getChildren().forEach(node -> { actionsBox.getChildren().forEach(node -> {
Button button = (Button) node; Button button = (Button) node;
ObservableValue<Boolean> buttonActive = accountArchived; ObservableValue<Boolean> buttonDisabled = accountArchived;
if (button.getText().equalsIgnoreCase("Unarchive")) { if (button.getText().equalsIgnoreCase("Unarchive")) {
buttonActive = BooleanExpression.booleanExpression(buttonActive).not(); buttonDisabled = BooleanExpression.booleanExpression(buttonDisabled).not();
} }
button.disableProperty().bind(buttonActive); if (button.getText().equalsIgnoreCase("Record Asset Value")) {
buttonDisabled = BooleanExpression.booleanExpression(
accountProperty.map(Account::getType)
.map(t -> !t.equals(AccountType.BROKERAGE))
).or(BooleanExpression.booleanExpression(accountArchived));
}
button.disableProperty().bind(buttonDisabled);
button.managedProperty().bind(button.visibleProperty()); button.managedProperty().bind(button.visibleProperty());
button.visibleProperty().bind(button.disableProperty().not()); button.visibleProperty().bind(button.disableProperty().not());
}); });
@ -81,7 +97,7 @@ public class AccountViewController implements RouteSelectionListener {
.toInstant(); .toInstant();
Profile.getCurrent().dataSource().mapRepoAsync( Profile.getCurrent().dataSource().mapRepoAsync(
AccountRepository.class, AccountRepository.class,
repo -> repo.deriveBalance(getAccount().id, timestamp) repo -> repo.deriveCashBalance(getAccount().id, timestamp)
).thenAccept(balance -> Platform.runLater(() -> { ).thenAccept(balance -> Platform.runLater(() -> {
String msg = String.format( String msg = String.format(
"Your balance as of %s is %s, according to Perfin's data.", "Your balance as of %s is %s, according to Perfin's data.",
@ -97,12 +113,20 @@ public class AccountViewController implements RouteSelectionListener {
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
accountHistory.clear(); accountHistory.clear();
balanceTextProperty.set(null); balanceTextProperty.set(null);
assetValueTextProperty.set(null);
if (context instanceof Account account) { if (context instanceof Account account) {
this.accountProperty.set(account); this.accountProperty.set(account);
accountHistory.setAccountId(account.id); accountHistory.setAccountId(account.id);
accountHistory.loadMoreHistory(); accountHistory.loadMoreHistory();
Profile.getCurrent().dataSource().getAccountBalanceText(account) Profile.getCurrent().dataSource().getAccountBalanceText(account)
.thenAccept(s -> Platform.runLater(() -> balanceTextProperty.set(s))); .thenAccept(s -> Platform.runLater(() -> balanceTextProperty.set(s)));
if (account.getType() == AccountType.BROKERAGE) {
Profile.getCurrent().dataSource().mapRepoAsync(
AccountRepository.class,
repo -> repo.getNearestAssetValue(account.id)
).thenApply(value -> CurrencyUtil.formatMoney(new MoneyValue(value, account.getCurrency())))
.thenAccept(text -> Platform.runLater(() -> assetValueTextProperty.set(text)));
}
} }
} }
@ -112,7 +136,11 @@ public class AccountViewController implements RouteSelectionListener {
} }
@FXML public void goToCreateBalanceRecord() { @FXML public void goToCreateBalanceRecord() {
router.navigate("create-balance-record", getAccount()); router.navigate("create-balance-record", new CreateBalanceRecordController.RouteContext(getAccount(), BalanceRecordType.CASH));
}
@FXML public void goToCreateAssetRecord() {
router.navigate("create-balance-record", new CreateBalanceRecordController.RouteContext(getAccount(), BalanceRecordType.ASSETS));
} }
@FXML @FXML

View File

@ -24,6 +24,7 @@ public class BalanceRecordViewController implements RouteSelectionListener {
@FXML public Label titleLabel; @FXML public Label titleLabel;
@FXML public Label typeLabel;
@FXML public Label timestampLabel; @FXML public Label timestampLabel;
@FXML public Label balanceLabel; @FXML public Label balanceLabel;
@FXML public Label currencyLabel; @FXML public Label currencyLabel;
@ -38,6 +39,7 @@ public class BalanceRecordViewController implements RouteSelectionListener {
this.balanceRecord = (BalanceRecord) context; this.balanceRecord = (BalanceRecord) context;
if (balanceRecord == null) return; if (balanceRecord == null) return;
titleLabel.setText("Balance Record #" + balanceRecord.id); titleLabel.setText("Balance Record #" + balanceRecord.id);
typeLabel.setText(balanceRecord.getType().toString());
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp()));
balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount())); balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount()));
currencyLabel.setText(balanceRecord.getCurrency().getDisplayName()); currencyLabel.setText(balanceRecord.getCurrency().getDisplayName());

View File

@ -35,6 +35,8 @@ import static com.andrewlalis.perfin.PerfinApp.router;
* account. * account.
*/ */
public class CreateBalanceRecordController implements RouteSelectionListener { public class CreateBalanceRecordController implements RouteSelectionListener {
public record RouteContext (Account account, BalanceRecordType type) {}
@FXML public TextField timestampField; @FXML public TextField timestampField;
@FXML public TextField balanceField; @FXML public TextField balanceField;
@FXML public Label balanceWarningLabel; @FXML public Label balanceWarningLabel;
@ -44,6 +46,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
@FXML public Button saveButton; @FXML public Button saveButton;
private Account account; private Account account;
private BalanceRecordType type = BalanceRecordType.CASH;
@FXML public void initialize() { @FXML public void initialize() {
var timestampValid = new ValidationApplier<>((ValidationFunction<String>) input -> { var timestampValid = new ValidationApplier<>((ValidationFunction<String>) input -> {
@ -62,7 +65,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
balanceWarningLabel.managedProperty().bind(balanceWarningLabel.visibleProperty()); balanceWarningLabel.managedProperty().bind(balanceWarningLabel.visibleProperty());
balanceWarningLabel.visibleProperty().set(false); balanceWarningLabel.visibleProperty().set(false);
balanceField.textProperty().addListener((observable, oldValue, newValue) -> { balanceField.textProperty().addListener((observable, oldValue, newValue) -> {
if (!balanceValidator.validate(newValue).isValid() || !timestampValid.get()) { if (!balanceValidator.validate(newValue).isValid() || !timestampValid.get() || type != BalanceRecordType.CASH) {
balanceWarningLabel.visibleProperty().set(false); balanceWarningLabel.visibleProperty().set(false);
return; return;
} }
@ -70,7 +73,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT); LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
LocalDateTime utcTimestamp = DateUtil.localToUTC(localTimestamp); LocalDateTime utcTimestamp = DateUtil.localToUTC(localTimestamp);
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> { Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal derivedBalance = repo.deriveBalance(account.id, utcTimestamp.toInstant(ZoneOffset.UTC)); BigDecimal derivedBalance = repo.deriveCashBalance(account.id, utcTimestamp.toInstant(ZoneOffset.UTC));
boolean balancesMatch = reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance); boolean balancesMatch = reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance);
Platform.runLater(() -> balanceWarningLabel.visibleProperty().set(!balancesMatch)); Platform.runLater(() -> balanceWarningLabel.visibleProperty().set(!balancesMatch));
}); });
@ -82,14 +85,19 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
@Override @Override
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
this.account = (Account) context; RouteContext ctx = (RouteContext) context;
this.account = ctx.account();
this.type = ctx.type();
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT)); timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> { balanceField.setText(null);
BigDecimal value = repo.deriveCurrentBalance(account.id); if (ctx.type() == BalanceRecordType.CASH) {
Platform.runLater(() -> balanceField.setText( Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency())) BigDecimal value = repo.deriveCurrentCashBalance(account.id);
)); Platform.runLater(() -> balanceField.setText(
}); CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency()))
));
});
}
attachmentSelectionArea.clear(); attachmentSelectionArea.clear();
} }
@ -97,17 +105,26 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT); LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
BigDecimal reportedBalance = new BigDecimal(balanceField.getText()); BigDecimal reportedBalance = new BigDecimal(balanceField.getText());
boolean confirm = Popups.confirm(timestampField, "Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted( String valueNoun = switch (type) {
case CASH -> "cash balance";
case ASSETS -> "asset value";
};
boolean confirm = Popups.confirm(timestampField, "Are you sure that you want to record the %s of account\n%s\nas %s,\nas of %s?".formatted(
valueNoun,
account.getShortName(), account.getShortName(),
CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())), CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())),
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE) localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
)); ));
if (confirm && confirmIfInconsistentBalance(reportedBalance, DateUtil.localToUTC(localTimestamp))) { if (
confirm &&
(type != BalanceRecordType.CASH || confirmIfInconsistentBalance(reportedBalance, DateUtil.localToUTC(localTimestamp)))
) {
Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> { Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> {
repo.insert( repo.insert(
DateUtil.localToUTC(localTimestamp), DateUtil.localToUTC(localTimestamp),
account.id, account.id,
BalanceRecordType.CASH, type,
reportedBalance, reportedBalance,
account.getCurrency(), account.getCurrency(),
attachmentSelectionArea.getSelectedPaths() attachmentSelectionArea.getSelectedPaths()
@ -124,7 +141,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance, LocalDateTime utcTimestamp) { private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance, LocalDateTime utcTimestamp) {
BigDecimal currentDerivedBalance = Profile.getCurrent().dataSource().mapRepo( BigDecimal currentDerivedBalance = Profile.getCurrent().dataSource().mapRepo(
AccountRepository.class, AccountRepository.class,
repo -> repo.deriveBalance(account.id, utcTimestamp.toInstant(ZoneOffset.UTC)) repo -> repo.deriveCashBalance(account.id, utcTimestamp.toInstant(ZoneOffset.UTC))
); );
if (!reportedBalance.setScale(currentDerivedBalance.scale(), RoundingMode.HALF_UP).equals(currentDerivedBalance)) { if (!reportedBalance.setScale(currentDerivedBalance.scale(), RoundingMode.HALF_UP).equals(currentDerivedBalance)) {
String msg = "The balance you reported (%s) doesn't match the balance that Perfin derived from your account's transactions (%s). It's encouraged to go back and add any missing transactions first, but you may proceed now if you understand the consequences of an inconsistent account balance history.\n\nAre you absolutely sure you want to create this balance record?".formatted( String msg = "The balance you reported (%s) doesn't match the balance that Perfin derived from your account's transactions (%s). It's encouraged to go back and add any missing transactions first, but you may proceed now if you understand the consequences of an inconsistent account balance history.\n\nAre you absolutely sure you want to create this balance record?".formatted(

View File

@ -28,10 +28,15 @@ public interface AccountRepository extends Repository, AutoCloseable {
void archive(long accountId); void archive(long accountId);
void unarchive(long accountId); void unarchive(long accountId);
BigDecimal deriveBalance(long accountId, Instant timestamp); BigDecimal deriveCashBalance(long accountId, Instant timestamp);
default BigDecimal deriveCurrentBalance(long accountId) { default BigDecimal deriveCurrentCashBalance(long accountId) {
return deriveBalance(accountId, Instant.now(Clock.systemUTC())); return deriveCashBalance(accountId, Instant.now(Clock.systemUTC()));
} }
BigDecimal getNearestAssetValue(long accountId, Instant timestamp);
default BigDecimal getNearestAssetValue(long accountId) {
return getNearestAssetValue(accountId, Instant.now(Clock.systemUTC()));
}
Set<Currency> findAllUsedCurrencies(); Set<Currency> findAllUsedCurrencies();
List<Timestamped> findEventsBefore(long accountId, LocalDateTime utcTimestamp, int maxResults); List<Timestamped> findEventsBefore(long accountId, LocalDateTime utcTimestamp, int maxResults);
} }

View File

@ -104,7 +104,7 @@ public interface DataSource {
default CompletableFuture<String> getAccountBalanceText(Account account) { default CompletableFuture<String> getAccountBalanceText(Account account) {
CompletableFuture<String> cf = new CompletableFuture<>(); CompletableFuture<String> cf = new CompletableFuture<>();
mapRepoAsync(AccountRepository.class, repo -> { mapRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(account.id); BigDecimal balance = repo.deriveCurrentCashBalance(account.id);
MoneyValue money = new MoneyValue(balance, account.getCurrency()); MoneyValue money = new MoneyValue(balance, account.getCurrency());
return CurrencyUtil.formatMoney(money); return CurrencyUtil.formatMoney(money);
}).thenAccept(s -> Platform.runLater(() -> cf.complete(s))); }).thenAccept(s -> Platform.runLater(() -> cf.complete(s)));
@ -123,9 +123,11 @@ public interface DataSource {
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 = repo.deriveBalance(account.id, timestamp); BigDecimal accountBalance = repo.deriveCashBalance(account.id, timestamp);
BigDecimal accountAssetsValue = repo.getNearestAssetValue(account.id, timestamp);
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)); BigDecimal accountTotal = accountBalance.add(accountAssetsValue);
totals.put(account.getCurrency(), currencyTotal.add(accountTotal));
} }
List<MoneyValue> values = new ArrayList<>(totals.size()); List<MoneyValue> values = new ArrayList<>(totals.size());
for (var entry : totals.entrySet()) { for (var entry : totals.entrySet()) {

View File

@ -114,7 +114,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
} }
@Override @Override
public BigDecimal deriveBalance(long accountId, Instant timestamp) { public BigDecimal deriveCashBalance(long accountId, Instant timestamp) {
// First find the account itself, since its properties influence the balance. // First find the account itself, since its properties influence the balance.
Account account = findById(accountId).orElse(null); Account account = findById(accountId).orElse(null);
if (account == null) throw new EntityNotFoundException(Account.class, accountId); if (account == null) throw new EntityNotFoundException(Account.class, accountId);
@ -152,6 +152,15 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
} }
} }
@Override
public BigDecimal getNearestAssetValue(long accountId, Instant timestamp) {
LocalDateTime utcTimestamp = timestamp.atZone(ZoneOffset.UTC).toLocalDateTime();
BalanceRecordRepository balanceRecordRepo = new JdbcBalanceRecordRepository(conn, contentDir);
Optional<BalanceRecord> mostRecentRecord = balanceRecordRepo.findClosestBefore(accountId, BalanceRecordType.ASSETS, utcTimestamp);
if (mostRecentRecord.isEmpty()) return BigDecimal.ZERO;
return mostRecentRecord.get().getBalance();
}
@Override @Override
public Set<Currency> findAllUsedCurrencies() { public Set<Currency> findAllUsedCurrencies() {
return new HashSet<>(DbUtil.findAll( return new HashSet<>(DbUtil.findAll(
@ -177,7 +186,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
LEFT JOIN history_account ha ON history_item.history_id = ha.history_id LEFT JOIN history_account ha ON history_item.history_id = ha.history_id
UNION ALL UNION ALL
SELECT id, timestamp, 'BALANCE_RECORD' AS type, account_id SELECT id, timestamp, 'BALANCE_RECORD' AS type, account_id
FROM balance_record WHERE type = 'CASH' FROM balance_record
) )
WHERE account_id = ? AND timestamp < ? WHERE account_id = ? AND timestamp < ?
ORDER BY timestamp DESC ORDER BY timestamp DESC

View File

@ -120,9 +120,13 @@ public class AccountHistoryView extends ScrollPane {
case BalanceRecord br -> { case BalanceRecord br -> {
Hyperlink brLink = new Hyperlink("Balance Record #" + br.id); Hyperlink brLink = new Hyperlink("Balance Record #" + br.id);
brLink.setOnAction(event -> router.navigate("balance-record", br)); brLink.setOnAction(event -> router.navigate("balance-record", br));
String phrase = switch(br.getType()) {
case CASH -> "a cash value";
case ASSETS -> "an asset value";
};
return CompletableFuture.completedFuture(new AccountHistoryTile(br.getTimestamp(), new TextFlow( return CompletableFuture.completedFuture(new AccountHistoryTile(br.getTimestamp(), new TextFlow(
brLink, brLink,
new Text("added with a value of %s.".formatted(CurrencyUtil.formatMoney(br.getMoneyAmount()))) new Text("added with %s of %s.".formatted(phrase, CurrencyUtil.formatMoney(br.getMoneyAmount())))
))); )));
} }
default -> { default -> {

View File

@ -113,7 +113,7 @@ public class AccountSelectionBox extends ComboBox<Account> {
nameLabel.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")"); nameLabel.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")");
if (showBalanceProp.get()) { if (showBalanceProp.get()) {
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> { Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(item.id); BigDecimal balance = repo.deriveCurrentCashBalance(item.id);
Platform.runLater(() -> { Platform.runLater(() -> {
balanceLabel.setText(CurrencyUtil.formatMoney(new MoneyValue(balance, item.getCurrency()))); balanceLabel.setText(CurrencyUtil.formatMoney(new MoneyValue(balance, item.getCurrency())));
balanceLabel.setVisible(true); balanceLabel.setVisible(true);

View File

@ -17,6 +17,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map; import java.util.Map;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
@ -83,7 +84,7 @@ public class AccountTile extends BorderPane {
balanceLabel.getStyleClass().addAll("mono-font"); balanceLabel.getStyleClass().addAll("mono-font");
balanceLabel.setDisable(true); balanceLabel.setDisable(true);
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> { Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal balance = repo.deriveCurrentBalance(account.id); BigDecimal balance = repo.deriveCurrentCashBalance(account.id);
String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency())); String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency()));
Platform.runLater(() -> { Platform.runLater(() -> {
balanceLabel.setText(text); balanceLabel.setText(text);
@ -104,6 +105,32 @@ public class AccountTile extends BorderPane {
newPropertyLabel("Current Balance"), newPropertyLabel("Current Balance"),
balanceLabel balanceLabel
); );
if (account.getType() == AccountType.BROKERAGE) {
Label assetValueLabel = new Label("Computing assets value...");
assetValueLabel.getStyleClass().addAll("mono-font");
assetValueLabel.setDisable(true);
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
BigDecimal assetValue = repo.getNearestAssetValue(account.id);
String text = CurrencyUtil.formatMoney(new MoneyValue(assetValue, account.getCurrency()));
Platform.runLater(() -> {
assetValueLabel.setText(text);
if (account.getType().areDebitsPositive() && assetValue.compareTo(BigDecimal.ZERO) < 0) {
assetValueLabel.getStyleClass().add("negative-color-text-fill");
} else if (!account.getType().areDebitsPositive() && assetValue.compareTo(BigDecimal.ZERO) < 0) {
assetValueLabel.getStyleClass().add("positive-color-text-fill");
}
assetValueLabel.setDisable(false);
});
});
propertiesPane.getChildren().addAll(
newPropertyLabel("Latest Assets Value"),
assetValueLabel
);
}
return propertiesPane; return propertiesPane;
} }

View File

@ -3,6 +3,7 @@ package com.andrewlalis.perfin.view.component.module;
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.AccountType;
import com.andrewlalis.perfin.model.MoneyValue; 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;
@ -91,13 +92,17 @@ public class AccountsModule extends DashboardModule {
Label typeLabel = new Label(account.getType().toString()); Label typeLabel = new Label(account.getType().toString());
typeLabel.getStyleClass().add("bold-text"); typeLabel.getStyleClass().add("bold-text");
typeLabel.setStyle("-fx-text-fill: " + AccountTile.ACCOUNT_TYPE_COLORS.get(account.getType())); typeLabel.setStyle("-fx-text-fill: " + AccountTile.ACCOUNT_TYPE_COLORS.get(account.getType()));
VBox rightSideVBox = new VBox();
rightSideVBox.getStyleClass().addAll("std-spacing");
Label balanceLabel = new Label("Computing balance..."); Label balanceLabel = new Label("Computing balance...");
balanceLabel.getStyleClass().addAll("mono-font"); balanceLabel.getStyleClass().addAll("mono-font");
balanceLabel.setDisable(true); balanceLabel.setDisable(true);
rightSideVBox.getChildren().add(balanceLabel);
Profile.getCurrent().dataSource().mapRepoAsync( Profile.getCurrent().dataSource().mapRepoAsync(
AccountRepository.class, AccountRepository.class,
repo -> repo.deriveCurrentBalance(account.id) repo -> repo.deriveCurrentCashBalance(account.id)
).thenAccept(bal -> Platform.runLater(() -> { ).thenAccept(bal -> Platform.runLater(() -> {
String text = CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(bal, account.getCurrency())); String text = CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(bal, account.getCurrency()));
balanceLabel.setText(text); balanceLabel.setText(text);
@ -109,9 +114,29 @@ public class AccountsModule extends DashboardModule {
balanceLabel.setDisable(false); balanceLabel.setDisable(false);
})); }));
if (account.getType() == AccountType.BROKERAGE) {
Label assetValueLabel = new Label("Computing assets value...");
assetValueLabel.getStyleClass().addAll("mono-font");
assetValueLabel.setDisable(true);
rightSideVBox.getChildren().add(assetValueLabel);
Profile.getCurrent().dataSource().mapRepoAsync(
AccountRepository.class,
repo -> repo.getNearestAssetValue(account.id)
).thenAccept(value -> Platform.runLater(() -> {
String text = CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(value, account.getCurrency()));
assetValueLabel.setText(text + " in assets");
if (account.getType().areDebitsPositive() && value.compareTo(BigDecimal.ZERO) < 0) {
assetValueLabel.getStyleClass().add("negative-color-text-fill");
} else if (!account.getType().areDebitsPositive() && value.compareTo(BigDecimal.ZERO) < 0) {
assetValueLabel.getStyleClass().add("positive-color-text-fill");
}
assetValueLabel.setDisable(false);
}));
}
VBox contentBox = new VBox(nameLabel, numberLabel, typeLabel); VBox contentBox = new VBox(nameLabel, numberLabel, typeLabel);
borderPane.setCenter(contentBox); borderPane.setCenter(contentBox);
borderPane.setRight(balanceLabel); borderPane.setRight(rightSideVBox);
return borderPane; return borderPane;
} }
} }

View File

@ -4,7 +4,8 @@
<?import com.andrewlalis.perfin.view.component.PropertiesPane?> <?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?> <?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<BorderPane <BorderPane
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
@ -15,34 +16,40 @@
</top> </top>
<center> <center>
<VBox> <VBox>
<!-- Main account properties and actions --> <!-- Main account properties. -->
<FlowPane styleClass="std-padding,std-spacing"> <PropertiesPane vgap="5" hgap="5" styleClass="std-padding,std-spacing">
<!-- Main account properties. --> <Label text="Name" styleClass="bold-text"/>
<PropertiesPane vgap="5" hgap="5"> <Label fx:id="accountNameLabel"/>
<Label text="Name" styleClass="bold-text"/>
<Label fx:id="accountNameLabel"/>
<Label text="Number" styleClass="bold-text"/> <Label text="Number" styleClass="bold-text"/>
<Label fx:id="accountNumberLabel" styleClass="mono-font"/> <Label fx:id="accountNumberLabel" styleClass="mono-font"/>
<Label text="Currency" styleClass="bold-text"/> <Label text="Currency" styleClass="bold-text"/>
<Label fx:id="accountCurrencyLabel"/> <Label fx:id="accountCurrencyLabel"/>
<Label text="Created At" styleClass="bold-text"/> <Label text="Created At" styleClass="bold-text"/>
<Label fx:id="accountCreatedAtLabel" styleClass="mono-font"/> <Label fx:id="accountCreatedAtLabel" styleClass="mono-font"/>
<VBox> <Label text="Current Balance" styleClass="bold-text"/>
<Label text="Current Balance" styleClass="bold-text" fx:id="balanceLabel"/> <VBox>
<Text
styleClass="small-font,secondary-color-fill"
wrappingWidth="${balanceLabel.width}"
>Computed using the last recorded balance and all transactions since.</Text>
</VBox>
<Label fx:id="accountBalanceLabel" styleClass="mono-font"/> <Label fx:id="accountBalanceLabel" styleClass="mono-font"/>
</PropertiesPane> <Label
</FlowPane> styleClass="small-font,secondary-color-fill"
>Derived using nearest recorded balance and transactions.</Label>
</VBox>
</PropertiesPane>
<PropertiesPane vgap="5" hgap="5" fx:id="descriptionPane"> <PropertiesPane vgap="5" hgap="5" fx:id="assetValuePane" styleClass="std-padding,std-spacing">
<Label text="Latest Assets Value" styleClass="bold-text" labelFor="${latestAssetsValueLabel}"/>
<VBox>
<Label fx:id="latestAssetsValueLabel" styleClass="mono-font"/>
<Label
styleClass="small-font,secondary-color-fill"
>Derived using nearest recorded asset value.</Label>
</VBox>
</PropertiesPane>
<PropertiesPane vgap="5" hgap="5" fx:id="descriptionPane" styleClass="std-padding,std-spacing">
<Label text="Description" styleClass="bold-text" labelFor="${accountDescriptionText}"/> <Label text="Description" styleClass="bold-text" labelFor="${accountDescriptionText}"/>
<TextFlow maxWidth="500"><Text fx:id="accountDescriptionText"/></TextFlow> <TextFlow maxWidth="500"><Text fx:id="accountDescriptionText"/></TextFlow>
</PropertiesPane> </PropertiesPane>
@ -51,6 +58,7 @@
<HBox fx:id="actionsBox" styleClass="std-padding,std-spacing,small-font"> <HBox fx:id="actionsBox" styleClass="std-padding,std-spacing,small-font">
<Button text="Edit" onAction="#goToEditPage"/> <Button text="Edit" onAction="#goToEditPage"/>
<Button text="Record Balance" onAction="#goToCreateBalanceRecord"/> <Button text="Record Balance" onAction="#goToCreateBalanceRecord"/>
<Button text="Record Asset Value" onAction="#goToCreateAssetRecord"/>
<Button text="Archive" onAction="#archiveAccount"/> <Button text="Archive" onAction="#archiveAccount"/>
<Button text="Delete" onAction="#deleteAccount"/> <Button text="Delete" onAction="#deleteAccount"/>
<Button text="Unarchive" onAction="#unarchiveAccount"/> <Button text="Unarchive" onAction="#unarchiveAccount"/>

View File

@ -22,6 +22,9 @@
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/> <ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
</columnConstraints> </columnConstraints>
<Label text="Type" styleClass="bold-text"/>
<Label fx:id="typeLabel" styleClass="mono-font"/>
<Label text="Timestamp" styleClass="bold-text"/> <Label text="Timestamp" styleClass="bold-text"/>
<Label fx:id="timestampLabel" styleClass="mono-font"/> <Label fx:id="timestampLabel" styleClass="mono-font"/>