From a860a1c2478bf62f54b7b02ce887f25dbc19d0d8 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:32:29 -0600 Subject: [PATCH] updated sales on desktop, and fixed sales with points again on back end --- .../petshop/backend/dto/sale/SaleRequest.java | 16 +- .../petshop/backend/service/SaleService.java | 56 ++- .../api/dto/sale/SaleRequest.java | 27 ++ .../api/dto/sale/SaleResponse.java | 63 ++++ .../api/dto/user/UserRequest.java | 18 + .../api/endpoints/CouponApi.java | 4 + .../api/endpoints/CustomerApi.java | 4 + .../api/endpoints/DropdownApi.java | 8 + .../controllers/SaleController.java | 340 +++++++++++++++++- .../AdoptionDialogController.java | 191 +++++++--- .../AppointmentDialogController.java | 22 +- .../CustomerEditDialogController.java | 2 + .../RefundDialogController.java | 80 ++++- .../SaleDetailDialogController.java | 40 +++ .../petshopdesktop/models/SaleDetail.java | 26 +- .../petshopdesktop/models/SaleLineItem.java | 8 +- .../customer-edit-dialog-view.fxml | 2 +- .../dialogviews/refund-dialog-view.fxml | 38 +- .../dialogviews/sale-detail-dialog-view.fxml | 38 +- .../petshopdesktop/modelviews/sale-view.fxml | 61 +++- 20 files changed, 943 insertions(+), 101 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java index 9faba27f..db9eb6cd 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -28,6 +28,8 @@ public class SaleRequest { private Long cartId; + private Integer pointsUsed; + public Long getStoreId() { return storeId; @@ -101,6 +103,14 @@ public class SaleRequest { this.cartId = cartId; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + @Override public boolean equals(Object o) { @@ -115,12 +125,13 @@ public class SaleRequest { Objects.equals(customerId, that.customerId) && Objects.equals(channel, that.channel) && Objects.equals(couponId, that.couponId) && - Objects.equals(cartId, that.cartId); + Objects.equals(cartId, that.cartId) && + Objects.equals(pointsUsed, that.pointsUsed); } @Override public int hashCode() { - return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId); + return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId, pointsUsed); } @Override @@ -135,6 +146,7 @@ public class SaleRequest { ", channel='" + channel + '\'' + ", couponId=" + couponId + ", cartId=" + cartId + + ", pointsUsed=" + pointsUsed + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b7c9c498..dde5dc43 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -162,10 +162,41 @@ public class SaleService { } subtotalAmount = subtotalAmount.negate(); sale.setSubtotalAmount(subtotalAmount); - sale.setTotalAmount(subtotalAmount); - sale.setCouponDiscountAmount(BigDecimal.ZERO); + + Sale originalSale = sale.getOriginalSale(); + BigDecimal originalSubtotal = originalSale.getSubtotalAmount(); + BigDecimal loyaltyDiscountRefunded = BigDecimal.ZERO; + BigDecimal couponDiscountRefunded = BigDecimal.ZERO; + BigDecimal refundTotal; + + if (originalSubtotal != null && originalSubtotal.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal ratio = subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP); + refundTotal = originalSale.getTotalAmount().abs().multiply(ratio).negate().setScale(2, RoundingMode.HALF_UP); + if (originalSale.getLoyaltyDiscountAmount() != null) { + loyaltyDiscountRefunded = originalSale.getLoyaltyDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP); + } + if (originalSale.getCouponDiscountAmount() != null) { + couponDiscountRefunded = originalSale.getCouponDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP); + } + User refundCustomer = originalSale.getCustomer(); + if (refundCustomer != null) { + sale.setCustomer(refundCustomer); + int pointsToRestore = toPointsUsed(loyaltyDiscountRefunded); + int pointsEarnedToReverse = originalSale.getPointsEarned() != null + ? ratio.multiply(BigDecimal.valueOf(originalSale.getPointsEarned())).setScale(0, RoundingMode.FLOOR).intValue() + : 0; + int currentPoints = refundCustomer.getLoyaltyPoints() != null ? refundCustomer.getLoyaltyPoints() : 0; + refundCustomer.setLoyaltyPoints(Math.max(0, currentPoints + pointsToRestore - pointsEarnedToReverse)); + userRepository.save(refundCustomer); + } + } else { + refundTotal = subtotalAmount.negate(); + } + + sale.setTotalAmount(refundTotal); + sale.setCouponDiscountAmount(couponDiscountRefunded); sale.setEmployeeDiscountAmount(BigDecimal.ZERO); - sale.setLoyaltyDiscountAmount(BigDecimal.ZERO); + sale.setLoyaltyDiscountAmount(loyaltyDiscountRefunded); sale.setPointsEarned(0); } else { if (request.getItems() == null || request.getItems().isEmpty()) { @@ -206,18 +237,29 @@ public class SaleService { BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); sale.setEmployeeDiscountAmount(employeeDiscount); - boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied()); - BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), useLoyaltyPoints); + BigDecimal remainingAfterDiscounts = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount); + BigDecimal loyaltyDiscount; + int pointsDeducted; + if (request.getPointsUsed() != null && request.getPointsUsed() > 0) { + loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed()) + .divide(BigDecimal.valueOf(LOYALTY_POINTS_PER_DOLLAR), 2, RoundingMode.HALF_UP) + .min(remainingAfterDiscounts.max(BigDecimal.ZERO)) + .setScale(2, RoundingMode.HALF_UP); + pointsDeducted = request.getPointsUsed(); + } else { + boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied()); + loyaltyDiscount = calculateLoyaltyDiscount(customer, remainingAfterDiscounts, useLoyaltyPoints); + pointsDeducted = toPointsUsed(loyaltyDiscount); + } sale.setLoyaltyDiscountAmount(loyaltyDiscount); BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); - int pointsUsed = toPointsUsed(loyaltyDiscount); sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); if (customer != null) { int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0; - int updatedPoints = currentPoints - pointsUsed + sale.getPointsEarned(); + int updatedPoints = currentPoints - pointsDeducted + sale.getPointsEarned(); customer.setLoyaltyPoints(Math.max(updatedPoints, 0)); userRepository.save(customer); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java index 991f7810..fe324bb5 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java @@ -8,6 +8,9 @@ public class SaleRequest { private List items; private Boolean isRefund; private Long originalSaleId; + private Long customerId; + private Long couponId; + private Integer pointsUsed; public SaleRequest() { } @@ -51,4 +54,28 @@ public class SaleRequest { public void setOriginalSaleId(Long originalSaleId) { this.originalSaleId = originalSaleId; } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getCouponId() { + return couponId; + } + + public void setCouponId(Long couponId) { + this.couponId = couponId; + } + + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java index 7ee8eb45..f814cc17 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java @@ -14,6 +14,13 @@ public class SaleResponse { private Boolean isRefund; private Long originalSaleId; private List items; + private Long customerId; + private String customerName; + private BigDecimal couponDiscountAmount; + private BigDecimal loyaltyDiscountAmount; + private Integer pointsUsed; + private Integer pointsEarned; + private BigDecimal subtotalAmount; public SaleResponse() { } @@ -89,4 +96,60 @@ public class SaleResponse { public void setItems(List items) { this.items = items; } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public BigDecimal getCouponDiscountAmount() { + return couponDiscountAmount; + } + + public void setCouponDiscountAmount(BigDecimal couponDiscountAmount) { + this.couponDiscountAmount = couponDiscountAmount; + } + + public BigDecimal getLoyaltyDiscountAmount() { + return loyaltyDiscountAmount; + } + + public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) { + this.loyaltyDiscountAmount = loyaltyDiscountAmount; + } + + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + + public Integer getPointsEarned() { + return pointsEarned; + } + + public void setPointsEarned(Integer pointsEarned) { + this.pointsEarned = pointsEarned; + } + + public BigDecimal getSubtotalAmount() { + return subtotalAmount; + } + + public void setSubtotalAmount(BigDecimal subtotalAmount) { + this.subtotalAmount = subtotalAmount; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java index 28eab2cf..e4dd940d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java @@ -3,6 +3,8 @@ package org.example.petshopdesktop.api.dto.user; public class UserRequest { private String username; private String password; + private String firstName; + private String lastName; private String fullName; private String email; private String phone; @@ -29,6 +31,22 @@ public class UserRequest { this.password = password; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return fullName; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CouponApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CouponApi.java index b409b5d1..a08e0016 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CouponApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CouponApi.java @@ -39,4 +39,8 @@ public class CouponApi { public void deleteCoupon(Long id) throws Exception { apiClient.delete("/api/v1/coupons/" + id); } + + public CouponResponse getCouponByCode(String code) throws Exception { + return apiClient.get("/api/v1/coupons/code/" + code, CouponResponse.class); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CustomerApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CustomerApi.java index 9f286f6b..54f89f66 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CustomerApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/CustomerApi.java @@ -45,4 +45,8 @@ public class CustomerApi { public UserResponse createCustomer(UserRequest request) throws Exception { return apiClient.post("/api/v1/customers", request, UserResponse.class); } + + public UserResponse getCustomerById(Long id) throws Exception { + return apiClient.get("/api/v1/customers/" + id, UserResponse.class); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index 75c0c297..4f0d34b9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -107,6 +107,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getAdoptionPets(Long storeId) throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/adoption-pets?storeId=" + storeId); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from adoption pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getCustomerPets(Long customerId) throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets"); if (response == null || response.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 9c7c6225..cdf014cf 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -10,6 +10,7 @@ import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; @@ -19,18 +20,25 @@ 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.HBox; import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.auth.UserSession; import javafx.concurrent.Task; +import org.example.petshopdesktop.api.endpoints.CustomerApi; +import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.ProductApi; import org.example.petshopdesktop.api.endpoints.SaleApi; +import org.example.petshopdesktop.api.endpoints.CouponApi; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.dto.coupon.CouponResponse; import org.example.petshopdesktop.api.dto.product.ProductResponse; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; import org.example.petshopdesktop.api.dto.sale.SaleRequest; import org.example.petshopdesktop.api.dto.sale.SaleResponse; +import org.example.petshopdesktop.api.dto.user.UserResponse; import org.example.petshopdesktop.models.Product; import org.example.petshopdesktop.models.SaleCartItem; import org.example.petshopdesktop.models.SaleDetail; @@ -111,6 +119,9 @@ public class SaleController { @FXML private TableColumn colEmployeeName; + @FXML + private TableColumn colCustomerName; + @FXML private TableColumn colServiceProduct; @@ -135,11 +146,53 @@ public class SaleController { @FXML private TextField txtSearch; + @FXML + private ComboBox cbCustomer; + + @FXML + private CheckBox chkUseLoyaltyPoints; + + @FXML + private Label lblLoyaltyPoints; + + @FXML + private TextField txtCouponCode; + + @FXML + private Button btnApplyCoupon; + + @FXML + private Button btnClearCoupon; + + @FXML + private Label lblCouponStatus; + + @FXML + private Label lblSubtotal; + + @FXML + private HBox hbCouponDiscount; + + @FXML + private Label lblCouponDiscount; + + @FXML + private HBox hbLoyaltyDiscount; + + @FXML + private Label lblLoyaltyDiscountLabel; + + @FXML + private Label lblLoyaltyDiscount; + private final ObservableList cartItems = FXCollections.observableArrayList(); private final ObservableList saleItems = FXCollections.observableArrayList(); private FilteredList filteredSales; private boolean saleSaveInProgress; + private CouponResponse appliedCoupon = null; + private UserResponse selectedCustomerData = null; + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @@ -173,6 +226,8 @@ public class SaleController { colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId")); colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate")); colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); + colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colCustomerName.setMinWidth(120); colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName")); colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); @@ -214,7 +269,23 @@ public class SaleController { setCreateSaleControlsDisabled(true); - Task> task = new Task<>() { + cbCustomer.valueProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null) { + loadCustomerDetails(newVal.getId()); + } else { + selectedCustomerData = null; + lblLoyaltyPoints.setVisible(false); + lblLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setVisible(false); + chkUseLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setSelected(false); + updateCartTotal(); + } + }); + + chkUseLoyaltyPoints.selectedProperty().addListener((obs, oldVal, newVal) -> updateCartTotal()); + + Task> productsTask = new Task<>() { @Override protected ObservableList call() throws Exception { List productResponses = ProductApi.getInstance().listProducts(null); @@ -232,13 +303,13 @@ public class SaleController { } }; - task.setOnSucceeded(event -> { - cbProduct.setItems(task.getValue()); + productsTask.setOnSucceeded(event -> { + cbProduct.setItems(productsTask.getValue()); setCreateSaleControlsDisabled(false); }); - task.setOnFailed(event -> { - Throwable e = task.getException(); + productsTask.setOnFailed(event -> { + Throwable e = productsTask.getException(); ActivityLogger.getInstance().logException( "SaleController.setupCreateSale", e instanceof Exception ? (Exception) e : new RuntimeException(e), @@ -248,6 +319,70 @@ public class SaleController { showError("Sales", "Could not load products. Check the backend connection and refresh the view."); }); + new Thread(productsTask).start(); + + Task> customersTask = new Task<>() { + @Override + protected List call() throws Exception { + return DropdownApi.getInstance().getCustomers(); + } + }; + + customersTask.setOnSucceeded(event -> { + List customers = customersTask.getValue(); + ObservableList customerOptions = FXCollections.observableArrayList(); + customerOptions.addAll(customers); + cbCustomer.setItems(customerOptions); + }); + + customersTask.setOnFailed(event -> { + Throwable e = customersTask.getException(); + ActivityLogger.getInstance().logException( + "SaleController.setupCreateSale", + e instanceof Exception ? (Exception) e : new RuntimeException(e), + "Loading customers" + ); + }); + + new Thread(customersTask).start(); + } + + private void loadCustomerDetails(Long customerId) { + Task task = new Task<>() { + @Override + protected UserResponse call() throws Exception { + return CustomerApi.getInstance().getCustomerById(customerId); + } + }; + + task.setOnSucceeded(event -> { + selectedCustomerData = task.getValue(); + if (selectedCustomerData != null && selectedCustomerData.getLoyaltyPoints() != null && selectedCustomerData.getLoyaltyPoints() >= 20) { + lblLoyaltyPoints.setText(selectedCustomerData.getLoyaltyPoints() + " pts available"); + lblLoyaltyPoints.setVisible(true); + lblLoyaltyPoints.setManaged(true); + chkUseLoyaltyPoints.setVisible(true); + chkUseLoyaltyPoints.setManaged(true); + } else { + lblLoyaltyPoints.setVisible(false); + lblLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setVisible(false); + chkUseLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setSelected(false); + } + updateCartTotal(); + }); + + task.setOnFailed(event -> { + selectedCustomerData = null; + Throwable e = task.getException(); + ActivityLogger.getInstance().logException( + "SaleController.loadCustomerDetails", + e instanceof Exception ? (Exception) e : new RuntimeException(e), + "Loading customer details" + ); + }); + new Thread(task).start(); } @@ -281,6 +416,9 @@ public class SaleController { : ""; if (sale.getItems() != null && !sale.getItems().isEmpty()) { + double saleSubtotal = sale.getSubtotalAmount() != null ? Math.abs(sale.getSubtotalAmount().doubleValue()) : 0; + double saleActualTotal = sale.getTotalAmount() != null ? Math.abs(sale.getTotalAmount().doubleValue()) : 0; + double discountRatio = saleSubtotal > 0 ? saleActualTotal / saleSubtotal : 1.0; for (SaleItemResponse item : sale.getItems()) { boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; @@ -288,7 +426,7 @@ public class SaleController { if (isRefund && quantity > 0) { quantity = -quantity; } - double lineTotal = unitPrice * quantity; + double lineTotal = unitPrice * quantity * discountRatio; lineItems.add(new SaleLineItem( sale.getSaleId().intValue(), saleDate, @@ -299,7 +437,8 @@ public class SaleController { lineTotal, sale.getPaymentMethod(), isRefund, - sale.getStoreName() + sale.getStoreName(), + sale.getCustomerName() )); } } @@ -379,6 +518,73 @@ public class SaleController { updateCartTotal(); } + @FXML + void btnApplyCoupon(ActionEvent event) { + String code = txtCouponCode.getText().trim(); + if (code.isEmpty()) { + lblCouponStatus.setText("Enter a coupon code."); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c")); + return; + } + + BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum()); + + Task task = new Task<>() { + @Override + protected CouponResponse call() throws Exception { + return CouponApi.getInstance().getCouponByCode(code); + } + }; + + task.setOnSucceeded(evt -> { + CouponResponse coupon = task.getValue(); + if (coupon == null) { + lblCouponStatus.setText("Coupon not found."); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c")); + return; + } + if (!Boolean.TRUE.equals(coupon.getActive())) { + lblCouponStatus.setText("Coupon is not active."); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c")); + return; + } + if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { + lblCouponStatus.setText("Order minimum not met ($" + coupon.getMinOrderAmount() + ")."); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c")); + return; + } + appliedCoupon = coupon; + txtCouponCode.setDisable(true); + lblCouponStatus.setText("Coupon applied: " + coupon.getCouponCode()); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#27ae60")); + updateCartTotal(); + }); + + task.setOnFailed(evt -> { + Throwable e = task.getException(); + lblCouponStatus.setText("Invalid or not found."); + lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c")); + ActivityLogger.getInstance().logException( + "SaleController.btnApplyCoupon", + e instanceof Exception ? (Exception) e : new RuntimeException(e), + "Applying coupon" + ); + }); + + new Thread(task).start(); + } + + @FXML + void btnClearCoupon(ActionEvent event) { + appliedCoupon = null; + txtCouponCode.clear(); + txtCouponCode.setDisable(false); + lblCouponStatus.setText(""); + hbCouponDiscount.setVisible(false); + hbCouponDiscount.setManaged(false); + updateCartTotal(); + } + @FXML void btnSaveSale(ActionEvent event) { if (saleSaveInProgress) { @@ -409,6 +615,7 @@ public class SaleController { SaleRequest request = new SaleRequest(); request.setStoreId(storeId); request.setPaymentMethod(payment); + request.setIsRefund(false); List itemRequests = new ArrayList<>(); for (SaleCartItem cartItem : cartItems) { @@ -419,6 +626,20 @@ public class SaleController { } request.setItems(itemRequests); + DropdownOption customerOption = cbCustomer.getValue(); + if (customerOption != null) { + request.setCustomerId(customerOption.getId()); + } + if (appliedCoupon != null) { + request.setCouponId(appliedCoupon.getCouponId()); + } + if (chkUseLoyaltyPoints.isSelected() && selectedCustomerData != null) { + BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum()); + BigDecimal couponDiscount = calculateCouponDiscount(subtotal); + BigDecimal subtotalAfterCoupon = subtotal.subtract(couponDiscount); + request.setPointsUsed(calculatePointsToUse(subtotalAfterCoupon)); + } + saleSaveInProgress = true; setCreateSaleControlsDisabled(true); btnRefund.setDisable(true); @@ -437,6 +658,21 @@ public class SaleController { SaleResponse response = task.getValue(); showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created."); cartItems.clear(); + appliedCoupon = null; + txtCouponCode.clear(); + txtCouponCode.setDisable(false); + lblCouponStatus.setText(""); + hbCouponDiscount.setVisible(false); + hbCouponDiscount.setManaged(false); + hbLoyaltyDiscount.setVisible(false); + hbLoyaltyDiscount.setManaged(false); + cbCustomer.setValue(null); + selectedCustomerData = null; + lblLoyaltyPoints.setVisible(false); + lblLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setVisible(false); + chkUseLoyaltyPoints.setManaged(false); + chkUseLoyaltyPoints.setSelected(false); updateCartTotal(); refreshSales(true); }); @@ -536,6 +772,9 @@ public class SaleController { private SaleDetail mapToSaleDetail(SaleResponse sale) { ObservableList items = FXCollections.observableArrayList(); + double saleSubtotal = sale.getSubtotalAmount() != null ? Math.abs(sale.getSubtotalAmount().doubleValue()) : 0; + double saleActualTotal = sale.getTotalAmount() != null ? Math.abs(sale.getTotalAmount().doubleValue()) : 0; + double discountRatio = saleSubtotal > 0 ? saleActualTotal / saleSubtotal : 1.0; if (sale.getItems() != null) { boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); for (SaleItemResponse item : sale.getItems()) { @@ -549,10 +788,13 @@ public class SaleController { item.getProductName(), quantity, unitPrice, - unitPrice * quantity + unitPrice * quantity * discountRatio )); } } + double subtotal = sale.getSubtotalAmount() != null ? sale.getSubtotalAmount().doubleValue() : 0.0; + double couponDiscount = sale.getCouponDiscountAmount() != null ? sale.getCouponDiscountAmount().doubleValue() : 0.0; + double loyaltyDiscount = sale.getLoyaltyDiscountAmount() != null ? sale.getLoyaltyDiscountAmount().doubleValue() : 0.0; return new SaleDetail( sale.getSaleId().intValue(), sale.getSaleDate(), @@ -560,18 +802,76 @@ public class SaleController { sale.getPaymentMethod(), sale.getEmployeeName(), Boolean.TRUE.equals(sale.getIsRefund()), - items + items, + sale.getCustomerName(), + subtotal, + couponDiscount, + loyaltyDiscount ); } private void updateCartTotal() { - double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); - lblCartTotal.setText(currency.format(total)); + BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum()); + BigDecimal couponDiscount = calculateCouponDiscount(subtotal); + BigDecimal subtotalAfterCoupon = subtotal.subtract(couponDiscount); + BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(subtotalAfterCoupon); + BigDecimal total = subtotalAfterCoupon.subtract(loyaltyDiscount); + + lblSubtotal.setText(currency.format(subtotal.doubleValue())); + + if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { + lblCouponDiscount.setText("-" + currency.format(couponDiscount.doubleValue())); + hbCouponDiscount.setVisible(true); + hbCouponDiscount.setManaged(true); + } else { + hbCouponDiscount.setVisible(false); + hbCouponDiscount.setManaged(false); + } + + if (loyaltyDiscount.compareTo(BigDecimal.ZERO) > 0) { + int pointsToUse = calculatePointsToUse(subtotalAfterCoupon); + lblLoyaltyDiscountLabel.setText("Loyalty Discount (" + pointsToUse + " pts):"); + lblLoyaltyDiscount.setText("-" + currency.format(loyaltyDiscount.doubleValue())); + hbLoyaltyDiscount.setVisible(true); + hbLoyaltyDiscount.setManaged(true); + } else { + hbLoyaltyDiscount.setVisible(false); + hbLoyaltyDiscount.setManaged(false); + } + + lblCartTotal.setText(currency.format(Math.max(0, total.doubleValue()))); + } + + private BigDecimal calculateCouponDiscount(BigDecimal subtotal) { + if (appliedCoupon == null) { + return BigDecimal.ZERO; + } + if ("PERCENTAGE".equalsIgnoreCase(appliedCoupon.getDiscountType())) { + return subtotal.multiply(appliedCoupon.getDiscountValue()).divide(BigDecimal.valueOf(100)); + } else { + return appliedCoupon.getDiscountValue().min(subtotal); + } + } + + private BigDecimal calculateLoyaltyDiscount(BigDecimal subtotalAfterCoupon) { + if (!chkUseLoyaltyPoints.isSelected() || selectedCustomerData == null) { + return BigDecimal.ZERO; + } + int pointsToUse = calculatePointsToUse(subtotalAfterCoupon); + return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05)); + } + + private int calculatePointsToUse(BigDecimal subtotalAfterCoupon) { + if (selectedCustomerData == null || selectedCustomerData.getLoyaltyPoints() == null) { + return 0; + } + int maxPointsNeeded = subtotalAfterCoupon.multiply(BigDecimal.valueOf(20)).intValue(); + return Math.min(selectedCustomerData.getLoyaltyPoints(), maxPointsNeeded); } private void updateSalesColumnWidths(double tableWidth) { double available = Math.max(tableWidth - 28.0, 0.0); - double baseWidth = 1255.0; + double baseWidth = 1395.0; if (available <= 0) { return; } @@ -580,6 +880,7 @@ public class SaleController { colSaleId.setPrefWidth(60.0); colSaleDate.setPrefWidth(170.0); colEmployeeName.setPrefWidth(160.0); + colCustomerName.setPrefWidth(140.0); colServiceProduct.setPrefWidth(320.0); colSaleQuantity.setPrefWidth(70.0); colSaleUnitPrice.setPrefWidth(115.0); @@ -591,14 +892,15 @@ public class SaleController { double extra = available - baseWidth; colSaleId.setPrefWidth(60.0); - colSaleDate.setPrefWidth(170.0 + extra * 0.16); - colEmployeeName.setPrefWidth(160.0 + extra * 0.16); - colServiceProduct.setPrefWidth(320.0 + extra * 0.38); + colSaleDate.setPrefWidth(170.0 + extra * 0.14); + colEmployeeName.setPrefWidth(160.0 + extra * 0.14); + colCustomerName.setPrefWidth(140.0 + extra * 0.12); + colServiceProduct.setPrefWidth(320.0 + extra * 0.34); colSaleQuantity.setPrefWidth(70.0); colSaleUnitPrice.setPrefWidth(115.0 + extra * 0.08); colSaleTotal.setPrefWidth(120.0 + extra * 0.08); colSalePaymentType.setPrefWidth(110.0 + extra * 0.06); - colStoreName.setPrefWidth(130.0 + extra * 0.08); + colStoreName.setPrefWidth(130.0 + extra * 0.04); } private void updateCartColumnWidths(double tableWidth) { @@ -631,6 +933,11 @@ public class SaleController { cbPaymentMethod.setDisable(disabled); btnClearCart.setDisable(disabled); btnSaveSale.setDisable(disabled); + cbCustomer.setDisable(disabled); + txtCouponCode.setDisable(disabled); + btnApplyCoupon.setDisable(disabled); + btnClearCoupon.setDisable(disabled); + chkUseLoyaltyPoints.setDisable(disabled); } private void applySalesFilter(String filter) { @@ -644,6 +951,7 @@ public class SaleController { String.valueOf(s.getSaleId()).contains(f) || safe(s.getSaleDate()).contains(f) || safe(s.getEmployeeName()).contains(f) + || safe(s.getCustomerName()).contains(f) || safe(s.getItemName()).contains(f) || safe(s.getPaymentMethod()).contains(f) ); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index f9041fb9..39947fcc 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -49,6 +49,7 @@ public class AdoptionDialogController { private Adoption selectedAdoption = null; private boolean suppressStatusListener = false; private Long pendingStoreId = null; + private boolean isEditing = false; private final ObservableList statusList = FXCollections.observableArrayList( "Pending", "Completed", "Missed", "Cancelled" @@ -63,7 +64,6 @@ public class AdoptionDialogController { } }); - cbEmployee.setPromptText("Select an employee"); txtAdoptionFee.setDisable(true); LocalDate today = LocalDate.now(); @@ -91,6 +91,12 @@ public class AdoptionDialogController { if (UserSession.getInstance().isAdmin()) { vbStore.setVisible(true); vbStore.setManaged(true); + cbPet.setDisable(true); + cbPet.setPromptText("Select a store first"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Select a store first"); + } else { + cbEmployee.setPromptText("Select an employee"); } loadDropdownsAsync(); @@ -126,49 +132,12 @@ public class AdoptionDialogController { } private void loadDropdownsAsync() { - new Thread(() -> { - try { - List pets = DropdownApi.getInstance().getAdoptionPets(); - Platform.runLater(() -> { - if (pets != null) { - ObservableList petsObs = FXCollections.observableArrayList(pets); - ensureSelectedPetOption(petsObs); - cbPet.setItems(petsObs); - applySelectedPet(); - } - }); - } catch (Exception e) { - Platform.runLater(() -> ActivityLogger.getInstance().logException( - "AdoptionDialogController.loadDropdownsAsync", e, "Loading pets")); - } - }).start(); - - new Thread(() -> { - try { - List employees = DropdownApi.getInstance().getEmployees(); - Platform.runLater(() -> { - ObservableList employeesObs = FXCollections.observableArrayList(employees); - ensureSelectedEmployeeOption(employeesObs); - cbEmployee.setItems(employeesObs); - applySelectedEmployee(); - }); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AdoptionDialogController.loadDropdownsAsync", e, "Loading employees"); - cbEmployee.setDisable(true); - cbEmployee.setPromptText("Unable to load employees"); - }); - } - }).start(); - new Thread(() -> { try { List customers = DropdownApi.getInstance().getCustomers(); Platform.runLater(() -> { if (customers != null) { - ObservableList customersObs = FXCollections.observableArrayList(customers); - cbCustomer.setItems(customersObs); + cbCustomer.setItems(FXCollections.observableArrayList(customers)); applySelectedCustomer(); } }); @@ -185,6 +154,44 @@ public class AdoptionDialogController { Platform.runLater(() -> { if (stores != null) { cbStore.setItems(FXCollections.observableArrayList(stores)); + cbStore.valueProperty().addListener((obs, oldVal, newVal) -> { + Long sid = newVal != null ? newVal.getId() : null; + cbEmployee.setValue(null); + cbEmployee.setItems(FXCollections.observableArrayList()); + if (sid != null) { + String status = cbAdoptionStatus.getValue(); + boolean locked = "Cancelled".equalsIgnoreCase(status) + || "Completed".equalsIgnoreCase(status) + || "Missed".equalsIgnoreCase(status); + if (!locked) { + cbEmployee.setDisable(false); + cbEmployee.setPromptText("Select an employee"); + } + loadEmployeesForStore(sid); + if (!isEditing) { + cbPet.setValue(null); + cbPet.setItems(FXCollections.observableArrayList()); + cbPet.setDisable(true); + cbPet.setPromptText("Select a pet"); + loadAdoptionPetsForStore(sid); + } + } else { + String status = cbAdoptionStatus.getValue(); + boolean locked = "Cancelled".equalsIgnoreCase(status) + || "Completed".equalsIgnoreCase(status) + || "Missed".equalsIgnoreCase(status); + if (!locked) { + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Select a store first"); + } + if (!isEditing) { + cbPet.setValue(null); + cbPet.setItems(FXCollections.observableArrayList()); + cbPet.setDisable(true); + cbPet.setPromptText("Select a store first"); + } + } + }); if (pendingStoreId != null) { for (DropdownOption store : cbStore.getItems()) { if (pendingStoreId.equals(store.getId())) { @@ -201,9 +208,98 @@ public class AdoptionDialogController { "AdoptionDialogController.loadDropdownsAsync", e, "Loading stores")); } }).start(); + } else { + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId != null && storeId > 0) { + new Thread(() -> { + try { + List employees = DropdownApi.getInstance().getStoreEmployees(storeId); + Platform.runLater(() -> { + ObservableList employeesObs = FXCollections.observableArrayList(employees != null ? employees : List.of()); + ensureSelectedEmployeeOption(employeesObs); + cbEmployee.setItems(employeesObs); + applySelectedEmployee(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.loadDropdownsAsync", e, "Loading employees"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + + new Thread(() -> { + try { + List pets = DropdownApi.getInstance().getAdoptionPets(storeId); + Platform.runLater(() -> { + if (!isEditing && pets != null) { + ObservableList petsObs = FXCollections.observableArrayList(pets); + cbPet.setItems(petsObs); + if (petsObs.isEmpty()) { + cbPet.setDisable(true); + cbPet.setPromptText("No available pets for this store"); + } + } + }); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "AdoptionDialogController.loadDropdownsAsync", e, "Loading pets")); + } + }).start(); + } } } + private void loadEmployeesForStore(Long storeId) { + new Thread(() -> { + try { + List employees = DropdownApi.getInstance().getStoreEmployees(storeId); + Platform.runLater(() -> { + ObservableList employeesObs = FXCollections.observableArrayList(employees != null ? employees : List.of()); + ensureSelectedEmployeeOption(employeesObs); + cbEmployee.setItems(employeesObs); + applySelectedEmployee(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.loadEmployeesForStore", e, "Loading employees for store"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + } + + private void loadAdoptionPetsForStore(Long storeId) { + new Thread(() -> { + try { + List pets = DropdownApi.getInstance().getAdoptionPets(storeId); + Platform.runLater(() -> { + if (pets != null) { + ObservableList petsObs = FXCollections.observableArrayList(pets); + cbPet.setItems(petsObs); + if (petsObs.isEmpty()) { + cbPet.setDisable(true); + cbPet.setPromptText("No available pets for this store"); + } else { + cbPet.setDisable(false); + } + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.loadAdoptionPetsForStore", e, "Loading adoption pets for store"); + cbPet.setDisable(true); + cbPet.setPromptText("Unable to load pets"); + }); + } + }).start(); + } + private void applyStatusFieldRules(String status) { if ("Cancelled".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status) || "Missed".equalsIgnoreCase(status)) { cbEmployee.setDisable(true); @@ -212,7 +308,7 @@ public class AdoptionDialogController { } else { cbEmployee.setDisable(false); dpAdoptionDate.setDisable(false); - if (UserSession.getInstance().isAdmin()) cbStore.setDisable(false); + if (UserSession.getInstance().isAdmin() && !isEditing) cbStore.setDisable(false); } } @@ -302,10 +398,19 @@ public class AdoptionDialogController { public void displayAdoptionDetails(Adoption adoption) { if (adoption == null) return; selectedAdoption = adoption; + isEditing = true; lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); pendingStoreId = adoption.getStoreId(); - ensureSelectedEmployeeOption(cbEmployee.getItems()); - applySelectedPet(); + + if (adoption.getPetId() > 0) { + DropdownOption petOption = new DropdownOption(); + petOption.setId((long) adoption.getPetId()); + petOption.setLabel(adoption.getPetName() != null && !adoption.getPetName().isBlank() + ? adoption.getPetName() : "Pet #" + adoption.getPetId()); + cbPet.setItems(FXCollections.observableArrayList(petOption)); + cbPet.setValue(petOption); + } + applySelectedCustomer(); applySelectedEmployee(); @@ -370,7 +475,7 @@ public class AdoptionDialogController { dpAdoptionDate.setDisable(false); cbEmployee.setDisable(false); cbAdoptionStatus.setDisable(false); - if (UserSession.getInstance().isAdmin()) cbStore.setDisable(false); + cbStore.setDisable(true); suppressStatusListener = true; cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled")); if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 2c9c66bc..1b5530cd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -51,6 +51,7 @@ public class AppointmentDialogController { private Long pendingStoreId = null; private boolean isOriginallyCancel = false; private boolean isOriginallyCompletedOrMissed = false; + private boolean isEditing = false; public void setMode(String mode) { this.mode = mode; @@ -83,7 +84,7 @@ public class AppointmentDialogController { cbHour.setDisable(false); cbMinute.setDisable(false); dpAppointmentDate.setDisable(false); - if (UserSession.getInstance().isAdmin()) cbStore.setDisable(false); + if (UserSession.getInstance().isAdmin() && !isEditing) cbStore.setDisable(false); } } }); @@ -203,7 +204,7 @@ public class AppointmentDialogController { Long customerId = newValue != null ? newValue.getId() : null; cbPet.setValue(null); cbPet.setItems(FXCollections.observableArrayList()); - cbPet.setDisable(customerId == null || isOriginallyCancel || isOriginallyCompletedOrMissed); + cbPet.setDisable(customerId == null || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing); if (customerId != null) { cbPet.setPromptText("Loading customer pets..."); loadCustomerPets(customerId); @@ -231,6 +232,8 @@ public class AppointmentDialogController { if (UserSession.getInstance().isAdmin()) { vbStore.setVisible(true); vbStore.setManaged(true); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Select a store first"); } btnSave.setOnMouseClicked(this::buttonSaveClicked); @@ -248,6 +251,7 @@ public class AppointmentDialogController { public void displayAppointmentDetails(AppointmentDTO appt) { selectedAppointment = appt; + isEditing = true; lblAppointmentId.setText("ID: " + appt.getAppointmentId()); pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null; pendingStoreId = appt.getStoreId(); @@ -314,6 +318,7 @@ public class AppointmentDialogController { cbService.setDisable(true); cbCustomer.setDisable(true); cbPet.setDisable(true); + cbStore.setDisable(true); cbEmployee.setDisable(false); cbHour.setDisable(false); cbMinute.setDisable(false); @@ -468,7 +473,7 @@ public class AppointmentDialogController { } } cbPet.setItems(petOptions); - cbPet.setDisable(petOptions.isEmpty() || isOriginallyCancel || isOriginallyCompletedOrMissed); + cbPet.setDisable(petOptions.isEmpty() || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing); cbPet.setPromptText(petOptions.isEmpty() ? "No pets for selected customer" : "Select a pet"); if (pendingPetSelectionId != null) { for (DropdownOption pet : cbPet.getItems()) { @@ -522,7 +527,7 @@ public class AppointmentDialogController { Platform.runLater(() -> { cbCustomer.setItems(FXCollections.observableArrayList(customers)); boolean hasCustomers = customers != null && !customers.isEmpty(); - cbCustomer.setDisable(!hasCustomers || isOriginallyCancel || isOriginallyCompletedOrMissed); + cbCustomer.setDisable(!hasCustomers || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing); cbPet.setDisable(true); cbPet.setItems(FXCollections.observableArrayList()); cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet"); @@ -557,7 +562,16 @@ public class AppointmentDialogController { cbEmployee.setValue(null); cbEmployee.setItems(FXCollections.observableArrayList()); if (sid != null) { + if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) { + cbEmployee.setDisable(false); + cbEmployee.setPromptText("Select an employee"); + } loadEmployeesForStore(sid); + } else { + if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) { + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Select a store first"); + } } }); if (pendingStoreId != null) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java index c997e51c..292c21be 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java @@ -138,6 +138,8 @@ public class CustomerEditDialogController { UserRequest request = new UserRequest(); request.setUsername(username); request.setPassword(password.isEmpty() ? null : password); + request.setFirstName(firstName); + request.setLastName(lastName); request.setFullName(firstName + " " + lastName); request.setEmail(email); request.setPhone(phone); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java index 2ffdbd03..11a186db 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -9,6 +9,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.HBox; import javafx.application.Platform; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; @@ -80,6 +81,21 @@ public class RefundDialogController { @FXML private ComboBox cbPaymentMethod; + @FXML + private Label lblRefundSubtotal; + + @FXML + private HBox hbRefundCouponDiscount; + + @FXML + private Label lblRefundCouponDiscount; + + @FXML + private HBox hbRefundLoyaltyDiscount; + + @FXML + private Label lblRefundLoyaltyDiscount; + @FXML private Label lblRefundTotal; @@ -448,8 +464,68 @@ public class RefundDialogController { } private void updateRefundTotal() { - double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum(); - lblRefundTotal.setText(currency.format(total)); + if (currentSale == null || refundItems.isEmpty()) { + lblRefundSubtotal.setText(currency.format(0.0)); + lblRefundTotal.setText(currency.format(0.0)); + hbRefundCouponDiscount.setVisible(false); + hbRefundCouponDiscount.setManaged(false); + hbRefundLoyaltyDiscount.setVisible(false); + hbRefundLoyaltyDiscount.setManaged(false); + return; + } + + double originalSubtotal = currentSale.getSubtotalAmount() != null + ? currentSale.getSubtotalAmount().doubleValue() : 0; + if (originalSubtotal == 0 && currentSale.getItems() != null) { + for (var item : currentSale.getItems()) { + if (item.getUnitPrice() != null && item.getQuantity() != null) { + originalSubtotal += item.getUnitPrice().doubleValue() * Math.abs(item.getQuantity()); + } + } + } + + double cartSubtotal = refundItems.stream().mapToDouble(RefundItem::getTotal).sum(); + double originalTotal = currentSale.getTotalAmount() != null + ? Math.abs(currentSale.getTotalAmount().doubleValue()) : 0; + + double refundTotal; + double couponDiscountRefunded = 0; + double loyaltyDiscountRefunded = 0; + + if (originalSubtotal > 0) { + double ratio = cartSubtotal / originalSubtotal; + refundTotal = originalTotal * ratio; + if (currentSale.getCouponDiscountAmount() != null) { + couponDiscountRefunded = currentSale.getCouponDiscountAmount().doubleValue() * ratio; + } + if (currentSale.getLoyaltyDiscountAmount() != null) { + loyaltyDiscountRefunded = currentSale.getLoyaltyDiscountAmount().doubleValue() * ratio; + } + } else { + refundTotal = cartSubtotal; + } + + lblRefundSubtotal.setText(currency.format(cartSubtotal)); + + if (couponDiscountRefunded > 0.001) { + lblRefundCouponDiscount.setText("-" + currency.format(couponDiscountRefunded)); + hbRefundCouponDiscount.setVisible(true); + hbRefundCouponDiscount.setManaged(true); + } else { + hbRefundCouponDiscount.setVisible(false); + hbRefundCouponDiscount.setManaged(false); + } + + if (loyaltyDiscountRefunded > 0.001) { + lblRefundLoyaltyDiscount.setText("-" + currency.format(loyaltyDiscountRefunded)); + hbRefundLoyaltyDiscount.setVisible(true); + hbRefundLoyaltyDiscount.setManaged(true); + } else { + hbRefundLoyaltyDiscount.setVisible(false); + hbRefundLoyaltyDiscount.setManaged(false); + } + + lblRefundTotal.setText(currency.format(refundTotal)); } private void closeDialog() { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java index 6cd3bc28..332be5e8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -27,6 +27,13 @@ public class SaleDetailDialogController { @FXML private Label lblSaleDate; @FXML private Label lblEmployee; @FXML private Label lblPayment; + @FXML private Label lblCustomer; + @FXML private javafx.scene.layout.HBox hbDetailSubtotal; + @FXML private Label lblDetailSubtotal; + @FXML private javafx.scene.layout.HBox hbDetailCouponDiscount; + @FXML private Label lblDetailCouponDiscount; + @FXML private javafx.scene.layout.HBox hbDetailLoyaltyDiscount; + @FXML private Label lblDetailLoyaltyDiscount; @FXML private Label lblTotal; @FXML private Button btnRefund; @FXML private TableView tvItems; @@ -61,6 +68,39 @@ public class SaleDetailDialogController { lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : ""); lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : ""); lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : ""); + lblCustomer.setText(sale.getCustomerName() != null && !sale.getCustomerName().isEmpty() ? sale.getCustomerName() : "-"); + + if (!sale.isRefund() && sale.getSubtotalAmount() > 0) { + hbDetailSubtotal.setVisible(true); + hbDetailSubtotal.setManaged(true); + lblDetailSubtotal.setText(currency.format(sale.getSubtotalAmount())); + + if (sale.getCouponDiscountAmount() > 0.001) { + lblDetailCouponDiscount.setText("-" + currency.format(sale.getCouponDiscountAmount())); + hbDetailCouponDiscount.setVisible(true); + hbDetailCouponDiscount.setManaged(true); + } else { + hbDetailCouponDiscount.setVisible(false); + hbDetailCouponDiscount.setManaged(false); + } + + if (sale.getLoyaltyDiscountAmount() > 0.001) { + lblDetailLoyaltyDiscount.setText("-" + currency.format(sale.getLoyaltyDiscountAmount())); + hbDetailLoyaltyDiscount.setVisible(true); + hbDetailLoyaltyDiscount.setManaged(true); + } else { + hbDetailLoyaltyDiscount.setVisible(false); + hbDetailLoyaltyDiscount.setManaged(false); + } + } else { + hbDetailSubtotal.setVisible(false); + hbDetailSubtotal.setManaged(false); + hbDetailCouponDiscount.setVisible(false); + hbDetailCouponDiscount.setManaged(false); + hbDetailLoyaltyDiscount.setVisible(false); + hbDetailLoyaltyDiscount.setManaged(false); + } + lblTotal.setText(currency.format(sale.getTotalAmount())); tvItems.setItems(sale.getItems()); if (btnRefund != null) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java index c3ff719e..13f500c4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java @@ -11,8 +11,12 @@ public class SaleDetail { private final String employeeName; private final boolean refund; private final ObservableList items; + private final String customerName; + private final double subtotalAmount; + private final double couponDiscountAmount; + private final double loyaltyDiscountAmount; - public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList items) { + public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList items, String customerName, double subtotalAmount, double couponDiscountAmount, double loyaltyDiscountAmount) { this.saleId = saleId; this.saleDate = saleDate; this.totalAmount = totalAmount; @@ -20,6 +24,10 @@ public class SaleDetail { this.employeeName = employeeName; this.refund = refund; this.items = items; + this.customerName = customerName != null ? customerName : ""; + this.subtotalAmount = subtotalAmount; + this.couponDiscountAmount = couponDiscountAmount; + this.loyaltyDiscountAmount = loyaltyDiscountAmount; } public int getSaleId() { @@ -50,6 +58,22 @@ public class SaleDetail { return items; } + public String getCustomerName() { + return customerName; + } + + public double getSubtotalAmount() { + return subtotalAmount; + } + + public double getCouponDiscountAmount() { + return couponDiscountAmount; + } + + public double getLoyaltyDiscountAmount() { + return loyaltyDiscountAmount; + } + public static class SaleDetailItem { private final int prodId; private final String productName; diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java index 0316e6be..f45938b0 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java @@ -11,8 +11,9 @@ public class SaleLineItem { private final String paymentMethod; private final boolean isRefund; private final String storeName; + private final String customerName; - public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod, boolean isRefund, String storeName) { + public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod, boolean isRefund, String storeName, String customerName) { this.saleId = saleId; this.saleDate = saleDate; this.employeeName = employeeName; @@ -23,6 +24,7 @@ public class SaleLineItem { this.paymentMethod = paymentMethod; this.isRefund = isRefund; this.storeName = storeName != null ? storeName : ""; + this.customerName = customerName != null ? customerName : ""; } public int getSaleId() { @@ -64,4 +66,8 @@ public class SaleLineItem { public String getStoreName() { return storeName; } + + public String getCustomerName() { + return customerName; + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml index 96b586ee..fe0b34d7 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml @@ -92,7 +92,7 @@ diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml index aad2f4c7..d4380e5d 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml @@ -165,16 +165,34 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -125,20 +147,34 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + - +