diff --git a/src/main/java/com/andrewlalis/perfin/Pair.java b/src/main/java/com/andrewlalis/perfin/Pair.java
new file mode 100644
index 0000000..85cd786
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/Pair.java
@@ -0,0 +1,7 @@
+package com.andrewlalis.perfin;
+
+public record Pair(A first, B second) {
+ public static Pair of(A first, B second) {
+ return new Pair<>(first, second);
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/SceneUtil.java b/src/main/java/com/andrewlalis/perfin/SceneUtil.java
index 736b4b2..e7c2e8f 100644
--- a/src/main/java/com/andrewlalis/perfin/SceneUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/SceneUtil.java
@@ -10,6 +10,17 @@ import java.net.URL;
import java.util.function.Consumer;
public class SceneUtil {
+ public static Pair loadNodeAndController(String fxml) {
+ FXMLLoader loader = new FXMLLoader(SceneUtil.class.getResource(fxml));
+ try {
+ N node = loader.load();
+ C controller = loader.getController();
+ return Pair.of(node, controller);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
public static Parent loadNode(String fxml, Consumer controllerConfig) {
FXMLLoader loader = new FXMLLoader(SceneUtil.class.getResource(fxml));
try {
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index 1f104b7..19b374a 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -1,6 +1,5 @@
package com.andrewlalis.perfin.control;
-import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.CurrencyUtil;
import com.andrewlalis.perfin.data.DateUtil;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
@@ -23,7 +22,7 @@ import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.router;
-public class TransactionViewController implements RouteSelectionListener {
+public class TransactionViewController {
private Transaction transaction;
@FXML public Label amountLabel;
@@ -37,9 +36,9 @@ public class TransactionViewController implements RouteSelectionListener {
@FXML public HBox attachmentsHBox;
private final ObservableList attachmentsList = FXCollections.observableArrayList();
- @Override
- public void onRouteSelected(Object context) {
- this.transaction = (Transaction) context;
+ public void setTransaction(Transaction transaction) {
+ this.transaction = transaction;
+ if (transaction == null) return;
amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency()));
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription());
@@ -75,6 +74,7 @@ public class TransactionViewController implements RouteSelectionListener {
new Label(attachment.getFilename()),
new Label(attachment.getContentType())
);
+ // TODO: Custom attachment preview element.
return vbox;
});
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
index 050dc44..6407eb2 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
@@ -1,23 +1,82 @@
package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
+import com.andrewlalis.perfin.Pair;
+import com.andrewlalis.perfin.SceneUtil;
+import com.andrewlalis.perfin.control.component.DataSourcePaginationControls;
import com.andrewlalis.perfin.control.component.TransactionTile;
+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.model.Profile;
+import com.andrewlalis.perfin.model.Transaction;
import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import static com.andrewlalis.perfin.PerfinApp.router;
public class TransactionsViewController implements RouteSelectionListener {
- @FXML
- public VBox transactionsVBox;
+ @FXML public BorderPane transactionsListBorderPane;
+ @FXML public VBox transactionsVBox;
+ private DataSourcePaginationControls paginationControls;
+
+
+ @FXML public VBox detailPanel;
+ private final ObjectProperty selectedTransaction = new SimpleObjectProperty<>(null);
+
+ @FXML public void initialize() {
+ // Initialize the left-hand paginated transactions list.
+ this.paginationControls = new DataSourcePaginationControls(
+ transactionsVBox.getChildren(),
+ new DataSourcePaginationControls.PageFetcherFunction() {
+ @Override
+ public Page extends Node> fetchPage(PageRequest pagination) throws Exception {
+ try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
+ return repo.findAll(pagination).map(TransactionsViewController.this::makeTile);
+ }
+ }
+
+ @Override
+ public int getTotalCount() throws Exception {
+ try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
+ return (int) repo.countAll();
+ }
+ }
+ }
+ );
+ transactionsListBorderPane.setBottom(paginationControls);
+
+ // Initialize the right-hand transaction detail view.
+ HBox container = (HBox) detailPanel.getParent();
+ ObservableValue halfWidthProp = container.widthProperty().map(n -> n.doubleValue() * 0.5);
+ detailPanel.minWidthProperty().bind(halfWidthProp);
+ detailPanel.maxWidthProperty().bind(halfWidthProp);
+ detailPanel.prefWidthProperty().bind(halfWidthProp);
+ detailPanel.managedProperty().bind(detailPanel.visibleProperty());
+ detailPanel.visibleProperty().bind(selectedTransaction.isNotNull());
+
+ Pair detailComponents = SceneUtil.loadNodeAndController("/transaction-view.fxml");
+ TransactionViewController transactionViewController = detailComponents.second();
+ BorderPane transactionDetailView = detailComponents.first();
+ transactionDetailView.managedProperty().bind(transactionDetailView.visibleProperty());
+ transactionDetailView.visibleProperty().bind(selectedTransaction.isNotNull());
+ detailPanel.getChildren().add(transactionDetailView);
+ selectedTransaction.addListener((observable, oldValue, newValue) -> {
+ transactionViewController.setTransaction(newValue);
+ });
+ }
@Override
public void onRouteSelected(Object context) {
- refreshTransactions();
+ paginationControls.sorts.setAll(Sort.desc("timestamp"));
+ this.paginationControls.setPage(1);
}
@FXML
@@ -25,13 +84,37 @@ public class TransactionsViewController implements RouteSelectionListener {
router.navigate("create-transaction");
}
- private void refreshTransactions() {
- Thread.ofVirtual().start(() -> {
- Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
- var page = repo.findAll(PageRequest.unpaged(Sort.desc("timestamp")));
- var components = page.items().stream().map(transaction -> new TransactionTile(transaction, this::refreshTransactions)).toList();
- Platform.runLater(() -> transactionsVBox.getChildren().setAll(components));
- });
+ private TransactionTile makeTile(Transaction transaction) {
+ var tile = new TransactionTile(transaction);
+ tile.setOnMouseClicked(event -> {
+ if (selectedTransaction.get() == null || selectedTransaction.get().getId() != transaction.getId()) {
+ selectedTransaction.set(transaction);
+ } else {
+ selectedTransaction.set(null);
+ }
});
+ tile.selected.bind(selectedTransaction.map(t -> t != null && t.getId() == transaction.getId()));
+ return tile;
}
+
+// private void refreshTransactions() {
+// Thread.ofVirtual().start(() -> {
+// Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
+// var page = repo.findAll(PageRequest.unpaged(Sort.desc("timestamp")));
+// var components = page.items().stream().map(transaction -> {
+// var tile = new TransactionTile(transaction, this::refreshTransactions);
+// tile.setOnMouseClicked(event -> {
+// if (selectedTransaction.get() == null || selectedTransaction.get().getId() != transaction.getId()) {
+// selectedTransaction.set(transaction);
+// } else {
+// selectedTransaction.set(null);
+// }
+// });
+// tile.selected.bind(selectedTransaction.map(t -> t != null && t.getId() == transaction.getId()));
+// return tile;
+// }).toList();
+// Platform.runLater(() -> transactionsVBox.getChildren().setAll(components));
+// });
+// });
+// }
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java b/src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java
new file mode 100644
index 0000000..dc07992
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java
@@ -0,0 +1,96 @@
+package com.andrewlalis.perfin.control.component;
+
+import com.andrewlalis.perfin.data.pagination.Page;
+import com.andrewlalis.perfin.data.pagination.PageRequest;
+import com.andrewlalis.perfin.data.pagination.Sort;
+import javafx.beans.property.BooleanProperty;
+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.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+
+/**
+ * A pane that contains some controls for navigating a paginated data source.
+ * That includes going to the next/previous page, setting the preferred page
+ * size.
+ */
+public class DataSourcePaginationControls extends BorderPane {
+ public interface PageFetcherFunction {
+ Page extends Node> fetchPage(PageRequest pagination) throws Exception;
+ default int getTotalCount() throws Exception {
+ return -1;
+ }
+ }
+
+ public final IntegerProperty currentPage = new SimpleIntegerProperty(1);
+ public final IntegerProperty maxPages = new SimpleIntegerProperty(-1);
+ public final IntegerProperty itemsPerPage = new SimpleIntegerProperty(5);
+ public final ObservableList sorts = FXCollections.observableArrayList();
+ private final BooleanProperty fetching = new SimpleBooleanProperty(false);
+ private final ObservableList target;
+ private final PageFetcherFunction fetcher;
+
+ public DataSourcePaginationControls(ObservableList target, PageFetcherFunction fetcher) {
+ this.target = target;
+ this.fetcher = fetcher;
+
+ Text currentPageLabel = new Text();
+ currentPageLabel.textProperty().bind(currentPage.asString());
+ Text maxPagesLabel = new Text();
+ maxPagesLabel.textProperty().bind(maxPages.asString());
+ TextFlow maxPagesText = new TextFlow(new Text(" / "), maxPagesLabel);
+ maxPagesText.managedProperty().bind(maxPagesText.visibleProperty());
+ maxPagesText.visibleProperty().bind(maxPages.isNotEqualTo(-1));
+ TextFlow pageText = new TextFlow(new Text("Page "), currentPageLabel, maxPagesText);
+
+
+ Button previousPageButton = new Button("Previous Page");
+ previousPageButton.disableProperty().bind(currentPage.lessThan(2).or(fetching));
+ previousPageButton.setOnAction(event -> setPage(currentPage.get() - 1));
+ Button nextPageButton = new Button("Next Page");
+ 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,
+ pageText,
+ nextPageButton
+ );
+ setCenter(hbox);
+ }
+
+ public void setPage(int page) {
+ try {
+ fetching.set(true);
+ PageRequest pagination = new PageRequest(page - 1, itemsPerPage.get(), sorts);
+ var p = fetcher.fetchPage(pagination);
+ int totalResults = fetcher.getTotalCount();
+ target.setAll(p.items());
+ if (totalResults != -1) {
+ int max = totalResults / itemsPerPage.get();
+ if (totalResults % itemsPerPage.get() != 0) {
+ max += 1;
+ }
+ maxPages.set(max);
+ }
+ currentPage.set(page);
+ } catch (Exception e) {
+ target.clear();
+ e.printStackTrace(System.err);
+ } finally {
+ fetching.set(false);
+ }
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java b/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java
index fe7a1c5..5d4c874 100644
--- a/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java
+++ b/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java
@@ -2,20 +2,21 @@ package com.andrewlalis.perfin.control.component;
import com.andrewlalis.perfin.data.CurrencyUtil;
import com.andrewlalis.perfin.data.DateUtil;
-import com.andrewlalis.perfin.model.*;
+import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
+import com.andrewlalis.perfin.model.Profile;
+import com.andrewlalis.perfin.model.Transaction;
import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
-import javafx.scene.control.Alert;
-import javafx.scene.control.ButtonType;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
-import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
-import javafx.util.Pair;
import java.util.concurrent.CompletableFuture;
@@ -25,21 +26,32 @@ import static com.andrewlalis.perfin.PerfinApp.router;
* A tile that displays a transaction's basic information.
*/
public class TransactionTile extends BorderPane {
- public TransactionTile(Transaction transaction, Runnable refresh) {
- setStyle("""
- -fx-border-color: lightgray;
- -fx-border-width: 1px;
- -fx-border-style: solid;
- -fx-border-radius: 5px;
- -fx-padding: 5px;
- -fx-max-width: 500px;
- -fx-cursor: hand;
- """);
+ public final BooleanProperty selected = new SimpleBooleanProperty(false);
+ private static final String UNSELECTED_STYLE = """
+ -fx-border-color: lightgray;
+ -fx-border-width: 1px;
+ -fx-border-style: solid;
+ -fx-border-radius: 5px;
+ -fx-padding: 5px;
+ -fx-cursor: hand;
+ """;
+ private static final String SELECTED_STYLE = """
+ -fx-border-color: white;
+ -fx-border-width: 1px;
+ -fx-border-style: solid;
+ -fx-border-radius: 5px;
+ -fx-padding: 5px;
+ -fx-cursor: hand;
+ """;
+
+ public TransactionTile(Transaction transaction) {
+ setStyle(UNSELECTED_STYLE);
setTop(getHeader(transaction));
setCenter(getBody(transaction));
- setBottom(getFooter(transaction, refresh));
- addEventHandler(MouseEvent.MOUSE_CLICKED, event -> router.navigate("transaction", transaction));
+ setBottom(getFooter(transaction));
+
+ styleProperty().bind(selected.map(value -> value ? SELECTED_STYLE : UNSELECTED_STYLE));
}
private Node getHeader(Transaction transaction) {
@@ -64,31 +76,23 @@ public class TransactionTile extends BorderPane {
accounts.ifCredit(acc -> {
Hyperlink link = new Hyperlink(acc.getShortName());
link.setOnAction(event -> router.navigate("account", acc));
- TextFlow text = new TextFlow(new Text("Credited from"), link);
- Platform.runLater(() -> bodyVBox.getChildren().add(text));
+ Text prefix = new Text("Credited from");
+ prefix.setFill(Color.RED);
+ Platform.runLater(() -> bodyVBox.getChildren().add(new TextFlow(prefix, link)));
});
accounts.ifDebit(acc -> {
Hyperlink link = new Hyperlink(acc.getShortName());
link.setOnAction(event -> router.navigate("account", acc));
- TextFlow text = new TextFlow(new Text("Debited to"), link);
- Platform.runLater(() -> bodyVBox.getChildren().add(text));
+ Text prefix = new Text("Debited to");
+ prefix.setFill(Color.GREEN);
+ Platform.runLater(() -> bodyVBox.getChildren().add(new TextFlow(prefix, link)));
});
});
return bodyVBox;
}
- private Node getFooter(Transaction transaction, Runnable refresh) {
+ private Node getFooter(Transaction transaction) {
Label timestampLabel = new Label(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
- Hyperlink deleteLink = new Hyperlink("Delete this transaction");
- deleteLink.setOnAction(event -> {
- var confirmResult = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this transaction?").showAndWait();
- if (confirmResult.isPresent() && confirmResult.get() == ButtonType.OK) {
- Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
- repo.delete(transaction.getId());
- });
- refresh.run();
- }
- });
HBox footerHBox = new HBox(
timestampLabel
);
diff --git a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java
index eb63994..43e07d1 100644
--- a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java
@@ -12,6 +12,7 @@ public interface TransactionRepository extends AutoCloseable {
long insert(Transaction transaction, Map accountsMap);
void addAttachments(long transactionId, List attachments);
Page findAll(PageRequest pagination);
+ long countAll();
Page findAllByAccounts(Set accountIds, PageRequest pagination);
Map findEntriesWithAccounts(long transactionId);
CreditAndDebitAccounts findLinkedAccounts(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 38f87cc..a5f812e 100644
--- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java
@@ -91,6 +91,11 @@ public record JdbcTransactionRepository(Connection conn) implements TransactionR
);
}
+ @Override
+ public long countAll() {
+ return DbUtil.findOne(conn, "SELECT COUNT(id) FROM transaction", Collections.emptyList(), 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/data/pagination/Page.java b/src/main/java/com/andrewlalis/perfin/data/pagination/Page.java
index 8154eab..df38600 100644
--- a/src/main/java/com/andrewlalis/perfin/data/pagination/Page.java
+++ b/src/main/java/com/andrewlalis/perfin/data/pagination/Page.java
@@ -1,5 +1,15 @@
package com.andrewlalis.perfin.data.pagination;
import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
-public record Page(List items, PageRequest pagination) {}
+public record Page(List items, PageRequest pagination) {
+ public Stream stream() {
+ return items.stream();
+ }
+
+ public Page map(Function mapper) {
+ return new Page<>(items.stream().map(mapper).toList(), pagination);
+ }
+}
diff --git a/src/main/resources/style/base.css b/src/main/resources/style/base.css
index 9034f01..99486cc 100644
--- a/src/main/resources/style/base.css
+++ b/src/main/resources/style/base.css
@@ -26,3 +26,24 @@
.std-spacing {
-fx-spacing: 3px;
}
+
+.spacing-extra {
+ -fx-spacing: 6px;
+}
+
+/* DEBUG BORDERS */
+.debug-border-1 {
+ -fx-border-color: red;
+}
+
+.debug-border-2 {
+ -fx-border-color: lime;
+}
+
+.debug-border-3 {
+ -fx-border-color: blue;
+}
+
+.debug-border-4 {
+ -fx-border-color: magenta;
+}
diff --git a/src/main/resources/transaction-view.fxml b/src/main/resources/transaction-view.fxml
index 543eb62..beecfec 100644
--- a/src/main/resources/transaction-view.fxml
+++ b/src/main/resources/transaction-view.fxml
@@ -8,49 +8,44 @@
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.TransactionViewController"
>
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
diff --git a/src/main/resources/transactions-view.fxml b/src/main/resources/transactions-view.fxml
index 119bfc0..849ca39 100644
--- a/src/main/resources/transactions-view.fxml
+++ b/src/main/resources/transactions-view.fxml
@@ -1,24 +1,27 @@
-
-
-
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+