Finished transaction editing logic.
This commit is contained in:
parent
f0b061c34d
commit
47ac75af45
|
@ -5,7 +5,6 @@ import com.andrewlalis.perfin.data.AccountRepository;
|
||||||
import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
import com.andrewlalis.perfin.data.BalanceRecordRepository;
|
||||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
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.data.util.FileUtil;
|
|
||||||
import com.andrewlalis.perfin.model.Account;
|
import com.andrewlalis.perfin.model.Account;
|
||||||
import com.andrewlalis.perfin.model.MoneyValue;
|
import com.andrewlalis.perfin.model.MoneyValue;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
@ -32,7 +31,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
@FXML public TextField timestampField;
|
@FXML public TextField timestampField;
|
||||||
@FXML public TextField balanceField;
|
@FXML public TextField balanceField;
|
||||||
@FXML public Label balanceWarningLabel;
|
@FXML public Label balanceWarningLabel;
|
||||||
private FileSelectionArea attachmentSelectionArea;
|
@FXML public FileSelectionArea attachmentSelectionArea;
|
||||||
@FXML public PropertiesPane propertiesPane;
|
@FXML public PropertiesPane propertiesPane;
|
||||||
|
|
||||||
@FXML public Button saveButton;
|
@FXML public Button saveButton;
|
||||||
|
@ -71,14 +70,6 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
|
|
||||||
var formValid = timestampValid.and(balanceValid);
|
var formValid = timestampValid.and(balanceValid);
|
||||||
saveButton.disableProperty().bind(formValid.not());
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
|
||||||
// Manually append the attachment selection area to the end of the properties pane.
|
|
||||||
attachmentSelectionArea = new FileSelectionArea(
|
|
||||||
FileUtil::newAttachmentsFileChooser,
|
|
||||||
() -> timestampField.getScene().getWindow()
|
|
||||||
);
|
|
||||||
attachmentSelectionArea.allowMultiple.set(true);
|
|
||||||
propertiesPane.getChildren().addLast(attachmentSelectionArea);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,7 +101,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
account.id,
|
account.id,
|
||||||
reportedBalance,
|
reportedBalance,
|
||||||
account.getCurrency(),
|
account.getCurrency(),
|
||||||
attachmentSelectionArea.getSelectedFiles()
|
attachmentSelectionArea.getSelectedPaths()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
router.navigateBackAndClear();
|
router.navigateBackAndClear();
|
||||||
|
|
|
@ -6,10 +6,7 @@ import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
import com.andrewlalis.perfin.data.pagination.Sort;
|
import com.andrewlalis.perfin.data.pagination.Sort;
|
||||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
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.data.util.FileUtil;
|
import com.andrewlalis.perfin.model.*;
|
||||||
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
|
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
|
||||||
import com.andrewlalis.perfin.model.Transaction;
|
|
||||||
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
||||||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
|
@ -20,8 +17,8 @@ import javafx.beans.property.Property;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -30,6 +27,7 @@ import java.nio.file.Path;
|
||||||
import java.time.DateTimeException;
|
import java.time.DateTimeException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -39,6 +37,7 @@ import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
public class EditTransactionController implements RouteSelectionListener {
|
public class EditTransactionController implements RouteSelectionListener {
|
||||||
private static final Logger log = LoggerFactory.getLogger(EditTransactionController.class);
|
private static final Logger log = LoggerFactory.getLogger(EditTransactionController.class);
|
||||||
|
|
||||||
|
@FXML public BorderPane container;
|
||||||
@FXML public Label titleLabel;
|
@FXML public Label titleLabel;
|
||||||
|
|
||||||
@FXML public TextField timestampField;
|
@FXML public TextField timestampField;
|
||||||
|
@ -50,8 +49,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
@FXML public AccountSelectionBox debitAccountSelector;
|
@FXML public AccountSelectionBox debitAccountSelector;
|
||||||
@FXML public AccountSelectionBox creditAccountSelector;
|
@FXML public AccountSelectionBox creditAccountSelector;
|
||||||
|
|
||||||
@FXML public VBox attachmentsVBox;
|
@FXML public FileSelectionArea attachmentsSelectionArea;
|
||||||
private FileSelectionArea attachmentsSelectionArea;
|
|
||||||
|
|
||||||
@FXML public Button saveButton;
|
@FXML public Button saveButton;
|
||||||
|
|
||||||
|
@ -85,45 +83,62 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
)
|
)
|
||||||
.addPredicate(
|
.addPredicate(
|
||||||
accounts -> (
|
accounts -> (
|
||||||
(!accounts.hasCredit() || accounts.creditAccount().getCurrency().equals(currencyChoiceBox.getValue())) ||
|
(!accounts.hasCredit() || accounts.creditAccount().getCurrency().equals(currencyChoiceBox.getValue())) &&
|
||||||
(!accounts.hasDebit() || accounts.debitAccount().getCurrency().equals(currencyChoiceBox.getValue()))
|
(!accounts.hasDebit() || accounts.debitAccount().getCurrency().equals(currencyChoiceBox.getValue()))
|
||||||
),
|
),
|
||||||
"Linked accounts must use the same currency."
|
"Linked accounts must use the same currency."
|
||||||
)
|
)
|
||||||
|
.addPredicate(
|
||||||
|
accounts -> (
|
||||||
|
(!accounts.hasCredit() || !accounts.creditAccount().isArchived()) &&
|
||||||
|
(!accounts.hasDebit() || !accounts.debitAccount().isArchived())
|
||||||
|
),
|
||||||
|
"Linked accounts must not be archived."
|
||||||
|
)
|
||||||
).validatedInitially().attach(linkedAccountsContainer, linkedAccountsProperty);
|
).validatedInitially().attach(linkedAccountsContainer, linkedAccountsProperty);
|
||||||
|
|
||||||
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
||||||
saveButton.disableProperty().bind(formValid.not());
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
|
||||||
// Initialize the file selection area.
|
|
||||||
attachmentsSelectionArea = new FileSelectionArea(
|
|
||||||
FileUtil::newAttachmentsFileChooser,
|
|
||||||
() -> attachmentsVBox.getScene().getWindow()
|
|
||||||
);
|
|
||||||
attachmentsSelectionArea.allowMultiple.set(true);
|
|
||||||
attachmentsVBox.getChildren().add(attachmentsSelectionArea);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void save() {
|
@FXML public void save() {
|
||||||
|
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
|
||||||
|
BigDecimal amount = new BigDecimal(amountField.getText());
|
||||||
|
Currency currency = currencyChoiceBox.getValue();
|
||||||
|
String description = getSanitizedDescription();
|
||||||
|
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
||||||
|
List<Path> newAttachmentPaths = attachmentsSelectionArea.getSelectedPaths();
|
||||||
|
List<Attachment> existingAttachments = attachmentsSelectionArea.getSelectedAttachments();
|
||||||
final long idToNavigate;
|
final long idToNavigate;
|
||||||
if (transaction == null) {
|
if (transaction == null) {
|
||||||
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
|
idToNavigate = Profile.getCurrent().getDataSource().mapRepo(
|
||||||
BigDecimal amount = new BigDecimal(amountField.getText());
|
TransactionRepository.class,
|
||||||
Currency currency = currencyChoiceBox.getValue();
|
repo -> repo.insert(
|
||||||
String description = getSanitizedDescription();
|
utcTimestamp,
|
||||||
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
amount,
|
||||||
List<Path> attachments = attachmentsSelectionArea.getSelectedFiles();
|
currency,
|
||||||
Profile.getCurrent().getDataSource().useRepo(TransactionRepository.class, repo -> {
|
description,
|
||||||
repo.insert(
|
linkedAccounts,
|
||||||
|
newAttachmentPaths
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Profile.getCurrent().getDataSource().useRepo(
|
||||||
|
TransactionRepository.class,
|
||||||
|
repo -> repo.update(
|
||||||
|
transaction.id,
|
||||||
utcTimestamp,
|
utcTimestamp,
|
||||||
amount,
|
amount,
|
||||||
currency,
|
currency,
|
||||||
description,
|
description,
|
||||||
linkedAccounts,
|
linkedAccounts,
|
||||||
attachments
|
existingAttachments,
|
||||||
);
|
newAttachmentPaths
|
||||||
});
|
)
|
||||||
|
);
|
||||||
|
idToNavigate = transaction.id;
|
||||||
}
|
}
|
||||||
|
router.replace("transactions", new TransactionsViewController.RouteContext(idToNavigate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void cancel() {
|
@FXML public void cancel() {
|
||||||
|
@ -133,57 +148,58 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
@Override
|
@Override
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
transaction = (Transaction) context;
|
transaction = (Transaction) context;
|
||||||
boolean creatingNew = transaction == null;
|
|
||||||
|
|
||||||
if (creatingNew) {
|
if (transaction == null) {
|
||||||
titleLabel.setText("Create New Transaction");
|
titleLabel.setText("Create New Transaction");
|
||||||
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
||||||
amountField.setText(null);
|
amountField.setText(null);
|
||||||
descriptionField.setText(null);
|
descriptionField.setText(null);
|
||||||
attachmentsSelectionArea.clear();
|
|
||||||
} else {
|
} else {
|
||||||
titleLabel.setText("Edit Transaction #" + transaction.id);
|
titleLabel.setText("Edit Transaction #" + transaction.id);
|
||||||
timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp()));
|
timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp()));
|
||||||
amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount()));
|
amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount()));
|
||||||
descriptionField.setText(transaction.getDescription());
|
descriptionField.setText(transaction.getDescription());
|
||||||
|
|
||||||
// TODO: Add an editable list of attachments from which some can be added and removed.
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch some account-specific data.
|
// Fetch some account-specific data.
|
||||||
currencyChoiceBox.setDisable(true);
|
container.setDisable(true);
|
||||||
creditAccountSelector.setDisable(true);
|
|
||||||
debitAccountSelector.setDisable(true);
|
|
||||||
Thread.ofVirtual().start(() -> {
|
Thread.ofVirtual().start(() -> {
|
||||||
try (
|
try (
|
||||||
var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
|
var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
|
||||||
var transactionRepo = Profile.getCurrent().getDataSource().getTransactionRepository()
|
var transactionRepo = Profile.getCurrent().getDataSource().getTransactionRepository()
|
||||||
) {
|
) {
|
||||||
var currencies = accountRepo.findAllUsedCurrencies().stream()
|
// First fetch all the data.
|
||||||
|
List<Currency> currencies = accountRepo.findAllUsedCurrencies().stream()
|
||||||
.sorted(Comparator.comparing(Currency::getCurrencyCode))
|
.sorted(Comparator.comparing(Currency::getCurrencyCode))
|
||||||
.toList();
|
.toList();
|
||||||
var accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
List<Account> accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
||||||
CreditAndDebitAccounts linkedAccounts = transaction == null ? null : transactionRepo.findLinkedAccounts(transaction.id);
|
final List<Attachment> attachments;
|
||||||
|
final CreditAndDebitAccounts linkedAccounts;
|
||||||
|
if (transaction == null) {
|
||||||
|
attachments = Collections.emptyList();
|
||||||
|
linkedAccounts = new CreditAndDebitAccounts(null, null);
|
||||||
|
} else {
|
||||||
|
attachments = transactionRepo.findAttachments(transaction.id);
|
||||||
|
linkedAccounts = transactionRepo.findLinkedAccounts(transaction.id);
|
||||||
|
}
|
||||||
|
// Then make updates to the view.
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
creditAccountSelector.setAccounts(accounts);
|
creditAccountSelector.setAccounts(accounts);
|
||||||
debitAccountSelector.setAccounts(accounts);
|
debitAccountSelector.setAccounts(accounts);
|
||||||
currencyChoiceBox.getItems().setAll(currencies);
|
currencyChoiceBox.getItems().setAll(currencies);
|
||||||
if (creatingNew) {
|
attachmentsSelectionArea.clear();
|
||||||
|
attachmentsSelectionArea.addAttachments(attachments);
|
||||||
|
if (transaction == null) {
|
||||||
// TODO: Allow user to select a default currency.
|
// TODO: Allow user to select a default currency.
|
||||||
currencyChoiceBox.getSelectionModel().selectFirst();
|
currencyChoiceBox.getSelectionModel().selectFirst();
|
||||||
creditAccountSelector.select(null);
|
creditAccountSelector.select(null);
|
||||||
debitAccountSelector.select(null);
|
debitAccountSelector.select(null);
|
||||||
} else {
|
} else {
|
||||||
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
|
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
|
||||||
if (linkedAccounts != null) {
|
creditAccountSelector.select(linkedAccounts.creditAccount());
|
||||||
creditAccountSelector.select(linkedAccounts.creditAccount());
|
debitAccountSelector.select(linkedAccounts.debitAccount());
|
||||||
debitAccountSelector.select(linkedAccounts.debitAccount());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currencyChoiceBox.setDisable(false);
|
container.setDisable(false);
|
||||||
creditAccountSelector.setDisable(false);
|
|
||||||
debitAccountSelector.setDisable(false);
|
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to get repositories.", e);
|
log.error("Failed to get repositories.", e);
|
||||||
|
|
|
@ -49,14 +49,18 @@ public class TransactionViewController {
|
||||||
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
|
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
|
||||||
List<Attachment> attachments = repo.findAttachments(transaction.id);
|
List<Attachment> attachments = repo.findAttachments(transaction.id);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
accounts.ifDebit(acc -> {
|
if (accounts.hasDebit()) {
|
||||||
debitAccountLink.setText(acc.getShortName());
|
debitAccountLink.setText(accounts.debitAccount().getShortName());
|
||||||
debitAccountLink.setOnAction(event -> router.navigate("account", acc));
|
debitAccountLink.setOnAction(event -> router.navigate("account", accounts.debitAccount()));
|
||||||
});
|
} else {
|
||||||
accounts.ifCredit(acc -> {
|
debitAccountLink.setText(null);
|
||||||
creditAccountLink.setText(acc.getShortName());
|
}
|
||||||
creditAccountLink.setOnAction(event -> router.navigate("account", acc));
|
if (accounts.hasCredit()) {
|
||||||
});
|
creditAccountLink.setText(accounts.creditAccount().getShortName());
|
||||||
|
creditAccountLink.setOnAction(event -> router.navigate("account", accounts.creditAccount()));
|
||||||
|
} else {
|
||||||
|
creditAccountLink.setText(null);
|
||||||
|
}
|
||||||
attachmentsViewPane.setAttachments(attachments);
|
attachmentsViewPane.setAttachments(attachments);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -126,12 +126,9 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
// Refresh account filter options.
|
// Refresh account filter options.
|
||||||
Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
|
Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
|
||||||
List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
||||||
accounts.add(null);
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
filterByAccountComboBox.getItems().clear();
|
filterByAccountComboBox.setAccounts(accounts);
|
||||||
filterByAccountComboBox.getItems().addAll(accounts);
|
filterByAccountComboBox.select(null);
|
||||||
filterByAccountComboBox.getSelectionModel().selectLast();
|
|
||||||
filterByAccountComboBox.getButtonCell().updateIndex(accounts.size() - 1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
package com.andrewlalis.perfin.data;
|
package com.andrewlalis.perfin.data;
|
||||||
|
|
||||||
public interface Repository {
|
/**
|
||||||
}
|
* Marker interface used to identify any data repository.
|
||||||
|
*/
|
||||||
|
public interface Repository {}
|
||||||
|
|
|
@ -32,4 +32,14 @@ public interface TransactionRepository extends Repository, AutoCloseable {
|
||||||
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
|
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
|
||||||
List<Attachment> findAttachments(long transactionId);
|
List<Attachment> findAttachments(long transactionId);
|
||||||
void delete(long transactionId);
|
void delete(long transactionId);
|
||||||
|
void update(
|
||||||
|
long id,
|
||||||
|
LocalDateTime utcTimestamp,
|
||||||
|
BigDecimal amount,
|
||||||
|
Currency currency,
|
||||||
|
String description,
|
||||||
|
CreditAndDebitAccounts linkedAccounts,
|
||||||
|
List<Attachment> existingAttachments,
|
||||||
|
List<Path> newAttachmentPaths
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,8 @@ public record JdbcAttachmentRepository(Connection conn, Path contentDir) impleme
|
||||||
public void deleteById(long attachmentId) {
|
public void deleteById(long attachmentId) {
|
||||||
var optionalAttachment = findById(attachmentId);
|
var optionalAttachment = findById(attachmentId);
|
||||||
if (optionalAttachment.isPresent()) {
|
if (optionalAttachment.isPresent()) {
|
||||||
deleteFileOnDisk(optionalAttachment.get());
|
|
||||||
DbUtil.updateOne(conn, "DELETE FROM attachment WHERE id = ?", List.of(attachmentId));
|
DbUtil.updateOne(conn, "DELETE FROM attachment WHERE id = ?", List.of(attachmentId));
|
||||||
|
deleteFileOnDisk(optionalAttachment.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,13 @@ import com.andrewlalis.perfin.data.AttachmentRepository;
|
||||||
import com.andrewlalis.perfin.data.TransactionRepository;
|
import com.andrewlalis.perfin.data.TransactionRepository;
|
||||||
import com.andrewlalis.perfin.data.pagination.Page;
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
|
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||||
|
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
import com.andrewlalis.perfin.model.*;
|
import com.andrewlalis.perfin.model.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
|
@ -40,13 +43,9 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
||||||
linkedAccounts.ifCredit(acc -> accountEntryRepository.insert(utcTimestamp, acc.id, txId, amount, AccountEntry.Type.CREDIT, currency));
|
linkedAccounts.ifCredit(acc -> accountEntryRepository.insert(utcTimestamp, acc.id, txId, amount, AccountEntry.Type.CREDIT, currency));
|
||||||
// 3. Add attachments.
|
// 3. Add attachments.
|
||||||
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
||||||
try (var stmt = conn.prepareStatement("INSERT INTO transaction_attachment (transaction_id, attachment_id) VALUES (?, ?)")) {
|
for (Path attachmentPath : attachments) {
|
||||||
for (var attachmentPath : attachments) {
|
Attachment attachment = attachmentRepo.insert(attachmentPath);
|
||||||
Attachment attachment = attachmentRepo.insert(attachmentPath);
|
insertAttachmentLink(txId, attachment.id);
|
||||||
// Insert the link-table entry.
|
|
||||||
DbUtil.setArgs(stmt, txId, attachment.id);
|
|
||||||
stmt.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return txId;
|
return txId;
|
||||||
});
|
});
|
||||||
|
@ -150,10 +149,78 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(long transactionId) {
|
public void delete(long transactionId) {
|
||||||
DbUtil.updateOne(conn, "DELETE FROM transaction WHERE id = ?", List.of(transactionId));
|
DbUtil.doTransaction(conn, () -> {
|
||||||
|
DbUtil.updateOne(conn, "DELETE FROM transaction WHERE id = ?", List.of(transactionId));
|
||||||
|
DbUtil.update(conn, "DELETE FROM account_entry WHERE transaction_id = ?", List.of(transactionId));
|
||||||
|
});
|
||||||
new JdbcAttachmentRepository(conn, contentDir).deleteAllOrphans();
|
new JdbcAttachmentRepository(conn, contentDir).deleteAllOrphans();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(
|
||||||
|
long id,
|
||||||
|
LocalDateTime utcTimestamp,
|
||||||
|
BigDecimal amount,
|
||||||
|
Currency currency,
|
||||||
|
String description,
|
||||||
|
CreditAndDebitAccounts linkedAccounts,
|
||||||
|
List<Attachment> existingAttachments,
|
||||||
|
List<Path> newAttachmentPaths
|
||||||
|
) {
|
||||||
|
DbUtil.doTransaction(conn, () -> {
|
||||||
|
Transaction tx = findById(id).orElseThrow();
|
||||||
|
CreditAndDebitAccounts currentLinkedAccounts = findLinkedAccounts(id);
|
||||||
|
List<Attachment> currentAttachments = findAttachments(id);
|
||||||
|
var entryRepo = new JdbcAccountEntryRepository(conn);
|
||||||
|
var attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
|
||||||
|
List<String> updateMessages = new ArrayList<>();
|
||||||
|
if (!tx.getTimestamp().equals(utcTimestamp)) {
|
||||||
|
DbUtil.updateOne(conn, "UPDATE transaction SET timestamp = ? WHERE id = ?", List.of(DbUtil.timestampFromUtcLDT(utcTimestamp), id));
|
||||||
|
updateMessages.add("Updated timestamp to UTC " + DateUtil.DEFAULT_DATETIME_FORMAT.format(utcTimestamp) + ".");
|
||||||
|
}
|
||||||
|
BigDecimal scaledAmount = amount.setScale(4, RoundingMode.HALF_UP);
|
||||||
|
if (!tx.getAmount().equals(scaledAmount)) {
|
||||||
|
DbUtil.updateOne(conn, "UPDATE transaction SET amount = ? WHERE id = ?", List.of(scaledAmount, id));
|
||||||
|
updateMessages.add("Updated amount to " + CurrencyUtil.formatMoney(new MoneyValue(scaledAmount, currency)) + ".");
|
||||||
|
}
|
||||||
|
if (!tx.getCurrency().equals(currency)) {
|
||||||
|
DbUtil.updateOne(conn, "UPDATE transaction SET currency = ? WHERE id = ?", List.of(currency.getCurrencyCode(), id));
|
||||||
|
updateMessages.add("Updated currency to " + currency.getCurrencyCode() + ".");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(tx.getDescription(), description)) {
|
||||||
|
DbUtil.updateOne(conn, "UPDATE transaction SET description = ? WHERE id = ?", List.of(description, id));
|
||||||
|
updateMessages.add("Updated description.");
|
||||||
|
}
|
||||||
|
boolean updateAccountEntries = !tx.getAmount().equals(scaledAmount) ||
|
||||||
|
!tx.getCurrency().equals(currency) ||
|
||||||
|
!tx.getTimestamp().equals(utcTimestamp) ||
|
||||||
|
!currentLinkedAccounts.equals(linkedAccounts);
|
||||||
|
if (updateAccountEntries) {
|
||||||
|
// Delete all entries and re-write them correctly?
|
||||||
|
DbUtil.update(conn, "DELETE FROM account_entry WHERE transaction_id = ?", List.of(id));
|
||||||
|
linkedAccounts.ifCredit(acc -> entryRepo.insert(utcTimestamp, acc.id, id, scaledAmount, AccountEntry.Type.CREDIT, currency));
|
||||||
|
linkedAccounts.ifDebit(acc -> entryRepo.insert(utcTimestamp, acc.id, id, scaledAmount, AccountEntry.Type.DEBIT, currency));
|
||||||
|
updateMessages.add("Updated linked accounts.");
|
||||||
|
}
|
||||||
|
// Manage attachments changes.
|
||||||
|
List<Attachment> removedAttachments = new ArrayList<>(currentAttachments);
|
||||||
|
removedAttachments.removeAll(existingAttachments);
|
||||||
|
for (Attachment removedAttachment : removedAttachments) {
|
||||||
|
attachmentRepo.deleteById(removedAttachment.id);
|
||||||
|
updateMessages.add("Removed attachment \"" + removedAttachment.getFilename() + "\".");
|
||||||
|
}
|
||||||
|
for (Path attachmentPath : newAttachmentPaths) {
|
||||||
|
Attachment attachment = attachmentRepo.insert(attachmentPath);
|
||||||
|
insertAttachmentLink(tx.id, attachment.id);
|
||||||
|
updateMessages.add("Added attachment \"" + attachment.getFilename() + "\".");
|
||||||
|
}
|
||||||
|
String updateMessageStr = "Transaction #" + tx.id + " was updated:\n" + String.join("\n", updateMessages);
|
||||||
|
var historyRepo = new JdbcAccountHistoryItemRepository(conn);
|
||||||
|
linkedAccounts.ifCredit(acc -> historyRepo.recordText(DateUtil.nowAsUTC(), acc.id, updateMessageStr));
|
||||||
|
linkedAccounts.ifDebit(acc -> historyRepo.recordText(DateUtil.nowAsUTC(), acc.id, updateMessageStr));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
conn.close();
|
conn.close();
|
||||||
|
@ -168,4 +235,12 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
||||||
rs.getString("description")
|
rs.getString("description")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void insertAttachmentLink(long transactionId, long attachmentId) {
|
||||||
|
DbUtil.insertOne(
|
||||||
|
conn,
|
||||||
|
"INSERT INTO transaction_attachment (transaction_id, attachment_id) VALUES (?, ?)",
|
||||||
|
List.of(transactionId, attachmentId)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
import java.nio.file.SimpleFileVisitor;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -85,4 +86,21 @@ public class FileUtil {
|
||||||
);
|
);
|
||||||
return fileChooser;
|
return fileChooser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] getHash(Path path) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
try (var in = Files.newInputStream(path)) {
|
||||||
|
int count = in.read(buffer);
|
||||||
|
while (count != -1) {
|
||||||
|
md.update(buffer, 0, count);
|
||||||
|
count = in.read(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return md.digest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ public class AttachmentsViewPane extends VBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAttachments(List<Attachment> attachments) {
|
public void setAttachments(List<Attachment> attachments) {
|
||||||
|
this.attachments.clear();
|
||||||
this.attachments.setAll(attachments);
|
this.attachments.setAll(attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package com.andrewlalis.perfin.view.component;
|
package com.andrewlalis.perfin.view.component;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
|
import com.andrewlalis.perfin.model.Attachment;
|
||||||
import com.andrewlalis.perfin.view.BindingUtil;
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.ListProperty;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleListProperty;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -17,33 +16,55 @@ import javafx.stage.Window;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pane within which a user can select one or more files.
|
* A pane within which a user can select one or more files.
|
||||||
*/
|
*/
|
||||||
public class FileSelectionArea extends VBox {
|
public class FileSelectionArea extends VBox {
|
||||||
public final BooleanProperty allowMultiple = new SimpleBooleanProperty(false);
|
interface FileItem {
|
||||||
private final ObservableList<Path> selectedFiles = FXCollections.observableArrayList();
|
String getName();
|
||||||
|
}
|
||||||
|
|
||||||
public FileSelectionArea(Supplier<FileChooser> fileChooserSupplier, Supplier<Window> windowSupplier) {
|
public record PathItem(Path path) implements FileItem {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return path.getFileName().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AttachmentItem(Attachment attachment) implements FileItem {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return attachment.getFilename();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final BooleanProperty allowMultiple = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
private final ObservableList<FileItem> selectedFiles = FXCollections.observableArrayList();
|
||||||
|
private final ObjectProperty<FileChooser> fileChooserProperty = new SimpleObjectProperty<>(getDefaultFileChooser());
|
||||||
|
|
||||||
|
public FileSelectionArea() {
|
||||||
getStyleClass().addAll("std-padding", "std-spacing");
|
getStyleClass().addAll("std-padding", "std-spacing");
|
||||||
|
|
||||||
VBox filesVBox = new VBox();
|
VBox filesVBox = new VBox();
|
||||||
filesVBox.getStyleClass().addAll("std-padding", "std-spacing");
|
filesVBox.getStyleClass().addAll("std-padding", "std-spacing");
|
||||||
BindingUtil.mapContent(filesVBox.getChildren(), selectedFiles, this::buildFileItem);
|
BindingUtil.mapContent(filesVBox.getChildren(), selectedFiles, this::buildFileItem);
|
||||||
ListProperty<Path> selectedFilesProperty = new SimpleListProperty<>(selectedFiles);
|
ListProperty<FileItem> selectedFilesProperty = new SimpleListProperty<>(selectedFiles);
|
||||||
|
|
||||||
Label noFilesLabel = new Label("No files selected.");
|
Label noFilesLabel = new Label("No files selected.");
|
||||||
noFilesLabel.managedProperty().bind(noFilesLabel.visibleProperty());
|
noFilesLabel.managedProperty().bind(noFilesLabel.visibleProperty());
|
||||||
noFilesLabel.visibleProperty().bind(selectedFilesProperty.emptyProperty());
|
noFilesLabel.visibleProperty().bind(selectedFilesProperty.emptyProperty());
|
||||||
|
|
||||||
Button selectFilesButton = new Button("Select files");
|
Button selectFilesButton = new Button("Select files");
|
||||||
selectFilesButton.setOnAction(event -> onSelectFileClicked(fileChooserSupplier.get(), windowSupplier.get()));
|
selectFilesButton.setOnAction(event -> {
|
||||||
|
onSelectFileClicked(fileChooserProperty.get(), getScene().getWindow());
|
||||||
|
});
|
||||||
selectFilesButton.disableProperty().bind(
|
selectFilesButton.disableProperty().bind(
|
||||||
allowMultiple.not().and(selectedFilesProperty.emptyProperty().not())
|
allowMultiple.not().and(selectedFilesProperty.emptyProperty().not())
|
||||||
|
.or(fileChooserProperty.isNull())
|
||||||
);
|
);
|
||||||
|
|
||||||
getChildren().addAll(
|
getChildren().addAll(
|
||||||
|
@ -53,23 +74,48 @@ public class FileSelectionArea extends VBox {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Path> getSelectedFiles() {
|
public List<Attachment> getSelectedAttachments() {
|
||||||
return Collections.unmodifiableList(selectedFiles);
|
List<Attachment> attachments = new ArrayList<>();
|
||||||
|
for (FileItem item : selectedFiles) {
|
||||||
|
if (item instanceof AttachmentItem a) {
|
||||||
|
attachments.add(a.attachment());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Path> getSelectedPaths() {
|
||||||
|
List<Path> paths = new ArrayList<>();
|
||||||
|
for (FileItem item : selectedFiles) {
|
||||||
|
if (item instanceof PathItem p) {
|
||||||
|
paths.add(p.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
selectedFiles.clear();
|
selectedFiles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedFiles(List<Path> files) {
|
public void addAttachments(List<Attachment> attachments) {
|
||||||
|
for (Attachment attachment : attachments) {
|
||||||
|
FileItem item = new AttachmentItem(attachment);
|
||||||
|
if (!selectedFiles.contains(item)) {
|
||||||
|
selectedFiles.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedFiles(List<FileItem> files) {
|
||||||
selectedFiles.setAll(files);
|
selectedFiles.setAll(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node buildFileItem(Path path) {
|
private Node buildFileItem(FileItem item) {
|
||||||
Label filenameLabel = new Label(path.getFileName().toString());
|
Label filenameLabel = new Label(item.getName());
|
||||||
filenameLabel.getStyleClass().addAll("mono-font");
|
filenameLabel.getStyleClass().addAll("mono-font");
|
||||||
Button removeButton = new Button("Remove");
|
Button removeButton = new Button("Remove");
|
||||||
removeButton.setOnAction(event -> selectedFiles.remove(path));
|
removeButton.setOnAction(event -> selectedFiles.remove(item));
|
||||||
AnchorPane pane = new AnchorPane(filenameLabel, removeButton);
|
AnchorPane pane = new AnchorPane(filenameLabel, removeButton);
|
||||||
AnchorPane.setLeftAnchor(filenameLabel, 0.0);
|
AnchorPane.setLeftAnchor(filenameLabel, 0.0);
|
||||||
AnchorPane.setTopAnchor(filenameLabel, 0.0);
|
AnchorPane.setTopAnchor(filenameLabel, 0.0);
|
||||||
|
@ -84,17 +130,35 @@ public class FileSelectionArea extends VBox {
|
||||||
var files = fileChooser.showOpenMultipleDialog(owner);
|
var files = fileChooser.showOpenMultipleDialog(owner);
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
Path path = file.toPath();
|
FileItem item = new PathItem(file.toPath());
|
||||||
if (!selectedFiles.contains(path)) {
|
if (!selectedFiles.contains(item)) {
|
||||||
selectedFiles.add(path);
|
selectedFiles.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
File file = fileChooser.showOpenDialog(owner);
|
File file = fileChooser.showOpenDialog(owner);
|
||||||
if (file != null && !selectedFiles.contains(file.toPath())) {
|
FileItem item = file == null ? null : new PathItem(file.toPath());
|
||||||
selectedFiles.add(file.toPath());
|
if (item != null && !selectedFiles.contains(item)) {
|
||||||
|
selectedFiles.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FileChooser getDefaultFileChooser() {
|
||||||
|
return FileUtil.newAttachmentsFileChooser();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property methods.
|
||||||
|
public final BooleanProperty allowMultipleProperty() {
|
||||||
|
return allowMultiple;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean getAllowMultiple() {
|
||||||
|
return allowMultiple.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setAllowMultiple(boolean value) {
|
||||||
|
allowMultiple.set(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import com.andrewlalis.perfin.view.component.FileSelectionArea?>
|
||||||
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
|
|
||||||
|
|
||||||
<Label text="Attachments" styleClass="bold-text"/>
|
<Label text="Attachments" styleClass="bold-text"/>
|
||||||
|
<FileSelectionArea fx:id="attachmentSelectionArea" allowMultiple="true"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import com.andrewlalis.perfin.view.component.AccountSelectionBox?>
|
<?import com.andrewlalis.perfin.view.component.AccountSelectionBox?>
|
||||||
|
<?import com.andrewlalis.perfin.view.component.FileSelectionArea?>
|
||||||
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<BorderPane xmlns="http://javafx.com/javafx"
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
fx:controller="com.andrewlalis.perfin.control.EditTransactionController"
|
fx:controller="com.andrewlalis.perfin.control.EditTransactionController"
|
||||||
|
fx:id="container"
|
||||||
>
|
>
|
||||||
<top>
|
<top>
|
||||||
<Label fx:id="titleLabel" styleClass="large-font,bold-text,std-padding"/>
|
<Label fx:id="titleLabel" styleClass="large-font,bold-text,std-padding"/>
|
||||||
|
@ -51,10 +53,10 @@
|
||||||
</VBox>
|
</VBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
<!-- Container for attachments -->
|
<!-- Container for attachments -->
|
||||||
<VBox fx:id="attachmentsVBox" styleClass="std-padding">
|
<VBox styleClass="std-padding">
|
||||||
<Label text="Attachments" styleClass="bold-text"/>
|
<Label text="Attachments" styleClass="bold-text"/>
|
||||||
<Label text="Attach receipts, invoices, or other content to this transaction." styleClass="small-font" wrapText="true"/>
|
<Label text="Attach receipts, invoices, or other content to this transaction." styleClass="small-font" wrapText="true"/>
|
||||||
<!-- FileSelectionArea inserted here! -->
|
<FileSelectionArea fx:id="attachmentsSelectionArea" allowMultiple="true"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
<HBox styleClass="std-padding,std-spacing">
|
||||||
<PropertiesPane hgap="5" vgap="5">
|
<PropertiesPane hgap="5" vgap="5">
|
||||||
<Label text="Filter by Account"/>
|
<Label text="Filter by Account"/>
|
||||||
<AccountSelectionBox fx:id="filterByAccountComboBox"/>
|
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
</HBox>
|
</HBox>
|
||||||
</top>
|
</top>
|
||||||
|
|
Loading…
Reference in New Issue