updated sales on desktop, and fixed sales with points again on back end

This commit is contained in:
Alex
2026-04-14 03:32:29 -06:00
parent a3fcebfa15
commit a860a1c247
20 changed files with 943 additions and 101 deletions

View File

@@ -28,6 +28,8 @@ public class SaleRequest {
private Long cartId; private Long cartId;
private Integer pointsUsed;
public Long getStoreId() { public Long getStoreId() {
return storeId; return storeId;
@@ -101,6 +103,14 @@ public class SaleRequest {
this.cartId = cartId; this.cartId = cartId;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@@ -115,12 +125,13 @@ public class SaleRequest {
Objects.equals(customerId, that.customerId) && Objects.equals(customerId, that.customerId) &&
Objects.equals(channel, that.channel) && Objects.equals(channel, that.channel) &&
Objects.equals(couponId, that.couponId) && Objects.equals(couponId, that.couponId) &&
Objects.equals(cartId, that.cartId); Objects.equals(cartId, that.cartId) &&
Objects.equals(pointsUsed, that.pointsUsed);
} }
@Override @Override
public int hashCode() { 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 @Override
@@ -135,6 +146,7 @@ public class SaleRequest {
", channel='" + channel + '\'' + ", channel='" + channel + '\'' +
", couponId=" + couponId + ", couponId=" + couponId +
", cartId=" + cartId + ", cartId=" + cartId +
", pointsUsed=" + pointsUsed +
'}'; '}';
} }
} }

View File

@@ -162,10 +162,41 @@ public class SaleService {
} }
subtotalAmount = subtotalAmount.negate(); subtotalAmount = subtotalAmount.negate();
sale.setSubtotalAmount(subtotalAmount); 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.setEmployeeDiscountAmount(BigDecimal.ZERO);
sale.setLoyaltyDiscountAmount(BigDecimal.ZERO); sale.setLoyaltyDiscountAmount(loyaltyDiscountRefunded);
sale.setPointsEarned(0); sale.setPointsEarned(0);
} else { } else {
if (request.getItems() == null || request.getItems().isEmpty()) { if (request.getItems() == null || request.getItems().isEmpty()) {
@@ -206,18 +237,29 @@ public class SaleService {
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
sale.setEmployeeDiscountAmount(employeeDiscount); sale.setEmployeeDiscountAmount(employeeDiscount);
boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied()); BigDecimal remainingAfterDiscounts = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), useLoyaltyPoints); 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); sale.setLoyaltyDiscountAmount(loyaltyDiscount);
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount); BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
int pointsUsed = toPointsUsed(loyaltyDiscount);
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
if (customer != null) { if (customer != null) {
int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0; 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)); customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
userRepository.save(customer); userRepository.save(customer);
} }

View File

