Added editing, saving, and deleting accounts.

This commit is contained in:
Andrew Lalis 2023-12-26 13:47:27 -05:00
parent 30df89d5b7
commit 2a47d93c97
15 changed files with 163 additions and 33 deletions

View File

@ -29,7 +29,7 @@
<dependency> <dependency>
<groupId>com.andrewlalis</groupId> <groupId>com.andrewlalis</groupId>
<artifactId>javafx-scene-router</artifactId> <artifactId>javafx-scene-router</artifactId>
<version>1.4.0</version> <version>1.5.1</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -2,12 +2,15 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.Profile;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import static com.andrewlalis.perfin.PerfinApp.router;
public class AccountViewController implements RouteSelectionListener { public class AccountViewController implements RouteSelectionListener {
private Account account; private Account account;
@ -32,4 +35,18 @@ public class AccountViewController implements RouteSelectionListener {
accountCurrencyField.setText(account.getCurrency().getDisplayName()); accountCurrencyField.setText(account.getCurrency().getDisplayName());
accountCreatedAtField.setText(account.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); accountCreatedAtField.setText(account.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} }
@FXML
public void goToEditPage() {
router.navigate("edit-account", account);
}
@FXML
public void deleteAccount() {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
repo.delete(account);
});
router.getHistory().clear();
router.navigate("accounts");
}
} }

View File

@ -25,7 +25,11 @@ public class EditAccountController implements RouteSelectionListener {
@FXML @FXML
public ComboBox<Currency> accountCurrencyComboBox; public ComboBox<Currency> accountCurrencyComboBox;
@FXML @FXML
public ChoiceBox<String> accountTypeChoiceBox; public ChoiceBox<AccountType> accountTypeChoiceBox;
private boolean editingNewAccount() {
return account == null;
}
@FXML @FXML
public void initialize() { public void initialize() {
@ -39,16 +43,16 @@ public class EditAccountController implements RouteSelectionListener {
} }
accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD")); accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD"));
accountTypeChoiceBox.getItems().add("Checking"); accountTypeChoiceBox.getItems().add(AccountType.CHECKING);
accountTypeChoiceBox.getItems().add("Savings"); accountTypeChoiceBox.getItems().add(AccountType.SAVINGS);
accountTypeChoiceBox.getItems().add("Credit Card"); accountTypeChoiceBox.getItems().add(AccountType.CREDIT_CARD);
accountTypeChoiceBox.getSelectionModel().select("Checking"); accountTypeChoiceBox.getSelectionModel().select(AccountType.CHECKING);
} }
@Override @Override
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
this.account = (Account) context; this.account = (Account) context;
if (account == null) { if (editingNewAccount()) {
titleLabel.setText("Editing New Account"); titleLabel.setText("Editing New Account");
} else { } else {
titleLabel.setText("Editing Account: " + account.getName()); titleLabel.setText("Editing Account: " + account.getName());
@ -58,26 +62,38 @@ public class EditAccountController implements RouteSelectionListener {
@FXML @FXML
public void save() { public void save() {
if (account == null) { try (var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository()) {
// If we're editing a new account. if (editingNewAccount()) {
String name = accountNameField.getText().strip(); String name = accountNameField.getText().strip();
String number = accountNumberField.getText().strip(); String number = accountNumberField.getText().strip();
AccountType type = AccountType.parse(accountTypeChoiceBox.getValue()); AccountType type = accountTypeChoiceBox.getValue();
Currency currency = accountCurrencyComboBox.getValue(); Currency currency = accountCurrencyComboBox.getValue();
Account newAccount = new Account(type, number, name, currency); Account newAccount = new Account(type, number, name, currency);
Profile.getCurrent().getDataSource().getAccountRepository().insert(newAccount); long id = accountRepo.insert(newAccount);
Account savedAccount = accountRepo.findById(id).orElseThrow();
// Once we create the new account, go to the account. // Once we create the new account, go to the account.
router.getHistory().clear(); router.getHistory().clear();
router.navigate("accounts"); router.navigate("account", savedAccount);
} else { } else {
throw new IllegalStateException("Not implemented."); System.out.println("Updating account " + account.getName());
account.setName(accountNameField.getText().strip());
account.setAccountNumber(accountNumberField.getText().strip());
account.setType(accountTypeChoiceBox.getValue());
account.setCurrency(accountCurrencyComboBox.getValue());
accountRepo.update(account);
Account updatedAccount = accountRepo.findById(account.getId()).orElseThrow();
router.getHistory().clear();
router.navigate("account", updatedAccount);
}
} catch (Exception e) {
e.printStackTrace();
} }
} }
@FXML @FXML
public void cancel() { public void cancel() {
router.navigateBack(); router.navigateBackAndClear();
} }
public void resetForm() { public void resetForm() {
@ -87,7 +103,10 @@ public class EditAccountController implements RouteSelectionListener {
accountTypeChoiceBox.getSelectionModel().selectFirst(); accountTypeChoiceBox.getSelectionModel().selectFirst();
accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD")); accountCurrencyComboBox.getSelectionModel().select(Currency.getInstance("USD"));
} else { } else {
// TODO: Set to original account. accountNameField.setText(account.getName());
accountNumberField.setText(account.getAccountNumber());
accountTypeChoiceBox.getSelectionModel().select(account.getType());
accountCurrencyComboBox.getSelectionModel().select(account.getCurrency());
} }
} }
} }

