Added better validation for creating transactions.

This commit is contained in:
Andrew Lalis 2023-12-28 11:55:10 -05:00
parent 69322620ca
commit c648c899cd
3 changed files with 62 additions and 16 deletions

View File

@ -26,7 +26,6 @@ public class PerfinApp extends Application {
@Override @Override
public void start(Stage stage) { public void start(Stage stage) {
// TODO: Cleanup the splash screen logic!
SplashScreenStage splashStage = new SplashScreenStage("Loading", SceneUtil.load("/startup-splash-screen.fxml")); SplashScreenStage splashStage = new SplashScreenStage("Loading", SceneUtil.load("/startup-splash-screen.fxml"));
splashStage.show(); splashStage.show();
defineRoutes(); defineRoutes();

View File

@ -16,6 +16,7 @@ import java.time.DateTimeException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
@ -27,6 +28,7 @@ public class CreateTransactionController implements RouteSelectionListener {
@FXML public TextField amountField; @FXML public TextField amountField;
@FXML public ChoiceBox<Currency> currencyChoiceBox; @FXML public ChoiceBox<Currency> currencyChoiceBox;
@FXML public TextArea descriptionField; @FXML public TextArea descriptionField;
@FXML public Label descriptionErrorLabel;
@FXML public ComboBox<Account> linkDebitAccountComboBox; @FXML public ComboBox<Account> linkDebitAccountComboBox;
@FXML public ComboBox<Account> linkCreditAccountComboBox; @FXML public ComboBox<Account> linkCreditAccountComboBox;
@ -41,6 +43,15 @@ public class CreateTransactionController implements RouteSelectionListener {
timestampInvalidLabel.setVisible(parsedTimestamp == null); timestampInvalidLabel.setVisible(parsedTimestamp == null);
timestampFutureLabel.setVisible(parsedTimestamp != null && parsedTimestamp.isAfter(LocalDateTime.now())); timestampFutureLabel.setVisible(parsedTimestamp != null && parsedTimestamp.isAfter(LocalDateTime.now()));
}); });
descriptionErrorLabel.managedProperty().bind(descriptionErrorLabel.visibleProperty());
descriptionErrorLabel.visibleProperty().bind(descriptionErrorLabel.textProperty().isNotEmpty());
descriptionField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && newValue.length() > 255) {
descriptionErrorLabel.setText("Description is too long.");
} else {
descriptionErrorLabel.setText(null);
}
});
linkedAccountsErrorLabel.managedProperty().bind(linkedAccountsErrorLabel.visibleProperty()); linkedAccountsErrorLabel.managedProperty().bind(linkedAccountsErrorLabel.visibleProperty());
linkedAccountsErrorLabel.visibleProperty().bind(linkedAccountsErrorLabel.textProperty().isNotEmpty()); linkedAccountsErrorLabel.visibleProperty().bind(linkedAccountsErrorLabel.textProperty().isNotEmpty());
linkDebitAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> onLinkedAccountsUpdated()); linkDebitAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> onLinkedAccountsUpdated());
@ -59,18 +70,28 @@ public class CreateTransactionController implements RouteSelectionListener {
} }
@FXML public void save() { @FXML public void save() {
// TODO: Validate data! var validationMessages = validateFormData();
if (!validationMessages.isEmpty()) {
LocalDateTime timestamp = parseTimestamp(); Alert alert = new Alert(
BigDecimal amount = new BigDecimal(amountField.getText()); Alert.AlertType.WARNING,
Currency currency = currencyChoiceBox.getValue(); "There are some issues with your data:\n\n" +
String description = descriptionField.getText().strip(); validationMessages.stream()
Map<Long, AccountEntry.Type> affectedAccounts = getSelectedAccounts(); .map(s -> "- " + s)
Transaction transaction = new Transaction(timestamp, amount, currency, description); .collect(Collectors.joining("\n\n"))
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { );
repo.insert(transaction, affectedAccounts); alert.show();
}); } else {
router.navigateBackAndClear(); LocalDateTime timestamp = parseTimestamp();
BigDecimal amount = new BigDecimal(amountField.getText());
Currency currency = currencyChoiceBox.getValue();
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
Map<Long, AccountEntry.Type> affectedAccounts = getSelectedAccounts();
Transaction transaction = new Transaction(timestamp, amount, currency, description);
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
repo.insert(transaction, affectedAccounts);
});
router.navigateBackAndClear();
}
} }
@FXML public void cancel() { @FXML public void cancel() {
@ -85,6 +106,7 @@ public class CreateTransactionController implements RouteSelectionListener {
private void resetForm() { private void resetForm() {
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT)); timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
amountField.setText("0"); amountField.setText("0");
descriptionField.setText(null);
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> { Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
var currencies = repo.findAllUsedCurrencies().stream() var currencies = repo.findAllUsedCurrencies().stream()
@ -92,7 +114,6 @@ public class CreateTransactionController implements RouteSelectionListener {
.toList(); .toList();
Platform.runLater(() -> { Platform.runLater(() -> {
currencyChoiceBox.getItems().setAll(currencies); currencyChoiceBox.getItems().setAll(currencies);
// TODO: cache most-recent currency for the app (maybe for different contexts).
currencyChoiceBox.getSelectionModel().selectFirst(); currencyChoiceBox.getSelectionModel().selectFirst();
}); });
}); });
@ -159,4 +180,29 @@ public class CreateTransactionController implements RouteSelectionListener {
linkedAccountsErrorLabel.setText(null); linkedAccountsErrorLabel.setText(null);
} }
} }
private List<String> validateFormData() {
List<String> errorMessages = new ArrayList<>();
if (parseTimestamp() == null) errorMessages.add("Invalid or missing timestamp.");
if (descriptionField.getText() != null && descriptionField.getText().strip().length() > 255) {
errorMessages.add("Description is too long.");
}
try {
BigDecimal value = new BigDecimal(amountField.getText());
if (value.compareTo(BigDecimal.ZERO) <= 0) {
errorMessages.add("Amount should be a positive number.");
}
} catch (NumberFormatException e) {
errorMessages.add("Invalid or missing amount.");
}
Account debitAccount = linkDebitAccountComboBox.getValue();
Account creditAccount = linkCreditAccountComboBox.getValue();
if (debitAccount == null && creditAccount == null) {
errorMessages.add("At least one account must be linked to this transaction.");
}
if (debitAccount != null && creditAccount != null && debitAccount.getId() == creditAccount.getId()) {
errorMessages.add("Credit and debit accounts cannot be the same.");
}
return errorMessages;
}
} }

View File

@ -26,8 +26,9 @@
</VBox> </VBox>
<VBox> <VBox>
<Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/> <Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/>
<Label text="Maximum of 256 characters." styleClass="small-text"/> <Label text="Maximum of 255 characters." styleClass="small-text"/>
<TextArea fx:id="descriptionField" styleClass="mono-font"/> <TextArea fx:id="descriptionField" styleClass="mono-font" wrapText="true"/>
<Label fx:id="descriptionErrorLabel" styleClass="error-text" wrapText="true"/>
</VBox> </VBox>
<VBox> <VBox>
<HBox spacing="3"> <HBox spacing="3">