Added final validation and warnings to the CreateBalanceRecordController for inconsistent balance records.
This commit is contained in:
parent
5f692bf8e2
commit
65595a47ac
|
@ -15,9 +15,11 @@ import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmoun
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
|
@ -27,6 +29,7 @@ import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
public class CreateBalanceRecordController implements RouteSelectionListener {
|
public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
@FXML public TextField timestampField;
|
@FXML public TextField timestampField;
|
||||||
@FXML public TextField balanceField;
|
@FXML public TextField balanceField;
|
||||||
|
@FXML public Label balanceWarningLabel;
|
||||||
private FileSelectionArea attachmentSelectionArea;
|
private FileSelectionArea attachmentSelectionArea;
|
||||||
@FXML public PropertiesPane propertiesPane;
|
@FXML public PropertiesPane propertiesPane;
|
||||||
|
|
||||||
|
@ -44,9 +47,25 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
}).validatedInitially().attachToTextField(timestampField);
|
}).validatedInitially().attachToTextField(timestampField);
|
||||||
|
|
||||||
var balanceValid = new ValidationApplier<>(
|
var balanceValidator = new CurrencyAmountValidator(() -> account == null ? null : account.getCurrency(), true, false);
|
||||||
new CurrencyAmountValidator(() -> account == null ? null : account.getCurrency(), true, false)
|
var balanceValid = new ValidationApplier<>(balanceValidator)
|
||||||
).validatedInitially().attachToTextField(balanceField);
|
.validatedInitially().attachToTextField(balanceField);
|
||||||
|
|
||||||
|
balanceWarningLabel.managedProperty().bind(balanceWarningLabel.visibleProperty());
|
||||||
|
balanceWarningLabel.visibleProperty().set(false);
|
||||||
|
balanceField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (!balanceValidator.validate(newValue).isValid()) {
|
||||||
|
balanceWarningLabel.visibleProperty().set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigDecimal reportedBalance = new BigDecimal(newValue);
|
||||||
|
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
|
||||||
|
BigDecimal derivedBalance = repo.deriveCurrentBalance(account.id);
|
||||||
|
Platform.runLater(() -> balanceWarningLabel.visibleProperty().set(
|
||||||
|
!reportedBalance.setScale(derivedBalance.scale(), RoundingMode.HALF_UP).equals(derivedBalance)
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
var formValid = timestampValid.and(balanceValid);
|
var formValid = timestampValid.and(balanceValid);
|
||||||
saveButton.disableProperty().bind(formValid.not());
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
@ -64,30 +83,25 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
this.account = (Account) context;
|
this.account = (Account) context;
|
||||||
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
||||||
Thread.ofVirtual().start(() -> {
|
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
|
||||||
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
|
BigDecimal value = repo.deriveCurrentBalance(account.id);
|
||||||
BigDecimal value = repo.deriveCurrentBalance(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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void save() {
|
@FXML public void save() {
|
||||||
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("Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
|
boolean confirm = Popups.confirm("Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
|
||||||
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) {
|
if (confirm && confirmIfInconsistentBalance(reportedBalance)) {
|
||||||
Profile.getCurrent().getDataSource().useAccountRepository(accountRepo -> {
|
|
||||||
BigDecimal currentDerivedBalance = accountRepo.deriveCurrentBalance(account.id);
|
|
||||||
|
|
||||||
});
|
|
||||||
Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
|
Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
|
||||||
repo.insert(
|
repo.insert(
|
||||||
DateUtil.localToUTC(localTimestamp),
|
DateUtil.localToUTC(localTimestamp),
|
||||||
|
@ -104,4 +118,21 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
@FXML public void cancel() {
|
@FXML public void cancel() {
|
||||||
router.navigateBackAndClear();
|
router.navigateBackAndClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean confirmIfInconsistentBalance(BigDecimal reportedBalance) {
|
||||||
|
BigDecimal currentDerivedBalance;
|
||||||
|
try (var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository()) {
|
||||||
|
currentDerivedBalance = accountRepo.deriveCurrentBalance(account.id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())),
|
||||||
|
CurrencyUtil.formatMoney(new MoneyValue(currentDerivedBalance, account.getCurrency()))
|
||||||
|
);
|
||||||
|
return Popups.confirm(msg);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.andrewlalis.perfin.data;
|
package com.andrewlalis.perfin.data;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.pagination.Sort;
|
|
||||||
import com.andrewlalis.perfin.model.AccountEntry;
|
import com.andrewlalis.perfin.model.AccountEntry;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.andrewlalis.perfin.data.impl;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
||||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||||
import com.andrewlalis.perfin.data.pagination.Sort;
|
|
||||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
import com.andrewlalis.perfin.model.AccountEntry;
|
import com.andrewlalis.perfin.model.AccountEntry;
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class JdbcDataSource implements DataSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountRepository getAccountRepository() {
|
public AccountRepository getAccountRepository() {
|
||||||
return new JdbcAccountRepository(getConnection());
|
return new JdbcAccountRepository(getConnection(), contentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -39,7 +39,16 @@
|
||||||
<TextField fx:id="timestampField" styleClass="mono-font"/>
|
<TextField fx:id="timestampField" styleClass="mono-font"/>
|
||||||
|
|
||||||
<Label text="Balance" labelFor="${balanceField}" styleClass="bold-text"/>
|
<Label text="Balance" labelFor="${balanceField}" styleClass="bold-text"/>
|
||||||
<TextField fx:id="balanceField" styleClass="mono-font"/>
|
<VBox>
|
||||||
|
<TextField fx:id="balanceField" styleClass="mono-font"/>
|
||||||
|
<Label
|
||||||
|
fx:id="balanceWarningLabel"
|
||||||
|
styleClass="warning-color-text-fill,small-font"
|
||||||
|
wrapText="true"
|
||||||
|
text="Balance isn't what Perfin expects, according to your transactions."
|
||||||
|
/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
|
||||||
<Label text="Attachments" styleClass="bold-text"/>
|
<Label text="Attachments" styleClass="bold-text"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
|
@ -12,6 +12,7 @@ rather than with your own CSS.
|
||||||
-fx-theme-background-3: rgb(220, 220, 220);
|
-fx-theme-background-3: rgb(220, 220, 220);
|
||||||
-fx-theme-negative: rgb(247, 37, 69);
|
-fx-theme-negative: rgb(247, 37, 69);
|
||||||
-fx-theme-positive: rgb(43, 196, 77);
|
-fx-theme-positive: rgb(43, 196, 77);
|
||||||
|
-fx-theme-warning: rgb(250, 177, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
@ -127,6 +128,10 @@ rather than with your own CSS.
|
||||||
-fx-fill: -fx-theme-positive;
|
-fx-fill: -fx-theme-positive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning-color-text-fill {
|
||||||
|
-fx-text-fill: -fx-theme-warning;
|
||||||
|
}
|
||||||
|
|
||||||
/* DEBUG BORDERS */
|
/* DEBUG BORDERS */
|
||||||
.debug-border-1 {
|
.debug-border-1 {
|
||||||
-fx-border-color: red;
|
-fx-border-color: red;
|
||||||
|
|
Loading…
Reference in New Issue