Refactored the account-view.fxml and associated stuff to include the account's description.

This commit is contained in:
Andrew Lalis 2024-02-11 08:48:46 -05:00
parent b52148fd3b
commit 7f65466d6d
7 changed files with 112 additions and 63 deletions

View File

@ -7,34 +7,39 @@ import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.MoneyValue;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.BindingUtil;
import com.andrewlalis.perfin.view.component.AccountHistoryView;
import com.andrewlalis.perfin.view.component.PropertiesPane;
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
import javafx.application.Platform;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import java.time.*;
import static com.andrewlalis.perfin.PerfinApp.router;
public class AccountViewController implements RouteSelectionListener {
private Account account;
private final ObjectProperty<Account> accountProperty = new SimpleObjectProperty<>(null);
private final ObservableValue<Boolean> accountArchived = accountProperty.map(a -> a != null && a.isArchived());
private final StringProperty balanceTextProperty = new SimpleStringProperty(null);
@FXML public Label titleLabel;
@FXML public Label accountNameLabel;
@FXML public Label accountNumberLabel;
@FXML public Label accountCurrencyLabel;
@FXML public Label accountCreatedAtLabel;
@FXML public Label accountBalanceLabel;
@FXML public BooleanProperty accountArchivedProperty = new SimpleBooleanProperty(false);
@FXML public PropertiesPane descriptionPane;
@FXML public Text accountDescriptionText;
@FXML public AccountHistoryView accountHistory;
@ -44,11 +49,21 @@ public class AccountViewController implements RouteSelectionListener {
@FXML public Button balanceCheckerButton;
@FXML public void initialize() {
titleLabel.textProperty().bind(accountProperty.map(a -> "Account #" + a.id));
accountNameLabel.textProperty().bind(accountProperty.map(Account::getName));
accountNumberLabel.textProperty().bind(accountProperty.map(Account::getAccountNumber));
accountCurrencyLabel.textProperty().bind(accountProperty.map(a -> a.getCurrency().getDisplayName()));
accountCreatedAtLabel.textProperty().bind(accountProperty.map(a -> DateUtil.formatUTCAsLocalWithZone(a.getCreatedAt())));
accountDescriptionText.textProperty().bind(accountProperty.map(Account::getDescription));
var hasDescription = accountProperty.map(a -> a.getDescription() != null);
BindingUtil.bindManagedAndVisible(descriptionPane, hasDescription);
accountBalanceLabel.textProperty().bind(balanceTextProperty);
actionsBox.getChildren().forEach(node -> {
Button button = (Button) node;
BooleanExpression buttonActive = accountArchivedProperty;
ObservableValue<Boolean> buttonActive = accountArchived;
if (button.getText().equalsIgnoreCase("Unarchive")) {
buttonActive = buttonActive.not();
buttonActive = BooleanExpression.booleanExpression(buttonActive).not();
}
button.disableProperty().bind(buttonActive);
button.managedProperty().bind(button.visibleProperty());
@ -66,41 +81,42 @@ public class AccountViewController implements RouteSelectionListener {
.toInstant();
Profile.getCurrent().dataSource().mapRepoAsync(
AccountRepository.class,
repo -> repo.deriveBalance(account.id, timestamp)
repo -> repo.deriveBalance(getAccount().id, timestamp)
).thenAccept(balance -> Platform.runLater(() -> {
String msg = String.format(
"Your balance as of %s is %s, according to Perfin's data.",
date,
CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency()))
CurrencyUtil.formatMoney(new MoneyValue(balance, getAccount().getCurrency()))
);
Popups.message(balanceCheckerButton, msg);
}));
});
accountProperty.addListener((observable, oldValue, newValue) -> {
accountHistory.clear();
if (newValue == null) {
balanceTextProperty.set(null);
} else {
accountHistory.setAccountId(newValue.id);
accountHistory.loadMoreHistory();
Profile.getCurrent().dataSource().getAccountBalanceText(newValue)
.thenAccept(s -> Platform.runLater(() -> balanceTextProperty.set(s)));
}
});
}
@Override
public void onRouteSelected(Object context) {
account = (Account) context;
accountArchivedProperty.set(account.isArchived());
titleLabel.setText("Account #" + account.id);
accountNameLabel.setText(account.getName());
accountNumberLabel.setText(account.getAccountNumber());
accountCurrencyLabel.setText(account.getCurrency().getDisplayName());
accountCreatedAtLabel.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
Profile.getCurrent().dataSource().getAccountBalanceText(account)
.thenAccept(accountBalanceLabel::setText);
accountHistory.clear();
accountHistory.setAccountId(account.id);
accountHistory.loadMoreHistory();
this.accountProperty.set((Account) context);
}
@FXML
public void goToEditPage() {
router.navigate("edit-account", account);
router.navigate("edit-account", getAccount());
}
@FXML public void goToCreateBalanceRecord() {
router.navigate("create-balance-record", account);
router.navigate("create-balance-record", getAccount());
}
@FXML
@ -114,7 +130,7 @@ public class AccountViewController implements RouteSelectionListener {
"later if you need to."
);
if (confirmResult) {
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.archive(account.id));
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.archive(getAccount().id));
router.replace("accounts");
}
}
@ -126,7 +142,7 @@ public class AccountViewController implements RouteSelectionListener {
"status?"
);
if (confirm) {
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.unarchive(account.id));
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.unarchive(getAccount().id));
router.replace("accounts");
}
}
@ -142,8 +158,12 @@ public class AccountViewController implements RouteSelectionListener {
"want to hide it."
);
if (confirm) {
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.delete(account));
Profile.getCurrent().dataSource().useRepo(AccountRepository.class, repo -> repo.delete(getAccount()));
router.replace("accounts");
}
}
private Account getAccount() {
return accountProperty.get();
}
}

