Refactored the account-view.fxml and associated stuff to include the account's description.
This commit is contained in:
parent
b52148fd3b
commit
7f65466d6d
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
DbUtil.timestampFromUtcNow(),
|
||||
type.name(),
|
||||
accountNumber,
|
||||
name,
|
||||
currency.getCurrencyCode()
|
||||
)
|
||||
"INSERT INTO account (created_at, account_type, account_number, name, currency, description) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
DbUtil.timestampFromUtcNow(),
|
||||
type.name(),
|
||||
accountNumber,
|
||||
name,
|
||||
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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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. -->
|
||||
|
|
Loading…
Reference in New Issue