Cleaned up CSS, and added a new transaction-view.fxml and associated controller for viewing the details of a transaction.

This commit is contained in:
Andrew Lalis 2023-12-28 21:31:42 -05:00
parent 53bfea2bad
commit 2658fc5c58
19 changed files with 285 additions and 126 deletions

View File

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

View File

@ -13,8 +13,6 @@ public class MainViewController {
@FXML
public BorderPane mainContainer;
@FXML
public HBox mainFooter;
@FXML
public HBox breadcrumbHBox;
@FXML

View File

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

View File

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

View File

@ -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<Pair<Account, Account>> getCreditAndDebitAccounts(Transaction transaction) {
CompletableFuture<Pair<Account, Account>> cf = new CompletableFuture<>();
private CompletableFuture<CreditAndDebitAccounts> getCreditAndDebitAccounts(Transaction transaction) {
CompletableFuture<CreditAndDebitAccounts> 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;

View File

@ -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<Transaction> findAll(PageRequest pagination);
Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination);
Map<AccountEntry, Account> findEntriesWithAccounts(long transactionId);
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
List<TransactionAttachment> findAttachments(long transactionId);
void delete(long transactionId);
}

View File

@ -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<TransactionAttachment> findAttachments(long transactionId) {
return DbUtil.findAll(

View File

@ -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<Account> accountConsumer) {
if (hasCredit()) accountConsumer.accept(creditAccount);
}
public void ifDebit(Consumer<Account> accountConsumer) {
if (hasDebit()) accountConsumer.accept(debitAccount);
}
}

View File

@ -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"
>
<top>
<Label fx:id="titleLabel"/>
<HBox styleClass="std-padding,std-spacing">
<Label fx:id="titleLabel" styleClass="large-text,bold-text"/>
</HBox>
</top>
<center>
<VBox>
<VBox styleClass="std-padding,std-spacing">
<HBox>
<!-- Main account properties. -->
<VBox HBox.hgrow="SOMETIMES" style="-fx-border-color: blue">
@ -23,7 +24,7 @@
</VBox>
<VBox styleClass="account-property-box">
<Label text="Number"/>
<TextField fx:id="accountNumberField" editable="false"/>
<TextField fx:id="accountNumberField" editable="false" styleClass="mono-font"/>
</VBox>
<VBox styleClass="account-property-box">
<Label text="Currency"/>
@ -45,8 +46,8 @@
</VBox>
</center>
<right>
<VBox styleClass="actions-box">
<Label text="Actions" style="-fx-font-weight: bold;"/>
<VBox styleClass="std-padding,std-spacing">
<Label text="Actions" styleClass="bold-text"/>
<Button text="Edit" onAction="#goToEditPage"/>
<Button text="Archive" onAction="#archiveAccount"/>
<Button text="Delete" onAction="#deleteAccount"/>

View File

@ -8,10 +8,10 @@
xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.andrewlalis.perfin.control.AccountsViewController"
stylesheets="@style/accounts-view.css"
stylesheets="@style/base.css"
>
<top>
<HBox styleClass="actionsBar">
<HBox styleClass="std-padding,std-spacing">
<Button text="Add an Account" onAction="#createNewAccount"/>
<Label fx:id="totalLabel"/>
</HBox>
@ -19,9 +19,9 @@
<center>
<VBox>
<ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
<FlowPane fx:id="accountsPane" BorderPane.alignment="TOP_LEFT" vgap="5" hgap="5"/>
<FlowPane fx:id="accountsPane" BorderPane.alignment="TOP_LEFT" vgap="5" hgap="5" styleClass="std-padding"/>
</ScrollPane>
<Label fx:id="noAccountsLabel" BorderPane.alignment="TOP_LEFT" text="No accounts have been added to this profile."/>
<Label fx:id="noAccountsLabel" BorderPane.alignment="TOP_LEFT" styleClass="std-padding" text="No accounts have been added to this profile."/>
</VBox>
</center>
</BorderPane>

View File

@ -5,11 +5,11 @@
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.CreateTransactionController"
stylesheets="@style/create-transaction.css"
stylesheets="@style/base.css"
>
<center>
<ScrollPane fitToWidth="true" fitToHeight="true">
<VBox styleClass="form-container">
<VBox styleClass="std-spacing,std-padding" style="-fx-max-width: 500px;">
<!-- Basic properties -->
<VBox>
<Label text="Timestamp" labelFor="${timestampField}" styleClass="bold-text"/>
@ -28,12 +28,17 @@
<VBox>
<Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/>
<Label text="Maximum of 255 characters." styleClass="small-text"/>
<TextArea fx:id="descriptionField" styleClass="mono-font" wrapText="true"/>
<TextArea
fx:id="descriptionField"
styleClass="mono-font"
wrapText="true"
style="-fx-pref-height: 100px;-fx-min-height: 100px;"
/>
<Label fx:id="descriptionErrorLabel" styleClass="error-text" wrapText="true"/>
</VBox>
<!-- Container for linked accounts -->
<VBox>
<HBox spacing="3">
<HBox styleClass="std-spacing">
<VBox>
<Label text="Debited Account" labelFor="${linkDebitAccountComboBox}" styleClass="bold-text"/>
<ComboBox fx:id="linkDebitAccountComboBox">
@ -60,7 +65,7 @@
<VBox>
<Label text="Attachments" styleClass="bold-text"/>
<Label text="Attach receipts, invoices, or other content to this transaction." styleClass="small-text" wrapText="true"/>
<VBox fx:id="selectedFilesVBox" style="-fx-spacing: 3px; -fx-padding: 3px;" VBox.vgrow="NEVER"/>
<VBox fx:id="selectedFilesVBox" styleClass="std-padding,std-spacing" VBox.vgrow="NEVER"/>
<Label text="No attachments selected." fx:id="noSelectedFilesLabel"/>
<Button text="Select attachments" onAction="#selectAttachmentFile"/>
</VBox>
@ -68,7 +73,7 @@
</ScrollPane>
</center>
<bottom>
<HBox styleClass="buttons-container">
<HBox styleClass="std-padding,std-spacing">
<Button text="Save" onAction="#save"/>
<Button text="Cancel" onAction="#cancel"/>
</HBox>

View File

@ -6,14 +6,15 @@
xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.andrewlalis.perfin.control.EditAccountController"
stylesheets="@style/edit-account.css"
styleClass="main-container"
stylesheets="@style/edit-account.css,@style/base.css"
>
<top>
<Label fx:id="titleLabel" text="Edit Account"/>
<HBox styleClass="std-padding,std-spacing">
<Label text="Edit Account" styleClass="large-text,bold-text"/>
</HBox>
</top>
<center>
<GridPane BorderPane.alignment="TOP_LEFT" styleClass="fields-grid">
<GridPane BorderPane.alignment="TOP_LEFT" styleClass="fields-grid,std-padding">
<columnConstraints>
<ColumnConstraints hgrow="NEVER" minWidth="10.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
@ -32,7 +33,7 @@
<VBox fx:id="initialBalanceContent" GridPane.columnIndex="0" GridPane.rowIndex="4" GridPane.columnSpan="2">
<Separator/>
<Label text="Initial Balance" style="-fx-font-weight: bold;"/>
<Label text="Initial Balance" styleClass="bold-text"/>
<TextField fx:id="initialBalanceField"/>
</VBox>
</GridPane>

View File

@ -7,22 +7,21 @@
xmlns:fx="http://javafx.com/fxml"
fx:id="mainContainer"
fx:controller="com.andrewlalis.perfin.control.MainViewController"
stylesheets="@style/main-view.css"
stylesheets="@style/base.css"
>
<top>
<VBox>
<HBox style="-fx-spacing: 3px; -fx-padding: 3px;">
<HBox styleClass="std-padding,std-spacing">
<Button text="Back" onAction="#goBack"/>
<Button text="Forward" onAction="#goForward"/>
<Button text="Accounts" onAction="#goToAccounts"/>
<Button text="Transactions" onAction="#goToTransactions"/>
</HBox>
<HBox fx:id="breadcrumbHBox"/>
<Separator/>
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-text"/>
</VBox>
</top>
<bottom>
<HBox fx:id="mainFooter" spacing="5">
<HBox styleClass="std-padding,std-spacing">
<Label text="Perfin v0.0.1"/>
</HBox>
</bottom>

View File

@ -1,12 +1,3 @@
.main-container {
-fx-padding: 5px;
}
#titleLabel {
-fx-font-weight: bold;
-fx-font-size: large;
}
.account-property-box {
-fx-padding: 5px 0 5px 0;
-fx-spacing: 3px;
@ -21,14 +12,6 @@
-fx-max-width: 300px;
}
#accountNumberField {
-fx-font-family: monospace;
}
.actions-box {
-fx-spacing: 3px;
}
.actions-box > Button {
-fx-max-width: 500px;
}