View File

@ -33,20 +33,14 @@ public class EditAccountController implements RouteSelectionListener {
private Account account;
private final BooleanProperty creatingNewAccount = new SimpleBooleanProperty(false);
@FXML
public Label titleLabel;
@FXML
public TextField accountNameField;
@FXML
public TextField accountNumberField;
@FXML
public ComboBox<Currency> accountCurrencyComboBox;
@FXML
public ChoiceBox<AccountType> accountTypeChoiceBox;
@FXML
public PropertiesPane initialBalanceContent;
@FXML
public TextField initialBalanceField;
@FXML public Label titleLabel;
@FXML public TextField accountNameField;
@FXML public TextField accountNumberField;
@FXML public ComboBox<Currency> accountCurrencyComboBox;
@FXML public ChoiceBox<AccountType> accountTypeChoiceBox;
@FXML public TextArea descriptionField;
@FXML public PropertiesPane initialBalanceContent;
@FXML public TextField initialBalanceField;
@FXML public Button saveButton;
@ -66,8 +60,12 @@ public class EditAccountController implements RouteSelectionListener {
new CurrencyAmountValidator(() -> accountCurrencyComboBox.getValue(), true, false)
).attachToTextField(initialBalanceField, accountCurrencyComboBox.valueProperty());
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
.addPredicate(s -> s == null || s.strip().length() <= Account.DESCRIPTION_MAX_LENGTH, "Description is too long.")
).attach(descriptionField, descriptionField.textProperty());
// Combine validity of all fields for an expression that determines if the whole form is valid.
BooleanExpression formValid = nameValid.and(numberValid).and(balanceValid.or(creatingNewAccount.not()));
BooleanExpression formValid = nameValid.and(numberValid).and(balanceValid.or(creatingNewAccount.not())).and(descriptionValid);
saveButton.disableProperty().bind(formValid.not());
List<Currency> priorityCurrencies = Stream.of("USD", "EUR", "GBP", "CAD", "AUD")
@ -111,6 +109,11 @@ public class EditAccountController implements RouteSelectionListener {
String number = accountNumberField.getText().strip();
AccountType type = accountTypeChoiceBox.getValue();
Currency currency = accountCurrencyComboBox.getValue();
String description = descriptionField.getText();
if (description != null) {
description = description.strip();
if (description.isBlank()) description = null;
}
try (
var accountRepo = Profile.getCurrent().dataSource().getAccountRepository();
var balanceRepo = Profile.getCurrent().dataSource().getBalanceRecordRepository()
@ -128,14 +131,14 @@ public class EditAccountController implements RouteSelectionListener {
);
boolean success = Popups.confirm(accountNameField, prompt);
if (success) {
long id = accountRepo.insert(type, number, name, currency);
long id = accountRepo.insert(type, number, name, currency, description);
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
// Once we create the new account, go to the account.
Account newAccount = accountRepo.findById(id).orElseThrow();
router.replace("account", newAccount);
}
} else {
accountRepo.update(account.id, type, number, name, currency);
accountRepo.update(account.id, type, number, name, currency, description);
Account updatedAccount = accountRepo.findById(account.id).orElseThrow();
router.replace("account", updatedAccount);
}
@ -157,11 +160,13 @@ public class EditAccountController implements RouteSelectionListener {
accountTypeChoiceBox.getSelectionModel().selectFirst();
accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD"));
initialBalanceField.setText(String.format("%.02f", 0f));
descriptionField.setText(null);
} else {
accountNameField.setText(account.getName());
accountNumberField.setText(account.getAccountNumber());
accountTypeChoiceBox.getSelectionModel().select(account.getType());
accountCurrencyComboBox.getSelectionModel().select(account.getCurrency());
descriptionField.setText(account.getDescription());
}
}
}

View File

