Add sales analytics screen

This commit is contained in:
2026-02-28 14:25:55 -07:00
parent 8f77fea963
commit f503a7e2b6
10 changed files with 710 additions and 1 deletions

View File

@@ -0,0 +1,183 @@
package org.example.petshopdesktop.controllers;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.chart.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.database.SaleDB;
import org.example.petshopdesktop.models.analytics.*;
import org.example.petshopdesktop.util.ActivityLogger;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class AnalyticsController {
@FXML
private Button btnRefresh;
@FXML
private Label lblError;
@FXML
private Label lblTotalRevenue;
@FXML
private Label lblTotalTransactions;
@FXML
private Label lblAvgTransaction;
@FXML
private Label lblTotalItems;
@FXML
private LineChart<String, Number> chartSalesOverTime;
@FXML
private BarChart<Number, String> chartTopRevenue;
@FXML
private BarChart<Number, String> chartTopQuantity;
@FXML
private PieChart chartPaymentMethods;
@FXML
private BarChart<String, Number> chartEmployeePerformance;
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
private final NumberFormat wholeNumber = NumberFormat.getIntegerInstance();
@FXML
public void initialize() {
if (!UserSession.getInstance().isAdmin()) {
lblError.setText("Access restricted to administrators only.");
lblError.setVisible(true);
disableAllCharts();
return;
}
configureCharts();
loadAnalyticsData();
}
private void disableAllCharts() {
chartSalesOverTime.setVisible(false);
chartTopRevenue.setVisible(false);
chartTopQuantity.setVisible(false);
chartPaymentMethods.setVisible(false);
chartEmployeePerformance.setVisible(false);
btnRefresh.setDisable(true);
}
private void configureCharts() {
chartSalesOverTime.setAnimated(true);
chartTopRevenue.setAnimated(true);
chartTopQuantity.setAnimated(true);
chartPaymentMethods.setAnimated(true);
chartEmployeePerformance.setAnimated(true);
}
private void loadAnalyticsData() {
lblError.setVisible(false);
try {
loadSummaryData();
loadSalesOverTime();
loadTopProductsByRevenue();
loadTopProductsByQuantity();
loadPaymentMethodDistribution();
loadEmployeePerformance();
} catch (Exception e) {
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data");
lblError.setText("Error loading analytics data. Please try again.");
lblError.setVisible(true);
}
}
private void loadSummaryData() throws Exception {
SalesSummary summary = SaleDB.getSalesSummary();
if (summary != null) {
lblTotalRevenue.setText(currency.format(summary.getTotalRevenue()));
lblTotalTransactions.setText(wholeNumber.format(summary.getTotalTransactions()));
lblAvgTransaction.setText(currency.format(summary.getAvgTransactionValue()));
lblTotalItems.setText(wholeNumber.format(summary.getTotalItemsSold()));
}
}
private void loadSalesOverTime() throws Exception {
ObservableList<DailySalesData> data = SaleDB.getDailySalesRevenue();
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName("Daily Revenue");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd");
for (DailySalesData dailySale : data) {
String dateStr = dailySale.getDate().format(formatter);
series.getData().add(new XYChart.Data<>(dateStr, dailySale.getRevenue()));
}
chartSalesOverTime.getData().clear();
chartSalesOverTime.getData().add(series);
}
private void loadTopProductsByRevenue() throws Exception {
ObservableList<ProductSalesData> data = SaleDB.getTopProductsByRevenue(10);
XYChart.Series<Number, String> series = new XYChart.Series<>();
series.setName("Revenue");
for (ProductSalesData product : data) {
series.getData().add(new XYChart.Data<>(product.getTotalRevenue(), product.getProductName()));
}
chartTopRevenue.getData().clear();
chartTopRevenue.getData().add(series);
}
private void loadTopProductsByQuantity() throws Exception {
ObservableList<ProductSalesData> data = SaleDB.getTopProductsByQuantity(10);
XYChart.Series<Number, String> series = new XYChart.Series<>();
series.setName("Quantity");
for (ProductSalesData product : data) {
series.getData().add(new XYChart.Data<>(product.getTotalQuantity(), product.getProductName()));
}
chartTopQuantity.getData().clear();
chartTopQuantity.getData().add(series);
}
private void loadPaymentMethodDistribution() throws Exception {
ObservableList<PaymentMethodData> data = SaleDB.getPaymentMethodDistribution();
chartPaymentMethods.getData().clear();
for (PaymentMethodData payment : data) {
PieChart.Data slice = new PieChart.Data(
payment.getPaymentMethod() + " (" + payment.getTransactionCount() + ")",
payment.getTransactionCount()
);
chartPaymentMethods.getData().add(slice);
}
}
private void loadEmployeePerformance() throws Exception {
ObservableList<EmployeeSalesData> data = SaleDB.getEmployeeSalesPerformance();
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName("Revenue");
for (EmployeeSalesData employee : data) {
series.getData().add(new XYChart.Data<>(employee.getEmployeeName(), employee.getTotalRevenue()));
}
chartEmployeePerformance.getData().clear();
chartEmployeePerformance.getData().add(series);
}
@FXML
void handleRefresh(ActionEvent event) {
loadAnalyticsData();
}
}

