From f3fc93d6e5a5690f7af8b1c01f7c124c969ecc0a Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:39:38 -0600 Subject: [PATCH] added filter to analytics desktop --- .../controllers/AnalyticsController.java | 313 +++++++++++++----- .../modelviews/analytics-view.fxml | 72 ++++ 2 files changed, 301 insertions(+), 84 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java index a231218f..b7ca17a2 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -1,19 +1,22 @@ package org.example.petshopdesktop.controllers; import javafx.application.Platform; +import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.chart.*; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DatePicker; import javafx.scene.control.Label; import javafx.scene.control.Tab; 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.DashboardResponse; import org.example.petshopdesktop.api.dto.analytics.TopProduct; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; 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.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; @@ -74,6 +77,32 @@ public class AnalyticsController { @FXML 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 cbPaymentFilter; + + @FXML + private ComboBox cbTopN; + + private List 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 REVENUE_COLOR = "#4ecdc4"; private static final String QUANTITY_COLOR = "#ff9f1c"; @@ -88,13 +117,15 @@ public class AnalyticsController { @FXML public void initialize() { configureCharts(); - if (tabPane != null) { - tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { - if (oldTab != newTab && newTab != null) { - loadAnalyticsData(); - } - }); - } + + cbTopN.setItems(FXCollections.observableArrayList("Top 5", "Top 10", "Top 15", "Top 20")); + cbTopN.getSelectionModel().selectFirst(); + + cbPaymentFilter.setItems(FXCollections.observableArrayList("All")); + cbPaymentFilter.getSelectionModel().selectFirst(); + + lblFilterSummary.setText("All time"); + loadAnalyticsData(); } @@ -142,64 +173,107 @@ public class AnalyticsController { private void loadAnalyticsData() { lblError.setVisible(false); - if (lblStatus != null) { - lblStatus.setVisible(false); - } + lblStatus.setVisible(false); + btnRefresh.setDisable(true); new Thread(() -> { try { - DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10); - + Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId(); + List sales = SaleApi.getInstance().listAllSales(null, storeId); Platform.runLater(() -> { - try { - boolean isAdmin = UserSession.getInstance().isAdmin(); - loadSummaryData(dashboard); - loadSalesOverTime(dashboard); - 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); - } + cachedSales = sales; + derivePaymentMethods(); + applyCurrentFilter(); + btnRefresh.setDisable(false); }); } 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(() -> { - 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.setVisible(true); + btnRefresh.setDisable(false); }); } }).start(); } + private void applyCurrentFilter() { + try { + List 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 filterSales(List 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 methods = new TreeSet<>(); + for (SaleResponse s : cachedSales) { + if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) { + methods.add(s.getPaymentMethod()); + } + } + List 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 { if (dashboard != null) { BigDecimal totalRevenue = BigDecimal.ZERO; @@ -326,26 +400,23 @@ public class AnalyticsController { } } - private DashboardResponse buildStaffFallbackDashboard() throws Exception { - Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId(); - List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null, storeId); - String employeeName = UserSession.getInstance().getEmployeeName(); - if (employeeName == null || employeeName.isBlank()) { - employeeName = UserSession.getInstance().getUsername(); + private List buildEmployeePerformance(List sales) { + Map empRevenue = new LinkedHashMap<>(); + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund())) continue; + String emp = sale.getEmployeeName() != null ? sale.getEmployeeName() : "Unknown"; + BigDecimal amount = sale.getTotalAmount() != null ? sale.getTotalAmount() : BigDecimal.ZERO; + empRevenue.merge(emp, amount, BigDecimal::add); } - final String employeeNameFilter = employeeName; - - List personalSales = sales.stream() - .filter(sale -> sale.getEmployeeName() != null && sale.getEmployeeName().equalsIgnoreCase(employeeNameFilter)) - .toList(); - - DashboardResponse dashboard = new DashboardResponse(); - dashboard.setSalesSummary(buildSalesSummary(personalSales)); - dashboard.setDailySales(buildDailySales(personalSales, 30)); - dashboard.setTopProducts(buildTopProducts(personalSales, 10)); - dashboard.setPaymentMethods(buildPaymentMethods(personalSales)); - dashboard.setEmployeePerformance(List.of(new DashboardResponse.EmployeePerformanceData())); - return dashboard; + return empRevenue.entrySet().stream() + .sorted((a, b) -> b.getValue().compareTo(a.getValue())) + .map(e -> { + DashboardResponse.EmployeePerformanceData d = new DashboardResponse.EmployeePerformanceData(); + d.setEmployeeName(e.getKey()); + d.setRevenue(e.getValue()); + return d; + }) + .collect(Collectors.toList()); } private DashboardResponse.SalesSummary buildSalesSummary(List sales) { @@ -379,21 +450,26 @@ public class AnalyticsController { return summary; } - private List buildDailySales(List sales, int days) { + private List buildDailySalesBetween(List sales, String startStr, String endStr) { Map daily = new LinkedHashMap<>(); - LocalDate start = LocalDate.now().minusDays(days - 1L); - for (int i = 0; i < days; i++) { - LocalDate date = start.plusDays(i); - DailySales row = new DailySales(); - row.setDate(date.toString()); - row.setRevenue(BigDecimal.ZERO); - row.setSalesCount(0L); - daily.put(date, row); + try { + LocalDate start = LocalDate.parse(startStr); + LocalDate end = LocalDate.parse(endStr); + if (start.plusDays(60).isBefore(end)) { + start = end.minusDays(59); + } + for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) { + 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) { - if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) { - continue; - } + if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) continue; LocalDate date = sale.getSaleDate().toLocalDate(); DailySales row = daily.get(date); if (row != null) { @@ -494,4 +570,73 @@ public class AnalyticsController { loadAnalyticsData(); 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; + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml index dba7511c..9aeb4fe9 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -7,9 +7,12 @@ + + + @@ -37,6 +40,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +