Finished transaction editing logic.

This commit is contained in:
Andrew Lalis 2024-01-13 12:54:59 -05:00
parent f0b061c34d
commit 47ac75af45
14 changed files with 289 additions and 107 deletions

View File

@ -5,7 +5,6 @@ import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.BalanceRecordRepository;
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.MoneyValue;
import com.andrewlalis.perfin.model.Profile;
@ -32,7 +31,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
@FXML public TextField timestampField;
@FXML public TextField balanceField;
@FXML public Label balanceWarningLabel;
private FileSelectionArea attachmentSelectionArea;
@FXML public FileSelectionArea attachmentSelectionArea;
@FXML public PropertiesPane propertiesPane;
@FXML public Button saveButton;
@ -71,14 +70,6 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
var formValid = timestampValid.and(balanceValid);
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
@ -110,7 +101,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
account.id,
reportedBalance,
account.getCurrency(),
attachmentSelectionArea.getSelectedFiles()
attachmentSelectionArea.getSelectedPaths()
);
});
router.navigateBackAndClear();

View File

@ -6,10 +6,7 @@ import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort;
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.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction;
import com.andrewlalis.perfin.model.*;
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
import com.andrewlalis.perfin.view.component.FileSelectionArea;
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
@ -20,8 +17,8 @@ import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,6 +27,7 @@ import java.nio.file.Path;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Comparator;
import java.util.Currency;
import java.util.List;
@ -39,6 +37,7 @@ import static com.andrewlalis.perfin.PerfinApp.router;
public class EditTransactionController implements RouteSelectionListener {
private static final Logger log = LoggerFactory.getLogger(EditTransactionController.class);
@FXML public BorderPane container;
@FXML public Label titleLabel;
@FXML public TextField timestampField;
@ -50,8 +49,7 @@ public class EditTransactionController implements RouteSelectionListener {
@FXML public AccountSelectionBox debitAccountSelector;
@FXML public AccountSelectionBox creditAccountSelector;
@FXML public VBox attachmentsVBox;
private FileSelectionArea attachmentsSelectionArea;
@FXML public FileSelectionArea attachmentsSelectionArea;
@FXML public Button saveButton;
@ -85,45 +83,62 @@ public class EditTransactionController implements RouteSelectionListener {
)
.addPredicate(
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()))
),
"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);
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
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() {
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;
if (transaction == null) {
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
BigDecimal amount = new BigDecimal(amountField.getText());
Currency currency = currencyChoiceBox.getValue();
String description = getSanitizedDescription();
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
List<Path> attachments = attachmentsSelectionArea.getSelectedFiles();
Profile.getCurrent().getDataSource().useRepo(TransactionRepository.class, repo -> {
repo.insert(
idToNavigate = Profile.getCurrent().getDataSource().mapRepo(
TransactionRepository.class,
repo -> repo.insert(
utcTimestamp,
amount,
currency,
description,
linkedAccounts,
newAttachmentPaths
)
);
} else {
Profile.getCurrent().getDataSource().useRepo(
TransactionRepository.class,
repo -> repo.update(
transaction.id,
utcTimestamp,
amount,
currency,
description,
linkedAccounts,
attachments
);
});
existingAttachments,
newAttachmentPaths
)
);
idToNavigate = transaction.id;
}
router.replace("transactions", new TransactionsViewController.RouteContext(idToNavigate));
}
@FXML public void cancel() {
@ -133,57 +148,58 @@ public class EditTransactionController implements RouteSelectionListener {
@Override
public void onRouteSelected(Object context) {
transaction = (Transaction) context;
boolean creatingNew = transaction == null;
if (creatingNew) {
if (transaction == null) {
titleLabel.setText("Create New Transaction");
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
amountField.setText(null);
descriptionField.setText(null);
attachmentsSelectionArea.clear();
} else {
titleLabel.setText("Edit Transaction #" + transaction.id);
timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp()));
amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount()));
descriptionField.setText(transaction.getDescription());
// TODO: Add an editable list of attachments from which some can be added and removed.
}
// Fetch some account-specific data.
currencyChoiceBox.setDisable(true);
creditAccountSelector.setDisable(true);
debitAccountSelector.setDisable(true);
container.setDisable(true);
Thread.ofVirtual().start(() -> {
try (
var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
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))
.toList();
var accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
CreditAndDebitAccounts linkedAccounts = transaction == null ? null : transactionRepo.findLinkedAccounts(transaction.id);
List<Account> accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
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(() -> {
creditAccountSelector.setAccounts(accounts);
debitAccountSelector.setAccounts(accounts);
currencyChoiceBox.getItems().setAll(currencies);
if (creatingNew) {
attachmentsSelectionArea.clear();
attachmentsSelectionArea.addAttachments(attachments);
if (transaction == null) {
// TODO: Allow user to select a default currency.
currencyChoiceBox.getSelectionModel().selectFirst();
creditAccountSelector.select(null);
debitAccountSelector.select(null);
} else {
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
if (linkedAccounts != null) {
creditAccountSelector.select(linkedAccounts.creditAccount());
debitAccountSelector.select(linkedAccounts.debitAccount());
}
creditAccountSelector.select(linkedAccounts.creditAccount());
debitAccountSelector.select(linkedAccounts.debitAccount());
}
currencyChoiceBox.setDisable(false);
creditAccountSelector.setDisable(false);
debitAccountSelector.setDisable(false);
container.setDisable(false);
});
} catch (Exception e) {
log.error("Failed to get repositories.", e);

View File

@ -49,14 +49,18 @@ public class TransactionViewController {
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
List<Attachment> attachments = repo.findAttachments(transaction.id);
Platform.runLater(() -> {
accounts.ifDebit(acc -> {
debitAccountLink.setText(acc.getShortName());
debitAccountLink.setOnAction(event -> router.navigate("account", acc));
});
accounts.ifCredit(acc -> {
creditAccountLink.setText(acc.getShortName());
creditAccountLink.setOnAction(event -> router.navigate("account", acc));
});
if (accounts.hasDebit()) {
debitAccountLink.setText(accounts.debitAccount().getShortName());
debitAccountLink.setOnAction(event -> router.navigate("account", accounts.debitAccount()));
} else {
debitAccountLink.setText(null);
}
if (accounts.hasCredit()) {
creditAccountLink.setText(accounts.creditAccount().getShortName());
creditAccountLink.setOnAction(event -> router.navigate("account", accounts.creditAccount()));
} else {
creditAccountLink.setText(null);
}
attachmentsViewPane.setAttachments(attachments);
});
});

View File

@ -126,12 +126,9 @@ public class TransactionsViewController implements RouteSelectionListener {
// Refresh account filter options.
Profile.getCurrent().getDataSource().useRepoAsync(AccountRepository.class, repo -> {
List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
accounts.add(null);
Platform.runLater(() -> {
filterByAccountComboBox.getItems().clear();
filterByAccountComboBox.getItems().addAll(accounts);
filterByAccountComboBox.getSelectionModel().selectLast();
filterByAccountComboBox.getButtonCell().updateIndex(accounts.size() - 1);
filterByAccountComboBox.setAccounts(accounts);
filterByAccountComboBox.select(null);
});
});

View File

@ -1,4 +1,6 @@
package com.andrewlalis.perfin.data;
public interface Repository {
}
/**
* Marker interface used to identify any data repository.
*/
public interface Repository {}

View File

@ -32,4 +32,14 @@ public interface TransactionRepository extends Repository, AutoCloseable {
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
List<Attachment> findAttachments(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
);
}

View File

@ -60,8 +60,8 @@ public record JdbcAttachmentRepository(Connection conn, Path contentDir) impleme
public void deleteById(long attachmentId) {
var optionalAttachment = findById(attachmentId);
if (optionalAttachment.isPresent()) {
deleteFileOnDisk(optionalAttachment.get());
DbUtil.updateOne(conn, "DELETE FROM attachment WHERE id = ?", List.of(attachmentId));
deleteFileOnDisk(optionalAttachment.get());
}
}

View File

@ -5,10 +5,13 @@ import com.andrewlalis.perfin.data.AttachmentRepository;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.data.pagination.Page;
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.model.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Path;
import java.sql.Connection;
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));
// 3. Add attachments.
AttachmentRepository attachmentRepo = new JdbcAttachmentRepository(conn, contentDir);
try (var stmt = conn.prepareStatement("INSERT INTO transaction_attachment (transaction_id, attachment_id) VALUES (?, ?)")) {
for (var attachmentPath : attachments) {
Attachment attachment = attachmentRepo.insert(attachmentPath);
// Insert the link-table entry.
DbUtil.setArgs(stmt, txId, attachment.id);
stmt.executeUpdate();
}
for (Path attachmentPath : attachments) {
Attachment attachment = attachmentRepo.insert(attachmentPath);
insertAttachmentLink(txId, attachment.id);
}
return txId;
});
@ -150,10 +149,78 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
@Override
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();
}
@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
public void close() throws Exception {
conn.close();
@ -168,4 +235,12 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
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)
);
}
}