View File

@@ -66,6 +66,9 @@ public class MainLayoutController {
@FXML
private Button btnStaffAccounts;
@FXML
private Button btnAnalytics;
@FXML
private Label lblUsername;
@@ -129,6 +132,12 @@ public class MainLayoutController {
updateButtons(btnStaffAccounts);
}
@FXML
void btnAnalyticsClicked(ActionEvent event) {
loadView("analytics-view.fxml");
updateButtons(btnAnalytics);
}
@FXML
void btnServicesClicked(ActionEvent event) {
loadView("service-view.fxml");
@@ -195,6 +204,11 @@ public class MainLayoutController {
btnStaffAccounts.setManaged(isAdmin);
}
if (btnAnalytics != null) {
btnAnalytics.setVisible(isAdmin);
btnAnalytics.setManaged(isAdmin);
}
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
@@ -234,7 +248,8 @@ public class MainLayoutController {
btnProductSuppliers,
btnProducts,
btnPurchaseOrders,
btnStaffAccounts
btnStaffAccounts,
btnAnalytics
};
for (Button button : buttons) {

View File

@@ -5,9 +5,11 @@ import javafx.collections.ObservableList;
import org.example.petshopdesktop.DTOs.SaleDTO;
import org.example.petshopdesktop.models.SaleCartItem;
import org.example.petshopdesktop.models.SaleLineItem;
import org.example.petshopdesktop.models.analytics.*;
import org.example.petshopdesktop.util.ActivityLogger;
import java.sql.*;
import java.time.LocalDate;
public class SaleDB {
@@ -230,4 +232,169 @@ public class SaleDB {
conn.close();
}
}
public static ObservableList<DailySalesData> getDailySalesRevenue() throws SQLException {
ObservableList<DailySalesData> dailySales = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT DATE(s.saleDate) as saleDate, SUM(s.totalAmount) as revenue
FROM sale s
GROUP BY DATE(s.saleDate)
ORDER BY saleDate ASC
""";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
LocalDate date = rs.getDate("saleDate").toLocalDate();
double revenue = rs.getDouble("revenue");
dailySales.add(new DailySalesData(date, revenue));
}
conn.close();
return dailySales;
}
public static ObservableList<ProductSalesData> getTopProductsByRevenue(int limit) throws SQLException {
ObservableList<ProductSalesData> products = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT p.prodName, SUM(si.quantity * si.unitPrice) as totalRevenue
FROM saleItem si
JOIN product p ON si.prodId = p.prodId
GROUP BY p.prodId, p.prodName
ORDER BY totalRevenue DESC
LIMIT ?
""";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, limit);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
String productName = rs.getString("prodName");
double totalRevenue = rs.getDouble("totalRevenue");
products.add(new ProductSalesData(productName, 0, totalRevenue));
}
conn.close();
return products;
}
public static ObservableList<ProductSalesData> getTopProductsByQuantity(int limit) throws SQLException {
ObservableList<ProductSalesData> products = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT p.prodName, SUM(si.quantity) as totalQuantity,
SUM(si.quantity * si.unitPrice) as totalRevenue
FROM saleItem si
JOIN product p ON si.prodId = p.prodId
GROUP BY p.prodId, p.prodName
ORDER BY totalQuantity DESC
LIMIT ?
""";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, limit);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
String productName = rs.getString("prodName");
int totalQuantity = rs.getInt("totalQuantity");
double totalRevenue = rs.getDouble("totalRevenue");
products.add(new ProductSalesData(productName, totalQuantity, totalRevenue));
}
conn.close();
return products;
}
public static ObservableList<PaymentMethodData> getPaymentMethodDistribution() throws SQLException {
ObservableList<PaymentMethodData> paymentMethods = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT s.paymentMethod, COUNT(*) as transactionCount,
SUM(s.totalAmount) as totalRevenue
FROM sale s
GROUP BY s.paymentMethod
ORDER BY totalRevenue DESC
""";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
String paymentMethod = rs.getString("paymentMethod");
int transactionCount = rs.getInt("transactionCount");
double totalRevenue = rs.getDouble("totalRevenue");
paymentMethods.add(new PaymentMethodData(paymentMethod, transactionCount, totalRevenue));
}
conn.close();
return paymentMethods;
}
public static ObservableList<EmployeeSalesData> getEmployeeSalesPerformance() throws SQLException {
ObservableList<EmployeeSalesData> employees = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT CONCAT(e.firstName, ' ', e.lastName) as employeeName,
COUNT(DISTINCT s.saleId) as transactionCount,
SUM(s.totalAmount) as totalRevenue,
COALESCE(SUM(si.quantity), 0) as totalItemsSold
FROM sale s
JOIN employee e ON s.employeeId = e.employeeId
LEFT JOIN saleItem si ON s.saleId = si.saleId
GROUP BY e.employeeId, e.firstName, e.lastName
ORDER BY totalRevenue DESC
""";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
String employeeName = rs.getString("employeeName");
int transactionCount = rs.getInt("transactionCount");
double totalRevenue = rs.getDouble("totalRevenue");
int totalItemsSold = rs.getInt("totalItemsSold");
employees.add(new EmployeeSalesData(employeeName, transactionCount, totalRevenue, totalItemsSold));
}
conn.close();
return employees;
}
public static SalesSummary getSalesSummary() throws SQLException {
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT COUNT(DISTINCT s.saleId) as totalTransactions,
COALESCE(SUM(s.totalAmount), 0) as totalRevenue,
COALESCE(AVG(s.totalAmount), 0) as avgTransactionValue,
COALESCE(SUM(si.quantity), 0) as totalItemsSold
FROM sale s
LEFT JOIN saleItem si ON s.saleId = si.saleId
""";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
SalesSummary summary = null;
if (rs.next()) {
int totalTransactions = rs.getInt("totalTransactions");
double totalRevenue = rs.getDouble("totalRevenue");
double avgTransactionValue = rs.getDouble("avgTransactionValue");
int totalItemsSold = rs.getInt("totalItemsSold");
summary = new SalesSummary(totalTransactions, totalRevenue, avgTransactionValue, totalItemsSold);
}
conn.close();
return summary;
}
}