View File

@ -21,7 +21,6 @@ public class MainViewController {
public void initialize() { public void initialize() {
AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView(); AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView();
mainContainer.setCenter(routerView.getAnchorPane()); mainContainer.setCenter(routerView.getAnchorPane());
routerView.getAnchorPane().setStyle("-fx-border-color: orange;");
// Set up a simple breadcrumb display in the top bar. // Set up a simple breadcrumb display in the top bar.
BindingUtil.mapContent( BindingUtil.mapContent(
breadcrumbHBox.getChildren(), breadcrumbHBox.getChildren(),
@ -40,6 +39,7 @@ public class MainViewController {
@FXML @FXML
public void goToAccounts() { public void goToAccounts() {
router.getHistory().clear();
router.navigate("accounts"); router.navigate("accounts");
} }

View File

@ -6,9 +6,11 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface AccountRepository { public interface AccountRepository extends AutoCloseable {
long insert(Account account); long insert(Account account);
List<Account> findAll(); List<Account> findAll();
Optional<Account> findById(long id); Optional<Account> findById(long id);
BigDecimal deriveCurrentBalance(long id); BigDecimal deriveCurrentBalance(long id);
void update(Account account);
void delete(Account account);
} }

View File

@ -2,4 +2,7 @@ package com.andrewlalis.perfin.data;
public interface DataSource { public interface DataSource {
AccountRepository getAccountRepository(); AccountRepository getAccountRepository();
default void useAccountRepository(ThrowableConsumer<AccountRepository> repoConsumer) {
DbUtil.useClosable(this::getAccountRepository, repoConsumer);
}
} }

View File

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
public final class DbUtil { public final class DbUtil {
private DbUtil() {} private DbUtil() {}
@ -90,4 +91,12 @@ public final class DbUtil {
public static LocalDateTime utcLDTFromTimestamp(Timestamp ts) { public static LocalDateTime utcLDTFromTimestamp(Timestamp ts) {
return ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime(); return ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime();
} }
public static <T extends AutoCloseable> void useClosable(Supplier<T> supplier, ThrowableConsumer<T> consumer) {
try (T t = supplier.get()) {
consumer.accept(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} }

View File

@ -0,0 +1,8 @@
package com.andrewlalis.perfin.data;
import java.sql.SQLException;
@FunctionalInterface
public interface SqlRunnable {
void run() throws SQLException;
}

View File

@ -0,0 +1,6 @@
package com.andrewlalis.perfin.data;
@FunctionalInterface
public interface ThrowableConsumer<T> {
void accept(T value) throws Exception;
}

View File

@ -2,6 +2,7 @@ package com.andrewlalis.perfin.data.impl;
import com.andrewlalis.perfin.data.AccountRepository; import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.DbUtil; import com.andrewlalis.perfin.data.DbUtil;
import com.andrewlalis.perfin.data.UncheckedSqlException;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.AccountType; import com.andrewlalis.perfin.model.AccountType;
@ -46,6 +47,32 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor
return BigDecimal.valueOf(0, 4); return BigDecimal.valueOf(0, 4);
} }
@Override
public void update(Account account) {
DbUtil.updateOne(
conn,
"UPDATE account SET name = ?, account_number = ?, currency = ?, account_type = ? WHERE id = ?",
List.of(
account.getName(),
account.getAccountNumber(),
account.getCurrency().getCurrencyCode(),
account.getType().name(),
account.getId()
)
);
}
@Override
public void delete(Account account) {
try (var stmt = conn.prepareStatement("DELETE FROM account WHERE id = ?")) {
stmt.setLong(1, account.getId());
int rows = stmt.executeUpdate();
if (rows != 1) throw new SQLException("Affected " + rows + " rows instead of expected 1.");
} catch (SQLException e) {
throw new UncheckedSqlException(e);
}
}
private static Account parseAccount(ResultSet rs) throws SQLException { private static Account parseAccount(ResultSet rs) throws SQLException {
long id = rs.getLong("id"); long id = rs.getLong("id");
LocalDateTime createdAt = DbUtil.utcLDTFromTimestamp(rs.getTimestamp("created_at")); LocalDateTime createdAt = DbUtil.utcLDTFromTimestamp(rs.getTimestamp("created_at"));
@ -55,4 +82,9 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor
Currency currency = Currency.getInstance(rs.getString("currency")); Currency currency = Currency.getInstance(rs.getString("currency"));
return new Account(id, createdAt, type, accountNumber, name, currency); return new Account(id, createdAt, type, accountNumber, name, currency);
} }
@Override
public void close() throws Exception {
conn.close();
}
} }

View File

@ -48,6 +48,22 @@ public class Account {
return currency; return currency;
} }
public void setType(AccountType type) {
this.type = type;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public void setName(String name) {
this.name = name;
}
public void setCurrency(Currency currency) {
this.currency = currency;
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

@ -1,9 +1,20 @@
package com.andrewlalis.perfin.model; package com.andrewlalis.perfin.model;
public enum AccountType { public enum AccountType {
CHECKING, CHECKING("Checking"),
SAVINGS, SAVINGS("Savings"),
CREDIT_CARD; CREDIT_CARD("Credit Card");
private final String name;
AccountType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
public static AccountType parse(String s) { public static AccountType parse(String s) {
s = s.strip().toUpperCase(); s = s.strip().toUpperCase();

View File

@ -8,13 +8,12 @@
fx:controller="com.andrewlalis.perfin.control.AccountViewController" fx:controller="com.andrewlalis.perfin.control.AccountViewController"
stylesheets="@style/account-view.css" stylesheets="@style/account-view.css"
styleClass="main-container" styleClass="main-container"
style="-fx-border-color: green;"
> >
<top> <top>
<Label fx:id="titleLabel"/> <Label fx:id="titleLabel"/>
</top> </top>
<center> <center>
<VBox style="-fx-border-color: blue;" > <VBox>
<VBox styleClass="account-property-box"> <VBox styleClass="account-property-box">
<Label text="Name"/> <Label text="Name"/>
<TextField fx:id="accountNameField" editable="false"/> <TextField fx:id="accountNameField" editable="false"/>
@ -34,9 +33,10 @@
</VBox> </VBox>
</center> </center>
<right> <right>
<VBox style="-fx-border-color: red;"> <VBox styleClass="actions-box">
<Button text="Edit"/> <Label text="Actions" style="-fx-font-weight: bold;"/>
<Button text="Delete"/> <Button text="Edit" onAction="#goToEditPage"/>
<Button text="Delete" onAction="#deleteAccount"/>
</VBox> </VBox>
</right> </right>
</BorderPane> </BorderPane>

View File

@ -8,7 +8,6 @@
fx:id="mainContainer" fx:id="mainContainer"
fx:controller="com.andrewlalis.perfin.control.MainViewController" fx:controller="com.andrewlalis.perfin.control.MainViewController"
stylesheets="@style/main-view.css" stylesheets="@style/main-view.css"
style="-fx-border-color: purple;"
> >
<top> <top>
<VBox> <VBox>

View File

@ -24,3 +24,11 @@
#accountNumberField { #accountNumberField {
-fx-font-family: monospace; -fx-font-family: monospace;
} }
.actions-box {
-fx-spacing: 3px;
}
.actions-box > Button {
-fx-max-width: 500px;
}