Improved attachments preview in transaction and balance record view.

This commit is contained in:
Andrew Lalis 2024-01-11 07:25:28 -05:00
parent 6b563003ec
commit 2c49dd5766
10 changed files with 217 additions and 51 deletions

View File

@ -86,6 +86,7 @@ public class PerfinApp extends Application {
router.map("transactions", PerfinApp.class.getResource("/transactions-view.fxml")); router.map("transactions", PerfinApp.class.getResource("/transactions-view.fxml"));
router.map("create-transaction", PerfinApp.class.getResource("/create-transaction.fxml")); router.map("create-transaction", PerfinApp.class.getResource("/create-transaction.fxml"));
router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml")); router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml"));
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
// Help pages. // Help pages.
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));

View File

@ -0,0 +1,59 @@
package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.BalanceRecord;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.component.AttachmentsViewPane;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.router;
/**
* Controller for the page which shows an overview of a balance record.
*/
public class BalanceRecordViewController implements RouteSelectionListener {
private BalanceRecord balanceRecord;
@FXML public Label titleLabel;
@FXML public Label timestampLabel;
@FXML public Label balanceLabel;
@FXML public Label currencyLabel;
@FXML public AttachmentsViewPane attachmentsViewPane;
@FXML public void initialize() {
attachmentsViewPane.hideIfEmpty();
}
@Override
public void onRouteSelected(Object context) {
this.balanceRecord = (BalanceRecord) context;
if (balanceRecord == null) return;
titleLabel.setText("Balance Record #" + balanceRecord.id);
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(balanceRecord.getTimestamp()));
balanceLabel.setText(CurrencyUtil.formatMoney(balanceRecord.getMoneyAmount()));
currencyLabel.setText(balanceRecord.getCurrency().getDisplayName());
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
List<Attachment> attachments = repo.findAttachments(balanceRecord.id);
Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments));
}));
}
@FXML public void delete() {
boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin.");
if (confirm) {
Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
repo.deleteById(balanceRecord.id);
});
router.navigateBackAndClear();
}
}
}

View File

@ -6,18 +6,11 @@ import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts; import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction; import com.andrewlalis.perfin.model.Transaction;
import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.component.AttachmentsViewPane;
import com.andrewlalis.perfin.view.component.AttachmentPreview;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import java.util.List; import java.util.List;
@ -36,9 +29,13 @@ public class TransactionViewController {
@FXML public Hyperlink debitAccountLink; @FXML public Hyperlink debitAccountLink;
@FXML public Hyperlink creditAccountLink; @FXML public Hyperlink creditAccountLink;
@FXML public VBox attachmentsContainer; @FXML public AttachmentsViewPane attachmentsViewPane;
@FXML public HBox attachmentsHBox;
private final ObservableList<Attachment> attachmentsList = FXCollections.observableArrayList(); @FXML public void initialize() {
configureAccountLinkBindings(debitAccountLink);
configureAccountLinkBindings(creditAccountLink);
attachmentsViewPane.hideIfEmpty();
}
public void setTransaction(Transaction transaction) { public void setTransaction(Transaction transaction) {
this.transaction = transaction; this.transaction = transaction;
@ -48,8 +45,6 @@ public class TransactionViewController {
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription()); descriptionLabel.setText(transaction.getDescription());
configureAccountLinkBindings(debitAccountLink);
configureAccountLinkBindings(creditAccountLink);
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id); CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
@ -66,19 +61,12 @@ public class TransactionViewController {
}); });
}); });
attachmentsContainer.managedProperty().bind(attachmentsContainer.visibleProperty());
attachmentsContainer.visibleProperty().bind(new SimpleListProperty<>(attachmentsList).emptyProperty().not());
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
List<Attachment> attachments = repo.findAttachments(transaction.id); List<Attachment> attachments = repo.findAttachments(transaction.id);
Platform.runLater(() -> attachmentsList.setAll(attachments)); Platform.runLater(() -> attachmentsViewPane.setAttachments(attachments));
}); });
}); });
attachmentsHBox.setMinHeight(AttachmentPreview.HEIGHT);
attachmentsHBox.setPrefHeight(AttachmentPreview.HEIGHT);
((ScrollPane) attachmentsHBox.getParent().getParent().getParent()).minHeightProperty().bind(attachmentsHBox.heightProperty().map(n -> n.doubleValue() + 2));
((ScrollPane) attachmentsHBox.getParent().getParent().getParent()).prefHeightProperty().bind(attachmentsHBox.heightProperty().map(n -> n.doubleValue() + 2));
BindingUtil.mapContent(attachmentsHBox.getChildren(), attachmentsList, AttachmentPreview::new);
} }
@FXML public void deleteTransaction() { @FXML public void deleteTransaction() {
@ -93,7 +81,6 @@ public class TransactionViewController {
); );
if (confirm) { if (confirm) {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
// TODO: Delete attachments first!
repo.delete(transaction.id); repo.delete(transaction.id);
router.replace("transactions"); router.replace("transactions");
}); });

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.data; package com.andrewlalis.perfin.data;
import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.BalanceRecord; import com.andrewlalis.perfin.model.BalanceRecord;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -14,5 +15,6 @@ public interface BalanceRecordRepository extends AutoCloseable {
BalanceRecord findLatestByAccountId(long accountId); BalanceRecord findLatestByAccountId(long accountId);
Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp); Optional<BalanceRecord> findClosestBefore(long accountId, LocalDateTime utcTimestamp);
Optional<BalanceRecord> findClosestAfter(long accountId, LocalDateTime utcTimestamp); Optional<BalanceRecord> findClosestAfter(long accountId, LocalDateTime utcTimestamp);
List<Attachment> findAttachments(long recordId);
void deleteById(long id); void deleteById(long id);
} }