View File

@@ -0,0 +1,21 @@
package org.example.petshopdesktop.models.analytics;
import java.time.LocalDate;
public class DailySalesData {
private final LocalDate date;
private final double revenue;
public DailySalesData(LocalDate date, double revenue) {
this.date = date;
this.revenue = revenue;
}
public LocalDate getDate() {
return date;
}
public double getRevenue() {
return revenue;
}
}

View File

@@ -0,0 +1,31 @@
package org.example.petshopdesktop.models.analytics;
public class EmployeeSalesData {
private final String employeeName;
private final int transactionCount;
private final double totalRevenue;
private final int totalItemsSold;
public EmployeeSalesData(String employeeName, int transactionCount, double totalRevenue, int totalItemsSold) {
this.employeeName = employeeName;
this.transactionCount = transactionCount;
this.totalRevenue = totalRevenue;
this.totalItemsSold = totalItemsSold;
}
public String getEmployeeName() {
return employeeName;
}
public int getTransactionCount() {
return transactionCount;
}
public double getTotalRevenue() {
return totalRevenue;
}
public int getTotalItemsSold() {
return totalItemsSold;
}
}

View File

@@ -0,0 +1,25 @@
package org.example.petshopdesktop.models.analytics;
public class PaymentMethodData {
private final String paymentMethod;
private final int transactionCount;
private final double totalRevenue;
public PaymentMethodData(String paymentMethod, int transactionCount, double totalRevenue) {
this.paymentMethod = paymentMethod;
this.transactionCount = transactionCount;
this.totalRevenue = totalRevenue;
}
public String getPaymentMethod() {
return paymentMethod;
}
public int getTransactionCount() {
return transactionCount;
}
public double getTotalRevenue() {
return totalRevenue;
}
}

View File

@@ -0,0 +1,25 @@
package org.example.petshopdesktop.models.analytics;
public class ProductSalesData {
private final String productName;
private final int totalQuantity;
private final double totalRevenue;
public ProductSalesData(String productName, int totalQuantity, double totalRevenue) {
this.productName = productName;
this.totalQuantity = totalQuantity;
this.totalRevenue = totalRevenue;
}
public String getProductName() {
return productName;
}
public int getTotalQuantity() {
return totalQuantity;
}
public double getTotalRevenue() {
return totalRevenue;
}
}

View File

@@ -0,0 +1,31 @@
package org.example.petshopdesktop.models.analytics;
public class SalesSummary {
private final int totalTransactions;
private final double totalRevenue;
private final double avgTransactionValue;
private final int totalItemsSold;
public SalesSummary(int totalTransactions, double totalRevenue, double avgTransactionValue, int totalItemsSold) {
this.totalTransactions = totalTransactions;
this.totalRevenue = totalRevenue;
this.avgTransactionValue = avgTransactionValue;
this.totalItemsSold = totalItemsSold;
}
public int getTotalTransactions() {
return totalTransactions;
}
public double getTotalRevenue() {
return totalRevenue;
}
public double getAvgTransactionValue() {
return avgTransactionValue;
}
public int getTotalItemsSold() {
return totalItemsSold;
}
}