Added transaction attachments.
This commit is contained in:
parent
c648c899cd
commit
616cac6c18
|
@ -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.
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ public interface AccountRepository extends AutoCloseable {
|
|||
Optional<Account> 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) {
|
||||
|
|
|
@ -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<Object> 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<Object> args) {
|
||||
try (var stmt = conn.prepareStatement(query)) {
|
||||
setArgs(stmt, args);
|
||||
|
|
|
@ -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<Account> 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<Account> 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<Currency> 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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,32 +14,38 @@
|
|||
</top>
|
||||
<center>
|
||||
<VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Name"/>
|
||||
<TextField fx:id="accountNameField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Number"/>
|
||||
<TextField fx:id="accountNumberField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Currency"/>
|
||||
<TextField fx:id="accountCurrencyField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Created At"/>
|
||||
<TextField fx:id="accountCreatedAtField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Current Balance"/>
|
||||
<TextField fx:id="accountBalanceField" editable="false"/>
|
||||
</VBox>
|
||||
<HBox>
|
||||
<!-- Main account properties. -->
|
||||
<VBox HBox.hgrow="SOMETIMES">
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Name"/>
|
||||
<TextField fx:id="accountNameField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Number"/>
|
||||
<TextField fx:id="accountNumberField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Currency"/>
|
||||
<TextField fx:id="accountCurrencyField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Created At"/>
|
||||
<TextField fx:id="accountCreatedAtField" editable="false"/>
|
||||
</VBox>
|
||||
<VBox styleClass="account-property-box">
|
||||
<Label text="Current Balance"/>
|
||||
<TextField fx:id="accountBalanceField" editable="false"/>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</center>
|
||||
<right>
|
||||
<VBox styleClass="actions-box">
|
||||
<Label text="Actions" style="-fx-font-weight: bold;"/>
|
||||
<Button text="Edit" onAction="#goToEditPage"/>
|
||||
<Button text="Archive" onAction="#archiveAccount"/>
|
||||
<Button text="Delete" onAction="#deleteAccount"/>
|
||||
</VBox>
|
||||
</right>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
>
|
||||
<top>
|
||||
<VBox>
|
||||
<HBox>
|
||||
<HBox style="-fx-spacing: 3px; -fx-padding: 3px;">
|
||||
<Button text="Back" onAction="#goBack"/>
|
||||
<Button text="Forward" onAction="#goForward"/>
|
||||
<Button text="Accounts" onAction="#goToAccounts"/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
CREATE TABLE account (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
account_type VARCHAR(31) NOT NULL,
|
||||
account_number VARCHAR(255) NOT NULL UNIQUE,
|
||||
name VARCHAR(63) NOT NULL,
|
||||
|
@ -31,6 +32,17 @@ CREATE TABLE account_entry (
|
|||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE transaction_attachment (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
uploaded_at TIMESTAMP NOT NULL,
|
||||
transaction_id BIGINT NOT NULL,
|
||||
content_path VARCHAR(1024) NOT NULL,
|
||||
content_type VARCHAR(255) NOT NULL,
|
||||
CONSTRAINT fk_transaction_attachment_transaction
|
||||
FOREIGN KEY (transaction_id) REFERENCES transaction(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE balance_record (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
|
|
Loading…
Reference in New Issue