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 6c7b9b9e..9d44f2d4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -1,26 +1,40 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.analytics.DashboardResponse; +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") -@PreAuthorize("hasRole('ADMIN')") +@PreAuthorize("hasAnyRole('ADMIN', 'STAFF')") public class AnalyticsController { private final AnalyticsService analyticsService; + private final UserRepository userRepository; - public AnalyticsController(AnalyticsService analyticsService) { + public AnalyticsController(AnalyticsService analyticsService, UserRepository userRepository) { this.analyticsService = analyticsService; + this.userRepository = userRepository; } @GetMapping("/dashboard") public ResponseEntity getDashboard( @RequestParam(defaultValue = "30") int days, @RequestParam(defaultValue = "10") int top) { - return ResponseEntity.ok(analyticsService.getDashboardData(days, 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/dto/analytics/DashboardResponse.java b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java index c884d24c..56788f77 100644 --- a/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java @@ -9,15 +9,19 @@ public class DashboardResponse { private InventorySummary inventorySummary; private List topProducts; private List dailySales; + private List paymentMethods; + private List employeePerformance; public DashboardResponse() { } - public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List topProducts, List dailySales) { + public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List topProducts, List dailySales, List paymentMethods, List employeePerformance) { this.salesSummary = salesSummary; this.inventorySummary = inventorySummary; this.topProducts = topProducts; this.dailySales = dailySales; + this.paymentMethods = paymentMethods; + this.employeePerformance = employeePerformance; } public SalesSummary getSalesSummary() { @@ -52,17 +56,33 @@ public class DashboardResponse { this.dailySales = dailySales; } + public List getPaymentMethods() { + return paymentMethods; + } + + public void setPaymentMethods(List paymentMethods) { + this.paymentMethods = paymentMethods; + } + + public List getEmployeePerformance() { + return employeePerformance; + } + + public void setEmployeePerformance(List employeePerformance) { + this.employeePerformance = employeePerformance; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DashboardResponse that = (DashboardResponse) o; - return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales); + return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales) && Objects.equals(paymentMethods, that.paymentMethods) && Objects.equals(employeePerformance, that.employeePerformance); } @Override public int hashCode() { - return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales); + return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance); } @Override @@ -72,6 +92,8 @@ public class DashboardResponse { ", inventorySummary=" + inventorySummary + ", topProducts=" + topProducts + ", dailySales=" + dailySales + + ", paymentMethods=" + paymentMethods + + ", employeePerformance=" + employeePerformance + '}'; } @@ -80,15 +102,17 @@ public class DashboardResponse { private Long totalSales; private BigDecimal totalRefunds; private Long totalRefundCount; + private Long totalItemsSold; public SalesSummary() { } - public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount) { + public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount, Long totalItemsSold) { this.totalRevenue = totalRevenue; this.totalSales = totalSales; this.totalRefunds = totalRefunds; this.totalRefundCount = totalRefundCount; + this.totalItemsSold = totalItemsSold; } public BigDecimal getTotalRevenue() { @@ -123,17 +147,25 @@ public class DashboardResponse { this.totalRefundCount = totalRefundCount; } + public Long getTotalItemsSold() { + return totalItemsSold; + } + + public void setTotalItemsSold(Long totalItemsSold) { + this.totalItemsSold = totalItemsSold; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SalesSummary that = (SalesSummary) o; - return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount); + return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount) && Objects.equals(totalItemsSold, that.totalItemsSold); } @Override public int hashCode() { - return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount); + return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold); } @Override @@ -143,10 +175,69 @@ public class DashboardResponse { ", totalSales=" + totalSales + ", totalRefunds=" + totalRefunds + ", totalRefundCount=" + totalRefundCount + + ", totalItemsSold=" + totalItemsSold + '}'; } } + public static class PaymentMethodData { + private String paymentMethod; + private Long count; + + public PaymentMethodData() { + } + + public PaymentMethodData(String paymentMethod, Long count) { + this.paymentMethod = paymentMethod; + this.count = count; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } +} + + public static class EmployeePerformanceData { + private String employeeName; + private BigDecimal revenue; + + public EmployeePerformanceData() { + } + + public EmployeePerformanceData(String employeeName, BigDecimal revenue) { + this.employeeName = employeeName; + this.revenue = revenue; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } +} + public static class InventorySummary { private Long totalProducts; private Long lowStockProducts; 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 32e6a5a7..c14a9511 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -1,9 +1,12 @@ package com.petshop.backend.service; import com.petshop.backend.dto.analytics.DashboardResponse; +import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Inventory; import com.petshop.backend.entity.Product; import com.petshop.backend.entity.Sale; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.InventoryRepository; import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.SaleRepository; @@ -23,28 +26,33 @@ public class AnalyticsService { private final SaleRepository saleRepository; private final InventoryRepository inventoryRepository; private final ProductRepository productRepository; + private final EmployeeRepository employeeRepository; public AnalyticsService(SaleRepository saleRepository, - InventoryRepository inventoryRepository, ProductRepository productRepository) { + InventoryRepository inventoryRepository, ProductRepository productRepository, EmployeeRepository employeeRepository) { this.saleRepository = saleRepository; this.inventoryRepository = inventoryRepository; this.productRepository = productRepository; + this.employeeRepository = employeeRepository; } @Transactional(readOnly = true) - public DashboardResponse getDashboardData(int days, int top) { + public DashboardResponse getDashboardData(int days, int top, User user) { LocalDateTime startDate = LocalDateTime.now().minusDays(days); List sales = saleRepository.findAll().stream() .filter(sale -> sale.getSaleDate().isAfter(startDate)) + .filter(sale -> includeSaleForUser(sale, user)) .collect(Collectors.toList()); DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales); - DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary(); + DashboardResponse.InventorySummary inventorySummary = user.getRole() == User.Role.ADMIN ? calculateInventorySummary() : null; List topProducts = calculateTopProducts(sales, top); List dailySales = calculateDailySales(sales, days); + List paymentMethods = calculatePaymentMethods(sales); + List employeePerformance = calculateEmployeePerformance(sales, user); - return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales); + return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance); } private DashboardResponse.SalesSummary calculateSalesSummary(List sales) { @@ -66,7 +74,13 @@ public class AnalyticsService { .filter(Sale::getIsRefund) .count(); - return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount); + Long totalItemsSold = sales.stream() + .filter(sale -> !sale.getIsRefund()) + .flatMap(sale -> sale.getItems().stream()) + .mapToLong(item -> item.getQuantity()) + .sum(); + + return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold); } private DashboardResponse.InventorySummary calculateInventorySummary() { @@ -93,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(); @@ -128,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); @@ -138,4 +158,50 @@ public class AnalyticsService { return new ArrayList<>(dailySalesMap.values()); } + + private List calculatePaymentMethods(List sales) { + return sales.stream() + .filter(sale -> !sale.getIsRefund()) + .collect(Collectors.groupingBy( + sale -> sale.getPaymentMethod() == null ? "Unknown" : sale.getPaymentMethod(), + TreeMap::new, + Collectors.counting())) + .entrySet().stream() + .map(entry -> new DashboardResponse.PaymentMethodData(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private List calculateEmployeePerformance(List sales, User user) { + Map employeeRevenue = new TreeMap<>(); + + for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } + String employeeName = sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName(); + employeeRevenue.merge(employeeName, sale.getTotalAmount(), BigDecimal::add); + } + + if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) { + Employee employee = employeeRepository.findByUserId(user.getId()).orElse(null); + if (employee != null) { + String employeeName = employee.getFirstName() + " " + employee.getLastName(); + employeeRevenue.put(employeeName, BigDecimal.ZERO); + } + } + + return employeeRevenue.entrySet().stream() + .map(entry -> new DashboardResponse.EmployeePerformanceData(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private boolean includeSaleForUser(Sale sale, User user) { + if (user.getRole() == User.Role.ADMIN) { + return true; + } + if (user.getRole() == User.Role.STAFF) { + return sale.getEmployee() != null && sale.getEmployee().getUserId() != null && sale.getEmployee().getUserId().equals(user.getId()); + } + return false; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java index 1b29b2b3..38b17b81 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java @@ -8,6 +8,8 @@ public class DashboardResponse { private InventorySummary inventorySummary; private List topProducts; private List dailySales; + private List paymentMethods; + private List employeePerformance; public DashboardResponse() { } @@ -44,11 +46,28 @@ public class DashboardResponse { this.dailySales = dailySales; } + public List getPaymentMethods() { + return paymentMethods; + } + + public void setPaymentMethods(List paymentMethods) { + this.paymentMethods = paymentMethods; + } + + public List getEmployeePerformance() { + return employeePerformance; + } + + public void setEmployeePerformance(List employeePerformance) { + this.employeePerformance = employeePerformance; + } + public static class SalesSummary { private BigDecimal totalRevenue; private Long totalSales; private BigDecimal totalRefunds; private Long totalRefundCount; + private Long totalItemsSold; public SalesSummary() { } @@ -84,6 +103,14 @@ public class DashboardResponse { public void setTotalRefundCount(Long totalRefundCount) { this.totalRefundCount = totalRefundCount; } + + public Long getTotalItemsSold() { + return totalItemsSold; + } + + public void setTotalItemsSold(Long totalItemsSold) { + this.totalItemsSold = totalItemsSold; + } } public static class InventorySummary { @@ -118,4 +145,46 @@ public class DashboardResponse { this.outOfStockProducts = outOfStockProducts; } } + + public static class PaymentMethodData { + private String paymentMethod; + private Long count; + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + } + + public static class EmployeePerformanceData { + private String employeeName; + private BigDecimal revenue; + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java index a578d0e4..cd5646f4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java +++ b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -92,4 +92,8 @@ public class UserSession { public boolean isAdmin() { return Role.ADMIN.equals(role); } + + public boolean isStaff() { + return Role.STAFF.equals(role); + } } 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 1ab0ffbe..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,9 +9,11 @@ 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; import java.math.BigDecimal; @@ -127,16 +129,17 @@ public class AnalyticsController { new Thread(() -> { try { DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10); - List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null); Platform.runLater(() -> { try { + boolean isAdmin = UserSession.getInstance().isAdmin(); loadSummaryData(dashboard); loadSalesOverTime(dashboard); loadTopProductsByRevenue(dashboard); loadTopProductsByQuantity(dashboard); - loadPaymentMethodDistribution(sales); - loadEmployeePerformance(sales); + 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."); @@ -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."); @@ -157,15 +185,12 @@ public class AnalyticsController { if (dashboard != null) { BigDecimal totalRevenue = BigDecimal.ZERO; Long totalSales = 0L; - Long totalProducts = 0L; - if (dashboard.getSalesSummary() != null) { totalRevenue = dashboard.getSalesSummary().getTotalRevenue() != null ? dashboard.getSalesSummary().getTotalRevenue() : BigDecimal.ZERO; totalSales = dashboard.getSalesSummary().getTotalSales() != null ? dashboard.getSalesSummary().getTotalSales() : 0L; - } - - if (dashboard.getInventorySummary() != null) { - totalProducts = dashboard.getInventorySummary().getTotalProducts() != null ? dashboard.getInventorySummary().getTotalProducts() : 0L; + lblTotalItems.setText(wholeNumber.format(dashboard.getSalesSummary().getTotalItemsSold() != null ? dashboard.getSalesSummary().getTotalItemsSold() : 0L)); + } else { + lblTotalItems.setText(wholeNumber.format(0)); } lblTotalRevenue.setText(currency.format(totalRevenue)); @@ -176,7 +201,6 @@ public class AnalyticsController { avgTransaction = totalRevenue.divide(BigDecimal.valueOf(totalSales), 2, RoundingMode.HALF_UP); } lblAvgTransaction.setText(currency.format(avgTransaction)); - lblTotalItems.setText(wholeNumber.format(totalProducts)); } } @@ -243,24 +267,14 @@ public class AnalyticsController { applyBarChartColor(chartTopQuantity, QUANTITY_COLOR); } - private void loadPaymentMethodDistribution(List sales) throws Exception { - Map paymentMethodCount = sales.stream() - .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) - .collect(Collectors.groupingBy( - sale -> sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "Unknown", - Collectors.counting() - )); - + private void loadPaymentMethodDistribution(DashboardResponse dashboard) throws Exception { chartPaymentMethods.getData().clear(); - List> paymentEntries = paymentMethodCount.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); - - for (Map.Entry entry : paymentEntries) { + List paymentEntries = dashboard.getPaymentMethods() != null ? dashboard.getPaymentMethods() : List.of(); + for (DashboardResponse.PaymentMethodData entry : paymentEntries) { PieChart.Data slice = new PieChart.Data( - entry.getKey() + " (" + entry.getValue() + ")", - entry.getValue() + entry.getPaymentMethod() + " (" + entry.getCount() + ")", + entry.getCount() ); chartPaymentMethods.getData().add(slice); } @@ -269,24 +283,14 @@ public class AnalyticsController { applyPieChartColors(); } - private void loadEmployeePerformance(List sales) throws Exception { - Map employeeRevenue = sales.stream() - .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) - .filter(sale -> sale.getEmployeeName() != null) - .collect(Collectors.groupingBy( - SaleResponse::getEmployeeName, - Collectors.summingDouble(sale -> sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0) - )); - + private void loadEmployeePerformance(DashboardResponse dashboard, boolean isAdmin) throws Exception { XYChart.Series series = new XYChart.Series<>(); - series.setName("Revenue"); + series.setName(isAdmin ? "Revenue" : "My Revenue"); - List> employeeEntries = employeeRevenue.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); - - for (Map.Entry entry : employeeEntries) { - series.getData().add(new XYChart.Data<>(entry.getKey(), entry.getValue())); + List employeeEntries = dashboard.getEmployeePerformance() != null ? dashboard.getEmployeePerformance() : List.of(); + for (DashboardResponse.EmployeePerformanceData entry : employeeEntries) { + BigDecimal revenue = entry.getRevenue() != null ? entry.getRevenue() : BigDecimal.ZERO; + series.getData().add(new XYChart.Data<>(entry.getEmployeeName(), revenue)); } chartEmployeePerformance.getData().clear(); @@ -294,6 +298,139 @@ public class AnalyticsController { applyBarChartColor(chartEmployeePerformance, EMPLOYEE_COLOR); } + private void applyRoleVisibility(boolean isAdmin) { + chartEmployeePerformance.setVisible(isAdmin); + chartEmployeePerformance.setManaged(isAdmin); + if (chartEmployeePerformance.getParent() != null) { + chartEmployeePerformance.getParent().setVisible(isAdmin); + chartEmployeePerformance.getParent().setManaged(isAdmin); + } + } + + 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()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index d87e181d..4481bc9c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -358,6 +358,7 @@ public class MainLayoutController { lblRole.setText("Leon's Petstore"); boolean isAdmin = session.isAdmin(); + boolean canViewAnalytics = isAdmin || session.isStaff(); btnInventory.setVisible(isAdmin); btnInventory.setManaged(isAdmin); btnSuppliers.setVisible(isAdmin); @@ -384,8 +385,8 @@ public class MainLayoutController { } if (btnAnalytics != null) { - btnAnalytics.setVisible(isAdmin); - btnAnalytics.setManaged(isAdmin); + btnAnalytics.setVisible(canViewAnalytics); + btnAnalytics.setManaged(canViewAnalytics); } btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");