Added the ability to navigate to a specific transaction.

This commit is contained in:
Andrew Lalis 2024-01-01 09:32:51 -05:00
parent b477e9ab3c
commit 173204c61c
8 changed files with 90 additions and 38 deletions

View File

@ -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);
}
}

View File

@ -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<Sort> 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;
}
}

View File

@ -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<Path> attachments
);
Optional<Transaction> findById(long id);
Page<Transaction> findAll(PageRequest pagination);
long countAll();
long countAllAfter(long transactionId);
Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination);
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
List<Attachment> findAttachments(long transactionId);

View File

@ -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<Transaction> findById(long id) {
return DbUtil.findById(conn, "SELECT * FROM transaction WHERE id = ?", id, JdbcTransactionRepository::parseTransaction);
}
@Override
public Page<Transaction> 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<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination) {
String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));

View File

@ -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;
}
}

View File

@ -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,6 +44,10 @@ 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 "),

View File

@ -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());
}
});
}

View File

@ -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<Sort>) 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<Void> setPage(int page) {
CompletableFuture<Void> 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;
}
}