Added properties pane and improved the account view.

This commit is contained in:
Andrew Lalis 2024-01-04 12:37:16 -05:00
parent 32fb7e8eb8
commit 4c69cd1662
7 changed files with 151 additions and 71 deletions

View File

@ -7,9 +7,15 @@ import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.history.AccountHistoryItem; import com.andrewlalis.perfin.model.history.AccountHistoryItem;
import com.andrewlalis.perfin.view.component.AccountHistoryItemTile; import com.andrewlalis.perfin.view.component.AccountHistoryItemTile;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -21,27 +27,44 @@ public class AccountViewController implements RouteSelectionListener {
private Account account; private Account account;
@FXML public Label titleLabel; @FXML public Label titleLabel;
@FXML public TextField accountNameField;
@FXML public TextField accountNumberField; @FXML public Label accountNameLabel;
@FXML public TextField accountCreatedAtField; @FXML public Label accountNumberLabel;
@FXML public TextField accountCurrencyField; @FXML public Label accountCurrencyLabel;
@FXML public TextField accountBalanceField; @FXML public Label accountCreatedAtLabel;
@FXML public Label accountBalanceLabel;
@FXML public BooleanProperty accountArchivedProperty = new SimpleBooleanProperty(false);
@FXML public VBox historyItemsVBox; @FXML public VBox historyItemsVBox;
@FXML public Button loadMoreHistoryButton; @FXML public Button loadMoreHistoryButton;
private LocalDateTime loadHistoryFrom; private LocalDateTime loadHistoryFrom;
private final int historyLoadSize = 5; private final int historyLoadSize = 5;
@FXML public VBox actionsVBox;
@FXML public void initialize() {
actionsVBox.getChildren().forEach(node -> {
Button button = (Button) node;
BooleanExpression buttonActive = accountArchivedProperty;
if (button.getText().equalsIgnoreCase("Unarchive")) {
buttonActive = buttonActive.not();
}
button.disableProperty().bind(buttonActive);
button.managedProperty().bind(button.visibleProperty());
button.visibleProperty().bind(button.disableProperty().not());
});
}
@Override @Override
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
account = (Account) context; account = (Account) context;
accountArchivedProperty.set(account.isArchived());
titleLabel.setText("Account #" + account.id); titleLabel.setText("Account #" + account.id);
accountNameLabel.setText(account.getName());
accountNameField.setText(account.getName()); accountNumberLabel.setText(account.getAccountNumber());
accountNumberField.setText(account.getAccountNumber()); accountCurrencyLabel.setText(account.getCurrency().getDisplayName());
accountCurrencyField.setText(account.getCurrency().getDisplayName()); accountCreatedAtLabel.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt())); Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceLabel::setText);
Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText);
reloadHistory(); reloadHistory();
} }
@ -73,12 +96,16 @@ public class AccountViewController implements RouteSelectionListener {
"later if you need to." "later if you need to."
).showAndWait(); ).showAndWait();
if (confirmResult.isPresent() && confirmResult.get() == ButtonType.OK) { if (confirmResult.isPresent() && confirmResult.get() == ButtonType.OK) {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.archive(account)); Profile.getCurrent().getDataSource().useAccountRepository(repo -> repo.archive(account.id));
router.getHistory().clear(); router.getHistory().clear();
router.navigate("accounts"); router.navigate("accounts");
} }
} }
@FXML public void unarchiveAccount() {
System.out.println("Unarchiving");
}
@FXML @FXML
public void deleteAccount() { public void deleteAccount() {
var confirmResult = new Alert( var confirmResult = new Alert(

View File

@ -21,7 +21,8 @@ public interface AccountRepository extends AutoCloseable {
void updateName(long id, String name); void updateName(long id, String name);
void update(Account account); void update(Account account);
void delete(Account account); void delete(Account account);
void archive(Account account); void archive(long accountId);
void unarchive(long accountId);
BigDecimal deriveBalance(long accountId, Instant timestamp); BigDecimal deriveBalance(long accountId, Instant timestamp);
default BigDecimal deriveCurrentBalance(long accountId) { default BigDecimal deriveCurrentBalance(long accountId) {

View File

@ -147,8 +147,19 @@ public record JdbcAccountRepository(Connection conn) implements AccountRepositor
} }
@Override @Override
public void archive(Account account) { public void archive(long accountId) {
DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(account.id)); DbUtil.doTransaction(conn, () -> {
DbUtil.updateOne(conn, "UPDATE account SET archived = TRUE WHERE id = ?", List.of(accountId));
new JdbcAccountHistoryItemRepository(conn).recordText(DateUtil.nowAsUTC(), accountId, "Account has been archived.");
});
}
@Override
public void unarchive(long accountId) {
DbUtil.doTransaction(conn, () -> {
DbUtil.updateOne(conn, "UPDATE account SET archived = FALSE WHERE id = ?", List.of(accountId));
new JdbcAccountHistoryItemRepository(conn).recordText(DateUtil.nowAsUTC(), accountId, "Account has been unarchived.");
});
} }
public static Account parseAccount(ResultSet rs) throws SQLException { public static Account parseAccount(ResultSet rs) throws SQLException {

View File

@ -10,11 +10,9 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.*;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment; import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
@ -54,9 +52,11 @@ public class DataSourcePaginationControls extends BorderPane {
maxPagesText.managedProperty().bind(maxPagesText.visibleProperty()); maxPagesText.managedProperty().bind(maxPagesText.visibleProperty());
maxPagesText.visibleProperty().bind(maxPages.greaterThan(0)); maxPagesText.visibleProperty().bind(maxPages.greaterThan(0));
TextFlow pageText = new TextFlow(new Text("Page "), currentPageLabel, maxPagesText); TextFlow pageText = new TextFlow(new Text("Page "), currentPageLabel, maxPagesText);
pageText.setTextAlignment(TextAlignment.CENTER); AnchorPane pageTextContainer = new AnchorPane(pageText);
BorderPane pageTextContainer = new BorderPane(pageText); AnchorPane.setTopAnchor(pageText, 4.0);
BorderPane.setAlignment(pageText, Pos.CENTER); AnchorPane.setRightAnchor(pageText, 0.0);
AnchorPane.setBottomAnchor(pageText, 0.0);
AnchorPane.setLeftAnchor(pageText, 0.0);
Button previousPageButton = new Button("Previous Page"); Button previousPageButton = new Button("Previous Page");

View File

@ -0,0 +1,55 @@
package com.andrewlalis.perfin.view.component;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import java.util.ArrayList;
import java.util.List;
/**
* A specially-formatted {@link GridPane} that arranges its children into a
* two-column grid representing key-value pairs.
*/
public class PropertiesPane extends GridPane {
public PropertiesPane() {
ColumnConstraints keyConstraints = new ColumnConstraints();
keyConstraints.setHgrow(Priority.NEVER);
keyConstraints.setHalignment(HPos.LEFT);
keyConstraints.setMinWidth(10.0);
ColumnConstraints valueConstraints = new ColumnConstraints();
valueConstraints.setHgrow(Priority.ALWAYS);
valueConstraints.setHalignment(HPos.LEFT);
valueConstraints.setMinWidth(10.0);
getColumnConstraints().setAll(keyConstraints, valueConstraints);
}
@Override
protected void layoutChildren() {
// Apply grid positioning to all children in the order in which they appear, like so:
// key 1 value 1
// key 2 value 2
// ... and so on.
int rowCount = getManagedChildren().size() / 2;
List<RowConstraints> rows = new ArrayList<>(rowCount);
for (int i = 0; i < rowCount; i++) {
RowConstraints c = new RowConstraints();
c.setValignment(VPos.TOP);
c.setVgrow(Priority.NEVER);
rows.add(c);
}
getRowConstraints().setAll(rows);
for (int i = 0; i < getManagedChildren().size(); i++) {
Node child = getManagedChildren().get(i);
int column = i % 2;
int row = i / 2;
GridPane.setRowIndex(child, row);
GridPane.setColumnIndex(child, column);
}
super.layoutChildren();
}
}

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?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?>
<BorderPane <BorderPane
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.AccountViewController" fx:controller="com.andrewlalis.perfin.control.AccountViewController"
stylesheets="@style/account-view.css,@style/base.css" stylesheets="@style/base.css"
> >
<top> <top>
<HBox styleClass="std-padding,std-spacing"> <Label fx:id="titleLabel" styleClass="std-padding,large-text,bold-text"/>
<Label fx:id="titleLabel" styleClass="large-text,bold-text"/>
</HBox>
</top> </top>
<center> <center>
<VBox> <VBox>
@ -19,50 +19,53 @@
<BorderPane> <BorderPane>
<center> <center>
<VBox styleClass="std-padding,std-spacing"> <VBox styleClass="std-padding,std-spacing">
<HBox> <FlowPane>
<!-- Main account properties. --> <!-- Main account properties. -->
<VBox HBox.hgrow="SOMETIMES"> <PropertiesPane vgap="5" hgap="5">
<VBox styleClass="account-property-box"> <Label text="Name" styleClass="bold-text"/>
<Label text="Name"/> <Label fx:id="accountNameLabel"/>
<TextField fx:id="accountNameField" editable="false"/>
<Label text="Number" styleClass="bold-text"/>
<Label fx:id="accountNumberLabel" styleClass="mono-font"/>
<Label text="Currency" styleClass="bold-text"/>
<Label fx:id="accountCurrencyLabel"/>
<Label text="Created At" styleClass="bold-text"/>
<Label fx:id="accountCreatedAtLabel" styleClass="mono-font"/>
<VBox>
<Label text="Current Balance" styleClass="bold-text" fx:id="balanceLabel"/>
<Text
style="-fx-font-size: x-small; -fx-fill: grey"
wrappingWidth="${balanceLabel.width}"
>Computed using the last recorded balance and all transactions since.</Text>
</VBox> </VBox>
<VBox styleClass="account-property-box"> <Label fx:id="accountBalanceLabel" styleClass="mono-font"/>
<Label text="Number"/> </PropertiesPane>
<TextField fx:id="accountNumberField" editable="false" styleClass="mono-font"/>
</VBox>
<VBox styleClass="account-property-box">
<Label text="Currency"/>
<TextField fx:id="accountCurrencyField" editable="false"/>
</VBox>
<VBox styleClass="account-property-box">
<Label text="Created At"/>
<TextField fx:id="accountCreatedAtField" editable="false"/>
</VBox>
<VBox styleClass="account-property-box">
<Label text="Current Balance"/>
<TextField fx:id="accountBalanceField" editable="false"/>
</VBox>
</VBox>
<VBox HBox.hgrow="SOMETIMES"> <VBox HBox.hgrow="SOMETIMES">
<Label text="Panel 2"/> <Label text="Panel 2"/>
</VBox> </VBox>
</HBox> </FlowPane>
</VBox> </VBox>
</center> </center>
<right> <right>
<VBox styleClass="std-padding,std-spacing"> <VBox styleClass="std-padding,std-spacing">
<Label text="Actions" styleClass="bold-text"/> <Label text="Actions" styleClass="bold-text"/>
<Button text="Edit" onAction="#goToEditPage"/> <VBox fx:id="actionsVBox" styleClass="std-spacing">
<Button text="Record Balance" onAction="#goToCreateBalanceRecord"/> <Button text="Edit" onAction="#goToEditPage"/>
<Button text="Archive" onAction="#archiveAccount"/> <Button text="Record Balance" onAction="#goToCreateBalanceRecord"/>
<Button text="Delete" onAction="#deleteAccount"/> <Button text="Archive" onAction="#archiveAccount"/>
<Button text="Delete" onAction="#deleteAccount"/>
<Button text="Unarchive" onAction="#unarchiveAccount"/>
</VBox>
</VBox> </VBox>
</right> </right>
</BorderPane> </BorderPane>
<!-- Account history --> <!-- Account history -->
<VBox VBox.vgrow="ALWAYS"> <VBox VBox.vgrow="ALWAYS">
<Label text="History" styleClass="bold-text"/> <Label text="History" styleClass="bold-text,std-padding"/>
<VBox> <VBox>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
<VBox fx:id="historyItemsVBox" style="-fx-padding: 10px; -fx-spacing: 10px;"/> <VBox fx:id="historyItemsVBox" style="-fx-padding: 10px; -fx-spacing: 10px;"/>

View File

@ -1,17 +0,0 @@
.account-property-box {
-fx-padding: 5px 0 5px 0;
-fx-spacing: 3px;
}
.account-property-box > Label {
-fx-font-weight: bold;
}
.account-property-box > TextField {
-fx-min-width: 100px;
-fx-max-width: 300px;
}
.actions-box > Button {
-fx-max-width: 500px;
}