diff --git a/README.md b/README.md index 7cf8a10..f6b64de 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,12 @@ interface and interoperable file formats for maximum compatibility. ## About Perfin -Perfin is a desktop app built with Java 21 and JavaFX, using the SQLite3 -database for most data storage. It's intended to be used by individuals to -track their finances across multiple accounts (savings, checking, credit, etc.). +Perfin is a desktop app built with Java 21 and JavaFX. It's intended to be used +by individuals to track their finances across multiple accounts (savings, +checking, credit, etc.). Because the app lives and works entirely on your local computer, you can rest assured that your data remains completely private. + +Currently, the application is still a work-in-progress, and is not yet suitable +for actual usage with your real financial data, so stay tuned for updates. diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java index 31c77b3..3b8415f 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java @@ -4,7 +4,10 @@ import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.data.DateUtil; import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Profile; +import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.control.Label; import javafx.scene.control.TextField; @@ -34,7 +37,7 @@ public class AccountViewController implements RouteSelectionListener { accountNameField.setText(account.getName()); accountNumberField.setText(account.getAccountNumber()); accountCurrencyField.setText(account.getCurrency().getDisplayName()); - accountCreatedAtField.setText(account.getCreatedAt().format(DateUtil.DEFAULT_DATETIME_FORMAT)); + accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt())); Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText); } @@ -43,10 +46,37 @@ public class AccountViewController implements RouteSelectionListener { router.navigate("edit-account", account); } + @FXML + public void archiveAccount() { + var confirmResult = new Alert( + Alert.AlertType.CONFIRMATION, + "Are you sure you want to archive this account? It will no " + + "longer show up in the app normally, and you won't be " + + "able to add new transactions to it. You'll still be " + + "able to view the account, and you can un-archive it " + + "later if you need to." + ).showAndWait(); + if (confirmResult.isPresent() && confirmResult.get() == ButtonType.OK) { + Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.archive(account)); + router.getHistory().clear(); + router.navigate("accounts"); + } + } + @FXML public void deleteAccount() { - Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.delete(account)); - router.getHistory().clear(); - router.navigate("accounts"); + var confirmResult = new Alert( + Alert.AlertType.CONFIRMATION, + "Are you sure you want to permanently delete this account and " + + "all data directly associated with it? This cannot be " + + "undone; deleted accounts are not recoverable at all. " + + "Consider archiving this account instead if you just " + + "want to hide it." + ).showAndWait(); + if (confirmResult.isPresent() && confirmResult.get() == ButtonType.OK) { + Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.delete(account)); + router.getHistory().clear(); + router.navigate("accounts"); + } } } diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java index acab5b9..2c25beb 100644 --- a/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/AccountRepository.java @@ -19,6 +19,7 @@ public interface AccountRepository extends AutoCloseable { Optional findById(long id); void update(Account account); void delete(Account account); + void archive(Account account); BigDecimal deriveBalance(long id, Instant timestamp); default BigDecimal deriveCurrentBalance(long id) { diff --git a/src/main/java/com/andrewlalis/perfin/data/DbUtil.java b/src/main/java/com/andrewlalis/perfin/data/DbUtil.java index df785c8..8b63560 100644 --- a/src/main/java/com/andrewlalis/perfin/data/DbUtil.java +++ b/src/main/java/com/andrewlalis/perfin/data/DbUtil.java @@ -69,6 +69,15 @@ public final class DbUtil { return findOne(conn, query, List.of(id), mapper); } + public static int update(Connection conn, String query, List args) { + try (var stmt = conn.prepareStatement(query)) { + setArgs(stmt, args); + return stmt.executeUpdate(); + } catch (SQLException e) { + throw new UncheckedSqlException(e); + } + } + public static void updateOne(Connection conn, String query, List args) { try (var stmt = conn.prepareStatement(query)) { setArgs(stmt, args); diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java index 0d20381..86d6a94 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountRepository.java @@ -2,7 +2,6 @@ package com.andrewlalis.perfin.data.impl; import com.andrewlalis.perfin.data.AccountRepository; import com.andrewlalis.perfin.data.DbUtil; -import com.andrewlalis.perfin.data.UncheckedSqlException; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.model.Account; @@ -36,14 +35,14 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor @Override public Page findAll(PageRequest pagination) { - return DbUtil.findAll(conn, "SELECT * FROM account", pagination, JdbcAccountRepository::parseAccount); + return DbUtil.findAll(conn, "SELECT * FROM account WHERE NOT archived", pagination, JdbcAccountRepository::parseAccount); } @Override public List findAllByCurrency(Currency currency) { return DbUtil.findAll( conn, - "SELECT * FROM account WHERE currency = ? ORDER BY created_at", + "SELECT * FROM account WHERE currency = ? AND NOT archived ORDER BY created_at", List.of(currency.getCurrencyCode()), JdbcAccountRepository::parseAccount ); @@ -115,7 +114,7 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor public Set findAllUsedCurrencies() { return new HashSet<>(DbUtil.findAll( conn, - "SELECT currency FROM account ORDER BY currency ASC", + "SELECT currency FROM account WHERE NOT archived ORDER BY currency ASC", rs -> Currency.getInstance(rs.getString(1)) )); } @@ -137,23 +136,23 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor @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); - } + DbUtil.updateOne(conn, "DELETE FROM account WHERE id = ?", List.of(account.getId())); + } + + @Override + public void archive(Account account) { + DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(account.getId())); } public static Account parseAccount(ResultSet rs) throws SQLException { long id = rs.getLong("id"); LocalDateTime createdAt = DbUtil.utcLDTFromTimestamp(rs.getTimestamp("created_at")); + boolean archived = rs.getBoolean("archived"); AccountType type = AccountType.valueOf(rs.getString("account_type").toUpperCase()); String accountNumber = rs.getString("account_number"); String name = rs.getString("name"); Currency currency = Currency.getInstance(rs.getString("currency")); - return new Account(id, createdAt, type, accountNumber, name, currency); + return new Account(id, createdAt, archived, type, accountNumber, name, currency); } @Override diff --git a/src/main/java/com/andrewlalis/perfin/model/Account.java b/src/main/java/com/andrewlalis/perfin/model/Account.java index 69e2002..6d73a35 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Account.java +++ b/src/main/java/com/andrewlalis/perfin/model/Account.java @@ -10,15 +10,17 @@ import java.util.Currency; public class Account { private long id; private LocalDateTime createdAt; + private boolean archived; private AccountType type; private String accountNumber; private String name; private Currency currency; - public Account(long id, LocalDateTime createdAt, AccountType type, String accountNumber, String name, Currency currency) { + public Account(long id, LocalDateTime createdAt, boolean archived, AccountType type, String accountNumber, String name, Currency currency) { this.id = id; this.createdAt = createdAt; + this.archived = archived; this.type = type; this.accountNumber = accountNumber; this.name = name; @@ -26,6 +28,7 @@ public class Account { } public Account(AccountType type, String accountNumber, String name, Currency currency) { + this.archived = false; this.type = type; this.accountNumber = accountNumber; this.name = name; @@ -81,4 +84,8 @@ public class Account { public long getId() { return id; } + + public boolean isArchived() { + return archived; + } } diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml index 6dbf9a1..e141449 100644 --- a/src/main/resources/account-view.fxml +++ b/src/main/resources/account-view.fxml @@ -14,32 +14,38 @@
- - - - - - - - - - + + + + + + + + + + + + + + +