added coupons to desktop app

This commit is contained in:
Alex
2026-04-12 23:47:22 -06:00
parent 2172bede74
commit 326182aeef
10 changed files with 849 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ module org.example.petshopdesktop {
opens org.example.petshopdesktop.api.dto.analytics to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.purchaseorder to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.activity to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.coupon to com.fasterxml.jackson.databind, javafx.base;
exports org.example.petshopdesktop;
exports org.example.petshopdesktop.controllers;

View File

@@ -0,0 +1,38 @@
package org.example.petshopdesktop.api.dto.coupon;
import java.math.BigDecimal;
public class CouponRequest {
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private String startsAt;
private String endsAt;
private Integer usageLimit;
public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public String getDiscountType() { return discountType; }
public void setDiscountType(String discountType) { this.discountType = discountType; }
public BigDecimal getDiscountValue() { return discountValue; }
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public String getStartsAt() { return startsAt; }
public void setStartsAt(String startsAt) { this.startsAt = startsAt; }
public String getEndsAt() { return endsAt; }
public void setEndsAt(String endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
}

View File

@@ -0,0 +1,52 @@
package org.example.petshopdesktop.api.dto.coupon;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.math.BigDecimal;
@JsonIgnoreProperties(ignoreUnknown = true)
public class CouponResponse {
private Long couponId;
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private String startsAt;
private String endsAt;
private Integer usageLimit;
private String createdAt;
private String updatedAt;
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public String getDiscountType() { return discountType; }
public void setDiscountType(String discountType) { this.discountType = discountType; }
public BigDecimal getDiscountValue() { return discountValue; }
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public String getStartsAt() { return startsAt; }
public void setStartsAt(String startsAt) { this.startsAt = startsAt; }
public String getEndsAt() { return endsAt; }
public void setEndsAt(String endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public String getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,42 @@
package org.example.petshopdesktop.api.endpoints;
import com.fasterxml.jackson.core.type.TypeReference;
import org.example.petshopdesktop.api.ApiClient;
import org.example.petshopdesktop.api.dto.common.PageResponse;
import org.example.petshopdesktop.api.dto.coupon.CouponRequest;
import org.example.petshopdesktop.api.dto.coupon.CouponResponse;
import java.util.List;
public class CouponApi {
private static final CouponApi INSTANCE = new CouponApi();
private final ApiClient apiClient;
private CouponApi() {
apiClient = ApiClient.getInstance();
}
public static CouponApi getInstance() {
return INSTANCE;
}
public List<CouponResponse> listCoupons() throws Exception {
String response = apiClient.getRawResponse("/api/v1/coupons?page=0&size=1000");
PageResponse<CouponResponse> page = apiClient.getObjectMapper()
.readValue(response, new TypeReference<PageResponse<CouponResponse>>() {});
List<CouponResponse> content = page.getContent();
return content != null ? content : java.util.Collections.emptyList();
}
public CouponResponse createCoupon(CouponRequest request) throws Exception {
return apiClient.post("/api/v1/coupons", request, CouponResponse.class);
}
public CouponResponse updateCoupon(Long id, CouponRequest request) throws Exception {
return apiClient.put("/api/v1/coupons/" + id, request, CouponResponse.class);
}
public void deleteCoupon(Long id) throws Exception {
apiClient.delete("/api/v1/coupons/" + id);
}
}

View File

@@ -0,0 +1,246 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.coupon.CouponResponse;
import org.example.petshopdesktop.api.endpoints.CouponApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.CouponDialogController;
import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TableViewSupport;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class CouponController {
@FXML private Button btnAdd;
@FXML private Button btnEdit;
@FXML private Button btnDelete;
@FXML private Button btnRefresh;
@FXML private Label lblStatus;
@FXML private TextField txtSearch;
@FXML private ComboBox<String> cbStatusFilter;
@FXML private ComboBox<String> cbTypeFilter;
@FXML private TableView<CouponResponse> tvCoupons;
@FXML private TableColumn<CouponResponse, Long> colId;
@FXML private TableColumn<CouponResponse, String> colCode;
@FXML private TableColumn<CouponResponse, String> colDiscount;
@FXML private TableColumn<CouponResponse, String> colMinOrder;
@FXML private TableColumn<CouponResponse, String> colStatus;
@FXML private TableColumn<CouponResponse, String> colStartsAt;
@FXML private TableColumn<CouponResponse, String> colEndsAt;
@FXML private TableColumn<CouponResponse, String> colUsageLimit;
private final ObservableList<CouponResponse> allCoupons = FXCollections.observableArrayList();
@FXML
void initialize() {
btnEdit.setDisable(true);
btnDelete.setDisable(true);
tvCoupons.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colId.setCellValueFactory(new PropertyValueFactory<>("couponId"));
colCode.setCellValueFactory(new PropertyValueFactory<>("couponCode"));
colDiscount.setCellValueFactory(data -> {
CouponResponse c = data.getValue();
String formatted = formatDiscount(c);
return new javafx.beans.property.SimpleStringProperty(formatted);
});
colMinOrder.setCellValueFactory(data -> {
BigDecimal min = data.getValue().getMinOrderAmount();
return new javafx.beans.property.SimpleStringProperty(
min != null ? "$" + min.toPlainString() : "");
});
colStatus.setCellValueFactory(data -> {
Boolean active = data.getValue().getActive();
return new javafx.beans.property.SimpleStringProperty(
Boolean.TRUE.equals(active) ? "Active" : "Inactive");
});
colStartsAt.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(formatDate(data.getValue().getStartsAt())));
colEndsAt.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(formatDate(data.getValue().getEndsAt())));
colUsageLimit.setCellValueFactory(data -> {
Integer limit = data.getValue().getUsageLimit();
return new javafx.beans.property.SimpleStringProperty(limit != null ? limit.toString() : "Unlimited");
});
cbStatusFilter.setItems(FXCollections.observableArrayList("All", "Active", "Inactive"));
cbStatusFilter.setValue("All");
cbTypeFilter.setItems(FXCollections.observableArrayList("All", "FIXED", "PERCENT"));
cbTypeFilter.setValue("All");
tvCoupons.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
boolean hasSelection = newVal != null;
btnEdit.setDisable(!hasSelection);
btnDelete.setDisable(!hasSelection);
});
txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
cbStatusFilter.valueProperty().addListener((obs, oldVal, newVal) -> applyFilters());
cbTypeFilter.valueProperty().addListener((obs, oldVal, newVal) -> applyFilters());
TableViewSupport.installDoubleClickAction(tvCoupons, selected -> openDialog(selected, "Edit"));
tvCoupons.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE
&& tvCoupons.getSelectionModel().getSelectedItem() != null) {
btnDeleteClicked(null);
}
});
loadCoupons();
}
@FXML
void btnAddClicked(ActionEvent event) {
openDialog(null, "Add");
}
@FXML
void btnEditClicked(ActionEvent event) {
CouponResponse selected = tvCoupons.getSelectionModel().getSelectedItem();
if (selected != null) {
openDialog(selected, "Edit");
}
}
@FXML
void btnDeleteClicked(ActionEvent event) {
List<CouponResponse> selected = tvCoupons.getSelectionModel().getSelectedItems();
if (selected.isEmpty()) return;
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Confirm Delete");
question.setContentText(selected.size() == 1
? "Are you sure you want to delete this coupon?"
: "Are you sure you want to delete " + selected.size() + " coupons?");
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
Optional<ButtonType> result = question.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selected.stream().map(CouponResponse::getCouponId).collect(Collectors.toList());
try {
for (Long id : ids) {
CouponApi.getInstance().deleteCoupon(id);
}
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Deleted");
alert.setContentText("Successfully deleted " + ids.size() + " coupon(s)");
alert.showAndWait();
} catch (Exception e) {
ActivityLogger.getInstance().logException("CouponController.btnDeleteClicked", e, "Deleting coupons");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Delete Failed");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
loadCoupons();
btnEdit.setDisable(true);
btnDelete.setDisable(true);
}
}
@FXML
void btnRefreshClicked(ActionEvent event) {
txtSearch.clear();
cbStatusFilter.setValue("All");
cbTypeFilter.setValue("All");
tvCoupons.getSortOrder().clear();
loadCoupons();
TableViewSupport.flashStatus(lblStatus, "Refreshed");
}
private void loadCoupons() {
new Thread(() -> {
try {
List<CouponResponse> coupons = CouponApi.getInstance().listCoupons();
Platform.runLater(() -> {
allCoupons.setAll(coupons != null ? coupons : java.util.Collections.emptyList());
applyFilters();
});
} catch (Exception e) {
ActivityLogger.getInstance().logException("CouponController.loadCoupons", e, "Loading coupons");
Platform.runLater(() -> {
lblStatus.setText("Failed to load coupons: " + e.getMessage());
lblStatus.setVisible(true);
lblStatus.setManaged(true);
});
}
}).start();
}
private void applyFilters() {
String search = txtSearch.getText() == null ? "" : txtSearch.getText().trim().toLowerCase();
String status = cbStatusFilter.getValue();
String type = cbTypeFilter.getValue();
List<CouponResponse> filtered = allCoupons.stream()
.filter(c -> search.isEmpty() || (c.getCouponCode() != null
&& c.getCouponCode().toLowerCase().contains(search)))
.filter(c -> "All".equals(status)
|| ("Active".equals(status) && Boolean.TRUE.equals(c.getActive()))
|| ("Inactive".equals(status) && !Boolean.TRUE.equals(c.getActive())))
.filter(c -> "All".equals(type) || type.equals(c.getDiscountType()))
.collect(Collectors.toList());
tvCoupons.setItems(FXCollections.observableArrayList(filtered));
}
private void openDialog(CouponResponse coupon, String mode) {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/example/petshopdesktop/dialogviews/coupon-dialog-view.fxml"));
Scene scene;
try {
scene = new Scene(loader.load());
} catch (IOException e) {
ActivityLogger.getInstance().logException("CouponController.openDialog", e, "Loading coupon dialog");
throw new RuntimeException(e);
}
CouponDialogController controller = loader.getController();
controller.setMode(mode);
if ("Edit".equals(mode)) {
controller.displayCouponDetails(coupon);
}
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle("Add".equals(mode) ? "Add Coupon" : "Edit Coupon");
stage.setScene(scene);
stage.showAndWait();
loadCoupons();
btnEdit.setDisable(true);
btnDelete.setDisable(true);
}
private String formatDiscount(CouponResponse c) {
if (c.getDiscountValue() == null) return "";
if ("PERCENT".equals(c.getDiscountType())) {
return c.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF";
}
return "$" + c.getDiscountValue().toPlainString() + " OFF";
}
private String formatDate(String isoDate) {
if (isoDate == null || isoDate.isBlank()) return "";
return isoDate.length() >= 10 ? isoDate.substring(0, 10) : isoDate;
}
}

