Added updates to use and show asset value of brokerage accounts.
This commit is contained in:
parent
ec6bc83353
commit
f23d2c85a9
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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));
|
||||||
|
balanceField.setText(null);
|
||||||
|
if (ctx.type() == BalanceRecordType.CASH) {
|
||||||
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
|
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
|
||||||
BigDecimal value = repo.deriveCurrentBalance(account.id);
|
BigDecimal value = repo.deriveCurrentCashBalance(account.id);
|
||||||
Platform.runLater(() -> balanceField.setText(
|
Platform.runLater(() -> balanceField.setText(
|
||||||
CurrencyUtil.formatMoneyAsBasicNumber(new MoneyValue(value, account.getCurrency()))
|
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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +16,8 @@
|
||||||
</top>
|
</top>
|
||||||
<center>
|
<center>
|
||||||
<VBox>
|
<VBox>
|
||||||
<!-- Main account properties and actions -->
|
|
||||||
<FlowPane styleClass="std-padding,std-spacing">
|
|
||||||
<!-- Main account properties. -->
|
<!-- Main account properties. -->
|
||||||
<PropertiesPane vgap="5" hgap="5">
|
<PropertiesPane vgap="5" hgap="5" styleClass="std-padding,std-spacing">
|
||||||
<Label text="Name" styleClass="bold-text"/>
|
<Label text="Name" styleClass="bold-text"/>
|
||||||
<Label fx:id="accountNameLabel"/>
|
<Label fx:id="accountNameLabel"/>
|
||||||
|
|
||||||
|
@ -31,18 +30,26 @@
|
||||||
<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"/>
|
||||||
|
|
||||||
|
<Label text="Current Balance" styleClass="bold-text"/>
|
||||||
<VBox>
|
<VBox>
|
||||||
<Label text="Current Balance" styleClass="bold-text" fx:id="balanceLabel"/>
|
|
||||||
<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"/>
|
||||||
|
<Label
|
||||||
|
styleClass="small-font,secondary-color-fill"
|
||||||
|
>Derived using nearest recorded balance and transactions.</Label>
|
||||||
|
</VBox>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
</FlowPane>
|
|
||||||
|
|
||||||
<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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue