Added account history.

This commit is contained in:
Andrew Lalis 2023-12-31 00:34:37 -05:00
parent d5bee39c20
commit 651396739f
6 changed files with 172 additions and 80 deletions

View File

@ -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;
}
} }

View File

@ -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);
} }

View File

@ -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,

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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>