From 173204c61c57125c169fe09259c8c0280f174b18 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Mon, 1 Jan 2024 09:32:51 -0500 Subject: [PATCH] Added the ability to navigate to a specific transaction. --- .../control/TransactionViewController.java | 17 ++++---- .../control/TransactionsViewController.java | 39 ++++++++++++++++--- .../perfin/data/TransactionRepository.java | 3 ++ .../data/impl/JdbcTransactionRepository.java | 22 ++++++++--- .../andrewlalis/perfin/model/Transaction.java | 20 +++++----- .../component/AccountHistoryItemTile.java | 9 ++++- .../view/component/AttachmentPreview.java | 5 ++- .../DataSourcePaginationControls.java | 13 ++++--- 8 files changed, 90 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java index 038a379..3aae013 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -54,14 +54,14 @@ public class TransactionViewController { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId()); Platform.runLater(() -> { - if (accounts.hasDebit()) { - debitAccountLink.setText(accounts.debitAccount().getShortName()); - debitAccountLink.setOnAction(event -> router.navigate("account", accounts.debitAccount())); - } - if (accounts.hasCredit()) { - creditAccountLink.setText(accounts.creditAccount().getShortName()); - creditAccountLink.setOnAction(event -> router.navigate("account", accounts.creditAccount())); - } + accounts.ifDebit(acc -> { + debitAccountLink.setText(acc.getShortName()); + debitAccountLink.setOnAction(event -> router.navigate("account", acc)); + }); + accounts.ifCredit(acc -> { + creditAccountLink.setText(acc.getShortName()); + creditAccountLink.setOnAction(event -> router.navigate("account", acc)); + }); }); }); }); @@ -105,5 +105,6 @@ public class TransactionViewController { TextFlow parent = (TextFlow) link.getParent(); parent.managedProperty().bind(parent.visibleProperty()); parent.visibleProperty().bind(link.textProperty().isNotEmpty()); + link.setText(null); } } diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java index e197d2f..ead3f71 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java @@ -1,13 +1,13 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; -import com.andrewlalis.perfin.data.util.Pair; -import com.andrewlalis.perfin.view.SceneUtil; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.Sort; +import com.andrewlalis.perfin.data.util.Pair; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Transaction; +import com.andrewlalis.perfin.view.SceneUtil; import com.andrewlalis.perfin.view.component.DataSourcePaginationControls; import com.andrewlalis.perfin.view.component.TransactionTile; import javafx.beans.property.ObjectProperty; @@ -19,9 +19,21 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import java.util.List; + import static com.andrewlalis.perfin.PerfinApp.router; +/** + * Controller for the view of all transactions in a user's profile. + * Transactions are displayed in a paginated manner, and this controller + * accepts as a route context a {@link PageRequest} to initialize the results + * to a specific page. + */ public class TransactionsViewController implements RouteSelectionListener { + public static List DEFAULT_SORTS = List.of(Sort.desc("timestamp")); + public static int DEFAULT_ITEMS_PER_PAGE = 5; + public record RouteContext(Long selectedTransactionId) {} + @FXML public BorderPane transactionsListBorderPane; @FXML public VBox transactionsVBox; private DataSourcePaginationControls paginationControls; @@ -80,8 +92,23 @@ public class TransactionsViewController implements RouteSelectionListener { @Override public void onRouteSelected(Object context) { - paginationControls.sorts.setAll(Sort.desc("timestamp")); - this.paginationControls.setPage(1); + paginationControls.sorts.setAll(DEFAULT_SORTS); + paginationControls.itemsPerPage.set(DEFAULT_ITEMS_PER_PAGE); + + // If a transaction id is given in the route context, navigate to the page it's on and select it. + if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) { + Thread.ofVirtual().start(() -> { + Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { + repo.findById(ctx.selectedTransactionId).ifPresent(tx -> { + long offset = repo.countAllAfter(tx.getId()); + int pageNumber = (int) (offset / DEFAULT_ITEMS_PER_PAGE) + 1; + paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx)); + }); + }); + }); + } else { + paginationControls.setPage(1); + } } @FXML @@ -92,13 +119,13 @@ public class TransactionsViewController implements RouteSelectionListener { private TransactionTile makeTile(Transaction transaction) { var tile = new TransactionTile(transaction); tile.setOnMouseClicked(event -> { - if (selectedTransaction.get() == null || selectedTransaction.get().getId() != transaction.getId()) { + if (selectedTransaction.get() == null || !selectedTransaction.get().equals(transaction)) { selectedTransaction.set(transaction); } else { selectedTransaction.set(null); } }); - tile.selected.bind(selectedTransaction.map(t -> t != null && t.getId() == transaction.getId())); + tile.selected.bind(selectedTransaction.map(t -> t != null && t.equals(transaction))); return tile; } } diff --git a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java index 4567318..0988441 100644 --- a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java @@ -11,6 +11,7 @@ import java.nio.file.Path; import java.time.LocalDateTime; import java.util.Currency; import java.util.List; +import java.util.Optional; import java.util.Set; public interface TransactionRepository extends AutoCloseable { @@ -22,8 +23,10 @@ public interface TransactionRepository extends AutoCloseable { CreditAndDebitAccounts linkedAccounts, List attachments ); + Optional findById(long id); Page findAll(PageRequest pagination); long countAll(); + long countAllAfter(long transactionId); Page findAllByAccounts(Set accountIds, PageRequest pagination); CreditAndDebitAccounts findLinkedAccounts(long transactionId); List findAttachments(long transactionId); diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java index 7681fcc..ed3ddd1 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java @@ -2,10 +2,10 @@ package com.andrewlalis.perfin.data.impl; import com.andrewlalis.perfin.data.AccountEntryRepository; import com.andrewlalis.perfin.data.AttachmentRepository; -import com.andrewlalis.perfin.data.util.DbUtil; import com.andrewlalis.perfin.data.TransactionRepository; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; +import com.andrewlalis.perfin.data.util.DbUtil; import com.andrewlalis.perfin.model.*; import java.math.BigDecimal; @@ -14,10 +14,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Currency; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; public record JdbcTransactionRepository(Connection conn, Path contentDir) implements TransactionRepository { @@ -55,6 +52,11 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem }); } + @Override + public Optional findById(long id) { + return DbUtil.findById(conn, "SELECT * FROM transaction WHERE id = ?", id, JdbcTransactionRepository::parseTransaction); + } + @Override public Page findAll(PageRequest pagination) { return DbUtil.findAll( @@ -70,6 +72,16 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem return DbUtil.findOne(conn, "SELECT COUNT(id) FROM transaction", Collections.emptyList(), rs -> rs.getLong(1)).orElse(0L); } + @Override + public long countAllAfter(long transactionId) { + return DbUtil.findOne( + conn, + "SELECT COUNT(id) FROM transaction WHERE timestamp > (SELECT timestamp FROM transaction WHERE id = ?)", + List.of(transactionId), + rs -> rs.getLong(1) + ).orElse(0L); + } + @Override public Page findAllByAccounts(Set accountIds, PageRequest pagination) { String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(",")); diff --git a/src/main/java/com/andrewlalis/perfin/model/Transaction.java b/src/main/java/com/andrewlalis/perfin/model/Transaction.java index ae00d4e..6527987 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Transaction.java +++ b/src/main/java/com/andrewlalis/perfin/model/Transaction.java @@ -11,11 +11,11 @@ import java.util.Currency; * entries that apply this transaction's amount to one or more accounts. */ public class Transaction { - private long id; - private LocalDateTime timestamp; + private final long id; + private final LocalDateTime timestamp; - private BigDecimal amount; - private Currency currency; + private final BigDecimal amount; + private final Currency currency; private String description; public Transaction(long id, LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) { @@ -26,13 +26,6 @@ public class Transaction { this.description = description; } - public Transaction(LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) { - this.timestamp = timestamp; - this.amount = amount; - this.currency = currency; - this.description = description; - } - public long getId() { return id; } @@ -52,4 +45,9 @@ public class Transaction { public String getDescription() { return description; } + + @Override + public boolean equals(Object other) { + return other instanceof Transaction tx && id == tx.id; + } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java index 20d90f2..84bc8bb 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java @@ -1,5 +1,6 @@ package com.andrewlalis.perfin.view.component; +import com.andrewlalis.perfin.control.TransactionsViewController; import com.andrewlalis.perfin.data.AccountHistoryItemRepository; import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.DateUtil; @@ -13,6 +14,8 @@ import javafx.scene.layout.BorderPane; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import static com.andrewlalis.perfin.PerfinApp.router; + /** * A tile that shows a brief bit of information about an account history item. */ @@ -41,9 +44,13 @@ public class AccountHistoryItemTile extends BorderPane { private Node buildAccountEntryItem(AccountEntry entry) { Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency())); Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId()); + transactionLink.setOnAction(event -> router.navigate( + "transactions", + new TransactionsViewController.RouteContext(entry.getTransactionId()) + )); return new TextFlow( transactionLink, - new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of"), + new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of "), amountText ); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java index 2263dee..92f05ed 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java @@ -1,5 +1,6 @@ package com.andrewlalis.perfin.view.component; +import com.andrewlalis.perfin.PerfinApp; import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.ImageCache; @@ -11,6 +12,7 @@ import javafx.scene.paint.Color; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Set; /** @@ -58,7 +60,8 @@ public class AttachmentPreview extends BorderPane { this.setCenter(stackPane); this.setOnMouseClicked(event -> { if (this.isHover()) { - System.out.println("Opening attachment: " + attachment.getFilename()); + Path filePath = attachment.getPath(Profile.getContentDir(Profile.getCurrent().getName())); + PerfinApp.instance.getHostServices().showDocument(filePath.toAbsolutePath().toUri().toString()); } }); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java index d62b589..427122e 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java @@ -9,7 +9,6 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Node; @@ -20,6 +19,8 @@ import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.scene.text.TextFlow; +import java.util.concurrent.CompletableFuture; + /** * A pane that contains some controls for navigating a paginated data source. * That includes going to the next/previous page, setting the preferred page @@ -66,10 +67,6 @@ public class DataSourcePaginationControls extends BorderPane { nextPageButton.disableProperty().bind(fetching.or(currentPage.greaterThanOrEqualTo(maxPages))); nextPageButton.setOnAction(event -> setPage(currentPage.get() + 1)); -// sorts.addListener((ListChangeListener) c -> { -// setPage(1); -// }); - HBox hbox = new HBox( previousPageButton, pageTextContainer, @@ -79,7 +76,8 @@ public class DataSourcePaginationControls extends BorderPane { setCenter(hbox); } - public void setPage(int page) { + public CompletableFuture setPage(int page) { + CompletableFuture cf = new CompletableFuture<>(); fetching.set(true); PageRequest pagination = new PageRequest(page - 1, itemsPerPage.get(), sorts); Thread.ofVirtual().start(() -> { @@ -97,14 +95,17 @@ public class DataSourcePaginationControls extends BorderPane { } currentPage.set(page); fetching.set(false); + cf.complete(null); }); } catch (Exception e) { e.printStackTrace(System.err); Platform.runLater(() -> { target.clear(); fetching.set(false); + cf.complete(null); }); } }); + return cf; } }