Fix staff analytics

This commit is contained in:
2026-03-29 23:50:31 -06:00
parent d2a6332633
commit 78aac62138
3 changed files with 166 additions and 0 deletions

View File

@@ -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<DashboardResponse> 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));
}

View File

@@ -107,6 +107,9 @@ public class AnalyticsService {
Map<Long, DashboardResponse.TopProduct> 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);

View File

@@ -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<SaleResponse> 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<SaleResponse> 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<SaleResponse> 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<DailySales> buildDailySales(List<SaleResponse> sales, int days) {
Map<LocalDate, DailySales> 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<TopProduct> buildTopProducts(List<SaleResponse> sales, int top) {
Map<Long, TopProduct> 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<DashboardResponse.PaymentMethodData> buildPaymentMethods(List<SaleResponse> sales) {
Map<String, Long> 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<Number, Number> chart, String color) {
Platform.runLater(() -> {
for (XYChart.Series<Number, Number> series : chart.getData()) {