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.analytics to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.purchaseorder 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.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;
exports org.example.petshopdesktop.controllers; 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 @FXML
private Button btnAnalytics; private Button btnAnalytics;
@FXML
private Button btnCoupons;
@FXML @FXML
private Button btnLogout; private Button btnLogout;
@@ -187,6 +190,12 @@ public class MainLayoutController {
updateButtons(btnActivityLogs); updateButtons(btnActivityLogs);
} }
@FXML
void btnCouponsClicked(ActionEvent event) {
loadView("coupon-view.fxml");
updateButtons(btnCoupons);
}
@FXML @FXML
void btnServicesClicked(ActionEvent event) { void btnServicesClicked(ActionEvent event) {
loadView("service-view.fxml"); loadView("service-view.fxml");
@@ -415,6 +424,11 @@ public class MainLayoutController {
btnActivityLogs.setManaged(isAdmin); btnActivityLogs.setManaged(isAdmin);
} }
if (btnCoupons != null) {
btnCoupons.setVisible(isAdmin);
btnCoupons.setManaged(isAdmin);
}
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales"); btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
// Initial chat state and subscription // Initial chat state and subscription
@@ -466,6 +480,7 @@ public class MainLayoutController {
btnStaffAccounts, btnStaffAccounts,
btnAnalytics, btnAnalytics,
btnActivityLogs, btnActivityLogs,
btnCoupons,
btnChat 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();
}
}

View File

@@ -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>

View File

@@ -236,6 +236,14 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" /> <Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding> </padding>
</Button> </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"> <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> <font>

View File

@@ -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>