diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java index 34424b0..f66818a 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java @@ -1,32 +1,40 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; +import com.andrewlalis.perfin.data.AccountHistoryItemRepository; +import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.model.history.AccountHistoryItem; +import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Label; -import javafx.scene.control.TextField; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.time.LocalDateTime; +import java.util.List; import static com.andrewlalis.perfin.PerfinApp.router; public class AccountViewController implements RouteSelectionListener { private Account account; - @FXML - public Label titleLabel; - @FXML - public TextField accountNameField; - @FXML - public TextField accountNumberField; - @FXML - public TextField accountCreatedAtField; - @FXML - public TextField accountCurrencyField; - @FXML - public TextField accountBalanceField; + @FXML public Label titleLabel; + @FXML public TextField accountNameField; + @FXML public TextField accountNumberField; + @FXML public TextField accountCreatedAtField; + @FXML public TextField accountCurrencyField; + @FXML public TextField accountBalanceField; + + @FXML public VBox historyItemsVBox; + @FXML public Button loadMoreHistoryButton; + private LocalDateTime loadHistoryFrom; + private final int historyLoadSize = 5; @Override public void onRouteSelected(Object context) { @@ -38,6 +46,11 @@ public class AccountViewController implements RouteSelectionListener { accountCurrencyField.setText(account.getCurrency().getDisplayName()); accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt())); Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText); + + loadHistoryFrom = DateUtil.nowAsUTC(); + historyItemsVBox.getChildren().clear(); + loadMoreHistoryButton.setDisable(false); + loadMoreHistory(); } @FXML @@ -78,4 +91,56 @@ public class AccountViewController implements RouteSelectionListener { router.navigate("accounts"); } } + + @FXML public void loadMoreHistory() { + Thread.ofVirtual().start(() -> { + try (var historyRepo = Profile.getCurrent().getDataSource().getAccountHistoryItemRepository()) { + List historyItems = historyRepo.findMostRecentForAccount( + account.getId(), + loadHistoryFrom, + historyLoadSize + ); + if (historyItems.size() < historyLoadSize) { + Platform.runLater(() -> loadMoreHistoryButton.setDisable(true)); + } else { + loadHistoryFrom = historyItems.getLast().getTimestamp(); + } + List nodes = historyItems.stream().map(item -> visualizeHistoryItem(item, historyRepo)).toList(); + Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private Node visualizeHistoryItem(AccountHistoryItem item, AccountHistoryItemRepository repo) { + BorderPane containerPane = new BorderPane(); + containerPane.setStyle(""" + -fx-border-color: lightgray; + -fx-border-radius: 5px; + -fx-padding: 5px; + """); + Label timestampLabel = new Label(item.getTimestamp().format(DateUtil.DEFAULT_DATETIME_FORMAT)); + timestampLabel.setStyle("-fx-font-size: small;"); + containerPane.setTop(timestampLabel); + containerPane.setCenter(switch (item.getType()) { + case TEXT -> { + var text = repo.getTextItem(item.getId()); + yield new TextFlow(new Text(text)); + } + case ACCOUNT_ENTRY -> { + var entry = repo.getAccountEntryItem(item.getId()); + Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency())); + TextFlow text = new TextFlow(new Text("Entry added with value of "), amountText); + yield text; + } + case BALANCE_RECORD -> { + var balanceRecord = repo.getBalanceRecordItem(item.getId()); + Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency())); + TextFlow text = new TextFlow(new Text("Balance record added with value of "), amountText); + yield text; + } + }); + return containerPane; + } } diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java index 0413e07..159a3ad 100644 --- a/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java @@ -1,9 +1,18 @@ package com.andrewlalis.perfin.data; +import com.andrewlalis.perfin.model.AccountEntry; +import com.andrewlalis.perfin.model.BalanceRecord; +import com.andrewlalis.perfin.model.history.AccountHistoryItem; + import java.time.LocalDateTime; +import java.util.List; public interface AccountHistoryItemRepository extends AutoCloseable { void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId); void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId); void recordText(LocalDateTime timestamp, long accountId, String text); + List findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count); + String getTextItem(long itemId); + AccountEntry getAccountEntryItem(long itemId); + BalanceRecord getBalanceRecordItem(long itemId); } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountHistoryItemRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountHistoryItemRepository.java index 5c501c1..eda1a7c 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountHistoryItemRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAccountHistoryItemRepository.java @@ -2,9 +2,14 @@ package com.andrewlalis.perfin.data.impl; import com.andrewlalis.perfin.data.AccountHistoryItemRepository; import com.andrewlalis.perfin.data.util.DbUtil; +import com.andrewlalis.perfin.model.AccountEntry; +import com.andrewlalis.perfin.model.BalanceRecord; +import com.andrewlalis.perfin.model.history.AccountHistoryItem; import com.andrewlalis.perfin.model.history.AccountHistoryItemType; import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDateTime; import java.util.List; @@ -34,16 +39,73 @@ public record JdbcAccountHistoryItemRepository(Connection conn) implements Accou long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.TEXT); DbUtil.insertOne( conn, - "INSERT INTO account_history_item_account_entry (item_id, description) VALUES (?, ?)", + "INSERT INTO account_history_item_text (item_id, description) VALUES (?, ?)", List.of(itemId, text) ); } + @Override + public List findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count) { + return DbUtil.findAll( + conn, + "SELECT * FROM account_history_item WHERE account_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT " + count, + List.of(accountId, DbUtil.timestampFromUtcLDT(utcTimestamp)), + JdbcAccountHistoryItemRepository::parseHistoryItem + ); + } + + @Override + public String getTextItem(long itemId) { + return DbUtil.findOne( + conn, + "SELECT description FROM account_history_item_text WHERE item_id = ?", + List.of(itemId), + rs -> rs.getString(1) + ).orElse(null); + } + + @Override + public AccountEntry getAccountEntryItem(long itemId) { + return DbUtil.findOne( + conn, + """ + SELECT * + FROM account_entry + LEFT JOIN account_history_item_account_entry h ON h.entry_id = account_entry.id + WHERE h.item_id = ?""", + List.of(itemId), + JdbcAccountEntryRepository::parse + ).orElse(null); + } + + @Override + public BalanceRecord getBalanceRecordItem(long itemId) { + return DbUtil.findOne( + conn, + """ + SELECT * + FROM balance_record + LEFT JOIN account_history_item_balance_record h ON h.record_id = balance_record.id + WHERE h.item_id = ?""", + List.of(itemId), + JdbcBalanceRecordRepository::parse + ).orElse(null); + } + @Override public void close() throws Exception { conn.close(); } + public static AccountHistoryItem parseHistoryItem(ResultSet rs) throws SQLException { + return new AccountHistoryItem( + rs.getLong("id"), + DbUtil.utcLDTFromTimestamp(rs.getTimestamp("timestamp")), + rs.getLong("account_id"), + AccountHistoryItemType.valueOf(rs.getString("type")) + ); + } + private long insertHistoryItem(LocalDateTime timestamp, long accountId, AccountHistoryItemType type) { return DbUtil.insertOne( conn, diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java index 5695d24..f921895 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java @@ -23,7 +23,7 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl long recordId = DbUtil.insertOne( conn, "INSERT INTO balance_record (timestamp, account_id, balance, currency) VALUES (?, ?, ?, ?)", - List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, balance, currency) + List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, balance, currency.getCurrencyCode()) ); // Insert attachments. AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir); diff --git a/src/main/java/com/andrewlalis/perfin/model/TransactionAttachment.java b/src/main/java/com/andrewlalis/perfin/model/TransactionAttachment.java deleted file mode 100644 index cdfb218..0000000 --- a/src/main/java/com/andrewlalis/perfin/model/TransactionAttachment.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.andrewlalis.perfin.model; - -import com.andrewlalis.perfin.data.util.DateUtil; - -import java.nio.file.Path; -import java.time.LocalDateTime; - -/** - * A file that's been attached to a transaction as additional context for it, - * like a receipt or invoice copy. - */ -@Deprecated -public class TransactionAttachment { - private long id; - private LocalDateTime uploadedAt; - private long transactionId; - - private String filename; - private String contentType; - - public TransactionAttachment(String filename, String contentType) { - this.filename = filename; - this.contentType = contentType; - } - - public TransactionAttachment(long id, LocalDateTime uploadedAt, long transactionId, String filename, String contentType) { - this.id = id; - this.uploadedAt = uploadedAt; - this.transactionId = transactionId; - this.filename = filename; - this.contentType = contentType; - } - - public long getId() { - return id; - } - - public LocalDateTime getUploadedAt() { - return uploadedAt; - } - - public long getTransactionId() { - return transactionId; - } - - public String getFilename() { - return filename; - } - - public String getContentType() { - return contentType; - } - - public Path getPath() { - String uploadDateStr = uploadedAt.format(DateUtil.DEFAULT_DATE_FORMAT); - return Profile.getContentDir(Profile.getCurrent().getName()) - .resolve("transaction-attachments") - .resolve(uploadDateStr) - .resolve("tx-" + transactionId) - .resolve(filename); - } -} diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml index 6a8ca3c..461a4f8 100644 --- a/src/main/resources/account-view.fxml +++ b/src/main/resources/account-view.fxml @@ -43,6 +43,24 @@