@@ -8,6 +8,9 @@ public class SaleRequest {
private List<SaleItemRequest> items; private List<SaleItemRequest> items;
private Boolean isRefund; private Boolean isRefund;
private Long originalSaleId; private Long originalSaleId;
private Long customerId;
private Long couponId;
private Integer pointsUsed;
public SaleRequest() { public SaleRequest() {
} }
@@ -51,4 +54,28 @@ public class SaleRequest {
public void setOriginalSaleId(Long originalSaleId) { public void setOriginalSaleId(Long originalSaleId) {
this.originalSaleId = 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;
}
} }

View File

@@ -14,6 +14,13 @@ public class SaleResponse {
private Boolean isRefund; private Boolean isRefund;
private Long originalSaleId; private Long originalSaleId;
private List<SaleItemResponse> items; private List<SaleItemResponse> items;
private Long customerId;
private String customerName;
private BigDecimal couponDiscountAmount;
private BigDecimal loyaltyDiscountAmount;
private Integer pointsUsed;
private Integer pointsEarned;
private BigDecimal subtotalAmount;
public SaleResponse() { public SaleResponse() {
} }
@@ -89,4 +96,60 @@ public class SaleResponse {
public void setItems(List<SaleItemResponse> items) { public void setItems(List<SaleItemResponse> items) {
this.items = 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;
}
} }

View File

@@ -3,6 +3,8 @@ package org.example.petshopdesktop.api.dto.user;
public class UserRequest { public class UserRequest {
private String username; private String username;
private String password; private String password;
private String firstName;
private String lastName;
private String fullName; private String fullName;
private String email; private String email;
private String phone; private String phone;
@@ -29,6 +31,22 @@ public class UserRequest {
this.password = password; 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() { public String getFullName() {
return fullName; return fullName;
} }

View File

@@ -39,4 +39,8 @@ public class CouponApi {
public void deleteCoupon(Long id) throws Exception { public void deleteCoupon(Long id) throws Exception {
apiClient.delete("/api/v1/coupons/" + id); apiClient.delete("/api/v1/coupons/" + id);
} }
public CouponResponse getCouponByCode(String code) throws Exception {
return apiClient.get("/api/v1/coupons/code/" + code, CouponResponse.class);
}
} }

View File

@@ -45,4 +45,8 @@ public class CustomerApi {
public UserResponse createCustomer(UserRequest request) throws Exception { public UserResponse createCustomer(UserRequest request) throws Exception {
return apiClient.post("/api/v1/customers", request, UserResponse.class); 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);
}
} }

View File

@@ -107,6 +107,14 @@ public class DropdownApi {
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> getCustomerPets(Long customerId) throws Exception { public List<DropdownOption> getCustomerPets(Long customerId) throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets"); String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets");
if (response == null || response.isEmpty()) { if (response == null || response.isEmpty()) {

View File

@@ -10,6 +10,7 @@ import javafx.application.Platform;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode; import javafx.scene.control.SelectionMode;
@@ -19,18 +20,25 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.auth.UserSession;
import javafx.concurrent.Task; 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.ProductApi;
import org.example.petshopdesktop.api.endpoints.SaleApi; 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.product.ProductResponse;
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
import org.example.petshopdesktop.api.dto.sale.SaleRequest; import org.example.petshopdesktop.api.dto.sale.SaleRequest;
import org.example.petshopdesktop.api.dto.sale.SaleResponse; 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.Product;
import org.example.petshopdesktop.models.SaleCartItem; import org.example.petshopdesktop.models.SaleCartItem;
import org.example.petshopdesktop.models.SaleDetail; import org.example.petshopdesktop.models.SaleDetail;
@@ -111,6 +119,9 @@ public class SaleController {
@FXML @FXML
private TableColumn<SaleLineItem, String> colEmployeeName; private TableColumn<SaleLineItem, String> colEmployeeName;
@FXML
private TableColumn<SaleLineItem, String> colCustomerName;
@FXML @FXML
private TableColumn<SaleLineItem, String> colServiceProduct; private TableColumn<SaleLineItem, String> colServiceProduct;
@@ -135,11 +146,53 @@ public class SaleController {
@FXML @FXML
private TextField txtSearch; private TextField txtSearch;
@FXML
private ComboBox<DropdownOption> 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<SaleCartItem> cartItems = FXCollections.observableArrayList(); private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList(); private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
private FilteredList<SaleLineItem> filteredSales; private FilteredList<SaleLineItem> filteredSales;
private boolean saleSaveInProgress; private boolean saleSaveInProgress;
private CouponResponse appliedCoupon = null;
private UserResponse selectedCustomerData = null;
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); 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")); colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId"));
colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate")); colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate"));
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
colCustomerName.setMinWidth(120);
colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName")); colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName"));
colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
@@ -214,7 +269,23 @@ public class SaleController {
setCreateSaleControlsDisabled(true); setCreateSaleControlsDisabled(true);
Task<ObservableList<Product>> 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<ObservableList<Product>> productsTask = new Task<>() {
@Override @Override
protected ObservableList<Product> call() throws Exception { protected ObservableList<Product> call() throws Exception {
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null); List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
@@ -232,13 +303,13 @@ public class SaleController {
} }
}; };
task.setOnSucceeded(event -> { productsTask.setOnSucceeded(event -> {
cbProduct.setItems(task.getValue()); cbProduct.setItems(productsTask.getValue());
setCreateSaleControlsDisabled(false); setCreateSaleControlsDisabled(false);
}); });
task.setOnFailed(event -> { productsTask.setOnFailed(event -> {
Throwable e = task.getException(); Throwable e = productsTask.getException();
ActivityLogger.getInstance().logException( ActivityLogger.getInstance().logException(
"SaleController.setupCreateSale", "SaleController.setupCreateSale",
e instanceof Exception ? (Exception) e : new RuntimeException(e), 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."); showError("Sales", "Could not load products. Check the backend connection and refresh the view.");
}); });
new Thread(productsTask).start();
Task<List<DropdownOption>> customersTask = new Task<>() {
@Override
protected List<DropdownOption> call() throws Exception {
return DropdownApi.getInstance().getCustomers();
}
};
customersTask.setOnSucceeded(event -> {
List<DropdownOption> customers = customersTask.getValue();
ObservableList<DropdownOption> 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<UserResponse> 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(); new Thread(task).start();
} }
@@ -281,6 +416,9 @@ public class SaleController {
: ""; : "";
if (sale.getItems() != null && !sale.getItems().isEmpty()) { 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()) { for (SaleItemResponse item : sale.getItems()) {
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0;
@@ -288,7 +426,7 @@ public class SaleController {
if (isRefund && quantity > 0) { if (isRefund && quantity > 0) {
quantity = -quantity; quantity = -quantity;
} }
double lineTotal = unitPrice * quantity; double lineTotal = unitPrice * quantity * discountRatio;
lineItems.add(new SaleLineItem( lineItems.add(new SaleLineItem(
sale.getSaleId().intValue(), sale.getSaleId().intValue(),
saleDate, saleDate,
@@ -299,7 +437,8 @@ public class SaleController {
lineTotal, lineTotal,
sale.getPaymentMethod(), sale.getPaymentMethod(),
isRefund, isRefund,
sale.getStoreName() sale.getStoreName(),
sale.getCustomerName()
)); ));
} }
} }
@@ -379,6 +518,73 @@ public class SaleController {
updateCartTotal(); 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<CouponResponse> 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 @FXML
void btnSaveSale(ActionEvent event) { void btnSaveSale(ActionEvent event) {
if (saleSaveInProgress) { if (saleSaveInProgress) {
@@ -409,6 +615,7 @@ public class SaleController {
SaleRequest request = new SaleRequest(); SaleRequest request = new SaleRequest();
request.setStoreId(storeId); request.setStoreId(storeId);
request.setPaymentMethod(payment); request.setPaymentMethod(payment);
request.setIsRefund(false);
List<SaleItemRequest> itemRequests = new ArrayList<>(); List<SaleItemRequest> itemRequests = new ArrayList<>();
for (SaleCartItem cartItem : cartItems) { for (SaleCartItem cartItem : cartItems) {
@@ -419,6 +626,20 @@ public class SaleController {
} }
request.setItems(itemRequests); 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; saleSaveInProgress = true;
setCreateSaleControlsDisabled(true); setCreateSaleControlsDisabled(true);
btnRefund.setDisable(true); btnRefund.setDisable(true);
@@ -437,6 +658,21 @@ public class SaleController {
SaleResponse response = task.getValue(); SaleResponse response = task.getValue();
showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created."); showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created.");
cartItems.clear(); 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(); updateCartTotal();
refreshSales(true); refreshSales(true);
}); });
@@ -536,6 +772,9 @@ public class SaleController {
private SaleDetail mapToSaleDetail(SaleResponse sale) { private SaleDetail mapToSaleDetail(SaleResponse sale) {
ObservableList<SaleDetail.SaleDetailItem> items = FXCollections.observableArrayList(); ObservableList<SaleDetail.SaleDetailItem> 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) { if (sale.getItems() != null) {
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
for (SaleItemResponse item : sale.getItems()) { for (SaleItemResponse item : sale.getItems()) {
@@ -549,10 +788,13 @@ public class SaleController {
item.getProductName(), item.getProductName(),
quantity, quantity,
unitPrice, 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( return new SaleDetail(
sale.getSaleId().intValue(), sale.getSaleId().intValue(),
sale.getSaleDate(), sale.getSaleDate(),
@@ -560,18 +802,76 @@ public class SaleController {
sale.getPaymentMethod(), sale.getPaymentMethod(),
sale.getEmployeeName(), sale.getEmployeeName(),
Boolean.TRUE.equals(sale.getIsRefund()), Boolean.TRUE.equals(sale.getIsRefund()),
items items,
sale.getCustomerName(),
subtotal,
couponDiscount,
loyaltyDiscount
); );
} }
private void updateCartTotal() { private void updateCartTotal() {
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum());
lblCartTotal.setText(currency.format(total)); 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) { private void updateSalesColumnWidths(double tableWidth) {
double available = Math.max(tableWidth - 28.0, 0.0); double available = Math.max(tableWidth - 28.0, 0.0);
double baseWidth = 1255.0; double baseWidth = 1395.0;
if (available <= 0) { if (available <= 0) {
return; return;
} }
@@ -580,6 +880,7 @@ public class SaleController {
colSaleId.setPrefWidth(60.0); colSaleId.setPrefWidth(60.0);
colSaleDate.setPrefWidth(170.0); colSaleDate.setPrefWidth(170.0);
colEmployeeName.setPrefWidth(160.0); colEmployeeName.setPrefWidth(160.0);
colCustomerName.setPrefWidth(140.0);
colServiceProduct.setPrefWidth(320.0); colServiceProduct.setPrefWidth(320.0);
colSaleQuantity.setPrefWidth(70.0); colSaleQuantity.setPrefWidth(70.0);
colSaleUnitPrice.setPrefWidth(115.0); colSaleUnitPrice.setPrefWidth(115.0);
@@ -591,14 +892,15 @@ public class SaleController {
double extra = available - baseWidth; double extra = available - baseWidth;
colSaleId.setPrefWidth(60.0); colSaleId.setPrefWidth(60.0);
colSaleDate.setPrefWidth(170.0 + extra * 0.16); colSaleDate.setPrefWidth(170.0 + extra * 0.14);
colEmployeeName.setPrefWidth(160.0 + extra * 0.16); colEmployeeName.setPrefWidth(160.0 + extra * 0.14);
colServiceProduct.setPrefWidth(320.0 + extra * 0.38); colCustomerName.setPrefWidth(140.0 + extra * 0.12);
colServiceProduct.setPrefWidth(320.0 + extra * 0.34);
colSaleQuantity.setPrefWidth(70.0); colSaleQuantity.setPrefWidth(70.0);
colSaleUnitPrice.setPrefWidth(115.0 + extra * 0.08); colSaleUnitPrice.setPrefWidth(115.0 + extra * 0.08);
colSaleTotal.setPrefWidth(120.0 + extra * 0.08); colSaleTotal.setPrefWidth(120.0 + extra * 0.08);
colSalePaymentType.setPrefWidth(110.0 + extra * 0.06); 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) { private void updateCartColumnWidths(double tableWidth) {
@@ -631,6 +933,11 @@ public class SaleController {
cbPaymentMethod.setDisable(disabled); cbPaymentMethod.setDisable(disabled);
btnClearCart.setDisable(disabled); btnClearCart.setDisable(disabled);
btnSaveSale.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) { private void applySalesFilter(String filter) {
@@ -644,6 +951,7 @@ public class SaleController {
String.valueOf(s.getSaleId()).contains(f) String.valueOf(s.getSaleId()).contains(f)
|| safe(s.getSaleDate()).contains(f) || safe(s.getSaleDate()).contains(f)
|| safe(s.getEmployeeName()).contains(f) || safe(s.getEmployeeName()).contains(f)
|| safe(s.getCustomerName()).contains(f)
|| safe(s.getItemName()).contains(f) || safe(s.getItemName()).contains(f)
|| safe(s.getPaymentMethod()).contains(f) || safe(s.getPaymentMethod()).contains(f)
); );

View File

@@ -49,6 +49,7 @@ public class AdoptionDialogController {
private Adoption selectedAdoption = null; private Adoption selectedAdoption = null;
private boolean suppressStatusListener = false; private boolean suppressStatusListener = false;
private Long pendingStoreId = null; private Long pendingStoreId = null;
private boolean isEditing = false;
private final ObservableList<String> statusList = FXCollections.observableArrayList( private final ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Missed", "Cancelled" "Pending", "Completed", "Missed", "Cancelled"
@@ -63,7 +64,6 @@ public class AdoptionDialogController {
} }
}); });
cbEmployee.setPromptText("Select an employee");
txtAdoptionFee.setDisable(true); txtAdoptionFee.setDisable(true);
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
@@ -91,6 +91,12 @@ public class AdoptionDialogController {
if (UserSession.getInstance().isAdmin()) { if (UserSession.getInstance().isAdmin()) {
vbStore.setVisible(true); vbStore.setVisible(true);
vbStore.setManaged(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(); loadDropdownsAsync();
@@ -126,49 +132,12 @@ public class AdoptionDialogController {
} }
private void loadDropdownsAsync() { private void loadDropdownsAsync() {
new Thread(() -> {
try {
List<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets();
Platform.runLater(() -> {
if (pets != null) {
ObservableList<DropdownOption> 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<DropdownOption> employees = DropdownApi.getInstance().getEmployees();
Platform.runLater(() -> {
ObservableList<DropdownOption> 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(() -> { new Thread(() -> {
try { try {
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers(); List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
Platform.runLater(() -> { Platform.runLater(() -> {
if (customers != null) { if (customers != null) {
ObservableList<DropdownOption> customersObs = FXCollections.observableArrayList(customers); cbCustomer.setItems(FXCollections.observableArrayList(customers));
cbCustomer.setItems(customersObs);
applySelectedCustomer(); applySelectedCustomer();
} }
}); });
@@ -185,6 +154,44 @@ public class AdoptionDialogController {
Platform.runLater(() -> { Platform.runLater(() -> {
if (stores != null) { if (stores != null) {
cbStore.setItems(FXCollections.observableArrayList(stores)); 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) { if (pendingStoreId != null) {
for (DropdownOption store : cbStore.getItems()) { for (DropdownOption store : cbStore.getItems()) {
if (pendingStoreId.equals(store.getId())) { if (pendingStoreId.equals(store.getId())) {
@@ -201,9 +208,98 @@ public class AdoptionDialogController {
"AdoptionDialogController.loadDropdownsAsync", e, "Loading stores")); "AdoptionDialogController.loadDropdownsAsync", e, "Loading stores"));
} }
}).start(); }).start();
} else {
Long storeId = UserSession.getInstance().getStoreId();
if (storeId != null && storeId > 0) {
new Thread(() -> {
try {
List<DropdownOption> employees = DropdownApi.getInstance().getStoreEmployees(storeId);
Platform.runLater(() -> {
ObservableList<DropdownOption> 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<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets(storeId);
Platform.runLater(() -> {
if (!isEditing && pets != null) {
ObservableList<DropdownOption> 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<DropdownOption> employees = DropdownApi.getInstance().getStoreEmployees(storeId);
Platform.runLater(() -> {
ObservableList<DropdownOption> 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<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets(storeId);
Platform.runLater(() -> {
if (pets != null) {
ObservableList<DropdownOption> 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) { private void applyStatusFieldRules(String status) {
if ("Cancelled".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status) || "Missed".equalsIgnoreCase(status)) { if ("Cancelled".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status) || "Missed".equalsIgnoreCase(status)) {
cbEmployee.setDisable(true); cbEmployee.setDisable(true);
@@ -212,7 +308,7 @@ public class AdoptionDialogController {
} else { } else {
cbEmployee.setDisable(false); cbEmployee.setDisable(false);
dpAdoptionDate.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) { public void displayAdoptionDetails(Adoption adoption) {
if (adoption == null) return; if (adoption == null) return;
selectedAdoption = adoption; selectedAdoption = adoption;
isEditing = true;
lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); lblAdoptionId.setText("ID: " + adoption.getAdoptionId());
pendingStoreId = adoption.getStoreId(); 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(); applySelectedCustomer();
applySelectedEmployee(); applySelectedEmployee();
@@ -370,7 +475,7 @@ public class AdoptionDialogController {
dpAdoptionDate.setDisable(false); dpAdoptionDate.setDisable(false);
cbEmployee.setDisable(false); cbEmployee.setDisable(false);
cbAdoptionStatus.setDisable(false); cbAdoptionStatus.setDisable(false);
if (UserSession.getInstance().isAdmin()) cbStore.setDisable(false); cbStore.setDisable(true);
suppressStatusListener = true; suppressStatusListener = true;
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled")); cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled"));
if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) { if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) {

View File

@@ -51,6 +51,7 @@ public class AppointmentDialogController {
private Long pendingStoreId = null; private Long pendingStoreId = null;
private boolean isOriginallyCancel = false; private boolean isOriginallyCancel = false;
private boolean isOriginallyCompletedOrMissed = false; private boolean isOriginallyCompletedOrMissed = false;
private boolean isEditing = false;
public void setMode(String mode) { public void setMode(String mode) {
this.mode = mode; this.mode = mode;
@@ -83,7 +84,7 @@ public class AppointmentDialogController {
cbHour.setDisable(false); cbHour.setDisable(false);
cbMinute.setDisable(false); cbMinute.setDisable(false);
dpAppointmentDate.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; Long customerId = newValue != null ? newValue.getId() : null;
cbPet.setValue(null); cbPet.setValue(null);
cbPet.setItems(FXCollections.observableArrayList()); cbPet.setItems(FXCollections.observableArrayList());
cbPet.setDisable(customerId == null || isOriginallyCancel || isOriginallyCompletedOrMissed); cbPet.setDisable(customerId == null || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing);
if (customerId != null) { if (customerId != null) {
cbPet.setPromptText("Loading customer pets..."); cbPet.setPromptText("Loading customer pets...");
loadCustomerPets(customerId); loadCustomerPets(customerId);
@@ -231,6 +232,8 @@ public class AppointmentDialogController {
if (UserSession.getInstance().isAdmin()) { if (UserSession.getInstance().isAdmin()) {
vbStore.setVisible(true); vbStore.setVisible(true);
vbStore.setManaged(true); vbStore.setManaged(true);
cbEmployee.setDisable(true);
cbEmployee.setPromptText("Select a store first");
} }
btnSave.setOnMouseClicked(this::buttonSaveClicked); btnSave.setOnMouseClicked(this::buttonSaveClicked);
@@ -248,6 +251,7 @@ public class AppointmentDialogController {
public void displayAppointmentDetails(AppointmentDTO appt) { public void displayAppointmentDetails(AppointmentDTO appt) {
selectedAppointment = appt; selectedAppointment = appt;
isEditing = true;
lblAppointmentId.setText("ID: " + appt.getAppointmentId()); lblAppointmentId.setText("ID: " + appt.getAppointmentId());
pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null; pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null;
pendingStoreId = appt.getStoreId(); pendingStoreId = appt.getStoreId();
@@ -314,6 +318,7 @@ public class AppointmentDialogController {
cbService.setDisable(true); cbService.setDisable(true);
cbCustomer.setDisable(true); cbCustomer.setDisable(true);
cbPet.setDisable(true); cbPet.setDisable(true);
cbStore.setDisable(true);
cbEmployee.setDisable(false); cbEmployee.setDisable(false);
cbHour.setDisable(false); cbHour.setDisable(false);
cbMinute.setDisable(false); cbMinute.setDisable(false);
@@ -468,7 +473,7 @@ public class AppointmentDialogController {
} }
} }
cbPet.setItems(petOptions); 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"); cbPet.setPromptText(petOptions.isEmpty() ? "No pets for selected customer" : "Select a pet");
if (pendingPetSelectionId != null) { if (pendingPetSelectionId != null) {
for (DropdownOption pet : cbPet.getItems()) { for (DropdownOption pet : cbPet.getItems()) {
@@ -522,7 +527,7 @@ public class AppointmentDialogController {
Platform.runLater(() -> { Platform.runLater(() -> {
cbCustomer.setItems(FXCollections.observableArrayList(customers)); cbCustomer.setItems(FXCollections.observableArrayList(customers));
boolean hasCustomers = customers != null && !customers.isEmpty(); boolean hasCustomers = customers != null && !customers.isEmpty();
cbCustomer.setDisable(!hasCustomers || isOriginallyCancel || isOriginallyCompletedOrMissed); cbCustomer.setDisable(!hasCustomers || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing);
cbPet.setDisable(true); cbPet.setDisable(true);
cbPet.setItems(FXCollections.observableArrayList()); cbPet.setItems(FXCollections.observableArrayList());
cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet"); cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet");
@@ -557,7 +562,16 @@ public class AppointmentDialogController {
cbEmployee.setValue(null); cbEmployee.setValue(null);
cbEmployee.setItems(FXCollections.observableArrayList()); cbEmployee.setItems(FXCollections.observableArrayList());
if (sid != null) { if (sid != null) {
if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) {
cbEmployee.setDisable(false);
cbEmployee.setPromptText("Select an employee");
}
loadEmployeesForStore(sid); loadEmployeesForStore(sid);
} else {
if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) {
cbEmployee.setDisable(true);
cbEmployee.setPromptText("Select a store first");
}
} }
}); });
if (pendingStoreId != null) { if (pendingStoreId != null) {

View File

@@ -138,6 +138,8 @@ public class CustomerEditDialogController {
UserRequest request = new UserRequest(); UserRequest request = new UserRequest();
request.setUsername(username); request.setUsername(username);
request.setPassword(password.isEmpty() ? null : password); request.setPassword(password.isEmpty() ? null : password);
request.setFirstName(firstName);
request.setLastName(lastName);
request.setFullName(firstName + " " + lastName); request.setFullName(firstName + " " + lastName);
request.setEmail(email); request.setEmail(email);
request.setPhone(phone); request.setPhone(phone);

View File

@@ -9,6 +9,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
@@ -80,6 +81,21 @@ public class RefundDialogController {
@FXML @FXML
private ComboBox<String> cbPaymentMethod; private ComboBox<String> cbPaymentMethod;
@FXML
private Label lblRefundSubtotal;
@FXML
private HBox hbRefundCouponDiscount;
@FXML
private Label lblRefundCouponDiscount;
@FXML
private HBox hbRefundLoyaltyDiscount;
@FXML
private Label lblRefundLoyaltyDiscount;
@FXML @FXML
private Label lblRefundTotal; private Label lblRefundTotal;
@@ -448,8 +464,68 @@ public class RefundDialogController {
} }
private void updateRefundTotal() { private void updateRefundTotal() {
double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum(); if (currentSale == null || refundItems.isEmpty()) {
lblRefundTotal.setText(currency.format(total)); 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() { private void closeDialog() {

View File

@@ -27,6 +27,13 @@ public class SaleDetailDialogController {
@FXML private Label lblSaleDate; @FXML private Label lblSaleDate;
@FXML private Label lblEmployee; @FXML private Label lblEmployee;
@FXML private Label lblPayment; @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 Label lblTotal;
@FXML private Button btnRefund; @FXML private Button btnRefund;
@FXML private TableView<SaleDetail.SaleDetailItem> tvItems; @FXML private TableView<SaleDetail.SaleDetailItem> tvItems;
@@ -61,6 +68,39 @@ public class SaleDetailDialogController {
lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : ""); lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : "");
lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : ""); lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : "");
lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : ""); 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())); lblTotal.setText(currency.format(sale.getTotalAmount()));
tvItems.setItems(sale.getItems()); tvItems.setItems(sale.getItems());
if (btnRefund != null) { if (btnRefund != null) {

View File

@@ -11,8 +11,12 @@ public class SaleDetail {
private final String employeeName; private final String employeeName;
private final boolean refund; private final boolean refund;
private final ObservableList<SaleDetailItem> items; private final ObservableList<SaleDetailItem> 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<SaleDetailItem> items) { public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList<SaleDetailItem> items, String customerName, double subtotalAmount, double couponDiscountAmount, double loyaltyDiscountAmount) {
this.saleId = saleId; this.saleId = saleId;
this.saleDate = saleDate; this.saleDate = saleDate;
this.totalAmount = totalAmount; this.totalAmount = totalAmount;
@@ -20,6 +24,10 @@ public class SaleDetail {
this.employeeName = employeeName; this.employeeName = employeeName;
this.refund = refund; this.refund = refund;
this.items = items; this.items = items;
this.customerName = customerName != null ? customerName : "";
this.subtotalAmount = subtotalAmount;
this.couponDiscountAmount = couponDiscountAmount;
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
} }
public int getSaleId() { public int getSaleId() {
@@ -50,6 +58,22 @@ public class SaleDetail {
return items; 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 { public static class SaleDetailItem {
private final int prodId; private final int prodId;
private final String productName; private final String productName;

View File

@@ -11,8 +11,9 @@ public class SaleLineItem {
private final String paymentMethod; private final String paymentMethod;
private final boolean isRefund; private final boolean isRefund;
private final String storeName; 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.saleId = saleId;
this.saleDate = saleDate; this.saleDate = saleDate;
this.employeeName = employeeName; this.employeeName = employeeName;
@@ -23,6 +24,7 @@ public class SaleLineItem {
this.paymentMethod = paymentMethod; this.paymentMethod = paymentMethod;
this.isRefund = isRefund; this.isRefund = isRefund;
this.storeName = storeName != null ? storeName : ""; this.storeName = storeName != null ? storeName : "";
this.customerName = customerName != null ? customerName : "";
} }
public int getSaleId() { public int getSaleId() {
@@ -64,4 +66,8 @@ public class SaleLineItem {
public String getStoreName() { public String getStoreName() {
return storeName; return storeName;
} }
public String getCustomerName() {
return customerName;
}
} }

View File

@@ -92,7 +92,7 @@
<VBox spacing="6.0" HBox.hgrow="ALWAYS"> <VBox spacing="6.0" HBox.hgrow="ALWAYS">
<children> <children>
<Label text="Status" /> <Label text="Status" />
<ComboBox fx:id="cbActive" maxWidth="1.7976931348623157E308" promptText="Select Status" /> <ComboBox fx:id="cbActive" maxWidth="1.7976931348623157E308" prefWidth="200.0" visibleRowCount="2" promptText="Select Status" />
</children> </children>
</VBox> </VBox>
<VBox spacing="6.0" HBox.hgrow="ALWAYS"> <VBox spacing="6.0" HBox.hgrow="ALWAYS">

View File

@@ -165,16 +165,34 @@
</padding> </padding>
</ComboBox> </ComboBox>
<Region HBox.hgrow="ALWAYS" /> <Region HBox.hgrow="ALWAYS" />
<Label text="Refund Total:" textFill="#2c3e50"> <VBox spacing="2.0" alignment="CENTER_RIGHT">
<font> <children>
<Font name="System Bold" size="14.0" /> <HBox spacing="8.0" alignment="CENTER_RIGHT">
</font> <children>
</Label> <Label text="Items Subtotal:" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblRefundTotal" text="\$0.00" textFill="#FF6b6b"> <Label fx:id="lblRefundSubtotal" text="" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
<font> </children>
<Font name="System Bold" size="18.0" /> </HBox>
</font> <HBox fx:id="hbRefundCouponDiscount" spacing="8.0" alignment="CENTER_RIGHT" visible="false" managed="false">
</Label> <children>
<Label text="Coupon Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblRefundCouponDiscount" text="" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox fx:id="hbRefundLoyaltyDiscount" spacing="8.0" alignment="CENTER_RIGHT" visible="false" managed="false">
<children>
<Label text="Loyalty Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblRefundLoyaltyDiscount" text="" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox spacing="8.0" alignment="CENTER_RIGHT">
<children>
<Label text="Refund Total:" textFill="#2c3e50"><font><Font name="System Bold" size="14.0" /></font></Label>
<Label fx:id="lblRefundTotal" text="" textFill="#FF6b6b"><font><Font name="System Bold" size="18.0" /></font></Label>
</children>
</HBox>
</children>
</VBox>
<Region prefWidth="20.0" /> <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"> <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>

View File

@@ -45,11 +45,45 @@
<Label fx:id="lblEmployee" GridPane.columnIndex="1" GridPane.rowIndex="1" /> <Label fx:id="lblEmployee" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label text="Payment" GridPane.columnIndex="2" GridPane.rowIndex="1" /> <Label text="Payment" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<Label fx:id="lblPayment" GridPane.columnIndex="3" GridPane.rowIndex="1" /> <Label fx:id="lblPayment" GridPane.columnIndex="3" GridPane.rowIndex="1" />
<Label text="Total" GridPane.columnIndex="0" GridPane.rowIndex="2" /> <Label text="Customer" GridPane.columnIndex="0" GridPane.rowIndex="2" />
<Label fx:id="lblTotal" GridPane.columnIndex="1" GridPane.rowIndex="2" /> <Label fx:id="lblCustomer" GridPane.columnIndex="1" GridPane.rowIndex="2" />
</children> </children>
</GridPane> </GridPane>
<HBox alignment="CENTER_RIGHT" style="-fx-background-color: white; -fx-background-radius: 8; -fx-border-color: #e6e6e6; -fx-border-radius: 8; -fx-border-width: 1;">
<padding><Insets bottom="10.0" left="16.0" right="16.0" top="10.0" /></padding>
<children>
<VBox spacing="3.0" alignment="CENTER_RIGHT">
<children>
<HBox fx:id="hbDetailSubtotal" spacing="8.0" alignment="CENTER_RIGHT" visible="false" managed="false">
<children>
<Label text="Subtotal:" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblDetailSubtotal" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox fx:id="hbDetailCouponDiscount" spacing="8.0" alignment="CENTER_RIGHT" visible="false" managed="false">
<children>
<Label text="Coupon Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblDetailCouponDiscount" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox fx:id="hbDetailLoyaltyDiscount" spacing="8.0" alignment="CENTER_RIGHT" visible="false" managed="false">
<children>
<Label text="Loyalty Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblDetailLoyaltyDiscount" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox spacing="8.0" alignment="CENTER_RIGHT">
<children>
<Label text="Total:" textFill="#2c3e50"><font><Font name="System Bold" size="13.0" /></font></Label>
<Label fx:id="lblTotal" textFill="#2c3e50"><font><Font name="System Bold" size="15.0" /></font></Label>
</children>
</HBox>
</children>
</VBox>
</children>
</HBox>
<TableView fx:id="tvItems" prefHeight="320.0" VBox.vgrow="ALWAYS"> <TableView fx:id="tvItems" prefHeight="320.0" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colProduct" text="Product" prefWidth="330.0" /> <TableColumn fx:id="colProduct" text="Product" prefWidth="330.0" />

View File

@@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?> <?import javafx.scene.control.Separator?>
@@ -103,6 +104,27 @@
</Button> </Button>
</children> </children>
</FlowPane> </FlowPane>
<FlowPane hgap="8.0" maxWidth="Infinity" prefWrapLength="680.0" vgap="8.0">
<children>
<ComboBox fx:id="cbCustomer" minWidth="200.0" prefHeight="24.0" prefWidth="260.0" promptText="Customer (optional)" />
<Label fx:id="lblLoyaltyPoints" text="" textFill="#16a085" visible="false" managed="false">
<font><Font name="System Bold" size="12.0" /></font>
</Label>
<CheckBox fx:id="chkUseLoyaltyPoints" text="Use Loyalty Points" visible="false" managed="false" />
<TextField fx:id="txtCouponCode" prefHeight="24.0" prefWidth="160.0" promptText="Coupon code" />
<Button fx:id="btnApplyCoupon" mnemonicParsing="false" onAction="#btnApplyCoupon" prefHeight="24.0" style="-fx-background-color: #27ae60; -fx-cursor: hand; -fx-background-radius: 8;" text="Apply" textFill="WHITE">
<font><Font name="System Bold" size="13.0" /></font>
<padding><Insets bottom="8.0" left="14.0" right="14.0" top="8.0" /></padding>
</Button>
<Button fx:id="btnClearCoupon" mnemonicParsing="false" onAction="#btnClearCoupon" prefHeight="24.0" style="-fx-background-color: #7f8c8d; -fx-cursor: hand; -fx-background-radius: 8;" text="Clear" textFill="WHITE">
<font><Font name="System Bold" size="13.0" /></font>
<padding><Insets bottom="8.0" left="14.0" right="14.0" top="8.0" /></padding>
</Button>
<Label fx:id="lblCouponStatus" text="" textFill="#27ae60" wrapText="false">
<font><Font size="12.0" /></font>
</Label>
</children>
</FlowPane>
<TableView fx:id="tvCart" prefHeight="120.0" style="-fx-background-color: white; -fx-background-radius: 10;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvCart" prefHeight="120.0" style="-fx-background-color: white; -fx-background-radius: 10;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colCartProduct" prefWidth="310.0" text="Product" /> <TableColumn fx:id="colCartProduct" prefWidth="310.0" text="Product" />
@@ -125,20 +147,34 @@
</children> </children>
</HBox> </HBox>
<Region HBox.hgrow="ALWAYS" /> <Region HBox.hgrow="ALWAYS" />
<HBox alignment="CENTER_LEFT" spacing="8.0"> <VBox spacing="2.0">
<children> <children>
<Label text="Total:" textFill="#2c3e50"> <HBox spacing="8.0" alignment="CENTER_LEFT">
<font> <children>
<Font name="System Bold" size="13.0" /> <Label text="Subtotal:" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
</font> <Label fx:id="lblSubtotal" text="" textFill="#2c3e50"><font><Font size="12.0" /></font></Label>
</Label> </children>
<Label fx:id="lblCartTotal" text="\$0.00" textFill="#2c3e50"> </HBox>
<font> <HBox fx:id="hbCouponDiscount" spacing="8.0" alignment="CENTER_LEFT" visible="false" managed="false">
<Font name="System Bold" size="16.0" /> <children>
</font> <Label fx:id="lblCouponDiscountLabel" text="Coupon Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</Label> <Label fx:id="lblCouponDiscount" text="-$0.00" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox fx:id="hbLoyaltyDiscount" spacing="8.0" alignment="CENTER_LEFT" visible="false" managed="false">
<children>
<Label fx:id="lblLoyaltyDiscountLabel" text="Loyalty Discount:" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
<Label fx:id="lblLoyaltyDiscount" text="-$0.00" textFill="#27ae60"><font><Font size="12.0" /></font></Label>
</children>
</HBox>
<HBox spacing="8.0" alignment="CENTER_LEFT">
<children>
<Label text="Total:" textFill="#2c3e50"><font><Font name="System Bold" size="13.0" /></font></Label>
<Label fx:id="lblCartTotal" text="" textFill="#2c3e50"><font><Font name="System Bold" size="16.0" /></font></Label>
</children>
</HBox>
</children> </children>
</HBox> </VBox>
<FlowPane hgap="8.0" prefWrapLength="220.0" vgap="8.0"> <FlowPane hgap="8.0" prefWrapLength="220.0" vgap="8.0">
<children> <children>
<Button fx:id="btnClearCart" mnemonicParsing="false" onAction="#btnClearCart" prefHeight="36.0" style="-fx-background-color: transparent; -fx-border-color: #FF6b6b; -fx-border-radius: 8; -fx-cursor: hand;" text="Clear" textFill="#FF6b6b"> <Button fx:id="btnClearCart" mnemonicParsing="false" onAction="#btnClearCart" prefHeight="36.0" style="-fx-background-color: transparent; -fx-border-color: #FF6b6b; -fx-border-radius: 8; -fx-cursor: hand;" text="Clear" textFill="#FF6b6b">
@@ -181,6 +217,7 @@
<TableColumn fx:id="colSaleId" minWidth="50.0" prefWidth="60.0" text="ID" /> <TableColumn fx:id="colSaleId" minWidth="50.0" prefWidth="60.0" text="ID" />
<TableColumn fx:id="colSaleDate" minWidth="150.0" prefWidth="170.0" text="Date" /> <TableColumn fx:id="colSaleDate" minWidth="150.0" prefWidth="170.0" text="Date" />
<TableColumn fx:id="colEmployeeName" minWidth="150.0" prefWidth="160.0" text="Employee" /> <TableColumn fx:id="colEmployeeName" minWidth="150.0" prefWidth="160.0" text="Employee" />
<TableColumn fx:id="colCustomerName" minWidth="120.0" prefWidth="140.0" text="Customer" />
<TableColumn fx:id="colServiceProduct" minWidth="260.0" prefWidth="320.0" text="Product" /> <TableColumn fx:id="colServiceProduct" minWidth="260.0" prefWidth="320.0" text="Product" />
<TableColumn fx:id="colSaleQuantity" minWidth="55.0" prefWidth="70.0" text="Qty" /> <TableColumn fx:id="colSaleQuantity" minWidth="55.0" prefWidth="70.0" text="Qty" />
<TableColumn fx:id="colSaleUnitPrice" minWidth="100.0" prefWidth="115.0" text="Unit Price" /> <TableColumn fx:id="colSaleUnitPrice" minWidth="100.0" prefWidth="115.0" text="Unit Price" />