@ -16,14 +16,14 @@ import java.util.Optional;
import java.util.Set;
public interface AccountRepository extends Repository, AutoCloseable {
long insert(AccountType type, String accountNumber, String name, Currency currency);
long insert(AccountType type, String accountNumber, String name, Currency currency, String description);
Page<Account> findAll(PageRequest pagination);
List<Account> findAllOrderedByRecentHistory();
List<Account> findTopNOrderedByRecentHistory(int n);
List<Account> findTopNRecentlyActive(int n, int daysSinceLastActive);
List<Account> findAllByCurrency(Currency currency);
Optional<Account> findById(long id);
void update(long accountId, AccountType type, String accountNumber, String name, Currency currency);
void update(long accountId, AccountType type, String accountNumber, String name, Currency currency, String description);
void delete(Account account);
void archive(long accountId);
void unarchive(long accountId);

View File

@ -23,18 +23,18 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
private static final Logger log = LoggerFactory.getLogger(JdbcAccountRepository.class);
@Override
public long insert(AccountType type, String accountNumber, String name, Currency currency) {
public long insert(AccountType type, String accountNumber, String name, Currency currency, String description) {
return DbUtil.doTransaction(conn, () -> {
long accountId = DbUtil.insertOne(
conn,
"INSERT INTO account (created_at, account_type, account_number, name, currency) VALUES (?, ?, ?, ?, ?)",
List.of(
"INSERT INTO account (created_at, account_type, account_number, name, currency, description) VALUES (?, ?, ?, ?, ?, ?)",
DbUtil.timestampFromUtcNow(),
type.name(),
accountNumber,
name,
currency.getCurrencyCode()
)
currency.getCurrencyCode(),
description
);
// Insert a history item indicating the creation of the account.
HistoryRepository historyRepo = new JdbcHistoryRepository(conn);
@ -210,7 +210,7 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
}
@Override
public void update(long accountId, AccountType type, String accountNumber, String name, Currency currency) {
public void update(long accountId, AccountType type, String accountNumber, String name, Currency currency, String description) {
DbUtil.doTransaction(conn, () -> {
Account account = findById(accountId).orElse(null);
if (account == null) return;
@ -231,6 +231,10 @@ public record JdbcAccountRepository(Connection conn, Path contentDir) implements
DbUtil.updateOne(conn, "UPDATE account SET currency = ? WHERE id = ?", currency.getCurrencyCode(), accountId);
updateMessages.add(String.format("Updated account currency from %s to %s.", account.getCurrency(), currency));
}
if (!Objects.equals(account.getDescription(), description)) {
DbUtil.updateOne(conn, "UPDATE account SET description = ? WHERE id = ?", description, accountId);
updateMessages.add("Updated account's description.");
}
if (!updateMessages.isEmpty()) {
var historyRepo = new JdbcHistoryRepository(conn);
long historyId = historyRepo.getOrCreateHistoryForAccount(accountId);

View File

@ -28,7 +28,13 @@ public final class DbUtil {
}
public static void setArgs(PreparedStatement stmt, Object... args) {
setArgs(stmt, List.of(args));
for (int i = 0; i < args.length; i++) {
try {
stmt.setObject(i + 1, args[i]);
} catch (SQLException e) {
throw new UncheckedSqlException("Failed to set parameter " + (i + 1) + " to " + args[i], e);
}
}
}
public static long getGeneratedId(PreparedStatement stmt) {
@ -107,6 +113,11 @@ public final class DbUtil {
}
public static void updateOne(Connection conn, String query, List<Object> args) {
Object[] argsArray = args.toArray();
updateOne(conn, query, argsArray);
}
public static void updateOne(Connection conn, String query, Object... args) {
try (var stmt = conn.prepareStatement(query)) {
setArgs(stmt, args);
int updateCount = stmt.executeUpdate();
@ -116,11 +127,12 @@ public final class DbUtil {
}
}
public static void updateOne(Connection conn, String query, Object... args) {
updateOne(conn, query, List.of(args));
public static long insertOne(Connection conn, String query, List<Object> args) {
Object[] argsArray = args.toArray();
return insertOne(conn, query, argsArray);
}
public static long insertOne(Connection conn, String query, List<Object> args) {
public static long insertOne(Connection conn, String query, Object... args) {
try (var stmt = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) {
setArgs(stmt, args);
int result = stmt.executeUpdate();
@ -131,10 +143,6 @@ public final class DbUtil {
}
}
public static long insertOne(Connection conn, String query, Object... args) {
return insertOne(conn, query, List.of(args));
}
public static Timestamp timestampFromUtcLDT(LocalDateTime utc) {
return Timestamp.from(utc.toInstant(ZoneOffset.UTC));
}

View File

@ -44,6 +44,11 @@
</PropertiesPane>
</FlowPane>
<PropertiesPane vgap="5" hgap="5" fx:id="descriptionPane">
<Label text="Description" styleClass="bold-text" labelFor="${accountDescriptionText}"/>
<TextFlow maxWidth="500"><Text fx:id="accountDescriptionText"/></TextFlow>
</PropertiesPane>
<!-- Action buttons -->
<HBox fx:id="actionsBox" styleClass="std-padding,std-spacing,small-font">
<Button text="Edit" onAction="#goToEditPage"/>

View File

@ -32,6 +32,13 @@
<Label text="Account Type" styleClass="bold-text"/>
<ChoiceBox fx:id="accountTypeChoiceBox"/>
<Label text="Description" styleClass="bold-text"/>
<TextArea
fx:id="descriptionField"
wrapText="true"
style="-fx-pref-height: 100px;-fx-min-height: 100px;"
/>
</PropertiesPane>
<!-- Initial balance content that's only visible when creating a new account. -->