diff --git a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java index 002733fa..9d44f2d4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -5,9 +5,11 @@ import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AnalyticsService; import com.petshop.backend.util.AuthenticationHelper; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/v1/analytics") @@ -26,6 +28,12 @@ public class AnalyticsController { public ResponseEntity getDashboard( @RequestParam(defaultValue = "30") int days, @RequestParam(defaultValue = "10") int top) { + if (days < 1 || days > 365) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "days must be between 1 and 365"); + } + if (top < 1 || top > 50) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "top must be between 1 and 50"); + } User user = AuthenticationHelper.getAuthenticatedUser(userRepository); return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user)); } diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java index cef0b620..c14a9511 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -107,6 +107,9 @@ public class AnalyticsService { Map productSalesMap = new HashMap<>(); for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } for (var item : sale.getItems()) { Long productId = item.getProduct().getProdId(); String productName = item.getProduct().getProdName(); @@ -142,6 +145,9 @@ public class AnalyticsService { } for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } LocalDate saleDate = sale.getSaleDate().toLocalDate(); if (dailySalesMap.containsKey(saleDate)) { DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate); 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 dcae41df..60d14e7f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -9,7 +9,10 @@ import javafx.scene.control.Label; 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; @@ -144,6 +147,31 @@ public class AnalyticsController { } }); } 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"); lblError.setText("Error loading analytics data. Please try again."); @@ -279,6 +307,130 @@ public class AnalyticsController { } } + private DashboardResponse buildStaffFallbackDashboard() throws Exception { + List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null); + String employeeName = UserSession.getInstance().getEmployeeName(); + if (employeeName == null || employeeName.isBlank()) { + employeeName = UserSession.getInstance().getUsername(); + } + 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; + } + + private DashboardResponse.SalesSummary buildSalesSummary(List sales) { + DashboardResponse.SalesSummary summary = new DashboardResponse.SalesSummary(); + BigDecimal totalRevenue = BigDecimal.ZERO; + long totalSales = 0L; + BigDecimal totalRefunds = BigDecimal.ZERO; + long totalRefundCount = 0L; + long totalItemsSold = 0L; + + for (SaleResponse sale : sales) { + boolean refund = Boolean.TRUE.equals(sale.getIsRefund()); + BigDecimal amount = sale.getTotalAmount() != null ? sale.getTotalAmount() : BigDecimal.ZERO; + if (refund) { + totalRefunds = totalRefunds.add(amount); + totalRefundCount++; + continue; + } + totalRevenue = totalRevenue.add(amount); + totalSales++; + if (sale.getItems() != null) { + totalItemsSold += sale.getItems().stream().mapToLong(item -> item.getQuantity() == null ? 0 : item.getQuantity()).sum(); + } + } + + summary.setTotalRevenue(totalRevenue); + summary.setTotalSales(totalSales); + summary.setTotalRefunds(totalRefunds); + summary.setTotalRefundCount(totalRefundCount); + summary.setTotalItemsSold(totalItemsSold); + return summary; + } + + private List buildDailySales(List sales, int days) { + 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); + } + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) { + continue; + } + LocalDate date = sale.getSaleDate().toLocalDate(); + DailySales row = daily.get(date); + if (row != null) { + row.setRevenue(row.getRevenue().add(sale.getTotalAmount() == null ? BigDecimal.ZERO : sale.getTotalAmount())); + row.setSalesCount(row.getSalesCount() + 1); + } + } + return new ArrayList<>(daily.values()); + } + + private List buildTopProducts(List sales, int top) { + Map totals = new HashMap<>(); + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getItems() == null) { + continue; + } + for (SaleItemResponse item : sale.getItems()) { + if (item.getProdId() == null) { + continue; + } + TopProduct product = totals.computeIfAbsent(item.getProdId(), id -> { + TopProduct p = new TopProduct(); + p.setProductId(id); + p.setProductName(item.getProductName()); + p.setQuantitySold(0L); + p.setRevenue(BigDecimal.ZERO); + return p; + }); + long quantity = item.getQuantity() == null ? 0 : item.getQuantity(); + BigDecimal unitPrice = item.getUnitPrice() == null ? BigDecimal.ZERO : item.getUnitPrice(); + product.setQuantitySold(product.getQuantitySold() + quantity); + product.setRevenue(product.getRevenue().add(unitPrice.multiply(BigDecimal.valueOf(quantity)))); + } + } + return totals.values().stream() + .sorted((a, b) -> b.getRevenue().compareTo(a.getRevenue())) + .limit(top) + .toList(); + } + + private List buildPaymentMethods(List sales) { + Map totals = new TreeMap<>(); + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund())) { + continue; + } + String method = sale.getPaymentMethod() == null || sale.getPaymentMethod().isBlank() ? "Unknown" : sale.getPaymentMethod(); + totals.merge(method, 1L, Long::sum); + } + return totals.entrySet().stream().map(entry -> { + DashboardResponse.PaymentMethodData data = new DashboardResponse.PaymentMethodData(); + data.setPaymentMethod(entry.getKey()); + data.setCount(entry.getValue()); + return data; + }).toList(); + } + private void applyLineChartColor(LineChart chart, String color) { Platform.runLater(() -> { for (XYChart.Series series : chart.getData()) {