View File

@ -1,11 +0,0 @@
#accountsPane {
-fx-padding: 3px;
}
#noAccountsLabel {
-fx-padding: 3px;
}
.actionsBar {
-fx-padding: 3px;
}

View File

@ -2,6 +2,11 @@
-fx-font-family: monospace;
}
.error-text {
-fx-font-size: small;
-fx-text-fill: red;
}
.bold-text {
-fx-font-weight: bold;
}
@ -10,23 +15,14 @@
-fx-font-size: small;
}
.error-text {
-fx-font-size: small;
-fx-text-fill: red;
.large-text {
-fx-font-size: large;
}
.form-container {
-fx-max-width: 500px;
-fx-spacing: 3px;
.std-padding {
-fx-padding: 3px;
}
.buttons-container {
-fx-padding: 3px;
.std-spacing {
-fx-spacing: 3px;
}
#descriptionField {
-fx-pref-height: 100px;
-fx-min-height: 100px;
}

View File

@ -1,15 +1,5 @@
.main-container {
-fx-padding: 5px;
}
.fields-grid {
-fx-hgap: 3px;
-fx-vgap: 3px;
-fx-padding: 5px;
-fx-max-width: 500px;
}
#titleLabel {
-fx-font-size: large;
-fx-font-weight: bold;
}

View File

@ -1,13 +0,0 @@
#mainHeader {
-fx-padding: 3px;
-fx-spacing: 3px;
}
#breadcrumbHBox {
-fx-spacing: 3px;
-fx-font-size: small;
}
#mainFooter {
-fx-padding: 3px;
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.TransactionViewController"
>
<top>
<HBox styleClass="std-padding,std-spacing">
<Label text="Transaction" styleClass="large-text,bold-text"/>
</HBox>
</top>
<center>
<VBox styleClass="std-padding,std-spacing">
<VBox>
<Label text="Amount" styleClass="bold-text"/>
<Label fx:id="amountLabel" styleClass="mono-font"/>
</VBox>
<VBox>
<Label text="Timestamp" styleClass="bold-text"/>
<Label fx:id="timestampLabel" styleClass="mono-font"/>
</VBox>
<VBox>
<Label text="Description" styleClass="bold-text"/>
<Label fx:id="descriptionLabel" wrapText="true"/>
</VBox>
<Separator/>
<VBox>
<TextFlow>
<Text text="Debited to"/>
<Hyperlink fx:id="debitAccountLink"/>
</TextFlow>
<TextFlow>
<Text text="Credited from"/>
<Hyperlink fx:id="creditAccountLink"/>
</TextFlow>
</VBox>
<Separator/>
<VBox fx:id="attachmentsContainer">
<Label text="Attachments" styleClass="bold-text"/>
<ScrollPane fitToWidth="true" fitToHeight="true">
<HBox fx:id="attachmentsHBox" styleClass="std-padding,std-spacing"/>
</ScrollPane>
</VBox>
</VBox>
</center>
<right>
<VBox styleClass="std-padding,std-spacing">
<Label text="Actions" styleClass="bold-text"/>
<Button text="Delete" onAction="#deleteTransaction"/>
</VBox>
</right>
</BorderPane>