Added account history.
This commit is contained in:
parent
d5bee39c20
commit
651396739f
|
@ -1,32 +1,40 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
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.model.Account;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
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;
|
||||
|
||||
public class AccountViewController implements RouteSelectionListener {
|
||||
private Account account;
|
||||
|
||||
@FXML
|
||||
public Label titleLabel;
|
||||
@FXML
|
||||
public TextField accountNameField;
|
||||
@FXML
|
||||
public TextField accountNumberField;
|
||||
@FXML
|
||||
public TextField accountCreatedAtField;
|
||||
@FXML
|
||||
public TextField accountCurrencyField;
|
||||
@FXML
|
||||
public TextField accountBalanceField;
|
||||
@FXML public Label titleLabel;
|
||||
@FXML public TextField accountNameField;
|
||||
@FXML public TextField accountNumberField;
|
||||
@FXML public TextField accountCreatedAtField;
|
||||
@FXML public TextField accountCurrencyField;
|
||||
@FXML public TextField accountBalanceField;
|
||||
|
||||
@FXML public VBox historyItemsVBox;
|
||||
@FXML public Button loadMoreHistoryButton;
|
||||
private LocalDateTime loadHistoryFrom;
|
||||
private final int historyLoadSize = 5;
|
||||
|
||||
@Override
|
||||
public void onRouteSelected(Object context) {
|
||||
|
@ -38,6 +46,11 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
accountCurrencyField.setText(account.getCurrency().getDisplayName());
|
||||
accountCreatedAtField.setText(DateUtil.formatUTCAsLocalWithZone(account.getCreatedAt()));
|
||||
Profile.getCurrent().getDataSource().getAccountBalanceText(account, accountBalanceField::setText);
|
||||
|
||||
loadHistoryFrom = DateUtil.nowAsUTC();
|
||||
historyItemsVBox.getChildren().clear();
|
||||
loadMoreHistoryButton.setDisable(false);
|
||||
loadMoreHistory();
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
@ -78,4 +91,56 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
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;
|
||||
|
||||
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.util.List;
|
||||
|
||||
public interface AccountHistoryItemRepository extends AutoCloseable {
|
||||
void recordAccountEntry(LocalDateTime timestamp, long accountId, long entryId);
|
||||
void recordBalanceRecord(LocalDateTime timestamp, long accountId, long recordId);
|
||||
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.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 java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -34,16 +39,73 @@ public record JdbcAccountHistoryItemRepository(Connection conn) implements Accou
|
|||
long itemId = insertHistoryItem(timestamp, accountId, AccountHistoryItemType.TEXT);
|
||||
DbUtil.insertOne(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
@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
|
||||
public void close() throws Exception {
|
||||
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) {
|
||||
return DbUtil.insertOne(
|
||||
conn,
|
||||
|
|
|
@ -23,7 +23,7 @@ public record JdbcBalanceRecordRepository(Connection conn, Path contentDir) impl
|
|||
long recordId = DbUtil.insertOne(
|
||||
conn,
|
||||
"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.
|
||||
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"/>
|
||||
</VBox>
|
||||
</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>
|
||||
</center>
|
||||
<right>
|
||||
|
|
Loading…
Reference in New Issue