View File

@ -72,6 +72,21 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
); );
} }
@Override
public List<Attachment> findAttachments(long recordId) {
return DbUtil.findAll(
conn,
"""
SELECT *
FROM attachment
LEFT JOIN balance_record_attachment ba ON ba.attachment_id = attachment.id
WHERE ba.balance_record_id = ?
ORDER BY uploaded_at ASC, filename ASC""",
List.of(recordId),
JdbcAttachmentRepository::parseAttachment
);
}
@Override @Override
public void deleteById(long id) { public void deleteById(long id) {
DbUtil.updateOne(conn, "DELETE FROM balance_record WHERE id = ?", List.of(id)); DbUtil.updateOne(conn, "DELETE FROM balance_record WHERE id = ?", List.of(id));

View File

@ -1,17 +1,16 @@
package com.andrewlalis.perfin.view.component; package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.control.AccountViewController; import com.andrewlalis.perfin.control.AccountViewController;
import com.andrewlalis.perfin.control.Popups;
import com.andrewlalis.perfin.data.AccountHistoryItemRepository; import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.model.BalanceRecord; import com.andrewlalis.perfin.model.BalanceRecord;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.history.AccountHistoryItem; import com.andrewlalis.perfin.model.history.AccountHistoryItem;
import javafx.application.Platform;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import static com.andrewlalis.perfin.PerfinApp.router;
public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile { public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile {
public AccountHistoryBalanceRecordTile(AccountHistoryItem item, AccountHistoryItemRepository repo, AccountViewController controller) { public AccountHistoryBalanceRecordTile(AccountHistoryItem item, AccountHistoryItemRepository repo, AccountViewController controller) {
super(item); super(item);
@ -25,16 +24,8 @@ public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile {
var text = new TextFlow(new Text("Balance record #" + balanceRecord.id + " added with value of "), amountText); var text = new TextFlow(new Text("Balance record #" + balanceRecord.id + " added with value of "), amountText);
setCenter(text); setCenter(text);
Hyperlink deleteLink = new Hyperlink("Delete this balance record"); Hyperlink viewLink = new Hyperlink("View this balance record");
deleteLink.setOnAction(event -> { viewLink.setOnAction(event -> router.navigate("balance-record", balanceRecord));
boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? It will be removed permanently, and cannot be undone."); setBottom(viewLink);
if (confirm) {
Profile.getCurrent().getDataSource().useBalanceRecordRepository(balanceRecordRepo -> {
balanceRecordRepo.deleteById(balanceRecord.id);
Platform.runLater(controller::reloadHistory);
});
}
});
setBottom(deleteLink);
} }
} }

View File

@ -4,10 +4,15 @@ import com.andrewlalis.perfin.PerfinApp;
import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ImageCache; import com.andrewlalis.perfin.view.ImageCache;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.*; import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import java.io.IOException; import java.io.IOException;
@ -22,17 +27,21 @@ import java.util.Set;
public class AttachmentPreview extends BorderPane { public class AttachmentPreview extends BorderPane {
public static final double IMAGE_SIZE = 64.0; public static final double IMAGE_SIZE = 64.0;
public static final double LABEL_SIZE = 18.0; public static final double LABEL_SIZE = 18.0;
public static final double HEIGHT = IMAGE_SIZE + LABEL_SIZE; public static final double HEIGHT = IMAGE_SIZE + LABEL_SIZE + 6;
public AttachmentPreview(Attachment attachment) { public AttachmentPreview(Attachment attachment) {
BorderPane contentContainer = new BorderPane(); BorderPane contentContainer = new BorderPane();
contentContainer.setPadding(new Insets(3));
Label nameLabel = new Label(attachment.getFilename()); Label nameLabel = new Label(attachment.getFilename());
nameLabel.getStyleClass().add("small-font"); nameLabel.getStyleClass().add("small-font");
VBox nameContainer = new VBox(nameLabel); nameLabel.setPrefHeight(LABEL_SIZE);
nameContainer.setPrefHeight(LABEL_SIZE); nameLabel.setMaxHeight(LABEL_SIZE);
nameContainer.setMaxHeight(LABEL_SIZE); nameLabel.setMinHeight(LABEL_SIZE);
nameContainer.setMinHeight(LABEL_SIZE); nameLabel.setMaxWidth(2 * IMAGE_SIZE);
contentContainer.setBottom(nameContainer); nameLabel.setAlignment(Pos.CENTER);
BorderPane.setAlignment(nameLabel, Pos.CENTER);
contentContainer.setBottom(nameLabel);
boolean showDocIcon = true; boolean showDocIcon = true;
Set<String> imageTypes = Set.of("image/png", "image/jpeg", "image/gif", "image/bmp"); Set<String> imageTypes = Set.of("image/png", "image/jpeg", "image/gif", "image/bmp");

View File

@ -0,0 +1,64 @@
package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.view.BindingUtil;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.List;
/**
* A pane which shows a list of attachments in a horizontally scrolling
* container.
*/
public class AttachmentsViewPane extends VBox {
private final StringProperty titleProperty = new SimpleStringProperty("Attachments");
private final ObservableList<Attachment> attachments = FXCollections.observableArrayList();
private final ListProperty<Attachment> attachmentListProperty = new SimpleListProperty<>(attachments);
public AttachmentsViewPane() {
Label titleLabel = new Label();
titleLabel.getStyleClass().add("bold-text");
titleLabel.textProperty().bind(titleProperty);
HBox attachmentsHBox = new HBox();
attachmentsHBox.setMinHeight(AttachmentPreview.HEIGHT);
attachmentsHBox.setPrefHeight(AttachmentPreview.HEIGHT);
attachmentsHBox.setMaxHeight(AttachmentPreview.HEIGHT);
attachmentsHBox.getStyleClass().add("std-spacing");
BindingUtil.mapContent(attachmentsHBox.getChildren(), attachments, AttachmentPreview::new);
ScrollPane scrollPane = new ScrollPane(attachmentsHBox);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
scrollPane.minViewportHeightProperty().bind(attachmentsHBox.heightProperty());
scrollPane.prefViewportHeightProperty().bind(attachmentsHBox.heightProperty());
getChildren().addAll(titleLabel, scrollPane);
}
public void setTitle(String title) {
titleProperty.set(title);
}
public void setAttachments(List<Attachment> attachments) {
this.attachments.setAll(attachments);
}
public ListProperty<Attachment> listProperty() {
return attachmentListProperty;
}
public void hideIfEmpty() {
managedProperty().bind(visibleProperty());
visibleProperty().bind(attachmentListProperty.emptyProperty().not());
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.AttachmentsViewPane?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.FlowPane?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.BalanceRecordViewController"
>
<top>
<Label fx:id="titleLabel" styleClass="large-font,bold-text,std-padding"/>
</top>
<center>
<VBox styleClass="std-padding,std-spacing">
<PropertiesPane vgap="5" hgap="5">
<columnConstraints>
<ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/>
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
</columnConstraints>
<Label text="Timestamp" styleClass="bold-text"/>
<Label fx:id="timestampLabel" styleClass="mono-font"/>
<Label text="Balance" styleClass="bold-text"/>
<Label fx:id="balanceLabel" styleClass="mono-font"/>
<Label text="Currency" styleClass="bold-text"/>
<Label fx:id="currencyLabel" styleClass="mono-font"/>
</PropertiesPane>
<AttachmentsViewPane fx:id="attachmentsViewPane"/>
</VBox>
</center>
<bottom>
<FlowPane styleClass="std-padding,std-spacing">
<Button text="Delete" onAction="#delete"/>
</FlowPane>
</bottom>
</BorderPane>

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.AttachmentsViewPane?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.Text?> <?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?> <?import javafx.scene.text.TextFlow?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<BorderPane xmlns="http://javafx.com/javafx" <BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.TransactionViewController" fx:controller="com.andrewlalis.perfin.control.TransactionViewController"
@ -22,7 +23,7 @@
<ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/> <ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/>
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/> <ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
</columnConstraints> </columnConstraints>
<Label text="Amount" styleClass="bold-text" style="-fx-min-width: 100px;"/> <Label text="Amount" styleClass="bold-text"/>
<Label fx:id="amountLabel" styleClass="mono-font"/> <Label fx:id="amountLabel" styleClass="mono-font"/>
<Label text="Timestamp" styleClass="bold-text"/> <Label text="Timestamp" styleClass="bold-text"/>
@ -41,12 +42,7 @@
<Hyperlink fx:id="creditAccountLink"/> <Hyperlink fx:id="creditAccountLink"/>
</TextFlow> </TextFlow>
</VBox> </VBox>
<VBox fx:id="attachmentsContainer"> <AttachmentsViewPane fx:id="attachmentsViewPane"/>
<Label text="Attachments" styleClass="bold-text"/>
<ScrollPane fitToWidth="true" fitToHeight="true">
<HBox fx:id="attachmentsHBox" styleClass="std-padding,std-spacing"/>
</ScrollPane>
</VBox>
<FlowPane styleClass="std-padding, std-spacing"> <FlowPane styleClass="std-padding, std-spacing">
<Button text="Delete this transaction" onAction="#deleteTransaction"/> <Button text="Delete this transaction" onAction="#deleteTransaction"/>
</FlowPane> </FlowPane>