diff --git a/backend/src/main/java/com/petshop/backend/controller/CategoryController.java b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java index fb938dd9..bbce6ec9 100644 --- a/backend/src/main/java/com/petshop/backend/controller/CategoryController.java +++ b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -25,8 +25,9 @@ public class CategoryController { @GetMapping public ResponseEntity> 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}") diff --git a/backend/src/main/java/com/petshop/backend/controller/PetController.java b/backend/src/main/java/com/petshop/backend/controller/PetController.java index 259b0f89..9fb93f0b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetController.java @@ -25,8 +25,10 @@ public class PetController { @GetMapping public ResponseEntity> 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}") diff --git a/backend/src/main/java/com/petshop/backend/controller/ProductController.java b/backend/src/main/java/com/petshop/backend/controller/ProductController.java index 6531c72d..418d113a 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ProductController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ProductController.java @@ -25,8 +25,9 @@ public class ProductController { @GetMapping public ResponseEntity> 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}") diff --git a/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java index ceb30e53..b1871d1f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java @@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository { Optional 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 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 searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index fa9aa5e7..a474fa8b 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository; public interface PetRepository extends JpaRepository { @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 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 searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java index 94f7fb81..7122e6ea 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java @@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository; public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE " + - "LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))") - Page 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 searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java index 3da87dd0..1bf175c7 100644 --- a/backend/src/main/java/com/petshop/backend/service/CategoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -20,14 +20,9 @@ public class CategoryService { this.categoryRepository = categoryRepository; } - public Page getAllCategories(String query, Pageable pageable) { - Page categories; - if (query != null && !query.trim().isEmpty()) { - categories = categoryRepository.searchCategories(query, pageable); - } else { - categories = categoryRepository.findAll(pageable); - } - return categories.map(this::mapToResponse); + public Page 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; + } } diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 5c35dfd1..4672ee85 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -33,14 +33,9 @@ public class PetService { this.catalogImageStorageService = catalogImageStorageService; } - public Page getAllPets(String query, Pageable pageable) { - Page pets; - if (query != null && !query.trim().isEmpty()) { - pets = petRepository.searchPets(query, pageable); - } else { - pets = petRepository.findAll(pageable); - } - return pets.map(this::mapToResponse); + public Page 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(), diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 0473a8eb..8d4b1da2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -32,14 +32,9 @@ public class ProductService { this.catalogImageStorageService = catalogImageStorageService; } - public Page getAllProducts(String query, Pageable pageable) { - Page products; - if (query != null && !query.trim().isEmpty()) { - products = productRepository.searchProducts(query, pageable); - } else { - products = productRepository.findAll(pageable); - } - return products.map(this::mapToResponse); + public Page 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; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java index f96372e8..92fa28c9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -24,11 +24,17 @@ public class PetApi { return INSTANCE; } - public List listPets(String query) throws Exception { + public List 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 pageResponse = apiClient.getObjectMapper().readValue( response, @@ -40,6 +46,10 @@ public class PetApi { return pageResponse.getContent(); } + public List 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); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java index 4b8c89f4..c5ec100d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java @@ -24,11 +24,14 @@ public class ProductApi { return INSTANCE; } - public List listProducts(String query) throws Exception { + public List 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 pageResponse = apiClient.getObjectMapper().readValue( response, @@ -40,6 +43,10 @@ public class ProductApi { return pageResponse.getContent(); } + public List 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); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 88928cb5..7c5d13c8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -64,6 +64,12 @@ public class PetController { @FXML private TableView tvPets; + @FXML + private ComboBox cbSpeciesFilter; + + @FXML + private ComboBox cbStatusFilter; + @FXML private TextField txtSearch; @@ -150,6 +156,12 @@ public class PetController { colPetPrice.setCellValueFactory(new PropertyValueFactory("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 pets = PetApi.getInstance().listPets(filter); + List pets = PetApi.getInstance().listPets(filter, species, status); List petList = pets.stream() .map(this::mapToPet) .collect(Collectors.toList()); @@ -203,7 +220,7 @@ public class PetController { private void displayPets() { new Thread(() -> { try { - List pets = PetApi.getInstance().listPets(null); + List pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus()); List 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")); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index 053e0105..8bb51296 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -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 tvProducts; + @FXML + private ComboBox cbCategoryFilter; + @FXML private TextField txtSearch; @@ -87,6 +92,7 @@ public class ProductController { colProductCategory.setCellValueFactory(new PropertyValueFactory("categoryName")); colProductDesc.setCellValueFactory(new PropertyValueFactory("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 products = ProductApi.getInstance().listProducts(null); + List products = ProductApi.getInstance().listProducts(null, selectedCategoryId()); List 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 products = ProductApi.getInstance().listProducts(filter); + List products = ProductApi.getInstance().listProducts(filter, selectedCategoryId()); List 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 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 diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index b4fcc592..c8e77504 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -2,6 +2,7 @@ + @@ -59,13 +60,15 @@ - - - - - - - + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml index b5392be9..eeff74b4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml @@ -2,6 +2,7 @@ + @@ -58,13 +59,14 @@ - - - - - - - + + + + + + + +