Azure deployment setup #297
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.DatePicker?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="480.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.CouponDialogController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
|
||||
<padding>
|
||||
<Insets left="15.0" right="15.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<VBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="299.0">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<Label fx:id="lblMode" text="Add Coupon" textFill="WHITE">
|
||||
<font>
|
||||
<Font name="Comic Sans MS Bold" size="30.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label fx:id="lblCouponId" text="" textFill="#ffe66d">
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
<VBox.margin>
|
||||
<Insets top="10.0" />
|
||||
</VBox.margin>
|
||||
</Label>
|
||||
</children>
|
||||
</VBox>
|
||||
<Region prefHeight="79.0" prefWidth="151.0" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="btnCancel" mnemonicParsing="false" style="-fx-background-color: #E74c3c; -fx-cursor: hand; -fx-background-radius: 8;" text="Cancel" textFill="WHITE">
|
||||
<font>
|
||||
<Font name="System Bold" size="14.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnSave" mnemonicParsing="false" style="-fx-background-color: #3fe06a; -fx-cursor: hand; -fx-background-radius: 8;" text="Save" textFill="WHITE">
|
||||
<font>
|
||||
<Font name="System Bold" size="14.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
<VBox prefHeight="370.0" prefWidth="750.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #5580b5; -fx-border-radius: 14;">
|
||||
<padding>
|
||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<GridPane hgap="25.0" VBox.vgrow="ALWAYS">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
|
||||
<children>
|
||||
<Label text="Coupon Code:" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<TextField fx:id="txtCode" promptText="e.g. SAVE10" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
|
||||
<padding><Insets bottom="7.0" left="10.0" right="10.0" top="7.0" /></padding>
|
||||
</TextField>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1">
|
||||
<children>
|
||||
<Label text="Discount Type:" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<ComboBox fx:id="cbDiscountType" prefHeight="29.0" prefWidth="336.0" promptText="Select type" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
|
||||
<padding><Insets bottom="3.0" left="10.0" right="10.0" top="3.0" /></padding>
|
||||
</ComboBox>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="1">
|
||||
<children>
|
||||
<Label text="Discount Value:" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<TextField fx:id="txtDiscountValue" promptText="e.g. 10.00" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
|
||||
<padding><Insets bottom="7.0" left="10.0" right="10.0" top="7.0" /></padding>
|
||||
</TextField>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<children>
|
||||
<Label text="Min Order Amount (optional):" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<TextField fx:id="txtMinOrder" promptText="e.g. 50.00" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
|
||||
<padding><Insets bottom="7.0" left="10.0" right="10.0" top="7.0" /></padding>
|
||||
</TextField>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="2">
|
||||
<children>
|
||||
<Label text="Starts At (optional):" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<DatePicker fx:id="dpStartsAt" prefHeight="29.0" prefWidth="336.0" promptText="YYYY-MM-DD" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
|
||||
<padding><Insets bottom="3.0" left="10.0" right="10.0" top="3.0" /></padding>
|
||||
</DatePicker>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||
<children>
|
||||
<Label text="Ends At (optional):" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<DatePicker fx:id="dpEndsAt" prefHeight="29.0" prefWidth="336.0" promptText="YYYY-MM-DD" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
|
||||
<padding><Insets bottom="3.0" left="10.0" right="10.0" top="3.0" /></padding>
|
||||
</DatePicker>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="3">
|
||||
<children>
|
||||
<Label text="Usage Limit (optional):" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<TextField fx:id="txtUsageLimit" promptText="e.g. 100" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
|
||||
<padding><Insets bottom="7.0" left="10.0" right="10.0" top="7.0" /></padding>
|
||||
</TextField>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox alignment="CENTER_LEFT" prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
|
||||
<children>
|
||||
<Label text="Active:" textFill="#2c3e50">
|
||||
<font><Font name="System Bold" size="16.0" /></font>
|
||||
</Label>
|
||||
<CheckBox fx:id="chkActive" selected="true" text="Coupon is active" />
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
<VBox.margin>
|
||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||
</VBox.margin>
|
||||
</GridPane>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -236,6 +236,14 @@
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnCoupons" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnCouponsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Coupons" textFill="#cbd5e1">
|
||||
<font>
|
||||
<Font name="System" size="12.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
|
||||
<Button fx:id="btnLogout" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnLogoutClicked" style="-fx-background-color: rgba(255,255,255,0.08); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Logout" textFill="#e2e8f0">
|
||||
<font>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.CouponController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" prefHeight="100.0" prefWidth="200.0" spacing="20.0">
|
||||
<children>
|
||||
<Label text="Coupons" textFill="#2c3e50">
|
||||
<font>
|
||||
<Font name="System Bold" size="30.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Region prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="btnAdd" mnemonicParsing="false" onAction="#btnAddClicked" style="-fx-background-color: #FF6B6B; -fx-cursor: hand; -fx-background-radius: 8;" text="Add New" textFill="WHITE">
|
||||
<font><Font name="System Bold" size="14.0" /></font>
|
||||
<padding><Insets bottom="12.0" left="24.0" right="24.0" top="12.0" /></padding>
|
||||
</Button>
|
||||
<Button fx:id="btnEdit" mnemonicParsing="false" onAction="#btnEditClicked" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Edit" textFill="WHITE">
|
||||
<font><Font name="System Bold" size="14.0" /></font>
|
||||
<padding><Insets bottom="12.0" left="24.0" right="24.0" top="12.0" /></padding>
|
||||
</Button>
|
||||
<Button fx:id="btnDelete" mnemonicParsing="false" onAction="#btnDeleteClicked" style="-fx-background-color: #E74C3C; -fx-cursor: hand; -fx-background-radius: 8;" text="Delete" textFill="WHITE">
|
||||
<font><Font name="System Bold" size="14.0" /></font>
|
||||
<padding><Insets bottom="12.0" left="24.0" right="24.0" top="12.0" /></padding>
|
||||
</Button>
|
||||
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
|
||||
<font><Font name="System Bold" size="14.0" /></font>
|
||||
<padding><Insets bottom="12.0" left="24.0" right="24.0" top="12.0" /></padding>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" prefHeight="37.0" prefWidth="727.0" spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-radius: 14;">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<TextField fx:id="txtSearch" prefHeight="31.0" prefWidth="150.0" promptText="Search by code..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font><Font size="15.0" /></font>
|
||||
</TextField>
|
||||
<ComboBox fx:id="cbStatusFilter" prefWidth="140.0" promptText="Status" />
|
||||
<ComboBox fx:id="cbTypeFilter" prefWidth="140.0" promptText="Discount Type" />
|
||||
</children>
|
||||
</HBox>
|
||||
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="false">
|
||||
<font><Font size="13.0" /></font>
|
||||
</Label>
|
||||
<TableView fx:id="tvCoupons" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="colId" prefWidth="55.0" text="ID" />
|
||||
<TableColumn fx:id="colCode" prefWidth="120.0" text="Code" />
|
||||
<TableColumn fx:id="colDiscount" prefWidth="130.0" text="Discount" />
|
||||
<TableColumn fx:id="colMinOrder" prefWidth="110.0" text="Min Order" />
|
||||
<TableColumn fx:id="colStatus" prefWidth="90.0" text="Status" />
|
||||
<TableColumn fx:id="colStartsAt" prefWidth="110.0" text="Starts At" />
|
||||
<TableColumn fx:id="colEndsAt" prefWidth="110.0" text="Ends At" />
|
||||
<TableColumn fx:id="colUsageLimit" prefWidth="100.0" text="Usage Limit" />
|
||||
</columns>
|
||||
</TableView>
|
||||
</children>
|
||||
</VBox>
|
||||
Reference in New Issue
Block a user