Add pet product filters
This commit is contained in:
@@ -25,8 +25,9 @@ public class CategoryController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<CategoryResponse>> getAllCategories(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String type,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(categoryService.getAllCategories(q, pageable));
|
||||
return ResponseEntity.ok(categoryService.getAllCategories(q, type, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -25,8 +25,10 @@ public class PetController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<PetResponse>> getAllPets(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String species,
|
||||
@RequestParam(required = false) String status,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(petService.getAllPets(q, pageable));
|
||||
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -25,8 +25,9 @@ public class ProductController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<ProductResponse>> getAllProducts(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) Long categoryId,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(productService.getAllProducts(q, pageable));
|
||||
return ResponseEntity.ok(productService.getAllProducts(q, categoryId, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||
Optional<Category> findByCategoryName(String categoryName);
|
||||
|
||||
@Query("SELECT c FROM Category c WHERE " +
|
||||
"LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Category> searchCategories(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:type IS NULL OR LOWER(c.categoryType) = LOWER(:type))")
|
||||
Page<Category> searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository;
|
||||
public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
|
||||
@Query("SELECT p FROM Pet p WHERE " +
|
||||
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Pet> searchPets(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
|
||||
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository;
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
@Query("SELECT p FROM Product p WHERE " +
|
||||
"LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Product> searchProducts(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
|
||||
Page<Product> searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,9 @@ public class CategoryService {
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public Page<CategoryResponse> getAllCategories(String query, Pageable pageable) {
|
||||
Page<Category> categories;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
categories = categoryRepository.searchCategories(query, pageable);
|
||||
} else {
|
||||
categories = categoryRepository.findAll(pageable);
|
||||
}
|
||||
return categories.map(this::mapToResponse);
|
||||
public Page<CategoryResponse> getAllCategories(String query, String type, Pageable pageable) {
|
||||
return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public CategoryResponse getCategoryById(Long id) {
|
||||
@@ -80,4 +75,12 @@ public class CategoryService {
|
||||
category.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,9 @@ public class PetService {
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
}
|
||||
|
||||
public Page<PetResponse> getAllPets(String query, Pageable pageable) {
|
||||
Page<Pet> pets;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
pets = petRepository.searchPets(query, pageable);
|
||||
} else {
|
||||
pets = petRepository.findAll(pageable);
|
||||
}
|
||||
return pets.map(this::mapToResponse);
|
||||
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
|
||||
return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public PetResponse getPetById(Long id) {
|
||||
@@ -182,6 +177,14 @@ public class PetService {
|
||||
return status == null ? "" : status.trim();
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private PetResponse mapToResponse(Pet pet) {
|
||||
return new PetResponse(
|
||||
pet.getPetId(),
|
||||
|
||||
@@ -32,14 +32,9 @@ public class ProductService {
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
}
|
||||
|
||||
public Page<ProductResponse> getAllProducts(String query, Pageable pageable) {
|
||||
Page<Product> products;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
products = productRepository.searchProducts(query, pageable);
|
||||
} else {
|
||||
products = productRepository.findAll(pageable);
|
||||
}
|
||||
return products.map(this::mapToResponse);
|
||||
public Page<ProductResponse> getAllProducts(String query, Long categoryId, Pageable pageable) {
|
||||
return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public ProductResponse getProductById(Long id) {
|
||||
@@ -168,4 +163,12 @@ public class ProductService {
|
||||
|
||||
public record ImagePayload(Resource resource, MediaType mediaType) {
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,17 @@ public class PetApi {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public List<PetResponse> listPets(String query) throws Exception {
|
||||
public List<PetResponse> listPets(String query, String species, String status) throws Exception {
|
||||
String path = "/api/v1/pets?page=0&size=1000";
|
||||
if (query != null && !query.isEmpty()) {
|
||||
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (species != null && !species.isEmpty()) {
|
||||
path += "&species=" + URLEncoder.encode(species, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (status != null && !status.isEmpty()) {
|
||||
path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8);
|
||||
}
|
||||
String response = apiClient.getRawResponse(path);
|
||||
PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
||||
response,
|
||||
@@ -40,6 +46,10 @@ public class PetApi {
|
||||
return pageResponse.getContent();
|
||||
}
|
||||
|
||||
public List<PetResponse> listPets(String query) throws Exception {
|
||||
return listPets(query, null, null);
|
||||
}
|
||||
|
||||
public PetResponse createPet(PetRequest request) throws Exception {
|
||||
return apiClient.post("/api/v1/pets", request, PetResponse.class);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,14 @@ public class ProductApi {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public List<ProductResponse> listProducts(String query) throws Exception {
|
||||
public List<ProductResponse> listProducts(String query, Long categoryId) throws Exception {
|
||||
String path = "/api/v1/products?page=0&size=1000";
|
||||
if (query != null && !query.isEmpty()) {
|
||||
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (categoryId != null) {
|
||||
path += "&categoryId=" + categoryId;
|
||||
}
|
||||
String response = apiClient.getRawResponse(path);
|
||||
PageResponse<ProductResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
||||
response,
|
||||
@@ -40,6 +43,10 @@ public class ProductApi {
|
||||
return pageResponse.getContent();
|
||||
}
|
||||
|
||||
public List<ProductResponse> listProducts(String query) throws Exception {
|
||||
return listProducts(query, null);
|
||||
}
|
||||
|
||||
public ProductResponse createProduct(ProductRequest request) throws Exception {
|
||||
return apiClient.post("/api/v1/products", request, ProductResponse.class);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ public class PetController {
|
||||
@FXML
|
||||
private TableView<Pet> tvPets;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> cbSpeciesFilter;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> cbStatusFilter;
|
||||
|
||||
@FXML
|
||||
private TextField txtSearch;
|
||||
|
||||
@@ -150,6 +156,12 @@ public class PetController {
|
||||
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
|
||||
configureImageColumn(colPetImage);
|
||||
|
||||
cbSpeciesFilter.setItems(FXCollections.observableArrayList("All Species", "Dog", "Cat", "Bird", "Fish", "Rabbit", "Hamster"));
|
||||
cbSpeciesFilter.getSelectionModel().selectFirst();
|
||||
|
||||
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending"));
|
||||
cbStatusFilter.getSelectionModel().selectFirst();
|
||||
|
||||
displayPets();
|
||||
|
||||
tvPets.getSelectionModel().selectedItemProperty().addListener(
|
||||
@@ -159,9 +171,12 @@ public class PetController {
|
||||
});
|
||||
|
||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
displayFilteredPet(newValue);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
cbSpeciesFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters());
|
||||
cbStatusFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters());
|
||||
|
||||
//EventListener for DELETE key
|
||||
tvPets.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
||||
@@ -173,12 +188,14 @@ public class PetController {
|
||||
}
|
||||
|
||||
private void displayFilteredPet(String filter) {
|
||||
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){
|
||||
String species = selectedSpecies();
|
||||
String status = selectedStatus();
|
||||
if ((filter == null || filter.isEmpty()) && species == null && status == null){
|
||||
displayPets();
|
||||
} else {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<PetResponse> pets = PetApi.getInstance().listPets(filter);
|
||||
List<PetResponse> pets = PetApi.getInstance().listPets(filter, species, status);
|
||||
List<Pet> petList = pets.stream()
|
||||
.map(this::mapToPet)
|
||||
.collect(Collectors.toList());
|
||||
@@ -203,7 +220,7 @@ public class PetController {
|
||||
private void displayPets() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<PetResponse> pets = PetApi.getInstance().listPets(null);
|
||||
List<PetResponse> pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus());
|
||||
List<Pet> petList = pets.stream()
|
||||
.map(this::mapToPet)
|
||||
.collect(Collectors.toList());
|
||||
@@ -224,6 +241,20 @@ public class PetController {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void applyFilters() {
|
||||
displayFilteredPet(txtSearch.getText());
|
||||
}
|
||||
|
||||
private String selectedSpecies() {
|
||||
String value = cbSpeciesFilter.getValue();
|
||||
return value == null || value.equals("All Species") ? null : value;
|
||||
}
|
||||
|
||||
private String selectedStatus() {
|
||||
String value = cbStatusFilter.getValue();
|
||||
return value == null || value.equals("All Statuses") ? null : value;
|
||||
}
|
||||
|
||||
private void openDialog(Pet pet, String mode){
|
||||
//Get new view
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml"));
|
||||
|
||||
@@ -16,6 +16,8 @@ import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.DTOs.ProductDTO;
|
||||
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
||||
import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
@@ -62,6 +64,9 @@ public class ProductController {
|
||||
@FXML
|
||||
private TableView<ProductDTO> tvProducts;
|
||||
|
||||
@FXML
|
||||
private ComboBox<DropdownOption> cbCategoryFilter;
|
||||
|
||||
@FXML
|
||||
private TextField txtSearch;
|
||||
|
||||
@@ -87,6 +92,7 @@ public class ProductController {
|
||||
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
|
||||
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc"));
|
||||
configureImageColumn(colProductImage);
|
||||
loadCategoryFilter();
|
||||
|
||||
displayProduct();
|
||||
|
||||
@@ -100,9 +106,11 @@ public class ProductController {
|
||||
|
||||
//EventListener to search when text is changed on searchbar
|
||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
displayFilteredProduct(newValue);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
cbCategoryFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters());
|
||||
|
||||
//EventListener for DELETE key press
|
||||
tvProducts.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
||||
@@ -120,7 +128,7 @@ public class ProductController {
|
||||
private void displayProduct(){
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<ProductResponse> products = ProductApi.getInstance().listProducts(null);
|
||||
List<ProductResponse> products = ProductApi.getInstance().listProducts(null, selectedCategoryId());
|
||||
List<ProductDTO> productDTOs = products.stream()
|
||||
.map(this::mapToProductDTO)
|
||||
.collect(Collectors.toList());
|
||||
@@ -222,12 +230,12 @@ public class ProductController {
|
||||
* @param filter word to filter table
|
||||
*/
|
||||
private void displayFilteredProduct(String filter){
|
||||
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){
|
||||
if ((txtSearch.getText() == null || txtSearch.getText().isEmpty()) && selectedCategoryId() == null){
|
||||
displayProduct();
|
||||
} else {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<ProductResponse> products = ProductApi.getInstance().listProducts(filter);
|
||||
List<ProductResponse> products = ProductApi.getInstance().listProducts(filter, selectedCategoryId());
|
||||
List<ProductDTO> productDTOs = products.stream()
|
||||
.map(this::mapToProductDTO)
|
||||
.collect(Collectors.toList());
|
||||
@@ -249,6 +257,37 @@ public class ProductController {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFilters() {
|
||||
displayFilteredProduct(txtSearch.getText());
|
||||
}
|
||||
|
||||
private void loadCategoryFilter() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<DropdownOption> options = new ArrayList<>();
|
||||
DropdownOption all = new DropdownOption();
|
||||
all.setId(null);
|
||||
all.setLabel("All Categories");
|
||||
options.add(all);
|
||||
options.addAll(DropdownApi.getInstance().getCategories());
|
||||
Platform.runLater(() -> {
|
||||
cbCategoryFilter.setItems(FXCollections.observableArrayList(options));
|
||||
cbCategoryFilter.getSelectionModel().selectFirst();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||
"ProductController.loadCategoryFilter",
|
||||
e,
|
||||
"Loading category filter options"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private Long selectedCategoryId() {
|
||||
DropdownOption option = cbCategoryFilter.getValue();
|
||||
return option == null ? null : option.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to open the new Dialog for edit or adding
|
||||
* depending on the mode given
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<?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?>
|
||||
@@ -59,13 +60,15 @@
|
||||
<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 Pets..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font>
|
||||
<Font size="15.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
</children>
|
||||
</HBox>
|
||||
<TextField fx:id="txtSearch" prefHeight="31.0" prefWidth="150.0" promptText="Search Pets..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font>
|
||||
<Font size="15.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<ComboBox fx:id="cbSpeciesFilter" prefWidth="150.0" promptText="Species" />
|
||||
<ComboBox fx:id="cbStatusFilter" prefWidth="150.0" promptText="Status" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="colPetId" prefWidth="55.0" text="ID" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<?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?>
|
||||
@@ -58,13 +59,14 @@
|
||||
<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 products..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font>
|
||||
<Font size="15.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
</children>
|
||||
</HBox>
|
||||
<TextField fx:id="txtSearch" prefHeight="31.0" prefWidth="150.0" promptText="Search products..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font>
|
||||
<Font size="15.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<ComboBox fx:id="cbCategoryFilter" prefWidth="180.0" promptText="Category" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="colProductId" prefWidth="55.0" text="ID" />
|
||||
|
||||
Reference in New Issue
Block a user