Merge pull request #32 from RecentRunner/refund-system

Refund system and logo
This commit is contained in:
2026-03-02 20:31:44 +00:00
committed by GitHub
18 changed files with 934 additions and 18 deletions

View File

@@ -132,8 +132,11 @@ CREATE TABLE sale (
paymentMethod VARCHAR(50) NOT NULL,
employeeId INT NOT NULL,
storeId INT NOT NULL,
isRefund BOOLEAN DEFAULT FALSE NOT NULL,
originalSaleId INT NULL,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId)
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
FOREIGN KEY (originalSaleId) REFERENCES sale(saleId)
);
CREATE TABLE saleItem (

33
log.txt
View File

@@ -12,3 +12,36 @@ The last packet sent successfully to the server was 0 milliseconds ago. The driv
[2026-02-28 14:35:14] [ERROR] EXCEPTION | Location: MainLayoutController.loadView | Type: LoadException | Message:
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/modelviews/analytics-view.fxml:58
| Context: Loading view: analytics-view.fxml
[2026-02-28 15:54:59] [ERROR] EXCEPTION | Location: LoginController.openMainLayout | Type: LoadException | Message: Error resolving onAction='#btnChatClicked', either the event handler is not in the Namespace or there is an error in the script.
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/main-layout-view.fxml:177
| Context: Loading main application layout after successful login
[2026-02-28 15:55:01] [ERROR] EXCEPTION | Location: LoginController.openMainLayout | Type: LoadException | Message: Error resolving onAction='#btnChatClicked', either the event handler is not in the Namespace or there is an error in the script.
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/main-layout-view.fxml:177
| Context: Loading main application layout after successful login
[2026-02-28 15:59:05] [ERROR] EXCEPTION | Location: LoginController.openMainLayout | Type: LoadException | Message:
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/main-layout-view.fxml:48
| Context: Loading main application layout after successful login
[2026-02-28 15:59:24] [ERROR] EXCEPTION | Location: LoginController.openMainLayout | Type: LoadException | Message:
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/main-layout-view.fxml:48
| Context: Loading main application layout after successful login
[2026-02-28 16:01:12] [ERROR] EXCEPTION | Location: LoginController.openMainLayout | Type: LoadException | Message:
/home/user/Documents/SAIT/Winter%202026/intellij/group-2-threaded-project-petshop-desktop/target/classes/org/example/petshopdesktop/main-layout-view.fxml:48
| Context: Loading main application layout after successful login
[2026-03-02 12:37:53] [ERROR] EXCEPTION | Location: ConnectionDB.getConnection | Type: CommunicationsException | Message: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. | Context: Establishing database connection
[2026-03-02 12:37:53] [ERROR] EXCEPTION | Location: ConnectionDB.getConnection | Type: CommunicationsException | Message: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. | Context: Establishing database connection
[2026-03-02 12:41:14] [ERROR] EXCEPTION | Location: ConnectionDB.getConnection | Type: CommunicationsException | Message: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. | Context: Establishing database connection
[2026-03-02 12:41:29] [ERROR] EXCEPTION | Location: SaleController.refreshSales | Type: SQLSyntaxErrorException | Message: Unknown column 's.isRefund' in 'field list' | Context: Loading sales
[2026-03-02 12:41:43] [ERROR] EXCEPTION | Location: SaleController.refreshSales | Type: SQLSyntaxErrorException | Message: Unknown column 's.isRefund' in 'field list' | Context: Loading sales
[2026-03-02 12:41:46] [ERROR] EXCEPTION | Location: SaleController.refreshSales | Type: SQLSyntaxErrorException | Message: Unknown column 's.isRefund' in 'field list' | Context: Loading sales
[2026-03-02 12:41:50] [ERROR] EXCEPTION | Location: SaleController.refreshSales | Type: SQLSyntaxErrorException | Message: Unknown column 's.isRefund' in 'field list' | Context: Loading sales
[2026-03-02 12:42:11] [ERROR] EXCEPTION | Location: SaleController.refreshSales | Type: SQLSyntaxErrorException | Message: Unknown column 's.isRefund' in 'field list' | Context: Loading sales
[2026-03-02 13:00:16] [ERROR] EXCEPTION | Location: ConnectionDB.getConnection | Type: CommunicationsException | Message: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. | Context: Establishing database connection
[2026-03-02 13:02:48] [INSERT] DB_INSERT | Table: sale | ID: Refund ID: 24 | Details: Created refund for sale ID 23 with 1 items, total: $240.00

View File

@@ -154,6 +154,8 @@ public class AnalyticsController {
);
chartPaymentMethods.getData().add(slice);
}
chartPaymentMethods.setLabelsVisible(false);
}
private void loadEmployeePerformance() throws Exception {

View File

@@ -9,6 +9,8 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
@@ -73,6 +75,9 @@ public class MainLayoutController {
@FXML
private Button btnChat;
@FXML
private ImageView imgLogo;
@FXML
private Label lblUsername;
@@ -166,6 +171,12 @@ public class MainLayoutController {
updateButtons(btnChat);
}
@FXML
void logoClicked(MouseEvent event) {
loadView("analytics-view.fxml");
updateButtons(btnAnalytics);
}
@FXML
void btnLogoutClicked(ActionEvent event) {
UserSession.getInstance().logout();

View File

@@ -5,6 +5,8 @@ import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
@@ -17,6 +19,8 @@ import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.database.InventoryDB;
import org.example.petshopdesktop.database.ProductDB;
@@ -38,6 +42,9 @@ public class SaleController {
@FXML
private Button btnRefresh;
@FXML
private Button btnRefund;
@FXML
private Label lblModeNote;
@@ -319,6 +326,30 @@ public class SaleController {
}
}
@FXML
void btnRefund(ActionEvent event) {
openRefundDialog();
}
private void openRefundDialog() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml"));
Stage dialog = new Stage();
dialog.initOwner(btnRefund.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Process Refund");
dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false);
dialog.showAndWait();
refreshInventory();
refreshSales(true);
} catch (Exception e) {
ActivityLogger.getInstance().logException("SaleController.openRefundDialog", e, "Opening refund dialog");
}
}
private void updateCartTotal() {
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
lblCartTotal.setText(currency.format(total));

View File

@@ -0,0 +1,329 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.database.SaleDB;
import org.example.petshopdesktop.models.SaleCartItem;
import org.example.petshopdesktop.models.SaleDetail;
import org.example.petshopdesktop.util.ActivityLogger;
import java.sql.SQLException;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Optional;
public class RefundDialogController {
@FXML
private TextField txtSaleId;
@FXML
private Button btnLoadSale;
@FXML
private Label lblSaleInfo;
@FXML
private TableView<SaleDetail.SaleDetailItem> tvOriginalItems;
@FXML
private TableColumn<SaleDetail.SaleDetailItem, String> colOriginalProduct;
@FXML
private TableColumn<SaleDetail.SaleDetailItem, Integer> colOriginalQuantity;
@FXML
private TableColumn<SaleDetail.SaleDetailItem, Double> colOriginalUnitPrice;
@FXML
private TableColumn<SaleDetail.SaleDetailItem, Double> colOriginalTotal;
@FXML
private Button btnAddToRefund;
@FXML
private TableView<RefundItem> tvRefundItems;
@FXML
private TableColumn<RefundItem, String> colRefundProduct;
@FXML
private TableColumn<RefundItem, Integer> colRefundQuantity;
@FXML
private TableColumn<RefundItem, Double> colRefundUnitPrice;
@FXML
private TableColumn<RefundItem, Double> colRefundTotal;
@FXML
private Button btnRemoveFromRefund;
@FXML
private ComboBox<String> cbPaymentMethod;
@FXML
private Label lblRefundTotal;
@FXML
private Button btnProcessRefund;
@FXML
private Button btnCancel;
private SaleDetail currentSale;
private final ObservableList<RefundItem> refundItems = FXCollections.observableArrayList();
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
@FXML
public void initialize() {
setupTables();
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card", "Debit"));
cbPaymentMethod.getSelectionModel().selectFirst();
updateRefundTotal();
}
private void setupTables() {
colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
tvRefundItems.setItems(refundItems);
tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
}
@FXML
void btnLoadSaleClicked(ActionEvent event) {
String saleIdText = txtSaleId.getText().trim();
if (saleIdText.isEmpty()) {
showError("Load Sale", "Enter a transaction ID.");
return;
}
int saleId;
try {
saleId = Integer.parseInt(saleIdText);
} catch (NumberFormatException e) {
showError("Load Sale", "Invalid transaction ID.");
return;
}
try {
if (SaleDB.isRefunded(saleId)) {
showError("Load Sale", "This sale has already been refunded.");
return;
}
currentSale = SaleDB.getSaleById(saleId);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s",
currentSale.getSaleDate().format(formatter),
currentSale.getEmployeeName(),
currency.format(currentSale.getTotalAmount()),
currentSale.getPaymentMethod());
lblSaleInfo.setText(saleInfo);
tvOriginalItems.setItems(currentSale.getItems());
cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod());
refundItems.clear();
updateRefundTotal();
} catch (SQLException e) {
ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale");
showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale.");
}
}
@FXML
void btnAddToRefundClicked(ActionEvent event) {
if (currentSale == null) {
showError("Add to Refund", "Load a sale first.");
return;
}
SaleDetail.SaleDetailItem selected = tvOriginalItems.getSelectionModel().getSelectedItem();
if (selected == null) {
showError("Add to Refund", "Select an item from the original sale.");
return;
}
int alreadyRefunded = refundItems.stream()
.filter(r -> r.getProdId() == selected.getProdId())
.mapToInt(RefundItem::getQuantity)
.sum();
int available = selected.getQuantity() - alreadyRefunded;
if (available <= 0) {
showError("Add to Refund", "All items of this product are already in the refund list.");
return;
}
TextInputDialog dialog = new TextInputDialog(String.valueOf(available));
dialog.setTitle("Refund Quantity");
dialog.setHeaderText("Product: " + selected.getProductName());
dialog.setContentText("Enter quantity to refund (max " + available + "):");
Optional<String> result = dialog.showAndWait();
if (result.isPresent()) {
try {
int quantity = Integer.parseInt(result.get().trim());
if (quantity <= 0) {
showError("Add to Refund", "Quantity must be at least 1.");
return;
}
if (quantity > available) {
showError("Add to Refund", "Cannot refund more than " + available + " items.");
return;
}
refundItems.add(new RefundItem(
selected.getProdId(),
selected.getProductName(),
quantity,
selected.getUnitPrice()
));
updateRefundTotal();
} catch (NumberFormatException e) {
showError("Add to Refund", "Invalid quantity.");
}
}
}
@FXML
void btnRemoveFromRefundClicked(ActionEvent event) {
RefundItem selected = tvRefundItems.getSelectionModel().getSelectedItem();
if (selected != null) {
refundItems.remove(selected);
updateRefundTotal();
}
}
@FXML
void btnProcessRefundClicked(ActionEvent event) {
if (currentSale == null) {
showError("Process Refund", "Load a sale first.");
return;
}
if (refundItems.isEmpty()) {
showError("Process Refund", "Add at least one item to refund.");
return;
}
Integer employeeId = UserSession.getInstance().getEmployeeId();
if (employeeId == null || employeeId <= 0) {
showError("Process Refund", "Employee is not set for this account.");
return;
}
String payment = cbPaymentMethod.getSelectionModel().getSelectedItem();
if (payment == null || payment.isBlank()) {
showError("Process Refund", "Select a payment method.");
return;
}
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Confirm Refund");
confirm.setHeaderText("Process refund for sale ID " + currentSale.getSaleId() + "?");
confirm.setContentText("Refund amount: " + lblRefundTotal.getText());
Optional<ButtonType> confirmResult = confirm.showAndWait();
if (confirmResult.isEmpty() || confirmResult.get() != ButtonType.OK) {
return;
}
try {
ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
for (RefundItem item : refundItems) {
cartItems.add(new SaleCartItem(item.getProdId(), item.getProductName(), item.getQuantity(), item.getUnitPrice()));
}
int refundId = SaleDB.createRefund(currentSale.getSaleId(), employeeId, payment, cartItems);
Alert success = new Alert(Alert.AlertType.INFORMATION);
success.setTitle("Refund Processed");
success.setHeaderText(null);
success.setContentText("Refund ID " + refundId + " was created successfully.");
success.showAndWait();
closeDialog();
} catch (SQLException e) {
ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund");
showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund.");
}
}
@FXML
void btnCancelClicked(ActionEvent event) {
closeDialog();
}
private void updateRefundTotal() {
double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum();
lblRefundTotal.setText(currency.format(total));
}
private void closeDialog() {
Stage stage = (Stage) btnCancel.getScene().getWindow();
stage.close();
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
public static class RefundItem {
private final int prodId;
private final String productName;
private final int quantity;
private final double unitPrice;
public RefundItem(int prodId, String productName, int quantity, double unitPrice) {
this.prodId = prodId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public int getProdId() {
return prodId;
}
public String getProductName() {
return productName;
}
public int getQuantity() {
return quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public double getTotal() {
return quantity * unitPrice;
}
}
}

View File

@@ -4,6 +4,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.example.petshopdesktop.DTOs.SaleDTO;
import org.example.petshopdesktop.models.SaleCartItem;
import org.example.petshopdesktop.models.SaleDetail;
import org.example.petshopdesktop.models.SaleLineItem;
import org.example.petshopdesktop.models.analytics.*;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -129,7 +130,8 @@ public class SaleDB {
si.quantity,
si.unitPrice,
(si.quantity * si.unitPrice) as total,
s.paymentMethod
s.paymentMethod,
s.isRefund
FROM sale s
JOIN saleItem si ON s.saleId = si.saleId
JOIN product p ON si.prodId = p.prodId
@@ -149,7 +151,8 @@ public class SaleDB {
rs.getInt("quantity"),
rs.getDouble("unitPrice"),
rs.getDouble("total"),
rs.getString("paymentMethod")
rs.getString("paymentMethod"),
rs.getBoolean("isRefund")
));
}
@@ -397,4 +400,173 @@ public class SaleDB {
conn.close();
return summary;
}
public static SaleDetail getSaleById(int saleId) throws SQLException {
Connection conn = ConnectionDB.getConnection();
String saleSql = """
SELECT s.saleId, s.saleDate, s.totalAmount, s.paymentMethod,
CONCAT(e.firstName, ' ', e.lastName) as employeeName,
s.isRefund
FROM sale s
JOIN employee e ON s.employeeId = e.employeeId
WHERE s.saleId = ?
""";
PreparedStatement saleStmt = conn.prepareStatement(saleSql);
saleStmt.setInt(1, saleId);
ResultSet saleRs = saleStmt.executeQuery();
if (!saleRs.next()) {
conn.close();
throw new SQLException("Sale not found with ID: " + saleId);
}
boolean isRefund = saleRs.getBoolean("isRefund");
if (isRefund) {
conn.close();
throw new SQLException("Cannot refund a refund transaction");
}
SaleDetail detail = new SaleDetail(
saleRs.getInt("saleId"),
saleRs.getTimestamp("saleDate").toLocalDateTime(),
saleRs.getDouble("totalAmount"),
saleRs.getString("paymentMethod"),
saleRs.getString("employeeName"),
FXCollections.observableArrayList()
);
String itemsSql = """
SELECT si.prodId, p.prodName, si.quantity, si.unitPrice,
(si.quantity * si.unitPrice) as total
FROM saleItem si
JOIN product p ON si.prodId = p.prodId
WHERE si.saleId = ?
""";
PreparedStatement itemsStmt = conn.prepareStatement(itemsSql);
itemsStmt.setInt(1, saleId);
ResultSet itemsRs = itemsStmt.executeQuery();
while (itemsRs.next()) {
detail.getItems().add(new SaleDetail.SaleDetailItem(
itemsRs.getInt("prodId"),
itemsRs.getString("prodName"),
itemsRs.getInt("quantity"),
itemsRs.getDouble("unitPrice"),
itemsRs.getDouble("total")
));
}
conn.close();
return detail;
}
public static boolean isRefunded(int saleId) throws SQLException {
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT COUNT(*) as refundCount
FROM sale
WHERE originalSaleId = ? AND isRefund = TRUE
""";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, saleId);
ResultSet rs = pstmt.executeQuery();
boolean refunded = false;
if (rs.next()) {
refunded = rs.getInt("refundCount") > 0;
}
conn.close();
return refunded;
}
public static int createRefund(int originalSaleId, int employeeId, String paymentMethod, ObservableList<SaleCartItem> refundItems) throws SQLException {
if (refundItems.isEmpty()) {
throw new SQLException("Cannot create refund with empty items");
}
Connection conn = ConnectionDB.getConnection();
SaleDetail originalSale = getSaleById(originalSaleId);
conn = ConnectionDB.getConnection();
if (isRefunded(originalSaleId)) {
throw new SQLException("This sale has already been refunded");
}
conn.setAutoCommit(false);
try {
double totalAmount = -refundItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
String insertSale = """
INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId, isRefund, originalSaleId)
VALUES (NOW(), ?, ?, ?, 1, TRUE, ?)
""";
PreparedStatement saleStmt = conn.prepareStatement(insertSale, Statement.RETURN_GENERATED_KEYS);
saleStmt.setDouble(1, totalAmount);
saleStmt.setString(2, paymentMethod);
saleStmt.setInt(3, employeeId);
saleStmt.setInt(4, originalSaleId);
saleStmt.executeUpdate();
ResultSet rs = saleStmt.getGeneratedKeys();
if (!rs.next()) {
throw new SQLException("Failed to get generated refund ID");
}
int refundId = rs.getInt(1);
String insertItem = """
INSERT INTO saleItem (saleId, prodId, quantity, unitPrice)
VALUES (?, ?, ?, ?)
""";
String updateInventory = """
UPDATE inventory
SET quantity = quantity + ?
WHERE prodId = ?
""";
PreparedStatement itemStmt = conn.prepareStatement(insertItem);
PreparedStatement invStmt = conn.prepareStatement(updateInventory);
for (SaleCartItem item : refundItems) {
itemStmt.setInt(1, refundId);
itemStmt.setInt(2, item.getProdId());
itemStmt.setInt(3, -item.getQuantity());
itemStmt.setDouble(4, item.getUnitPrice());
itemStmt.executeUpdate();
invStmt.setInt(1, item.getQuantity());
invStmt.setInt(2, item.getProdId());
int updated = invStmt.executeUpdate();
if (updated == 0) {
throw new SQLException("Failed to update inventory for product ID " + item.getProdId());
}
}
conn.commit();
ActivityLogger.getInstance().logInsert("sale",
String.format("Refund ID: %d", refundId),
String.format("Created refund for sale ID %d with %d items, total: $%.2f", originalSaleId, refundItems.size(), Math.abs(totalAmount)));
return refundId;
} catch (SQLException e) {
conn.rollback();
ActivityLogger.getInstance().logException("SaleDB.createRefund", e, "Creating refund");
throw e;
} finally {
conn.setAutoCommit(true);
conn.close();
}
}
}

View File

@@ -0,0 +1,82 @@
package org.example.petshopdesktop.models;
import javafx.collections.ObservableList;
import java.time.LocalDateTime;
public class SaleDetail {
private final int saleId;
private final LocalDateTime saleDate;
private final double totalAmount;
private final String paymentMethod;
private final String employeeName;
private final ObservableList<SaleDetailItem> items;
public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, ObservableList<SaleDetailItem> items) {
this.saleId = saleId;
this.saleDate = saleDate;
this.totalAmount = totalAmount;
this.paymentMethod = paymentMethod;
this.employeeName = employeeName;
this.items = items;
}
public int getSaleId() {
return saleId;
}
public LocalDateTime getSaleDate() {
return saleDate;
}
public double getTotalAmount() {
return totalAmount;
}
public String getPaymentMethod() {
return paymentMethod;
}
public String getEmployeeName() {
return employeeName;
}
public ObservableList<SaleDetailItem> getItems() {
return items;
}
public static class SaleDetailItem {
private final int prodId;
private final String productName;
private final int quantity;
private final double unitPrice;
private final double total;
public SaleDetailItem(int prodId, String productName, int quantity, double unitPrice, double total) {
this.prodId = prodId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.total = total;
}
public int getProdId() {
return prodId;
}
public String getProductName() {
return productName;
}
public int getQuantity() {
return quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public double getTotal() {
return total;
}
}
}

View File

@@ -9,8 +9,9 @@ public class SaleLineItem {
private final double unitPrice;
private final double total;
private final String paymentMethod;
private final boolean isRefund;
public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod) {
public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod, boolean isRefund) {
this.saleId = saleId;
this.saleDate = saleDate;
this.employeeName = employeeName;
@@ -19,6 +20,7 @@ public class SaleLineItem {
this.unitPrice = unitPrice;
this.total = total;
this.paymentMethod = paymentMethod;
this.isRefund = isRefund;
}
public int getSaleId() {
@@ -52,4 +54,8 @@ public class SaleLineItem {
public String getPaymentMethod() {
return paymentMethod;
}
public boolean isRefund() {
return isRefund;
}
}

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0" prefWidth="900.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<HBox alignment="CENTER_LEFT" spacing="20.0" style="-fx-background-color: #FF6b6b; -fx-background-radius: 14;">
<padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</padding>
<children>
<VBox alignment="CENTER_LEFT">
<children>
<Label text="Process Refund" textFill="WHITE">
<font>
<Font name="Comic Sans MS Bold" size="30.0" />
</font>
</Label>
</children>
</VBox>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnCancel" mnemonicParsing="false" onAction="#btnCancelClicked" style="-fx-background-color: #E74c3c; -fx-cursor: hand; -fx-background-radius: 8;" text="Cancel" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
</children>
</HBox>
<VBox spacing="15.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;">
<padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</padding>
<children>
<Label text="Load Sale" textFill="#2c3e50">
<font>
<Font name="System Bold" size="18.0" />
</font>
</Label>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="Transaction ID:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<TextField fx:id="txtSaleId" prefWidth="150.0" promptText="Enter sale ID" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
<padding>
<Insets bottom="7.0" left="10.0" right="10.0" top="7.0" />
</padding>
</TextField>
<Button fx:id="btnLoadSale" mnemonicParsing="false" onAction="#btnLoadSaleClicked" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Load Sale" textFill="WHITE">
<font>
<Font name="System Bold" size="13.0" />
</font>
<padding>
<Insets bottom="8.0" left="18.0" right="18.0" top="8.0" />
</padding>
</Button>
</children>
</HBox>
<Label fx:id="lblSaleInfo" text="" textFill="#7f8c8d">
<font>
<Font size="14.0" />
</font>
</Label>
</children>
</VBox>
<VBox spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;" VBox.vgrow="ALWAYS">
<padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</padding>
<children>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="Original Items" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnAddToRefund" mnemonicParsing="false" onAction="#btnAddToRefundClicked" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Add to Refund" textFill="WHITE">
<font>
<Font name="System Bold" size="13.0" />
</font>
<padding>
<Insets bottom="8.0" left="18.0" right="18.0" top="8.0" />
</padding>
</Button>
</children>
</HBox>
<TableView fx:id="tvOriginalItems" prefHeight="150.0" style="-fx-background-color: white; -fx-background-radius: 10;">
<columns>
<TableColumn fx:id="colOriginalProduct" prefWidth="350.0" text="Product Name" />
<TableColumn fx:id="colOriginalQuantity" prefWidth="120.0" text="Quantity" />
<TableColumn fx:id="colOriginalUnitPrice" prefWidth="150.0" text="Unit Price" />
<TableColumn fx:id="colOriginalTotal" prefWidth="150.0" text="Total" />
</columns>
</TableView>
<Separator />
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="Refund Items" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnRemoveFromRefund" mnemonicParsing="false" onAction="#btnRemoveFromRefundClicked" style="-fx-background-color: #34495E; -fx-cursor: hand; -fx-background-radius: 8;" text="Remove Selected" textFill="WHITE">
<font>
<Font name="System Bold" size="13.0" />
</font>
<padding>
<Insets bottom="8.0" left="18.0" right="18.0" top="8.0" />
</padding>
</Button>
</children>
</HBox>
<TableView fx:id="tvRefundItems" prefHeight="150.0" style="-fx-background-color: white; -fx-background-radius: 10;">
<columns>
<TableColumn fx:id="colRefundProduct" prefWidth="350.0" text="Product Name" />
<TableColumn fx:id="colRefundQuantity" prefWidth="120.0" text="Quantity" />
<TableColumn fx:id="colRefundUnitPrice" prefWidth="150.0" text="Unit Price" />
<TableColumn fx:id="colRefundTotal" prefWidth="150.0" text="Total" />
</columns>
</TableView>
</children>
</VBox>
<VBox spacing="12.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;">
<padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</padding>
<children>
<HBox alignment="CENTER_LEFT" spacing="15.0">
<children>
<Label text="Payment Method:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<ComboBox fx:id="cbPaymentMethod" prefWidth="160.0" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
<padding>
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding>
</ComboBox>
<Region HBox.hgrow="ALWAYS" />
<Label text="Refund Total:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<Label fx:id="lblRefundTotal" text="\$0.00" textFill="#FF6b6b">
<font>
<Font name="System Bold" size="18.0" />
</font>
</Label>
<Region prefWidth="20.0" />
<Button fx:id="btnProcessRefund" mnemonicParsing="false" onAction="#btnProcessRefundClicked" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
</children>
</HBox>
</children>
</VBox>
</children>
</VBox>

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 512 512" role="img" aria-label="Leon's Pet Store logo">
<defs>
<style>
.ring { fill: none; stroke: #A83B2B; stroke-width: 18; }
</style>
</defs>
<circle class="ring" cx="256" cy="256" r="236"/>
<g>
<path d="M 174.96 419.66 C 166.94 420.40 157.23 419.87 150.27 417.76 C 143.30 415.66 137.45 410.43 133.17 407.00 C 128.90 403.57 127.42 402.52 124.63 397.19 C 121.83 391.86 117.34 384.53 116.40 375.03 C 115.45 365.53 117.03 349.39 118.93 340.21 C 120.83 331.03 123.36 327.65 127.79 319.95 C 132.22 312.24 132.49 307.76 145.52 293.99 C 158.55 280.22 190.84 249.93 205.98 237.32 C 221.13 224.71 224.98 222.13 236.37 218.33 C 247.77 214.53 264.76 214.32 274.36 214.53 C 283.97 214.74 287.76 217.06 293.99 219.60 C 300.21 222.13 297.00 218.38 311.72 229.73 C 326.44 241.07 368.43 275.36 382.31 287.66 C 396.19 299.95 391.17 297.36 394.97 303.48 C 398.77 309.61 402.57 317.20 405.10 324.38 C 407.63 331.55 409.64 338.52 410.17 346.54 C 410.69 354.56 410.59 364.79 408.27 372.50 C 405.94 380.20 401.78 387.01 396.24 392.76 C 390.70 398.50 381.41 403.99 375.03 407.00 C 368.65 410.01 363.32 410.48 357.93 410.80 C 352.55 411.12 352.44 412.07 342.74 408.90 C 333.03 405.74 313.93 395.29 299.69 391.81 C 285.44 388.32 268.03 387.69 257.27 388.01 C 246.50 388.32 244.92 389.48 235.11 393.71 C 225.29 397.93 208.41 409.01 198.39 413.33 C 188.36 417.66 182.98 418.92 174.96 419.66 Z" fill="#E75D40" fill-rule="evenodd"/>
<path d="M 205.98 202.50 C 201.44 203.56 200.28 203.66 195.22 201.87 C 190.15 200.08 181.56 196.85 175.59 191.74 C 169.63 186.62 163.09 177.44 159.45 171.16 C 155.81 164.88 154.17 162.09 153.75 154.07 C 153.33 146.04 154.12 131.12 156.92 123.04 C 159.71 114.97 166.04 109.70 170.53 105.63 C 175.01 101.57 179.39 99.93 183.82 98.67 C 188.26 97.40 191.74 96.77 197.12 98.03 C 202.50 99.30 210.68 102.31 216.11 106.26 C 221.55 110.22 226.08 116.03 229.73 121.78 C 233.37 127.53 236.37 134.65 237.96 140.77 C 239.54 146.89 240.06 151.53 239.22 158.50 C 238.38 165.46 235.69 176.38 232.89 182.56 C 230.09 188.73 226.93 192.21 222.44 195.54 C 217.96 198.86 210.52 201.44 205.98 202.50 Z" fill="#E75D40" fill-rule="evenodd"/>
<path d="M 314.25 195.54 C 307.92 197.44 301.11 196.85 295.25 194.90 C 289.40 192.95 283.28 187.89 279.11 183.82 C 274.94 179.76 272.35 177.18 270.25 170.53 C 268.14 163.88 266.45 151.32 266.45 143.94 C 266.45 136.55 267.92 132.43 270.25 126.21 C 272.57 119.98 276.21 111.70 280.38 106.58 C 284.54 101.47 290.45 97.88 295.25 95.50 C 300.05 93.13 303.80 92.23 309.18 92.34 C 314.56 92.44 322.59 94.03 327.54 96.13 C 332.50 98.24 335.19 99.25 338.94 105.00 C 342.69 110.75 348.28 122.36 350.02 130.64 C 351.76 138.92 350.86 147.63 349.39 154.70 C 347.91 161.77 343.85 168.26 341.16 173.06 C 338.47 177.86 337.72 179.76 333.24 183.51 C 328.76 187.25 320.58 193.64 314.25 195.54 Z" fill="#E75D40" fill-rule="evenodd"/>
<path d="M 126.84 268.98 C 122.30 269.72 118.29 269.40 113.55 268.35 C 108.80 267.29 103.37 265.76 98.35 262.65 C 93.34 259.53 87.95 255.42 83.47 249.67 C 78.99 243.92 73.55 236.69 71.44 228.14 C 69.33 219.60 70.07 205.03 70.81 198.39 C 71.55 191.74 71.92 192.32 75.87 188.26 C 79.83 184.19 88.70 176.59 94.55 174.01 C 100.41 171.43 105.11 171.69 111.01 172.74 C 116.92 173.80 123.09 174.69 130.01 180.34 C 136.92 185.99 148.00 198.86 152.48 206.62 C 156.97 214.37 156.81 219.80 156.92 226.88 C 157.02 233.95 155.81 242.86 153.12 249.04 C 150.43 255.21 145.15 260.59 140.77 263.91 C 136.39 267.24 131.38 268.24 126.84 268.98 Z" fill="#E75D40" fill-rule="evenodd"/>
<path d="M 396.55 256.32 C 389.06 257.79 380.88 255.63 375.66 253.78 C 370.44 251.94 368.33 249.51 365.21 245.24 C 362.10 240.96 358.36 234.68 356.98 228.14 C 355.61 221.60 355.83 212.74 356.98 205.98 C 358.14 199.23 360.42 193.59 363.95 187.62 C 367.48 181.66 372.65 174.80 378.19 170.21 C 383.73 165.62 391.17 161.77 397.19 160.08 C 403.20 158.39 407.90 157.60 414.28 160.08 C 420.66 162.56 430.79 169.42 435.49 174.96 C 440.19 180.50 441.51 186.99 442.46 193.32 C 443.41 199.65 443.20 205.98 441.19 212.95 C 439.18 219.91 433.86 229.78 430.43 235.11 C 427.00 240.44 426.26 241.39 420.61 244.92 C 414.97 248.45 404.04 254.84 396.55 256.32 Z" fill="#E75D40" fill-rule="evenodd"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -5,6 +5,8 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
@@ -17,14 +19,14 @@
<Insets bottom="40.0" left="50.0" right="50.0" top="40.0" />
</padding>
<children>
<Label text="Pet Shop Manager" textFill="WHITE">
<font>
<Font name="System Bold" size="22.0" />
</font>
<ImageView fitHeight="180.0" fitWidth="320.0" preserveRatio="true">
<image>
<Image url="@images/leons-pet-store-badge-text.png" />
</image>
<VBox.margin>
<Insets bottom="10.0" />
<Insets bottom="35.0" />
</VBox.margin>
</Label>
</ImageView>
<Label text="Username" textFill="#cccccc">
<font>

View File

@@ -5,7 +5,10 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
@@ -19,6 +22,16 @@
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</padding>
<children>
<HBox alignment="CENTER">
<ImageView fx:id="imgLogo" fitHeight="90.0" fitWidth="90.0" preserveRatio="true" onMouseClicked="#logoClicked" style="-fx-cursor: hand;">
<image>
<Image url="@images/leons-pet-store-badge.png" />
</image>
</ImageView>
<VBox.margin>
<Insets bottom="15.0" />
</VBox.margin>
</HBox>
<Label fx:id="lblUsername" text="Name" textFill="WHITE">
<font>
<Font name="System Bold" size="18.0" />

View File

@@ -38,6 +38,14 @@
</padding>
</Label>
<Region prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
<Button fx:id="btnRefund" mnemonicParsing="false" onAction="#btnRefund" prefHeight="44.0" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefresh" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
@@ -145,14 +153,14 @@
</HBox>
<TableView fx:id="tvSales" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colSaleId" prefWidth="50.0" text="ID" />
<TableColumn fx:id="colSaleDate" prefWidth="150.0" text="Date" />
<TableColumn fx:id="colEmployeeName" prefWidth="180.0" text="Employee" />
<TableColumn fx:id="colServiceProduct" prefWidth="170.0" text="Product" />
<TableColumn fx:id="colSaleQuantity" prefWidth="80.0" text="Qty" />
<TableColumn fx:id="colSaleUnitPrice" prefWidth="100.0" text="Unit Price" />
<TableColumn fx:id="colSaleTotal" prefWidth="100.0" text="Total" />
<TableColumn fx:id="colSalePaymentType" prefWidth="120.0" text="Payment" />
<TableColumn fx:id="colSaleId" prefWidth="40.0" text="ID" />
<TableColumn fx:id="colSaleDate" prefWidth="130.0" text="Date" />
<TableColumn fx:id="colEmployeeName" prefWidth="140.0" text="Employee" />
<TableColumn fx:id="colServiceProduct" prefWidth="150.0" text="Product" />
<TableColumn fx:id="colSaleQuantity" prefWidth="50.0" text="Qty" />
<TableColumn fx:id="colSaleUnitPrice" prefWidth="90.0" text="Unit Price" />
<TableColumn fx:id="colSaleTotal" prefWidth="90.0" text="Total" />
<TableColumn fx:id="colSalePaymentType" prefWidth="90.0" text="Payment" />
</columns>
</TableView>
</children>