Compare commits
No commits in common. "e6d5b280aa9808a4907680f5e24ec9c0425f1dae" and "20eed2108f3580fc13bdac5c4c75e91da263852c" have entirely different histories.
e6d5b280aa
...
20eed2108f
|
@ -91,19 +91,23 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
Popups.message(balanceCheckerButton, msg);
|
Popups.message(balanceCheckerButton, msg);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
accountProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
accountHistory.clear();
|
||||||
|
if (newValue == null) {
|
||||||
|
balanceTextProperty.set(null);
|
||||||
|
} else {
|
||||||
|
accountHistory.setAccountId(newValue.id);
|
||||||
|
accountHistory.loadMoreHistory();
|
||||||
|
Profile.getCurrent().dataSource().getAccountBalanceText(newValue)
|
||||||
|
.thenAccept(s -> Platform.runLater(() -> balanceTextProperty.set(s)));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
accountHistory.clear();
|
this.accountProperty.set((Account) context);
|
||||||
balanceTextProperty.set(null);
|
|
||||||
if (context instanceof Account account) {
|
|
||||||
this.accountProperty.set(account);
|
|
||||||
accountHistory.setAccountId(account.id);
|
|
||||||
accountHistory.loadMoreHistory();
|
|
||||||
Profile.getCurrent().dataSource().getAccountBalanceText(account)
|
|
||||||
.thenAccept(s -> Platform.runLater(() -> balanceTextProperty.set(s)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
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.AnalyticsRepository;
|
|
||||||
import com.andrewlalis.perfin.data.DataSource;
|
import com.andrewlalis.perfin.data.DataSource;
|
||||||
import com.andrewlalis.perfin.data.TimestampRange;
|
|
||||||
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
||||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
@ -29,8 +24,6 @@ public class EditVendorController implements RouteSelectionListener {
|
||||||
@FXML public TextArea descriptionField;
|
@FXML public TextArea descriptionField;
|
||||||
@FXML public Button saveButton;
|
@FXML public Button saveButton;
|
||||||
|
|
||||||
@FXML public Label totalSpentField;
|
|
||||||
|
|
||||||
@FXML public void initialize() {
|
@FXML public void initialize() {
|
||||||
var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
|
var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Name is required.")
|
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Name is required.")
|
||||||
|
@ -70,19 +63,9 @@ public class EditVendorController implements RouteSelectionListener {
|
||||||
this.vendor = tv;
|
this.vendor = tv;
|
||||||
nameField.setText(vendor.getName());
|
nameField.setText(vendor.getName());
|
||||||
descriptionField.setText(vendor.getDescription());
|
descriptionField.setText(vendor.getDescription());
|
||||||
Profile.getCurrent().dataSource().mapRepoAsync(
|
|
||||||
AnalyticsRepository.class,
|
|
||||||
repo -> repo.getVendorSpend(TimestampRange.unbounded(), vendor.id)
|
|
||||||
).thenAccept(amounts -> {
|
|
||||||
String text = amounts.stream()
|
|
||||||
.map(CurrencyUtil::formatMoney)
|
|
||||||
.collect(Collectors.joining(", "));
|
|
||||||
Platform.runLater(() -> totalSpentField.setText(text.isBlank() ? "None" : text));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
nameField.setText(null);
|
nameField.setText(null);
|
||||||
descriptionField.setText(null);
|
descriptionField.setText(null);
|
||||||
totalSpentField.setText(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,7 @@ import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
* to a specific page.
|
* to a specific page.
|
||||||
*/
|
*/
|
||||||
public class TransactionsViewController implements RouteSelectionListener {
|
public class TransactionsViewController implements RouteSelectionListener {
|
||||||
public static List<Sort> DEFAULT_SORTS = List.of(
|
public static List<Sort> DEFAULT_SORTS = List.of(Sort.desc("timestamp"));
|
||||||
Sort.desc("timestamp"),
|
|
||||||
Sort.desc("amount"),
|
|
||||||
Sort.desc("currency")
|
|
||||||
);
|
|
||||||
public record RouteContext(Long selectedTransactionId) {}
|
public record RouteContext(Long selectedTransactionId) {}
|
||||||
|
|
||||||
@FXML public BorderPane transactionsListBorderPane;
|
@FXML public BorderPane transactionsListBorderPane;
|
||||||
|
@ -192,28 +188,15 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
private List<SearchFilter> getCurrentSearchFilters() {
|
private List<SearchFilter> getCurrentSearchFilters() {
|
||||||
List<SearchFilter> filters = new ArrayList<>();
|
List<SearchFilter> filters = new ArrayList<>();
|
||||||
if (searchField.getText() != null && !searchField.getText().isBlank()) {
|
if (searchField.getText() != null && !searchField.getText().isBlank()) {
|
||||||
final String text = searchField.getText().strip();
|
|
||||||
// Special case: for input like "#123", search directly for the transaction id.
|
// Special case: for input like "#123", search directly for the transaction id.
|
||||||
if (text.matches("#\\d+")) {
|
if (searchField.getText().strip().matches("#\\d+")) {
|
||||||
int idQuery = Integer.parseInt(text.substring(1));
|
int idQuery = Integer.parseInt(searchField.getText().strip().substring(1));
|
||||||
var filter = new SearchFilter.Builder().where("id = ?").withArg(idQuery).build();
|
var filter = new SearchFilter.Builder().where("id = ?").withArg(idQuery).build();
|
||||||
return List.of(filter);
|
return List.of(filter);
|
||||||
}
|
}
|
||||||
// Special case: for input like "tag: abc", search directly for transactions with tags like that.
|
|
||||||
if (text.matches("tag:\\s*.+")) {
|
|
||||||
String tagQuery = "%" + text.substring(4).strip().toLowerCase() + "%";
|
|
||||||
var filter = new SearchFilter.Builder().where("""
|
|
||||||
id IN (
|
|
||||||
SELECT ttj.transaction_id
|
|
||||||
FROM transaction_tag_join ttj
|
|
||||||
LEFT JOIN transaction_tag tt ON tt.id = ttj.tag_id
|
|
||||||
WHERE LOWER(tt.name) LIKE ?
|
|
||||||
)""").withArg(tagQuery).build();
|
|
||||||
return List.of(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// General case: split the input into a list of terms, then apply each term in a LIKE %term% query.
|
// General case: split the input into a list of terms, then apply each term in a LIKE %term% query.
|
||||||
var likeTerms = Arrays.stream(text.toLowerCase().split("\\s+"))
|
var likeTerms = Arrays.stream(searchField.getText().strip().toLowerCase().split("\\s+"))
|
||||||
.map(t -> '%'+t+'%')
|
.map(t -> '%'+t+'%')
|
||||||
.toList();
|
.toList();
|
||||||
var builder = new SearchFilter.Builder();
|
var builder = new SearchFilter.Builder();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.andrewlalis.perfin.data;
|
package com.andrewlalis.perfin.data;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.util.Pair;
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
import com.andrewlalis.perfin.model.MoneyValue;
|
|
||||||
import com.andrewlalis.perfin.model.TransactionCategory;
|
import com.andrewlalis.perfin.model.TransactionCategory;
|
||||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
|
|
||||||
|
@ -15,13 +14,4 @@ public interface AnalyticsRepository extends Repository, AutoCloseable {
|
||||||
List<Pair<TransactionCategory, BigDecimal>> getIncomeByCategory(TimestampRange range, Currency currency);
|
List<Pair<TransactionCategory, BigDecimal>> getIncomeByCategory(TimestampRange range, Currency currency);
|
||||||
List<Pair<TransactionCategory, BigDecimal>> getIncomeByRootCategory(TimestampRange range, Currency currency);
|
List<Pair<TransactionCategory, BigDecimal>> getIncomeByRootCategory(TimestampRange range, Currency currency);
|
||||||
List<Pair<TransactionVendor, BigDecimal>> getSpendByVendor(TimestampRange range, Currency currency);
|
List<Pair<TransactionVendor, BigDecimal>> getSpendByVendor(TimestampRange range, Currency currency);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the amount spent, grouped by currency, on a specific vendor.
|
|
||||||
* @param range The time range to search in.
|
|
||||||
* @param vendorId The id of the vendor to search with.
|
|
||||||
* @return A list of money values with the total amount spent in each
|
|
||||||
* currency. An empty list is returned if no money is spent.
|
|
||||||
*/
|
|
||||||
List<MoneyValue> getVendorSpend(TimestampRange range, long vendorId);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import com.andrewlalis.perfin.data.TimestampRange;
|
||||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
import com.andrewlalis.perfin.data.util.Pair;
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
import com.andrewlalis.perfin.model.AccountEntry;
|
import com.andrewlalis.perfin.model.AccountEntry;
|
||||||
import com.andrewlalis.perfin.model.MoneyValue;
|
|
||||||
import com.andrewlalis.perfin.model.TransactionCategory;
|
import com.andrewlalis.perfin.model.TransactionCategory;
|
||||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
@ -73,38 +72,6 @@ public record JdbcAnalyticsRepository(Connection conn) implements AnalyticsRepos
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<MoneyValue> getVendorSpend(TimestampRange range, long vendorId) {
|
|
||||||
return DbUtil.findAll(
|
|
||||||
conn,
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
SUM(transaction.amount) AS total,
|
|
||||||
transaction.currency AS currency,
|
|
||||||
FROM transaction
|
|
||||||
WHERE
|
|
||||||
transaction.vendor_id = ? AND
|
|
||||||
transaction.timestamp >= ? AND
|
|
||||||
transaction.timestamp <= ? AND
|
|
||||||
'!exclude' NOT IN (
|
|
||||||
SELECT tt.name
|
|
||||||
FROM transaction_tag tt
|
|
||||||
LEFT JOIN transaction_tag_join ttj ON tt.id = ttj.tag_id
|
|
||||||
WHERE ttj.transaction_id = transaction.id
|
|
||||||
) AND
|
|
||||||
(SELECT COUNT(ae.id) FROM account_entry ae WHERE ae.transaction_id = transaction.id) = 1 AND
|
|
||||||
(SELECT COUNT(ae.id) FROM account_entry ae WHERE ae.transaction_id = transaction.id AND ae.type = 'CREDIT') = 1
|
|
||||||
GROUP BY transaction.currency
|
|
||||||
ORDER BY total DESC""",
|
|
||||||
List.of(vendorId, range.start(), range.end()),
|
|
||||||
rs -> {
|
|
||||||
BigDecimal total = rs.getBigDecimal(1);
|
|
||||||
String currencyCode = rs.getString(2);
|
|
||||||
return new MoneyValue(total, Currency.getInstance(currencyCode));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
conn.close();
|
conn.close();
|
||||||
|
|
|
@ -11,45 +11,24 @@
|
||||||
<Label text="Edit Vendor" styleClass="bold-text,large-font,std-padding"/>
|
<Label text="Edit Vendor" styleClass="bold-text,large-font,std-padding"/>
|
||||||
</top>
|
</top>
|
||||||
<center>
|
<center>
|
||||||
<ScrollPane fitToWidth="true" fitToHeight="true">
|
<VBox>
|
||||||
<VBox style="-fx-max-width: 500px;">
|
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding" maxWidth="500">
|
||||||
<!-- Basic properties -->
|
|
||||||
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding">
|
|
||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||||
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
|
|
||||||
<Label text="Name" labelFor="${nameField}" styleClass="bold-text"/>
|
<Label text="Name" labelFor="${nameField}"/>
|
||||||
<TextField fx:id="nameField"/>
|
<TextField fx:id="nameField"/>
|
||||||
|
|
||||||
<Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/>
|
<Label text="Description" labelFor="${descriptionField}"/>
|
||||||
<TextArea
|
<TextArea fx:id="descriptionField" wrapText="true"/>
|
||||||
fx:id="descriptionField"
|
|
||||||
wrapText="true"
|
|
||||||
style="-fx-pref-height: 100px; -fx-min-height: 100px;"
|
|
||||||
/>
|
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
||||||
<!-- Some stats about the vendor -->
|
|
||||||
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding">
|
|
||||||
<columnConstraints>
|
|
||||||
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
|
||||||
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
|
||||||
</columnConstraints>
|
|
||||||
|
|
||||||
<Label text="Total Spent" labelFor="${totalSpentField}" styleClass="bold-text"/>
|
|
||||||
<Label fx:id="totalSpentField" styleClass="mono-font"/>
|
|
||||||
</PropertiesPane>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
||||||
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</ScrollPane>
|
|
||||||
</center>
|
</center>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
Loading…
Reference in New Issue