diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index e2ab789..fe678a0 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -53,5 +53,6 @@ public class PerfinApp extends Application { mapResourceRoute("edit-account", "/edit-account.fxml"); mapResourceRoute("transactions", "/transactions-view.fxml"); mapResourceRoute("create-transaction", "/create-transaction.fxml"); + mapResourceRoute("transaction", "/transaction-view.fxml"); } } \ No newline at end of file diff --git a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java index 129ce30..3b4d295 100644 --- a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java @@ -13,8 +13,6 @@ public class MainViewController { @FXML public BorderPane mainContainer; @FXML - public HBox mainFooter; - @FXML public HBox breadcrumbHBox; @FXML diff --git a/src/main/java/com/andrewlalis/perfin/control/Popups.java b/src/main/java/com/andrewlalis/perfin/control/Popups.java new file mode 100644 index 0000000..beae679 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/control/Popups.java @@ -0,0 +1,14 @@ +package com.andrewlalis.perfin.control; + +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.stage.Modality; + +public class Popups { + public static boolean confirm(String text) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION, text); + alert.initModality(Modality.APPLICATION_MODAL); + var result = alert.showAndWait(); + return result.isPresent() && result.get() == ButtonType.OK; + } +} diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java new file mode 100644 index 0000000..1f104b7 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -0,0 +1,104 @@ +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; +import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.model.Transaction; +import com.andrewlalis.perfin.model.TransactionAttachment; +import com.andrewlalis.perfin.view.BindingUtil; +import javafx.application.Platform; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.TextFlow; + +import java.util.List; + +import static com.andrewlalis.perfin.PerfinApp.router; + +public class TransactionViewController implements RouteSelectionListener { + private Transaction transaction; + + @FXML public Label amountLabel; + @FXML public Label timestampLabel; + @FXML public Label descriptionLabel; + + @FXML public Hyperlink debitAccountLink; + @FXML public Hyperlink creditAccountLink; + + @FXML public VBox attachmentsContainer; + @FXML public HBox attachmentsHBox; + private final ObservableList attachmentsList = FXCollections.observableArrayList(); + + @Override + public void onRouteSelected(Object context) { + this.transaction = (Transaction) context; + amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency())); + timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); + descriptionLabel.setText(transaction.getDescription()); + + configureAccountLinkBindings(debitAccountLink); + configureAccountLinkBindings(creditAccountLink); + Thread.ofVirtual().start(() -> { + 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())); + } + }); + }); + }); + + attachmentsContainer.managedProperty().bind(attachmentsContainer.visibleProperty()); + attachmentsContainer.visibleProperty().bind(new SimpleListProperty<>(attachmentsList).emptyProperty().not()); + Thread.ofVirtual().start(() -> { + Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { + List attachments = repo.findAttachments(transaction.getId()); + Platform.runLater(() -> attachmentsList.setAll(attachments)); + }); + }); + BindingUtil.mapContent(attachmentsHBox.getChildren(), attachmentsList, attachment -> { + VBox vbox = new VBox( + new Label(attachment.getFilename()), + new Label(attachment.getContentType()) + ); + return vbox; + }); + } + + @FXML public void deleteTransaction() { + boolean confirm = Popups.confirm( + "Are you sure you want to delete this transaction? This will " + + "permanently remove the transaction and its effects on any linked " + + "accounts, as well as remove any attachments from storage within " + + "this app." + ); + if (confirm) { + Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { + // TODO: Delete attachments first! + repo.delete(transaction.getId()); + router.getHistory().clear(); + router.navigate("transactions"); + }); + } + } + + private void configureAccountLinkBindings(Hyperlink link) { + TextFlow parent = (TextFlow) link.getParent(); + parent.managedProperty().bind(parent.visibleProperty()); + parent.visibleProperty().bind(link.textProperty().isNotEmpty()); + } +} 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 232f784..fe7a1c5 100644 --- a/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java +++ b/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java @@ -2,16 +2,14 @@ package com.andrewlalis.perfin.control.component; import com.andrewlalis.perfin.data.CurrencyUtil; import com.andrewlalis.perfin.data.DateUtil; -import com.andrewlalis.perfin.model.Account; -import com.andrewlalis.perfin.model.AccountEntry; -import com.andrewlalis.perfin.model.Profile; -import com.andrewlalis.perfin.model.Transaction; +import com.andrewlalis.perfin.model.*; import javafx.application.Platform; 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; @@ -35,11 +33,13 @@ public class TransactionTile extends BorderPane { -fx-border-radius: 5px; -fx-padding: 5px; -fx-max-width: 500px; + -fx-cursor: hand; """); setTop(getHeader(transaction)); setCenter(getBody(transaction)); setBottom(getFooter(transaction, refresh)); + addEventHandler(MouseEvent.MOUSE_CLICKED, event -> router.navigate("transaction", transaction)); } private Node getHeader(Transaction transaction) { @@ -61,19 +61,18 @@ public class TransactionTile extends BorderPane { descriptionLabel ); getCreditAndDebitAccounts(transaction).thenAccept(accounts -> { - Account creditAccount = accounts.getKey(); - Account debitAccount = accounts.getValue(); - if (creditAccount != null) { - Hyperlink link = new Hyperlink(creditAccount.getShortName()); - link.setOnAction(event -> router.navigate("account", creditAccount)); + 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)); - } if (debitAccount != null) { - Hyperlink link = new Hyperlink(debitAccount.getShortName()); - link.setOnAction(event -> router.navigate("account", debitAccount)); + }); + 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)); - } + }); }); return bodyVBox; } @@ -91,8 +90,7 @@ public class TransactionTile extends BorderPane { } }); HBox footerHBox = new HBox( - timestampLabel, - deleteLink + timestampLabel ); footerHBox.setStyle(""" -fx-spacing: 3px; @@ -101,18 +99,12 @@ public class TransactionTile extends BorderPane { return footerHBox; } - private CompletableFuture> getCreditAndDebitAccounts(Transaction transaction) { - CompletableFuture> cf = new CompletableFuture<>(); + private CompletableFuture getCreditAndDebitAccounts(Transaction transaction) { + CompletableFuture cf = new CompletableFuture<>(); Thread.ofVirtual().start(() -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { - var entriesAndAccounts = repo.findEntriesWithAccounts(transaction.getId()); - AccountEntry creditEntry = entriesAndAccounts.keySet().stream() - .filter(entry -> entry.getType() == AccountEntry.Type.CREDIT) - .findFirst().orElse(null); - AccountEntry debitEntry = entriesAndAccounts.keySet().stream() - .filter(entry -> entry.getType() == AccountEntry.Type.DEBIT) - .findFirst().orElse(null); - cf.complete(new Pair<>(entriesAndAccounts.get(creditEntry), entriesAndAccounts.get(debitEntry))); + CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId()); + cf.complete(accounts); }); }); return cf; diff --git a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java index 11c34f9..eb63994 100644 --- a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java @@ -2,10 +2,7 @@ package com.andrewlalis.perfin.data; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; -import com.andrewlalis.perfin.model.Account; -import com.andrewlalis.perfin.model.AccountEntry; -import com.andrewlalis.perfin.model.Transaction; -import com.andrewlalis.perfin.model.TransactionAttachment; +import com.andrewlalis.perfin.model.*; import java.util.List; import java.util.Map; @@ -17,6 +14,7 @@ public interface TransactionRepository extends AutoCloseable { Page findAll(PageRequest pagination); Page findAllByAccounts(Set accountIds, PageRequest pagination); Map findEntriesWithAccounts(long transactionId); + CreditAndDebitAccounts findLinkedAccounts(long transactionId); List findAttachments(long transactionId); void delete(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 8cb531b..38f87cc 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java @@ -4,10 +4,7 @@ import com.andrewlalis.perfin.data.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.model.Account; -import com.andrewlalis.perfin.model.AccountEntry; -import com.andrewlalis.perfin.model.Transaction; -import com.andrewlalis.perfin.model.TransactionAttachment; +import com.andrewlalis.perfin.model.*; import java.sql.Connection; import java.sql.ResultSet; @@ -127,6 +124,33 @@ public record JdbcTransactionRepository(Connection conn) implements TransactionR return map; } + @Override + public CreditAndDebitAccounts findLinkedAccounts(long transactionId) { + Account creditAccount = DbUtil.findOne( + conn, + """ + SELECT * + FROM account + LEFT JOIN account_entry ON account_entry.account_id = account.id + WHERE account_entry.transaction_id = ? AND account_entry.type = 'CREDIT' + """, + List.of(transactionId), + JdbcAccountRepository::parseAccount + ).orElse(null); + Account debitAccount = DbUtil.findOne( + conn, + """ + SELECT * + FROM account + LEFT JOIN account_entry ON account_entry.account_id = account.id + WHERE account_entry.transaction_id = ? AND account_entry.type = 'DEBIT' + """, + List.of(transactionId), + JdbcAccountRepository::parseAccount + ).orElse(null); + return new CreditAndDebitAccounts(creditAccount, debitAccount); + } + @Override public List findAttachments(long transactionId) { return DbUtil.findAll( diff --git a/src/main/java/com/andrewlalis/perfin/model/CreditAndDebitAccounts.java b/src/main/java/com/andrewlalis/perfin/model/CreditAndDebitAccounts.java new file mode 100644 index 0000000..a98621b --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/model/CreditAndDebitAccounts.java @@ -0,0 +1,21 @@ +package com.andrewlalis.perfin.model; + +import java.util.function.Consumer; + +public record CreditAndDebitAccounts(Account creditAccount, Account debitAccount) { + public boolean hasCredit() { + return creditAccount != null; + } + + public boolean hasDebit() { + return debitAccount != null; + } + + public void ifCredit(Consumer accountConsumer) { + if (hasCredit()) accountConsumer.accept(creditAccount); + } + + public void ifDebit(Consumer accountConsumer) { + if (hasDebit()) accountConsumer.accept(debitAccount); + } +} diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml index 1742bf0..6a8ca3c 100644 --- a/src/main/resources/account-view.fxml +++ b/src/main/resources/account-view.fxml @@ -6,14 +6,15 @@ xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.andrewlalis.perfin.control.AccountViewController" - stylesheets="@style/account-view.css" - styleClass="main-container" + stylesheets="@style/account-view.css,@style/base.css" > -
- + @@ -23,7 +24,7 @@
- -