diff --git a/design/perfin-logo.svg b/design/perfin-logo.svg
index 2dcc35f..0e953e2 100644
--- a/design/perfin-logo.svg
+++ b/design/perfin-logo.svg
@@ -26,9 +26,9 @@
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
- inkscape:zoom="5.6568543"
- inkscape:cx="3.2703689"
- inkscape:cy="25.102291"
+ inkscape:zoom="4"
+ inkscape:cx="-0.99999999"
+ inkscape:cy="38.25"
inkscape:window-width="1920"
inkscape:window-height="1025"
inkscape:window-x="1080"
@@ -54,25 +54,25 @@
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
-
-
-
-
+
+
+
+
+
+
diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
index 9b578e2..54426b4 100644
--- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java
+++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java
@@ -44,6 +44,7 @@ public class PerfinApp extends Application {
splashScreen.showAndWait();
if (splashScreen.isStartupSuccessful()) {
stage.show();
+ stage.setMaximized(true);
}
}
@@ -70,6 +71,7 @@ public class PerfinApp extends Application {
mapResourceRoute("edit-account", "/edit-account.fxml");
mapResourceRoute("transactions", "/transactions-view.fxml");
mapResourceRoute("create-transaction", "/create-transaction.fxml");
+ mapResourceRoute("create-balance-record", "/create-balance-record.fxml");
});
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
index f3e51b0..886fdf0 100644
--- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
@@ -54,6 +54,10 @@ public class AccountViewController implements RouteSelectionListener {
router.navigate("edit-account", account);
}
+ @FXML public void goToCreateBalanceRecord() {
+ router.navigate("create-balance-record", account);
+ }
+
@FXML
public void archiveAccount() {
var confirmResult = new Alert(
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
new file mode 100644
index 0000000..da48bed
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
@@ -0,0 +1,68 @@
+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.data.util.FileUtil;
+import com.andrewlalis.perfin.model.Account;
+import com.andrewlalis.perfin.model.Profile;
+import com.andrewlalis.perfin.view.component.FileSelectionArea;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.VBox;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static com.andrewlalis.perfin.PerfinApp.router;
+
+public class CreateBalanceRecordController implements RouteSelectionListener {
+ @FXML public TextField timestampField;
+ @FXML public TextField balanceField;
+ @FXML public VBox attachmentsVBox;
+ private FileSelectionArea attachmentSelectionArea;
+
+ private Account account;
+
+ @FXML public void initialize() {
+ attachmentSelectionArea = new FileSelectionArea(FileUtil::newAttachmentsFileChooser, () -> attachmentsVBox.getScene().getWindow());
+ attachmentSelectionArea.allowMultiple.set(true);
+ attachmentsVBox.getChildren().add(attachmentSelectionArea);
+ }
+
+ @Override
+ public void onRouteSelected(Object context) {
+ this.account = (Account) context;
+ timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
+ Thread.ofVirtual().start(() -> {
+ Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
+ BigDecimal value = repo.deriveCurrentBalance(account.getId());
+ Platform.runLater(() -> balanceField.setText(
+ CurrencyUtil.formatMoneyAsBasicNumber(value, account.getCurrency())
+ ));
+ });
+ });
+ attachmentSelectionArea.clear();
+ }
+
+ @FXML public void save() {
+ // TODO: Add validation.
+ Profile.getCurrent().getDataSource().useBalanceRecordRepository(repo -> {
+ LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
+ BigDecimal reportedBalance = new BigDecimal(balanceField.getText());
+ repo.insert(
+ DateUtil.localToUTC(localTimestamp),
+ account.getId(),
+ reportedBalance,
+ account.getCurrency(),
+ attachmentSelectionArea.getSelectedFiles()
+ );
+ });
+ router.navigateBackAndClear();
+ }
+
+ @FXML public void cancel() {
+ router.navigateBackAndClear();
+ }
+}
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
index 0bd3429..c0a4ba6 100644
--- a/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateTransactionController.java
@@ -2,22 +2,17 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.util.DateUtil;
+import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
-import com.andrewlalis.perfin.view.BindingUtil;
+import com.andrewlalis.perfin.view.component.FileSelectionArea;
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.*;
-import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
-import javafx.stage.FileChooser;
-import java.io.File;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.DateTimeException;
@@ -45,9 +40,8 @@ public class CreateTransactionController implements RouteSelectionListener {
@FXML public ComboBox linkCreditAccountComboBox;
@FXML public Label linkedAccountsErrorLabel;
- private final ObservableList selectedAttachmentFiles = FXCollections.observableArrayList();
- @FXML public VBox selectedFilesVBox;
- @FXML public Label noSelectedFilesLabel;
+ @FXML public VBox attachmentsVBox;
+ private FileSelectionArea attachmentsSelectionArea;
@FXML public void initialize() {
// Setup error field validation.
@@ -83,40 +77,13 @@ public class CreateTransactionController implements RouteSelectionListener {
updateLinkAccountComboBoxes(newValue);
});
- // Show the "no files selected" label when the list is empty. And sync the vbox with the selected files.
- noSelectedFilesLabel.managedProperty().bind(noSelectedFilesLabel.visibleProperty());
- var filesListProp = new SimpleListProperty<>(selectedAttachmentFiles);
- noSelectedFilesLabel.visibleProperty().bind(filesListProp.emptyProperty());
- BindingUtil.mapContent(selectedFilesVBox.getChildren(), selectedAttachmentFiles, file -> {
- Label filenameLabel = new Label(file.getName());
- Button removeButton = new Button("Remove");
- removeButton.setOnAction(event -> {
- selectedAttachmentFiles.remove(file);
- });
- AnchorPane fileBox = new AnchorPane(filenameLabel, removeButton);
- AnchorPane.setLeftAnchor(filenameLabel, 0.0);
- AnchorPane.setRightAnchor(removeButton, 0.0);
- return fileBox;
- });
- }
-
- @FXML public void selectAttachmentFile() {
- FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle("Select Transaction Attachment(s)");
- fileChooser.getExtensionFilters().addAll(
- new FileChooser.ExtensionFilter(
- "Attachment Files",
- "*.pdf", "*.docx", "*.odt", "*.html", "*.txt", "*.md", "*.xml", "*.json",
- "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.bmp", "*.tiff"
- )
+ // Initialize the file selection area.
+ attachmentsSelectionArea = new FileSelectionArea(
+ FileUtil::newAttachmentsFileChooser,
+ () -> attachmentsVBox.getScene().getWindow()
);
- List files = fileChooser.showOpenMultipleDialog(amountField.getScene().getWindow());
- if (files == null) return;
- for (var file : files) {
- if (selectedAttachmentFiles.stream().noneMatch(f -> !f.equals(file) && f.getName().equals(file.getName()))) {
- selectedAttachmentFiles.add(file);
- }
- }
+ attachmentsSelectionArea.allowMultiple.set(true);
+ attachmentsVBox.getChildren().add(attachmentsSelectionArea);
}
@FXML public void save() {
@@ -136,7 +103,7 @@ public class CreateTransactionController implements RouteSelectionListener {
Currency currency = currencyChoiceBox.getValue();
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
- List attachments = selectedAttachmentFiles.stream().map(File::toPath).toList();
+ List attachments = attachmentsSelectionArea.getSelectedFiles();
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
repo.insert(
utcTimestamp,
@@ -164,7 +131,7 @@ public class CreateTransactionController implements RouteSelectionListener {
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
amountField.setText("0");
descriptionField.setText(null);
- selectedAttachmentFiles.clear();
+ attachmentsSelectionArea.clear();
Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
var currencies = repo.findAllUsedCurrencies().stream()
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index 1ea948e..038a379 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -86,7 +86,10 @@ public class TransactionViewController {
"Are you sure you want to delete this transaction? This will " +
"permanently remove the transaction and its effects on any linked " +
"accounts, as well as remove any attachments from storage within " +
- "this app."
+ "this app.\n\n" +
+ "Note that incorrect or missing transactions can cause your " +
+ "account's balance to be incorrectly reported in Perfin, because " +
+ "it's derived from the most recent balance-record, and transactions."
);
if (confirm) {
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
diff --git a/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java
index 159a3ad..f3e9838 100644
--- a/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java
+++ b/src/main/java/com/andrewlalis/perfin/data/AccountHistoryItemRepository.java
@@ -1,17 +1,24 @@
package com.andrewlalis.perfin.data;
+import com.andrewlalis.perfin.data.util.DateUtil;
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;
+import java.util.Optional;
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 findMostRecentForAccount(long accountId, LocalDateTime utcTimestamp, int count);
+ default Optional getMostRecentForAccount(long accountId) {
+ var items = findMostRecentForAccount(accountId, DateUtil.nowAsUTC(), 1);
+ if (items.isEmpty()) return Optional.empty();
+ return Optional.of(items.getFirst());
+ }
String getTextItem(long itemId);
AccountEntry getAccountEntryItem(long itemId);
BalanceRecord getBalanceRecordItem(long itemId);
diff --git a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
index 9649461..3f7a1dc 100644
--- a/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/util/CurrencyUtil.java
@@ -14,4 +14,9 @@ public class CurrencyUtil {
BigDecimal displayValue = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
return nf.format(displayValue);
}
+
+ public static String formatMoneyAsBasicNumber(BigDecimal amount, Currency currency) {
+ BigDecimal displayValue = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
+ return displayValue.toString();
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
index 7e98fd5..8f811f7 100644
--- a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
+++ b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java
@@ -1,5 +1,7 @@
package com.andrewlalis.perfin.data.util;
+import javafx.stage.FileChooser;
+
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@@ -54,4 +56,17 @@ public class FileUtil {
public static String getTypeSuffix(Path filePath) {
return getTypeSuffix(filePath.getFileName().toString());
}
+
+ public static FileChooser newAttachmentsFileChooser() {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Select Attachments");
+ fileChooser.getExtensionFilters().addAll(
+ new FileChooser.ExtensionFilter(
+ "Attachment Files",
+ "*.pdf", "*.docx", "*.odt", "*.html", "*.txt", "*.md", "*.xml", "*.json",
+ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.bmp", "*.tiff"
+ )
+ );
+ return fileChooser;
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
index b4f56c7..20d90f2 100644
--- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
+++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java
@@ -42,11 +42,9 @@ public class AccountHistoryItemTile extends BorderPane {
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
return new TextFlow(
- new Text("Entry added with value of "),
- amountText,
- new Text(", linked with "),
transactionLink,
- new Text(".")
+ new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of"),
+ amountText
);
}
diff --git a/src/main/java/com/andrewlalis/perfin/view/component/FileSelectionArea.java b/src/main/java/com/andrewlalis/perfin/view/component/FileSelectionArea.java
new file mode 100644
index 0000000..57b063c
--- /dev/null
+++ b/src/main/java/com/andrewlalis/perfin/view/component/FileSelectionArea.java
@@ -0,0 +1,96 @@
+package com.andrewlalis.perfin.view.component;
+
+import com.andrewlalis.perfin.view.BindingUtil;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import javafx.stage.Window;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A pane within which a user can select one or more files.
+ */
+public class FileSelectionArea extends VBox {
+ public final BooleanProperty allowMultiple = new SimpleBooleanProperty(false);
+ private final ObservableList selectedFiles = FXCollections.observableArrayList();
+
+ public FileSelectionArea(Supplier fileChooserSupplier, Supplier windowSupplier) {
+ getStyleClass().addAll("std-padding", "std-spacing");
+
+ VBox filesVBox = new VBox();
+ filesVBox.getStyleClass().addAll("std-padding", "std-spacing");
+ BindingUtil.mapContent(filesVBox.getChildren(), selectedFiles, this::buildFileItem);
+ ListProperty selectedFilesProperty = new SimpleListProperty<>(selectedFiles);
+
+ Label noFilesLabel = new Label("No files selected.");
+ noFilesLabel.managedProperty().bind(noFilesLabel.visibleProperty());
+ noFilesLabel.visibleProperty().bind(selectedFilesProperty.emptyProperty());
+
+ Button selectFilesButton = new Button("Select files");
+ selectFilesButton.setOnAction(event -> onSelectFileClicked(fileChooserSupplier.get(), windowSupplier.get()));
+ selectFilesButton.disableProperty().bind(
+ allowMultiple.not().and(selectedFilesProperty.emptyProperty().not())
+ );
+
+ getChildren().addAll(
+ filesVBox,
+ noFilesLabel,
+ selectFilesButton
+ );
+ }
+
+ public List getSelectedFiles() {
+ return Collections.unmodifiableList(selectedFiles);
+ }
+
+ public void clear() {
+ selectedFiles.clear();
+ }
+
+ private Node buildFileItem(Path path) {
+ Label filenameLabel = new Label(path.getFileName().toString());
+ filenameLabel.getStyleClass().addAll("mono-font");
+ Button removeButton = new Button("Remove");
+ removeButton.setOnAction(event -> selectedFiles.remove(path));
+ AnchorPane pane = new AnchorPane(filenameLabel, removeButton);
+ AnchorPane.setLeftAnchor(filenameLabel, 0.0);
+ AnchorPane.setTopAnchor(filenameLabel, 0.0);
+ AnchorPane.setBottomAnchor(filenameLabel, 0.0);
+
+ AnchorPane.setRightAnchor(removeButton, 0.0);
+ return pane;
+ }
+
+ private void onSelectFileClicked(FileChooser fileChooser, Window owner) {
+ if (allowMultiple.get()) {
+ var files = fileChooser.showOpenMultipleDialog(owner);
+ if (files != null) {
+ for (File file : files) {
+ Path path = file.toPath();
+ if (!selectedFiles.contains(path)) {
+ selectedFiles.add(path);
+ }
+ }
+ }
+ } else {
+ File file = fileChooser.showOpenDialog(owner);
+ if (file != null && !selectedFiles.contains(file.toPath())) {
+ selectedFiles.add(file.toPath());
+ }
+ }
+ }
+}
diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml
index dab0c8b..fef1343 100644
--- a/src/main/resources/account-view.fxml
+++ b/src/main/resources/account-view.fxml
@@ -53,6 +53,7 @@
+
diff --git a/src/main/resources/create-balance-record.fxml b/src/main/resources/create-balance-record.fxml
new file mode 100644
index 0000000..6a4f4ee
--- /dev/null
+++ b/src/main/resources/create-balance-record.fxml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/create-transaction.fxml b/src/main/resources/create-transaction.fxml
index 8c06a03..2a6a1c6 100644
--- a/src/main/resources/create-transaction.fxml
+++ b/src/main/resources/create-transaction.fxml
@@ -53,21 +53,12 @@
-
-
+
-
-
-
+
diff --git a/src/main/resources/images/perfin-logo_64.png b/src/main/resources/images/perfin-logo_64.png
index 5ca250b..f3cf99a 100644
Binary files a/src/main/resources/images/perfin-logo_64.png and b/src/main/resources/images/perfin-logo_64.png differ
diff --git a/src/main/resources/transaction-view.fxml b/src/main/resources/transaction-view.fxml
index bb2f870..5bbe70b 100644
--- a/src/main/resources/transaction-view.fxml
+++ b/src/main/resources/transaction-view.fxml
@@ -47,7 +47,7 @@
-
+