added filter to analytics desktop
This commit is contained in:
@@ -1,19 +1,22 @@
|
|||||||
package org.example.petshopdesktop.controllers;
|
package org.example.petshopdesktop.controllers;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.chart.*;
|
import javafx.scene.chart.*;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.DailySales;
|
import org.example.petshopdesktop.api.dto.analytics.DailySales;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.DashboardResponse;
|
import org.example.petshopdesktop.api.dto.analytics.DashboardResponse;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.TopProduct;
|
import org.example.petshopdesktop.api.dto.analytics.TopProduct;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.AnalyticsApi;
|
|
||||||
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
@@ -74,6 +77,32 @@ public class AnalyticsController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TabPane tabPane;
|
private TabPane tabPane;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnToggleFilters;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblFilterSummary;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox vbFilterContent;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpStartDate;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpEndDate;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbPaymentFilter;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbTopN;
|
||||||
|
|
||||||
|
private List<SaleResponse> cachedSales = new ArrayList<>();
|
||||||
|
private FilterState currentFilter = new FilterState();
|
||||||
|
|
||||||
|
private static final int[] TOP_N_VALUES = {5, 10, 15, 20};
|
||||||
|
|
||||||
private static final String SALES_COLOR = "#ff6b35";
|
private static final String SALES_COLOR = "#ff6b35";
|
||||||
private static final String REVENUE_COLOR = "#4ecdc4";
|
private static final String REVENUE_COLOR = "#4ecdc4";
|
||||||
private static final String QUANTITY_COLOR = "#ff9f1c";
|
private static final String QUANTITY_COLOR = "#ff9f1c";
|
||||||
@@ -88,13 +117,15 @@ public class AnalyticsController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
configureCharts();
|
configureCharts();
|
||||||
if (tabPane != null) {
|
|
||||||
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
|
cbTopN.setItems(FXCollections.observableArrayList("Top 5", "Top 10", "Top 15", "Top 20"));
|
||||||
if (oldTab != newTab && newTab != null) {
|
cbTopN.getSelectionModel().selectFirst();
|
||||||
loadAnalyticsData();
|
|
||||||
}
|
cbPaymentFilter.setItems(FXCollections.observableArrayList("All"));
|
||||||
});
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
}
|
|
||||||
|
lblFilterSummary.setText("All time");
|
||||||
|
|
||||||
loadAnalyticsData();
|
loadAnalyticsData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,64 +173,107 @@ public class AnalyticsController {
|
|||||||
|
|
||||||
private void loadAnalyticsData() {
|
private void loadAnalyticsData() {
|
||||||
lblError.setVisible(false);
|
lblError.setVisible(false);
|
||||||
if (lblStatus != null) {
|
lblStatus.setVisible(false);
|
||||||
lblStatus.setVisible(false);
|
btnRefresh.setDisable(true);
|
||||||
}
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10);
|
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
||||||
|
List<SaleResponse> sales = SaleApi.getInstance().listAllSales(null, storeId);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
try {
|
cachedSales = sales;
|
||||||
boolean isAdmin = UserSession.getInstance().isAdmin();
|
derivePaymentMethods();
|
||||||
loadSummaryData(dashboard);
|
applyCurrentFilter();
|
||||||
loadSalesOverTime(dashboard);
|
btnRefresh.setDisable(false);
|
||||||
loadTopProductsByRevenue(dashboard);
|
|
||||||
loadTopProductsByQuantity(dashboard);
|
|
||||||
loadPaymentMethodDistribution(dashboard);
|
|
||||||
loadEmployeePerformance(dashboard, isAdmin);
|
|
||||||
applyRoleVisibility(isAdmin);
|
|
||||||
} catch (Exception e) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data");
|
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
|
||||||
lblError.setVisible(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (UserSession.getInstance().isStaff()) {
|
|
||||||
try {
|
|
||||||
DashboardResponse fallback = buildStaffFallbackDashboard();
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
try {
|
|
||||||
loadSummaryData(fallback);
|
|
||||||
loadSalesOverTime(fallback);
|
|
||||||
loadTopProductsByRevenue(fallback);
|
|
||||||
loadTopProductsByQuantity(fallback);
|
|
||||||
loadPaymentMethodDistribution(fallback);
|
|
||||||
loadEmployeePerformance(fallback, false);
|
|
||||||
applyRoleVisibility(false);
|
|
||||||
lblError.setVisible(false);
|
|
||||||
} catch (Exception inner) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", inner, "Rendering fallback analytics data");
|
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
|
||||||
lblError.setVisible(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch (Exception fallbackError) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", fallbackError, "Loading fallback analytics data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data");
|
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading sales data for analytics");
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
lblError.setText("Error loading analytics data. Please try again.");
|
||||||
lblError.setVisible(true);
|
lblError.setVisible(true);
|
||||||
|
btnRefresh.setDisable(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyCurrentFilter() {
|
||||||
|
try {
|
||||||
|
List<SaleResponse> filtered = filterSales(cachedSales, currentFilter);
|
||||||
|
String start = currentFilter.startDate.isEmpty() ? LocalDate.now().minusDays(6).toString() : currentFilter.startDate;
|
||||||
|
String end = currentFilter.endDate.isEmpty() ? LocalDate.now().toString() : currentFilter.endDate;
|
||||||
|
|
||||||
|
DashboardResponse dashboard = new DashboardResponse();
|
||||||
|
dashboard.setSalesSummary(buildSalesSummary(filtered));
|
||||||
|
dashboard.setDailySales(buildDailySalesBetween(filtered, start, end));
|
||||||
|
dashboard.setTopProducts(buildTopProducts(filtered, currentFilter.topN > 0 ? currentFilter.topN : 5));
|
||||||
|
dashboard.setPaymentMethods(buildPaymentMethods(filtered));
|
||||||
|
dashboard.setEmployeePerformance(buildEmployeePerformance(filtered));
|
||||||
|
|
||||||
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
loadSummaryData(dashboard);
|
||||||
|
loadSalesOverTime(dashboard);
|
||||||
|
loadTopProductsByRevenue(dashboard);
|
||||||
|
loadTopProductsByQuantity(dashboard);
|
||||||
|
loadPaymentMethodDistribution(dashboard);
|
||||||
|
loadEmployeePerformance(dashboard, isAdmin);
|
||||||
|
applyRoleVisibility(isAdmin);
|
||||||
|
updateFilterSummary();
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("AnalyticsController.applyCurrentFilter", e, "Computing analytics from filtered sales");
|
||||||
|
lblError.setText("Error computing analytics. Please try again.");
|
||||||
|
lblError.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SaleResponse> filterSales(List<SaleResponse> sales, FilterState filter) {
|
||||||
|
return sales.stream().filter(sale -> {
|
||||||
|
String date = sale.getSaleDate() != null ? sale.getSaleDate().toLocalDate().toString() : "";
|
||||||
|
if (!filter.startDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.startDate) < 0) return false;
|
||||||
|
if (!filter.endDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.endDate) > 0) return false;
|
||||||
|
if (!filter.paymentMethod.equals("All") && !filter.paymentMethod.isEmpty()) {
|
||||||
|
if (!filter.paymentMethod.equalsIgnoreCase(sale.getPaymentMethod())) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void derivePaymentMethods() {
|
||||||
|
Set<String> methods = new TreeSet<>();
|
||||||
|
for (SaleResponse s : cachedSales) {
|
||||||
|
if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) {
|
||||||
|
methods.add(s.getPaymentMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
|
items.add("All");
|
||||||
|
items.addAll(methods);
|
||||||
|
String current = cbPaymentFilter.getValue();
|
||||||
|
cbPaymentFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
if (current != null && items.contains(current)) {
|
||||||
|
cbPaymentFilter.setValue(current);
|
||||||
|
} else {
|
||||||
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFilterSummary() {
|
||||||
|
String start = currentFilter.startDate;
|
||||||
|
String end = currentFilter.endDate;
|
||||||
|
if (start.isEmpty() && end.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("All time");
|
||||||
|
} else if (start.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("Up to " + shortDate(end));
|
||||||
|
} else if (end.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("From " + shortDate(start));
|
||||||
|
} else {
|
||||||
|
lblFilterSummary.setText(shortDate(start) + " – " + shortDate(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String shortDate(String date) {
|
||||||
|
return (date != null && date.length() >= 10) ? date.substring(5) : date;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadSummaryData(DashboardResponse dashboard) throws Exception {
|
private void loadSummaryData(DashboardResponse dashboard) throws Exception {
|
||||||
if (dashboard != null) {
|
if (dashboard != null) {
|
||||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||||
@@ -326,26 +400,23 @@ public class AnalyticsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardResponse buildStaffFallbackDashboard() throws Exception {
|
private List<DashboardResponse.EmployeePerformanceData> buildEmployeePerformance(List<SaleResponse> sales) {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Map<String, BigDecimal> empRevenue = new LinkedHashMap<>();
|
||||||
List<SaleResponse> sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null, storeId);
|
for (SaleResponse sale : sales) {
|
||||||
String employeeName = UserSession.getInstance().getEmployeeName();
|
if (Boolean.TRUE.equals(sale.getIsRefund())) continue;
|
||||||
if (employeeName == null || employeeName.isBlank()) {
|
String emp = sale.getEmployeeName() != null ? sale.getEmployeeName() : "Unknown";
|
||||||
employeeName = UserSession.getInstance().getUsername();
|
BigDecimal amount = sale.getTotalAmount() != null ? sale.getTotalAmount() : BigDecimal.ZERO;
|
||||||
|
empRevenue.merge(emp, amount, BigDecimal::add);
|
||||||
}
|
}
|
||||||
final String employeeNameFilter = employeeName;
|
return empRevenue.entrySet().stream()
|
||||||
|
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
|
||||||
List<SaleResponse> personalSales = sales.stream()
|
.map(e -> {
|
||||||
.filter(sale -> sale.getEmployeeName() != null && sale.getEmployeeName().equalsIgnoreCase(employeeNameFilter))
|
DashboardResponse.EmployeePerformanceData d = new DashboardResponse.EmployeePerformanceData();
|
||||||
.toList();
|
d.setEmployeeName(e.getKey());
|
||||||
|
d.setRevenue(e.getValue());
|
||||||
DashboardResponse dashboard = new DashboardResponse();
|
return d;
|
||||||
dashboard.setSalesSummary(buildSalesSummary(personalSales));
|
})
|
||||||
dashboard.setDailySales(buildDailySales(personalSales, 30));
|
.collect(Collectors.toList());
|
||||||
dashboard.setTopProducts(buildTopProducts(personalSales, 10));
|
|
||||||
dashboard.setPaymentMethods(buildPaymentMethods(personalSales));
|
|
||||||
dashboard.setEmployeePerformance(List.of(new DashboardResponse.EmployeePerformanceData()));
|
|
||||||
return dashboard;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardResponse.SalesSummary buildSalesSummary(List<SaleResponse> sales) {
|
private DashboardResponse.SalesSummary buildSalesSummary(List<SaleResponse> sales) {
|
||||||
@@ -379,21 +450,26 @@ public class AnalyticsController {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DailySales> buildDailySales(List<SaleResponse> sales, int days) {
|
private List<DailySales> buildDailySalesBetween(List<SaleResponse> sales, String startStr, String endStr) {
|
||||||
Map<LocalDate, DailySales> daily = new LinkedHashMap<>();
|
Map<LocalDate, DailySales> daily = new LinkedHashMap<>();
|
||||||
LocalDate start = LocalDate.now().minusDays(days - 1L);
|
try {
|
||||||
for (int i = 0; i < days; i++) {
|
LocalDate start = LocalDate.parse(startStr);
|
||||||
LocalDate date = start.plusDays(i);
|
LocalDate end = LocalDate.parse(endStr);
|
||||||
DailySales row = new DailySales();
|
if (start.plusDays(60).isBefore(end)) {
|
||||||
row.setDate(date.toString());
|
start = end.minusDays(59);
|
||||||
row.setRevenue(BigDecimal.ZERO);
|
}
|
||||||
row.setSalesCount(0L);
|
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
|
||||||
daily.put(date, row);
|
DailySales row = new DailySales();
|
||||||
|
row.setDate(date.toString());
|
||||||
|
row.setRevenue(BigDecimal.ZERO);
|
||||||
|
row.setSalesCount(0L);
|
||||||
|
daily.put(date, row);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
for (SaleResponse sale : sales) {
|
for (SaleResponse sale : sales) {
|
||||||
if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) {
|
if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
LocalDate date = sale.getSaleDate().toLocalDate();
|
LocalDate date = sale.getSaleDate().toLocalDate();
|
||||||
DailySales row = daily.get(date);
|
DailySales row = daily.get(date);
|
||||||
if (row != null) {
|
if (row != null) {
|
||||||
@@ -494,4 +570,73 @@ public class AnalyticsController {
|
|||||||
loadAnalyticsData();
|
loadAnalyticsData();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnToggleFilters(ActionEvent event) {
|
||||||
|
boolean expanded = vbFilterContent.isVisible();
|
||||||
|
vbFilterContent.setVisible(!expanded);
|
||||||
|
vbFilterContent.setManaged(!expanded);
|
||||||
|
btnToggleFilters.setText(expanded ? "▼ Filters" : "▲ Filters");
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPresetToday(ActionEvent event) { applyPreset(0, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset7D(ActionEvent event) { applyPreset(-6, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset30D(ActionEvent event) { applyPreset(-29, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset3M(ActionEvent event) { applyPreset(-89, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset1Y(ActionEvent event) { applyPreset(-364, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPresetAll(ActionEvent event) {
|
||||||
|
dpStartDate.setValue(null);
|
||||||
|
dpEndDate.setValue(null);
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnApplyFilter(ActionEvent event) {
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnResetFilter(ActionEvent event) {
|
||||||
|
dpStartDate.setValue(null);
|
||||||
|
dpEndDate.setValue(null);
|
||||||
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
|
cbTopN.getSelectionModel().selectFirst();
|
||||||
|
currentFilter = new FilterState();
|
||||||
|
applyCurrentFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPreset(int startOffset, int endOffset) {
|
||||||
|
dpStartDate.setValue(LocalDate.now().plusDays(startOffset));
|
||||||
|
dpEndDate.setValue(LocalDate.now().plusDays(endOffset));
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFiltersFromUI() {
|
||||||
|
currentFilter = new FilterState();
|
||||||
|
currentFilter.startDate = dpStartDate.getValue() != null ? dpStartDate.getValue().toString() : "";
|
||||||
|
currentFilter.endDate = dpEndDate.getValue() != null ? dpEndDate.getValue().toString() : "";
|
||||||
|
String pm = cbPaymentFilter.getValue();
|
||||||
|
currentFilter.paymentMethod = pm != null ? pm : "All";
|
||||||
|
int topNPos = cbTopN.getSelectionModel().getSelectedIndex();
|
||||||
|
currentFilter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||||
|
applyCurrentFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FilterState {
|
||||||
|
String startDate = "";
|
||||||
|
String endDate = "";
|
||||||
|
String paymentMethod = "All";
|
||||||
|
int topN = 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
<?import javafx.scene.chart.NumberAxis?>
|
<?import javafx.scene.chart.NumberAxis?>
|
||||||
<?import javafx.scene.chart.PieChart?>
|
<?import javafx.scene.chart.PieChart?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ComboBox?>
|
||||||
|
<?import javafx.scene.control.DatePicker?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.Tab?>
|
<?import javafx.scene.control.Tab?>
|
||||||
<?import javafx.scene.control.TabPane?>
|
<?import javafx.scene.control.TabPane?>
|
||||||
|
<?import javafx.scene.layout.FlowPane?>
|
||||||
<?import javafx.scene.layout.Region?>
|
<?import javafx.scene.layout.Region?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
@@ -37,6 +40,75 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
|
<VBox style="-fx-background-color: white; -fx-background-radius: 8; -fx-border-color: #e2e8f0; -fx-border-radius: 8; -fx-border-width: 1;">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="10.0" left="12.0" right="12.0" top="10.0" />
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="btnToggleFilters" mnemonicParsing="false" onAction="#btnToggleFilters" style="-fx-background-color: transparent; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="▼ Filters" textFill="#2c3e50">
|
||||||
|
<font><Font name="System Bold" size="12.0" /></font>
|
||||||
|
</Button>
|
||||||
|
<Label fx:id="lblFilterSummary" text="All time" textFill="#64748b">
|
||||||
|
<font><Font size="12.0" /></font>
|
||||||
|
</Label>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<VBox fx:id="vbFilterContent" spacing="8.0" visible="false" managed="false">
|
||||||
|
<VBox.margin><Insets top="8.0" /></VBox.margin>
|
||||||
|
<children>
|
||||||
|
<FlowPane alignment="CENTER_LEFT" hgap="8.0" vgap="8.0">
|
||||||
|
<children>
|
||||||
|
<DatePicker fx:id="dpStartDate" prefWidth="145.0" promptText="Start date" />
|
||||||
|
<Label text="to" textFill="#64748b" />
|
||||||
|
<DatePicker fx:id="dpEndDate" prefWidth="145.0" promptText="End date" />
|
||||||
|
<Button fx:id="btnPresetToday" mnemonicParsing="false" onAction="#btnPresetToday" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Today" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnPreset7D" mnemonicParsing="false" onAction="#btnPreset7D" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="7D" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnPreset30D" mnemonicParsing="false" onAction="#btnPreset30D" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="30D" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnPreset3M" mnemonicParsing="false" onAction="#btnPreset3M" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="3M" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnPreset1Y" mnemonicParsing="false" onAction="#btnPreset1Y" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="1Y" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnPresetAll" mnemonicParsing="false" onAction="#btnPresetAll" style="-fx-background-color: #f1f5f9; -fx-background-radius: 6; -fx-border-color: #e2e8f0; -fx-border-radius: 6; -fx-border-width: 1; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="All" textFill="#475569">
|
||||||
|
<font><Font size="11.0" /></font>
|
||||||
|
<padding><Insets bottom="5.0" left="10.0" right="10.0" top="5.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
</children>
|
||||||
|
</FlowPane>
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="8.0">
|
||||||
|
<children>
|
||||||
|
<ComboBox fx:id="cbPaymentFilter" prefWidth="145.0" promptText="All Payments" />
|
||||||
|
<ComboBox fx:id="cbTopN" prefWidth="110.0" />
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button fx:id="btnApplyFilter" mnemonicParsing="false" onAction="#btnApplyFilter" style="-fx-background-color: #4ECDC4; -fx-background-radius: 6; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Apply" textFill="WHITE">
|
||||||
|
<font><Font name="System Bold" size="12.0" /></font>
|
||||||
|
<padding><Insets bottom="6.0" left="18.0" right="18.0" top="6.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
<Button fx:id="btnResetFilter" mnemonicParsing="false" onAction="#btnResetFilter" style="-fx-background-color: transparent; -fx-border-color: #cbd5e1; -fx-border-radius: 6; -fx-background-radius: 6; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Reset" textFill="#64748b">
|
||||||
|
<font><Font size="12.0" /></font>
|
||||||
|
<padding><Insets bottom="6.0" left="14.0" right="14.0" top="6.0" /></padding>
|
||||||
|
</Button>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
<Label fx:id="lblStatus" textFill="#64748b" visible="false" managed="true">
|
<Label fx:id="lblStatus" textFill="#64748b" visible="false" managed="true">
|
||||||
<font>
|
<font>
|
||||||
<Font size="13.0" />
|
<Font size="13.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user