View File

@@ -91,6 +91,9 @@ public class MainLayoutController {
@FXML
private Button btnAnalytics;
@FXML
private Button btnCoupons;
@FXML
private Button btnLogout;
@@ -187,6 +190,12 @@ public class MainLayoutController {
updateButtons(btnActivityLogs);
}
@FXML
void btnCouponsClicked(ActionEvent event) {
loadView("coupon-view.fxml");
updateButtons(btnCoupons);
}
@FXML
void btnServicesClicked(ActionEvent event) {
loadView("service-view.fxml");
@@ -415,6 +424,11 @@ public class MainLayoutController {
btnActivityLogs.setManaged(isAdmin);
}
if (btnCoupons != null) {
btnCoupons.setVisible(isAdmin);
btnCoupons.setManaged(isAdmin);
}
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
// Initial chat state and subscription
@@ -466,6 +480,7 @@ public class MainLayoutController {
btnStaffAccounts,
btnAnalytics,
btnActivityLogs,
btnCoupons,
btnChat
};

View File

@@ -0,0 +1,203 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.coupon.CouponRequest;
import org.example.petshopdesktop.api.dto.coupon.CouponResponse;
import org.example.petshopdesktop.api.endpoints.CouponApi;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
import java.time.LocalDate;
public class CouponDialogController {
@FXML private Button btnSave;
@FXML private Button btnCancel;
@FXML private Label lblMode;
@FXML private Label lblCouponId;
@FXML private TextField txtCode;
@FXML private ComboBox<String> cbDiscountType;
@FXML private TextField txtDiscountValue;
@FXML private TextField txtMinOrder;
@FXML private DatePicker dpStartsAt;
@FXML private DatePicker dpEndsAt;
@FXML private TextField txtUsageLimit;
@FXML private CheckBox chkActive;
private String mode;
private Long couponId;
@FXML
void initialize() {
cbDiscountType.setItems(FXCollections.observableArrayList("FIXED", "PERCENT"));
dpEndsAt.setDisable(true);
dpStartsAt.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
@Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
setDisable(empty || item.isBefore(LocalDate.now()));
}
});
dpStartsAt.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null) {
dpEndsAt.setValue(null);
dpEndsAt.setDisable(true);
} else {
dpEndsAt.setDisable(false);
dpEndsAt.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
@Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
setDisable(empty || !item.isAfter(newVal));
}
});
if (dpEndsAt.getValue() != null && !dpEndsAt.getValue().isAfter(newVal)) {
dpEndsAt.setValue(null);
}
}
});
btnSave.setOnMouseClicked(this::handleSave);
btnCancel.setOnMouseClicked(this::handleCancel);
}
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Coupon");
lblCouponId.setVisible("Edit".equals(mode));
lblCouponId.setManaged("Edit".equals(mode));
}
public void displayCouponDetails(CouponResponse coupon) {
if (coupon == null) return;
couponId = coupon.getCouponId();
lblCouponId.setText("ID: " + couponId);
txtCode.setText(coupon.getCouponCode() != null ? coupon.getCouponCode() : "");
cbDiscountType.setValue(coupon.getDiscountType());
txtDiscountValue.setText(coupon.getDiscountValue() != null ? coupon.getDiscountValue().toPlainString() : "");
txtMinOrder.setText(coupon.getMinOrderAmount() != null ? coupon.getMinOrderAmount().toPlainString() : "");
txtUsageLimit.setText(coupon.getUsageLimit() != null ? coupon.getUsageLimit().toString() : "");
chkActive.setSelected(Boolean.TRUE.equals(coupon.getActive()));
if (coupon.getStartsAt() != null && coupon.getStartsAt().length() >= 10) {
dpStartsAt.setValue(LocalDate.parse(coupon.getStartsAt().substring(0, 10)));
}
if (coupon.getEndsAt() != null && coupon.getEndsAt().length() >= 10 && dpStartsAt.getValue() != null) {
dpEndsAt.setValue(LocalDate.parse(coupon.getEndsAt().substring(0, 10)));
}
}
private void handleSave(MouseEvent event) {
String errorMsg = "";
if (txtCode.getText() == null || txtCode.getText().trim().isEmpty()) {
errorMsg += "Coupon Code is required\n";
}
if (cbDiscountType.getValue() == null) {
errorMsg += "Discount Type is required\n";
}
if (txtDiscountValue.getText() == null || txtDiscountValue.getText().trim().isEmpty()) {
errorMsg += "Discount Value is required\n";
} else {
try {
BigDecimal val = new BigDecimal(txtDiscountValue.getText().trim());
if (val.compareTo(BigDecimal.ZERO) <= 0) {
errorMsg += "Discount Value must be greater than 0\n";
}
} catch (NumberFormatException e) {
errorMsg += "Discount Value must be a valid number\n";
}
}
if (!txtMinOrder.getText().trim().isEmpty()) {
try {
BigDecimal min = new BigDecimal(txtMinOrder.getText().trim());
if (min.compareTo(BigDecimal.ZERO) < 0) {
errorMsg += "Min Order Amount must be 0 or greater\n";
}
} catch (NumberFormatException e) {
errorMsg += "Min Order Amount must be a valid number\n";
}
}
if (!txtUsageLimit.getText().trim().isEmpty()) {
try {
int limit = Integer.parseInt(txtUsageLimit.getText().trim());
if (limit <= 0) {
errorMsg += "Usage Limit must be a positive number\n";
}
} catch (NumberFormatException e) {
errorMsg += "Usage Limit must be a valid integer\n";
}
}
if (dpStartsAt.getValue() != null && dpEndsAt.getValue() != null
&& !dpEndsAt.getValue().isAfter(dpStartsAt.getValue())) {
errorMsg += "Ends At must be after Starts At\n";
}
if (!errorMsg.isEmpty()) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
return;
}
CouponRequest request = buildRequest();
try {
if ("Add".equals(mode)) {
CouponApi.getInstance().createCoupon(request);
} else {
CouponApi.getInstance().updateCoupon(couponId, request);
}
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(event);
} catch (Exception e) {
ActivityLogger.getInstance().logException("CouponDialogController.handleSave", e, mode + " coupon");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Operation Error");
alert.setContentText(mode + " failed: " + e.getMessage());
alert.showAndWait();
}
}
private void handleCancel(MouseEvent event) {
closeStage(event);
}
private CouponRequest buildRequest() {
CouponRequest request = new CouponRequest();
request.setCouponCode(txtCode.getText().trim().toUpperCase());
request.setDiscountType(cbDiscountType.getValue());
request.setDiscountValue(new BigDecimal(txtDiscountValue.getText().trim()));
request.setActive(chkActive.isSelected());
if (!txtMinOrder.getText().trim().isEmpty()) {
request.setMinOrderAmount(new BigDecimal(txtMinOrder.getText().trim()));
}
if (!txtUsageLimit.getText().trim().isEmpty()) {
request.setUsageLimit(Integer.parseInt(txtUsageLimit.getText().trim()));
}
if (dpStartsAt.getValue() != null) {
request.setStartsAt(dpStartsAt.getValue().toString() + "T00:00:00");
}
if (dpEndsAt.getValue() != null) {
request.setEndsAt(dpEndsAt.getValue().toString() + "T23:59:59");
}
return request;
}
private void closeStage(MouseEvent event) {
Node node = (Node) event.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
}