diff --git a/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index a54139a2..753dbd81 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -1,11 +1,37 @@ package org.example.petshopdesktop.controllers; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.VBox; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.database.InventoryDB; +import org.example.petshopdesktop.database.ProductDB; +import org.example.petshopdesktop.database.SaleDB; +import org.example.petshopdesktop.models.Inventory; +import org.example.petshopdesktop.models.Product; +import org.example.petshopdesktop.models.SaleCartItem; +import org.example.petshopdesktop.models.SaleLineItem; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.sql.SQLException; +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; public class SaleController { @@ -13,38 +39,324 @@ public class SaleController { private Button btnRefresh; @FXML - private TableColumn colCustomerName; + private Label lblModeNote; @FXML - private TableColumn colSaleDate; + private VBox vbCreateSale; @FXML - private TableColumn colSaleId; + private ComboBox cbProduct; @FXML - private TableColumn colSalePaymentType; + private Spinner spQuantity; @FXML - private TableColumn colSaleQuantity; + private Button btnAddToCart; @FXML - private TableColumn colSaleTotal; + private Button btnRemoveSelected; @FXML - private TableColumn colSaleUnitPrice; + private TableView tvCart; @FXML - private TableColumn colServiceProduct; + private TableColumn colCartProduct; @FXML - private TableView tvSales; + private TableColumn colCartQty; + + @FXML + private TableColumn colCartUnitPrice; + + @FXML + private TableColumn colCartTotal; + + @FXML + private ComboBox cbPaymentMethod; + + @FXML + private Label lblCartTotal; + + @FXML + private Button btnClearCart; + + @FXML + private Button btnSaveSale; + + @FXML + private TableColumn colSaleId; + + @FXML + private TableColumn colSaleDate; + + @FXML + private TableColumn colEmployeeName; + + @FXML + private TableColumn colServiceProduct; + + @FXML + private TableColumn colSaleQuantity; + + @FXML + private TableColumn colSaleUnitPrice; + + @FXML + private TableColumn colSaleTotal; + + @FXML + private TableColumn colSalePaymentType; + + @FXML + private TableView tvSales; @FXML private TextField txtSearch; - @FXML - void btnRefresh(ActionEvent event) { + private final ObservableList cartItems = FXCollections.observableArrayList(); + private final ObservableList saleItems = FXCollections.observableArrayList(); + private FilteredList filteredSales; + private final Map inventoryByProdId = new HashMap<>(); + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + + @FXML + public void initialize() { + setupTables(); + setupCreateSale(); + applyRoleMode(); + + refreshInventory(); + refreshSales(); } + private void setupTables() { + colCartProduct.setCellValueFactory(new PropertyValueFactory<>("prodName")); + colCartQty.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colCartUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colCartTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvCart.setItems(cartItems); + tvCart.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId")); + colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); + colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName")); + colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod")); + + filteredSales = new FilteredList<>(saleItems, s -> true); + tvSales.setItems(filteredSales); + + txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal)); + } + + private void setupCreateSale() { + spQuantity.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 999, 1)); + spQuantity.setEditable(true); + + cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card")); + cbPaymentMethod.getSelectionModel().selectFirst(); + + updateCartTotal(); + + try { + cbProduct.setItems(ProductDB.getProducts()); + } catch (SQLException e) { + ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Loading products"); + } catch (RuntimeException e) { + ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Database connection"); + } + } + + private void applyRoleMode() { + boolean isAdmin = UserSession.getInstance().isAdmin(); + vbCreateSale.setVisible(!isAdmin); + vbCreateSale.setManaged(!isAdmin); + lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)"); + } + + private void refreshInventory() { + inventoryByProdId.clear(); + try { + for (Inventory inv : InventoryDB.getInventory()) { + inventoryByProdId.put(inv.getProdId(), inv.getQuantity()); + } + } catch (SQLException e) { + ActivityLogger.getInstance().logException("SaleController.refreshInventory", e, "Loading inventory"); + } catch (RuntimeException e) { + ActivityLogger.getInstance().logException("SaleController.refreshInventory", e, "Database connection"); + } + } + + private void refreshSales() { + refreshSales(false); + } + + private void refreshSales(boolean showErrorDialog) { + try { + saleItems.setAll(SaleDB.getSaleLineItems()); + } catch (SQLException e) { + ActivityLogger.getInstance().logException("SaleController.refreshSales", e, "Loading sales"); + if (showErrorDialog) { + showError("Sales", "Could not load sales."); + } + } catch (RuntimeException e) { + ActivityLogger.getInstance().logException("SaleController.refreshSales", e, "Database connection"); + if (showErrorDialog) { + showError("Sales", "Database is not connected."); + } + } + } + + @FXML + void btnRefresh(ActionEvent event) { + refreshInventory(); + refreshSales(true); + } + + @FXML + void btnAddToCart(ActionEvent event) { + Product product = cbProduct.getSelectionModel().getSelectedItem(); + if (product == null) { + showError("Create Sale", "Select a product."); + return; + } + + int requestedQty; + try { + requestedQty = spQuantity.getValue(); + } catch (Exception e) { + showError("Create Sale", "Enter a valid quantity."); + return; + } + if (requestedQty <= 0) { + showError("Create Sale", "Quantity must be at least 1."); + return; + } + + int stock = inventoryByProdId.getOrDefault(product.getProdId(), 0); + int alreadyInCart = cartItems.stream() + .filter(i -> i.getProdId() == product.getProdId()) + .mapToInt(SaleCartItem::getQuantity) + .sum(); + + int available = stock - alreadyInCart; + if (requestedQty > available) { + showError("Create Sale", "Not enough stock. Available: " + Math.max(0, available)); + return; + } + + for (SaleCartItem item : cartItems) { + if (item.getProdId() == product.getProdId()) { + item.setQuantity(item.getQuantity() + requestedQty); + tvCart.refresh(); + updateCartTotal(); + return; + } + } + + cartItems.add(new SaleCartItem(product.getProdId(), product.getProdName(), requestedQty, product.getProdPrice())); + updateCartTotal(); + } + + @FXML + void btnRemoveSelected(ActionEvent event) { + SaleCartItem selected = tvCart.getSelectionModel().getSelectedItem(); + if (selected != null) { + cartItems.remove(selected); + updateCartTotal(); + } + } + + @FXML + void btnClearCart(ActionEvent event) { + cartItems.clear(); + updateCartTotal(); + } + + @FXML + void btnSaveSale(ActionEvent event) { + if (UserSession.getInstance().isAdmin()) { + showError("Create Sale", "This action is restricted to staff."); + return; + } + + Integer employeeId = UserSession.getInstance().getEmployeeId(); + if (employeeId == null || employeeId <= 0) { + showError("Create Sale", "Employee is not set for this account."); + return; + } + + if (cartItems.isEmpty()) { + showError("Create Sale", "Add at least one item."); + return; + } + + String payment = cbPaymentMethod.getSelectionModel().getSelectedItem(); + if (payment == null || payment.isBlank()) { + showError("Create Sale", "Select a payment method."); + return; + } + + try { + int saleId = SaleDB.createSale(employeeId, payment, cartItems); + showInfo("Sale saved", "Sale ID " + saleId + " was created."); + + cartItems.clear(); + updateCartTotal(); + + refreshInventory(); + refreshSales(true); + } catch (SQLException e) { + ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Creating sale"); + showError("Create Sale", e.getMessage() == null ? "Could not save the sale." : e.getMessage()); + } catch (RuntimeException e) { + ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Database connection"); + showError("Create Sale", "Database is not connected."); + } + } + + private void updateCartTotal() { + double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); + lblCartTotal.setText(currency.format(total)); + } + + private void applySalesFilter(String filter) { + String f = filter == null ? "" : filter.trim().toLowerCase(); + if (f.isEmpty()) { + filteredSales.setPredicate(s -> true); + return; + } + + filteredSales.setPredicate(s -> + String.valueOf(s.getSaleId()).contains(f) + || safe(s.getSaleDate()).contains(f) + || safe(s.getEmployeeName()).contains(f) + || safe(s.getItemName()).contains(f) + || safe(s.getPaymentMethod()).contains(f) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } + + 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(); + } + + private void showInfo(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } }