Added account history.
This commit is contained in:
parent
d5bee39c20
commit
651396739f
|
@ -1,32 +1,40 @@
|
||||||
package com.andrewlalis.perfin.control;
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
|
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||||
|
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
import com.andrewlalis.perfin.model.Account;
|
import com.andrewlalis.perfin.model.Account;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
public class AccountViewController implements RouteSelectionListener {
|
public class AccountViewController implements RouteSelectionListener {
|
||||||
private Account account;
|
private Account account;
|
||||||
|
|
||||||
@FXML
|
@FXML public Label titleLabel;
|
||||||
public Label titleLabel;
|
@FXML public TextField accountNameField;
|
||||||
@FXML
|
@FXML public TextField accountNumberField;
|
||||||
public TextField accountNameField;
|
@FXML public TextField accountCreatedAtField;
|
||||||
@FXML
|
@FXML public TextField accountCurrencyField;
|
||||||
public TextField accountNumberField;
|
@FXML public TextField accountBalanceField;
|
||||||
@FXML
|
|
||||||
public TextField accountCreatedAtField;
|
@FXML public VBox historyItemsVBox;
|
||||||
@FXML
|
@FXML public Button loadMoreHistoryButton;
|
||||||
public TextField accountCurrencyField;
|
private LocalDateTime loadHistoryFrom;
|
||||||
@FXML
|
private final int historyLoadSize = 5;
|
||||||
public TextField accountBalanceField;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
|
@ -38,6 +46,11 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
accountCurrencyField.setText(account.getCurrency().getDisplayName());
|
accountCurrencyField.setText(account.getCurrency().getDisplayName());
|
||||||
accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
|
accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
|
||||||
Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText);
|
Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText);
|
||||||
|
|
||||||
|
loadHistoryFrom = DateUtil.nowAsUTC();
|
||||||
|
historyItemsVBox.getChildren().clear();
|
||||||
|
loadMoreHistoryButton.setDisable(false);
|
||||||
|
loadMoreHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
@ -78,4 +91,56 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
router.navigate("accounts");
|
router.navigate("accounts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML public void loadMoreHistory() {
|
||||||
|
Thread.ofVirtual().start(() -> {
|
||||||
|
try (var historyRepo = Profile.getCurrent().getDataSource().getAccountHistoryItemRepository()) {
|
||||||
|
List<AccountHistoryItem> historyItems = historyRepo.findMostRecentForAccount(
|
||||||
|
account.getId(),
|
||||||
|
loadHistoryFrom,
|
||||||
|
historyLoadSize
|
||||||
|
);
|
||||||
|
if (historyItems.size() < historyLoadSize) {
|
||||||
|
Platform.runLater(() -> loadMoreHistoryButton.setDisable(true));
|
||||||
|
} else {
|
||||||
|
loadHistoryFrom = historyItems.getLast().getTimestamp();
|
||||||
|
}
|
||||||
|
List<Node> nodes = historyItems.stream().map(item -> visualizeHistoryItem(item, historyRepo)).toList();
|
||||||
|
Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node visualizeHistoryItem(AccountHistoryItem item, AccountHistoryItemRepository repo) {
|
||||||
|
BorderPane containerPane = new BorderPane();
|
||||||
|
containerPane.setStyle("""
|
||||||
|
-fx-border-color: lightgray;
|
||||||
|
-fx-border-radius: 5px;
|
||||||
|
-fx-padding: 5px;
|
||||||
|
""");
|
||||||
|
Label timestampLabel = new Label(item.getTimestamp().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
||||||
|
timestampLabel.setStyle("-fx-font-size: small;");
|
||||||
|
containerPane.setTop(timestampLabel);
|
||||||
|
containerPane.setCenter(switch (item.getType()) {
|
||||||
|
case TEXT -> {
|
||||||
|
var text = repo.getTextItem(item.getId());
|
||||||
|
yield new TextFlow(new Text(text));
|
||||||
|
}
|
||||||
|
case ACCOUNT_ENTRY -> {
|
||||||
|
var entry = repo.getAccountEntryItem(item.getId());
|
||||||
|
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
|
||||||
|
TextFlow text = new TextFlow(new Text("Entry added with value of "), amountText);
|
||||||
|
yield text;
|
||||||
|
}
|
||||||
|
case BALANCE_RECORD -> {
|
||||||
|
var balanceRecord = repo.getBalanceRecordItem(item.getId());
|
||||||
|
Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency()));
|
||||||
|
TextFlow text = new TextFlow(new Text("Balance record added with value of "), amountText);
|
||||||
|
yield text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return containerPane;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
package com.andrewlalis.perfin.data;
|
package com.andrewlalis.perfin.data;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.model.AccountEntry;
|
||||||
|
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||||
|
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface AccountHistoryItemRepository extends AutoCloseable {
|
public interface AccountHistoryItemRepository extends AutoCloseable {
|
||||||
void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId);
|
void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId);
|
||||||
void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId);
|
void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId);
|
||||||
void recordText(LocalDateTime timestamp, long accountId, String text);
|
void recordText(LocalDateTime timestamp, long accountId, String text);
|
||||||
|
List<AccountHistoryItem> findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count);
|
||||||
|
String getTextItem(long itemId);
|
||||||
|
AccountEntry getAccountEntryItem(long itemId);
|
||||||
|
BalanceRecord getBalanceRecordItem(long itemId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,14 @@ package com.andrewlalis.perfin.data.impl;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
|
import com.andrewlalis.perfin.model.AccountEntry;
|
||||||
|
import com.andrewlalis.perfin.model.BalanceRecord;
|
||||||
|
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||||
import com.andrewlalis.perfin.model.history.AccountHistoryItemType;
|
import com.andrewlalis.perfin.model.history.AccountHistoryItemType;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -34,16 +39,73 @@ public record JdbcAccountHistoryItemRepository(Connection conn) implements Accou
|
||||||
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.TEXT);
|
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.TEXT);
|
||||||
DbUtil.insertOne(
|
DbUtil.insertOne(
|
||||||
conn,
|
conn,
|
||||||
"INSERT INTO account_history_item_account_entry (item_id, description) VALUES (?, ?)",
|
"INSERT INTO account_history_item_text (item_id, description) VALUES (?, ?)",
|
||||||
List.of(itemId, text)
|
List.of(itemId, text)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AccountHistoryItem> findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count) {
|
||||||
|
return DbUtil.findAll(
|
||||||
|
conn,
|
||||||
|
"SELECT * FROM account_history_item WHERE account_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT " + count,
|
||||||
|
List.of(accountId, DbUtil.timestampFromUtcLDT(utcTimestamp)),
|
||||||
|
JdbcAccountHistoryItemRepository::parseHistoryItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTextItem(long itemId) {
|
||||||
|
return DbUtil.findOne(
|
||||||
|
conn,
|
||||||
|
"SELECT description FROM account_history_item_text WHERE item_id = ?",
|
||||||
|
List.of(itemId),
|
||||||
|
rs -> rs.getString(1)
|
||||||
|
).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountEntry getAccountEntryItem(long itemId) {
|
||||||
|
return DbUtil.findOne(
|
||||||
|
conn,
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM account_entry
|
||||||
|
LEFT JOIN account_history_item_account_entry h ON h.entry_id = account_entry.id
|
||||||
|
WHERE h.item_id = ?""",
|
||||||
|
List.of(itemId),
|
||||||
|
JdbcAccountEntryRepository::parse
|
||||||
|
).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BalanceRecord getBalanceRecordItem(long itemId) {
|
||||||
|
return DbUtil.findOne(
|
||||||
|
conn,
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM balance_record
|
||||||
|
LEFT JOIN account_history_item_balance_record h ON h.record_id = balance_record.id
|
||||||
|
WHERE h.item_id = ?""",
|
||||||
|
List.of(itemId),
|
||||||
|
JdbcBalanceRecordRepository::parse
|
||||||
|
).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
conn.close();
|
conn.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountHistoryItem parseHistoryItem(ResultSet rs) throws SQLException {
|
||||||
|
return new AccountHistoryItem(
|
||||||
|
rs.getLong("id"),
|
||||||
|
DbUtil.utcLDTFromTimestamp(rs.getTimestamp("timestamp")),
|
||||||
|
rs.getLong("account_id"),
|
||||||
|
AccountHistoryItemType.valueOf(rs.getString("type"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private long insertHistoryItem(LocalDateTime timestamp, long accountId, AccountHistoryItemType type) {
|
private long insertHistoryItem(LocalDateTime timestamp, long accountId, AccountHistoryItemType type) {
|
||||||
return DbUtil.insertOne(
|
return DbUtil.insertOne(
|
||||||
conn,
|
conn,
|
||||||
|
|
|
@ -23,7 +23,7 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
||||||
long recordId = DbUtil.insertOne(
|
long recordId = DbUtil.insertOne(
|
||||||
conn,
|
conn,
|
||||||
"INSERT INTO balance_record (timestamp, account_id, balance, currency) VALUES (?, ?, ?, ?)",
|
"INSERT INTO balance_record (timestamp, account_id, balance, currency) VALUES (?, ?, ?, ?)",
|
||||||
List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, balance, currency)
|
List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), accountId, balance, currency.getCurrencyCode())
|
||||||
);
|
);
|
||||||
// Insert attachments.
|
// Insert attachments.
|
||||||
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
package com.andrewlalis.perfin.model;
|
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A file that's been attached to a transaction as additional context for it,
|
|
||||||
* like a receipt or invoice copy.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public class TransactionAttachment {
|
|
||||||
private long id;
|
|
||||||
private LocalDateTime uploadedAt;
|
|
||||||
private long transactionId;
|
|
||||||
|
|
||||||
private String filename;
|
|
||||||
private String contentType;
|
|
||||||
|
|
||||||
public TransactionAttachment(String filename, String contentType) {
|
|
||||||
this.filename = filename;
|
|
||||||
this.contentType = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransactionAttachment(long id, LocalDateTime uploadedAt, long transactionId, String filename, String contentType) {
|
|
||||||
this.id = id;
|
|
||||||
this.uploadedAt = uploadedAt;
|
|
||||||
this.transactionId = transactionId;
|
|
||||||
this.filename = filename;
|
|
||||||
this.contentType = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getUploadedAt() {
|
|
||||||
return uploadedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTransactionId() {
|
|
||||||
return transactionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename() {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() {
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getPath() {
|
|
||||||
String uploadDateStr = uploadedAt.format(DateUtil.DEFAULT_DATE_FORMAT);
|
|
||||||
return Profile.getContentDir(Profile.getCurrent().getName())
|
|
||||||
.resolve("transaction-attachments")
|
|
||||||
.resolve(uploadDateStr)
|
|
||||||
.resolve("tx-" + transactionId)
|
|
||||||
.resolve(filename);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,6 +43,24 @@
|
||||||
<Label text="Panel 2"/>
|
<Label text="Panel 2"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
<Separator/>
|
||||||
|
<VBox>
|
||||||
|
<Label text="History" styleClass="bold-text"/>
|
||||||
|
<VBox>
|
||||||
|
<ScrollPane fitToHeight="true" fitToWidth="true">
|
||||||
|
<VBox fx:id="historyItemsVBox" style="-fx-padding: 10px; -fx-spacing: 10px;"/>
|
||||||
|
</ScrollPane>
|
||||||
|
<AnchorPane>
|
||||||
|
<Button
|
||||||
|
fx:id="loadMoreHistoryButton"
|
||||||
|
text="Load more history"
|
||||||
|
onAction="#loadMoreHistory"
|
||||||
|
AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
/>
|
||||||
|
</AnchorPane>
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</center>
|
</center>
|
||||||
<right>
|
<right>
|
||||||
|
|
Loading…
Reference in New Issue