View File

@ -10,6 +10,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
@ -85,4 +86,21 @@ public class FileUtil {
);
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);
}
}
}

View File

@ -50,6 +50,7 @@ public class AttachmentsViewPane extends VBox {
}
public void setAttachments(List<Attachment> attachments) {
this.attachments.clear();
this.attachments.setAll(attachments);
}

View File

@ -1,10 +1,9 @@
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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
@ -17,33 +16,55 @@ import javafx.stage.Window;
import java.io.File;
import java.nio.file.Path;
import java.util.Collections;
import java.util.ArrayList;
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<Path> selectedFiles = FXCollections.observableArrayList();
interface FileItem {
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");
VBox filesVBox = new VBox();
filesVBox.getStyleClass().addAll("std-padding", "std-spacing");
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.");
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.setOnAction(event -> {
onSelectFileClicked(fileChooserProperty.get(), getScene().getWindow());
});
selectFilesButton.disableProperty().bind(
allowMultiple.not().and(selectedFilesProperty.emptyProperty().not())
.or(fileChooserProperty.isNull())
);
getChildren().addAll(
@ -53,23 +74,48 @@ public class FileSelectionArea extends VBox {
);
}
public List<Path> getSelectedFiles() {
return Collections.unmodifiableList(selectedFiles);
public List<Attachment> getSelectedAttachments() {
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() {
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);
}
private Node buildFileItem(Path path) {
Label filenameLabel = new Label(path.getFileName().toString());
private Node buildFileItem(FileItem item) {
Label filenameLabel = new Label(item.getName());
filenameLabel.getStyleClass().addAll("mono-font");
Button removeButton = new Button("Remove");
removeButton.setOnAction(event -> selectedFiles.remove(path));
removeButton.setOnAction(event -> selectedFiles.remove(item));
AnchorPane pane = new AnchorPane(filenameLabel, removeButton);
AnchorPane.setLeftAnchor(filenameLabel, 0.0);
AnchorPane.setTopAnchor(filenameLabel, 0.0);
@ -84,17 +130,35 @@ public class FileSelectionArea extends VBox {
var files = fileChooser.showOpenMultipleDialog(owner);
if (files != null) {
for (File file : files) {
Path path = file.toPath();
if (!selectedFiles.contains(path)) {
selectedFiles.add(path);
FileItem item = new PathItem(file.toPath());
if (!selectedFiles.contains(item)) {
selectedFiles.add(item);
}
}
}
} else {
File file = fileChooser.showOpenDialog(owner);
if (file != null && !selectedFiles.contains(file.toPath())) {
selectedFiles.add(file.toPath());
FileItem item = file == null ? null : new PathItem(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);
}
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.FileSelectionArea?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
@ -50,6 +51,7 @@
<Label text="Attachments" styleClass="bold-text"/>
<FileSelectionArea fx:id="attachmentSelectionArea" allowMultiple="true"/>
</PropertiesPane>
<Separator/>

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.AccountSelectionBox?>
<?import com.andrewlalis.perfin.view.component.FileSelectionArea?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.EditTransactionController"
fx:id="container"
>
<top>
<Label fx:id="titleLabel" styleClass="large-font,bold-text,std-padding"/>
@ -51,10 +53,10 @@
</VBox>
</HBox>
<!-- Container for attachments -->
<VBox fx:id="attachmentsVBox" styleClass="std-padding">
<VBox styleClass="std-padding">
<Label text="Attachments" styleClass="bold-text"/>
<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>
<!-- Buttons -->

View File

@ -23,7 +23,7 @@
<HBox styleClass="std-padding,std-spacing">
<PropertiesPane hgap="5" vgap="5">
<Label text="Filter by Account"/>
<AccountSelectionBox fx:id="filterByAccountComboBox"/>
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
</PropertiesPane>
</HBox>
</top>