diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index 2d8b810..73ff2f1 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -86,6 +86,7 @@ public class PerfinApp extends Application { router.map("transactions", PerfinApp.class.getResource("/transactions-view.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("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml")); // Help pages. helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); diff --git a/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java new file mode 100644 index 0000000..d435767 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java @@ -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 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(); + } + } +} diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java index 1c6155e..1a9a10c 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -6,18 +6,11 @@ import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.CreditAndDebitAccounts; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Transaction; -import com.andrewlalis.perfin.view.BindingUtil; -import com.andrewlalis.perfin.view.component.AttachmentPreview; +import com.andrewlalis.perfin.view.component.AttachmentsViewPane; 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.control.ScrollPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import javafx.scene.text.TextFlow; import java.util.List; @@ -36,9 +29,13 @@ public class TransactionViewController { @FXML public Hyperlink debitAccountLink; @FXML public Hyperlink creditAccountLink; - @FXML public VBox attachmentsContainer; - @FXML public HBox attachmentsHBox; - private final ObservableList attachmentsList = FXCollections.observableArrayList(); + @FXML public AttachmentsViewPane attachmentsViewPane; + + @FXML public void initialize() { + configureAccountLinkBindings(debitAccountLink); + configureAccountLinkBindings(creditAccountLink); + attachmentsViewPane.hideIfEmpty(); + } public void setTransaction(Transaction transaction) { this.transaction = transaction; @@ -48,8 +45,6 @@ public class TransactionViewController { 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.id); @@ -66,19 +61,12 @@ public class TransactionViewController { }); }); - 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.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() { @@ -93,7 +81,6 @@ public class TransactionViewController { ); if (confirm) { Profile.getCurrent().getDataSource().useTransactionRepository(repo -> { - // TODO: Delete attachments first! repo.delete(transaction.id); router.replace("transactions"); }); diff --git a/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java b/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java index 22c7c67..7931ad8 100644 --- a/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/BalanceRecordRepository.java @@ -1,5 +1,6 @@ package com.andrewlalis.perfin.data; +import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.BalanceRecord; import java.math.BigDecimal; @@ -14,5 +15,6 @@ public interface BalanceRecordRepository extends AutoCloseable { BalanceRecord findLatestByAccountId(long accountId); Optional findClosestBefore(long accountId, LocalDateTime utcTimestamp); Optional findClosestAfter(long accountId, LocalDateTime utcTimestamp); + List findAttachments(long recordId); void deleteById(long id); } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java index 5055eb9..44f42d2 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcBalanceRecordRepository.java @@ -72,6 +72,21 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl ); } + @Override + public List 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 public void deleteById(long id) { DbUtil.updateOne(conn, "DELETE FROM balance_record WHERE id = ?", List.of(id)); diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java index c9f3c04..b263498 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryBalanceRecordTile.java @@ -1,17 +1,16 @@ package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.control.AccountViewController; -import com.andrewlalis.perfin.control.Popups; import com.andrewlalis.perfin.data.AccountHistoryItemRepository; import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.model.BalanceRecord; -import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.history.AccountHistoryItem; -import javafx.application.Platform; import javafx.scene.control.Hyperlink; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import static com.andrewlalis.perfin.PerfinApp.router; + public class AccountHistoryBalanceRecordTile extends AccountHistoryItemTile { public AccountHistoryBalanceRecordTile(AccountHistoryItem item, AccountHistoryItemRepository repo, AccountViewController controller) { 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); setCenter(text); - Hyperlink deleteLink = new Hyperlink("Delete this balance record"); - deleteLink.setOnAction(event -> { - boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? It will be removed permanently, and cannot be undone."); - if (confirm) { - Profile.getCurrent().getDataSource().useBalanceRecordRepository(balanceRecordRepo -> { - balanceRecordRepo.deleteById(balanceRecord.id); - Platform.runLater(controller::reloadHistory); - }); - } - }); - setBottom(deleteLink); + Hyperlink viewLink = new Hyperlink("View this balance record"); + viewLink.setOnAction(event -> router.navigate("balance-record", balanceRecord)); + setBottom(viewLink); } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java index 7e15772..2e986d0 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java @@ -4,10 +4,15 @@ import com.andrewlalis.perfin.PerfinApp; import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.ImageCache; +import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.image.Image; 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 java.io.IOException; @@ -22,17 +27,21 @@ import java.util.Set; public class AttachmentPreview extends BorderPane { public static final double IMAGE_SIZE = 64.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) { BorderPane contentContainer = new BorderPane(); + contentContainer.setPadding(new Insets(3)); + Label nameLabel = new Label(attachment.getFilename()); nameLabel.getStyleClass().add("small-font"); - VBox nameContainer = new VBox(nameLabel); - nameContainer.setPrefHeight(LABEL_SIZE); - nameContainer.setMaxHeight(LABEL_SIZE); - nameContainer.setMinHeight(LABEL_SIZE); - contentContainer.setBottom(nameContainer); + nameLabel.setPrefHeight(LABEL_SIZE); + nameLabel.setMaxHeight(LABEL_SIZE); + nameLabel.setMinHeight(LABEL_SIZE); + nameLabel.setMaxWidth(2 * IMAGE_SIZE); + nameLabel.setAlignment(Pos.CENTER); + BorderPane.setAlignment(nameLabel, Pos.CENTER); + contentContainer.setBottom(nameLabel); boolean showDocIcon = true; Set imageTypes = Set.of("image/png", "image/jpeg", "image/gif", "image/bmp"); diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentsViewPane.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentsViewPane.java new file mode 100644 index 0000000..a6c384b --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentsViewPane.java @@ -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 attachments = FXCollections.observableArrayList(); + private final ListProperty 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 attachments) { + this.attachments.setAll(attachments); + } + + public ListProperty listProperty() { + return attachmentListProperty; + } + + public void hideIfEmpty() { + managedProperty().bind(visibleProperty()); + visibleProperty().bind(attachmentListProperty.emptyProperty().not()); + } +} diff --git a/src/main/resources/balance-record-view.fxml b/src/main/resources/balance-record-view.fxml new file mode 100644 index 0000000..0cec9e5 --- /dev/null +++ b/src/main/resources/balance-record-view.fxml @@ -0,0 +1,42 @@ + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + +