Merge pull request #56 from RecentRunner/expand-pets-products-data

Expand catalog
This commit was merged in pull request #56.
This commit is contained in:
2026-03-29 23:08:37 -06:00
committed by GitHub
21 changed files with 491 additions and 62 deletions

View File

@@ -33,6 +33,7 @@ public class DevStackApplication {
docker.ensureDockerAvailable(); docker.ensureDockerAvailable();
docker.startDatabase(); docker.startDatabase();
context = new SpringApplicationBuilder(BackendApplication.class) context = new SpringApplicationBuilder(BackendApplication.class)
.profiles("local")
.initializers(new FlywayContextInitializer()) .initializers(new FlywayContextInitializer())
.run(args); .run(args);
context.addApplicationListener(event -> { context.addApplicationListener(event -> {

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.config;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
@Profile("local")
public class LocalCatalogSeedInitializer implements CommandLineRunner {
private final DataSource dataSource;
private final PetRepository petRepository;
private final ProductRepository productRepository;
public LocalCatalogSeedInitializer(DataSource dataSource, PetRepository petRepository, ProductRepository productRepository) {
this.dataSource = dataSource;
this.petRepository = petRepository;
this.productRepository = productRepository;
}
@Override
public void run(String... args) {
if (petRepository.count() > 6 || productRepository.count() > 6) {
return;
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8",
new ClassPathResource("dev/expand_pet_product_seed.sql"));
populator.execute(dataSource);
}
}

View File

@@ -25,8 +25,9 @@ public class CategoryController {
@GetMapping @GetMapping
public ResponseEntity<Page<CategoryResponse>> getAllCategories( public ResponseEntity<Page<CategoryResponse>> getAllCategories(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) String type,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(categoryService.getAllCategories(q, pageable)); return ResponseEntity.ok(categoryService.getAllCategories(q, type, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -82,6 +82,29 @@ public class DropdownController {
); );
} }
@GetMapping("/product-categories")
public ResponseEntity<List<DropdownOption>> getProductCategories() {
return ResponseEntity.ok(
categoryRepository.findAll().stream()
.filter(c -> "product".equalsIgnoreCase(c.getCategoryType()))
.map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName()))
.collect(Collectors.toList())
);
}
@GetMapping("/pet-species")
public ResponseEntity<List<DropdownOption>> getPetSpecies() {
return ResponseEntity.ok(
petRepository.findAll().stream()
.map(p -> p.getPetSpecies())
.filter(species -> species != null && !species.isBlank())
.distinct()
.sorted(String.CASE_INSENSITIVE_ORDER)
.map(species -> new DropdownOption(null, species))
.collect(Collectors.toList())
);
}
@GetMapping("/stores") @GetMapping("/stores")
public ResponseEntity<List<DropdownOption>> getStores() { public ResponseEntity<List<DropdownOption>> getStores() {
return ResponseEntity.ok( return ResponseEntity.ok(

View File

@@ -25,8 +25,10 @@ public class PetController {
@GetMapping @GetMapping
public ResponseEntity<Page<PetResponse>> getAllPets( public ResponseEntity<Page<PetResponse>> getAllPets(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) String species,
@RequestParam(required = false) String status,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(petService.getAllPets(q, pageable)); return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -25,8 +25,9 @@ public class ProductController {
@GetMapping @GetMapping
public ResponseEntity<Page<ProductResponse>> getAllProducts( public ResponseEntity<Page<ProductResponse>> getAllProducts(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) Long categoryId,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(productService.getAllProducts(q, pageable)); return ResponseEntity.ok(productService.getAllProducts(q, categoryId, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByCategoryName(String categoryName); Optional<Category> findByCategoryName(String categoryName);
@Query("SELECT c FROM Category c WHERE " + @Query("SELECT c FROM Category c WHERE " +
"LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "(:q IS NULL OR LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))") "(:type IS NULL OR LOWER(c.categoryType) = LOWER(:type))")
Page<Category> searchCategories(@Param("q") String query, Pageable pageable); Page<Category> searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable);
} }

View File

@@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository;
public interface PetRepository extends JpaRepository<Pet, Long> { public interface PetRepository extends JpaRepository<Pet, Long> {
@Query("SELECT p FROM Pet p WHERE " + @Query("SELECT p FROM Pet p WHERE " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "(: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 " +
"LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))") "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchPets(@Param("q") String query, Pageable pageable); Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
} }

View File

@@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository;
public interface ProductRepository extends JpaRepository<Product, Long> { public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE " + @Query("SELECT p FROM Product p WHERE " +
"LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))") "(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
Page<Product> searchProducts(@Param("q") String query, Pageable pageable); Page<Product> searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable);
} }

View File

@@ -20,14 +20,9 @@ public class CategoryService {
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
} }
public Page<CategoryResponse> getAllCategories(String query, Pageable pageable) { public Page<CategoryResponse> getAllCategories(String query, String type, Pageable pageable) {
Page<Category> categories; return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable)
if (query != null && !query.trim().isEmpty()) { .map(this::mapToResponse);
categories = categoryRepository.searchCategories(query, pageable);
} else {
categories = categoryRepository.findAll(pageable);
}
return categories.map(this::mapToResponse);
} }
public CategoryResponse getCategoryById(Long id) { public CategoryResponse getCategoryById(Long id) {
@@ -80,4 +75,12 @@ public class CategoryService {
category.getUpdatedAt() category.getUpdatedAt()
); );
} }
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
} }

View File

@@ -33,14 +33,9 @@ public class PetService {
this.catalogImageStorageService = catalogImageStorageService; this.catalogImageStorageService = catalogImageStorageService;
} }
public Page<PetResponse> getAllPets(String query, Pageable pageable) { public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
Page<Pet> pets; return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable)
if (query != null && !query.trim().isEmpty()) { .map(this::mapToResponse);
pets = petRepository.searchPets(query, pageable);
} else {
pets = petRepository.findAll(pageable);
}
return pets.map(this::mapToResponse);
} }
public PetResponse getPetById(Long id) { public PetResponse getPetById(Long id) {
@@ -182,6 +177,14 @@ public class PetService {
return status == null ? "" : status.trim(); 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) { private PetResponse mapToResponse(Pet pet) {
return new PetResponse( return new PetResponse(
pet.getPetId(), pet.getPetId(),

View File

@@ -32,14 +32,9 @@ public class ProductService {
this.catalogImageStorageService = catalogImageStorageService; this.catalogImageStorageService = catalogImageStorageService;
} }
public Page<ProductResponse> getAllProducts(String query, Pageable pageable) { public Page<ProductResponse> getAllProducts(String query, Long categoryId, Pageable pageable) {
Page<Product> products; return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable)
if (query != null && !query.trim().isEmpty()) { .map(this::mapToResponse);
products = productRepository.searchProducts(query, pageable);
} else {
products = productRepository.findAll(pageable);
}
return products.map(this::mapToResponse);
} }
public ProductResponse getProductById(Long id) { public ProductResponse getProductById(Long id) {
@@ -168,4 +163,12 @@ public class ProductService {
public record ImagePayload(Resource resource, MediaType mediaType) { 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;
}
} }

View File

@@ -0,0 +1,226 @@
-- Expand pet and product seed data
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice)
VALUES
('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00),
('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00),
('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00),
('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00),
('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00),
('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00),
('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00),
('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00),
('Leo', 'Dog', 'Husky', 4, 'Available', 749.00),
('Penny', 'Dog', 'Border Collie', 5, 'Available', 808.00),
('Jax', 'Dog', 'German Shepherd', 1, 'Available', 823.00),
('Nala', 'Dog', 'Poodle', 2, 'Available', 871.00),
('Finn', 'Dog', 'Bulldog', 3, 'Available', 447.00),
('Sadie', 'Dog', 'Boxer', 4, 'Available', 495.00),
('Ace', 'Dog', 'Dachshund', 5, 'Available', 510.00),
('Zoe', 'Dog', 'Shih Tzu', 1, 'Available', 547.00),
('Ollie', 'Dog', 'Rottweiler', 2, 'Available', 606.00),
('Millie', 'Dog', 'Corgi', 3, 'Available', 654.00),
('Murphy', 'Dog', 'Husky', 4, 'Available', 691.00),
('Willow', 'Dog', 'Border Collie', 5, 'Available', 728.00),
('Bentley', 'Dog', 'German Shepherd', 1, 'Available', 776.00),
('Lily', 'Dog', 'Poodle', 2, 'Available', 780.00),
('Scout', 'Dog', 'Bulldog', 3, 'Available', 828.00),
('Gracie', 'Dog', 'Boxer', 4, 'Available', 876.00),
('Ranger', 'Dog', 'Dachshund', 5, 'Available', 452.00),
('Hazel', 'Dog', 'Shih Tzu', 1, 'Available', 478.00),
('Moose', 'Dog', 'Rottweiler', 2, 'Available', 515.00),
('Mia', 'Dog', 'Corgi', 3, 'Available', 530.00),
('Simba', 'Cat', 'Ragdoll', 1, 'Available', 295.00),
('Cleo', 'Cat', 'Bengal', 2, 'Available', 321.00),
('Oreo', 'Cat', 'British Shorthair', 3, 'Available', 358.00),
('Pepper', 'Cat', 'Sphynx', 4, 'Available', 417.00),
('Jasper', 'Cat', 'Scottish Fold', 5, 'Available', 454.00),
('Phoebe', 'Cat', 'Russian Blue', 1, 'Available', 491.00),
('Shadow', 'Cat', 'Abyssinian', 2, 'Available', 528.00),
('Mochi', 'Cat', 'Birman', 3, 'Available', 554.00),
('Louie', 'Cat', 'Ragdoll', 4, 'Available', 591.00),
('Ivy', 'Cat', 'Bengal', 5, 'Available', 606.00),
('Theo', 'Cat', 'British Shorthair', 1, 'Available', 654.00),
('Piper', 'Cat', 'Sphynx', 2, 'Available', 251.00),
('Nova', 'Cat', 'Scottish Fold', 3, 'Available', 277.00),
('Archie', 'Cat', 'Russian Blue', 4, 'Available', 336.00),
('Olive', 'Cat', 'Abyssinian', 5, 'Available', 362.00),
('Boots', 'Cat', 'Birman', 1, 'Available', 399.00),
('Maple', 'Cat', 'Ragdoll', 2, 'Available', 436.00),
('Gizmo', 'Cat', 'Bengal', 3, 'Available', 473.00),
('Nina', 'Cat', 'British Shorthair', 4, 'Available', 499.00),
('Salem', 'Cat', 'Sphynx', 5, 'Available', 547.00),
('Stella', 'Cat', 'Scottish Fold', 1, 'Available', 595.00),
('Kiki', 'Cat', 'Russian Blue', 2, 'Available', 610.00),
('Sunny', 'Cat', 'Abyssinian', 3, 'Available', 658.00),
('Mabel', 'Cat', 'Birman', 4, 'Available', 244.00),
('Coco', 'Bird', 'Cockatiel', 1, 'Available', 119.00),
('Sky', 'Bird', 'Parakeet', 2, 'Available', 145.00),
('Sunny', 'Bird', 'Canary', 3, 'Available', 204.00),
('Kiwi', 'Bird', 'Lovebird', 1, 'Available', 230.00),
('Pico', 'Bird', 'Finch', 2, 'Available', 81.00),
('Blue', 'Bird', 'Conure', 3, 'Available', 118.00),
('Rio', 'Bird', 'Cockatiel', 1, 'Available', 144.00),
('Angel', 'Bird', 'Parakeet', 2, 'Available', 203.00),
('Chirpy', 'Bird', 'Canary', 3, 'Available', 251.00),
('Peach', 'Bird', 'Lovebird', 1, 'Available', 91.00),
('Mango', 'Bird', 'Finch', 2, 'Available', 128.00),
('Pearl', 'Bird', 'Conure', 3, 'Available', 165.00),
('Bubbles', 'Fish', 'Goldfish', 1, 'Available', 30.00),
('Splash', 'Fish', 'Betta', 2, 'Available', 56.00),
('Coral', 'Fish', 'Guppy', 1, 'Available', 23.00),
('Neptune', 'Fish', 'Molly', 2, 'Available', 23.00),
('Marlin', 'Fish', 'Tetra', 1, 'Available', 49.00),
('Finley', 'Fish', 'Angelfish', 2, 'Available', 27.00),
('Pebble', 'Fish', 'Goldfish', 1, 'Available', 64.00),
('Wave', 'Fish', 'Betta', 2, 'Available', 20.00),
('Aqua', 'Fish', 'Guppy', 1, 'Available', 57.00),
('Flash', 'Fish', 'Molly', 2, 'Available', 46.00),
('Nemo', 'Fish', 'Tetra', 1, 'Available', 13.00),
('Pearl', 'Fish', 'Angelfish', 2, 'Available', 61.00),
('Thumper', 'Rabbit', 'Mini Lop', 1, 'Adopted', 147.00),
('Clover', 'Rabbit', 'Netherland Dwarf', 2, 'Adopted', 138.00),
('Biscuit', 'Rabbit', 'Lionhead', 3, 'Adopted', 177.00),
('Hazel', 'Rabbit', 'Rex', 1, 'Adopted', 91.00),
('Juniper', 'Rabbit', 'Mini Lop', 2, 'Adopted', 83.00),
('Poppy', 'Rabbit', 'Netherland Dwarf', 3, 'Adopted', 111.00),
('Snowball', 'Rabbit', 'Lionhead', 1, 'Adopted', 172.00),
('Maple', 'Rabbit', 'Rex', 2, 'Adopted', 150.00),
('Peanut', 'Hamster', 'Syrian', 1, 'Adopted', 29.00),
('Nibbles', 'Hamster', 'Dwarf', 2, 'Adopted', 42.00),
('Pumpkin', 'Hamster', 'Roborovski', 1, 'Pending', 49.00),
('Mocha', 'Hamster', 'Syrian', 2, 'Pending', 48.00),
('Buttons', 'Hamster', 'Dwarf', 1, 'Pending', 61.00),
('Teddy', 'Hamster', 'Roborovski', 2, 'Pending', 35.00),
('Pip', 'Hamster', 'Syrian', 1, 'Pending', 39.00),
('Toffee', 'Hamster', 'Dwarf', 2, 'Pending', 52.00),
('Sprout', 'Hamster', 'Roborovski', 1, 'Available', 26.00),
('Bean', 'Hamster', 'Syrian', 2, 'Available', 28.00);
INSERT INTO product (prodName, prodPrice, categoryId, prodDesc)
VALUES
('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'),
('Beef Feast Dog Food', 51.00, 1, 'Nutritious food and treats for dogs'),
('Salmon Blend Dog Food', 17.00, 1, 'Nutritious food and treats for dogs'),
('Lamb Dinner Dog Food', 28.00, 1, 'Nutritious food and treats for dogs'),
('Puppy Starter Kibble', 39.00, 1, 'Nutritious food and treats for dogs'),
('Senior Care Dog Food', 40.00, 1, 'Nutritious food and treats for dogs'),
('Small Breed Kibble', 44.00, 1, 'Nutritious food and treats for dogs'),
('Large Breed Kibble', 57.00, 1, 'Nutritious food and treats for dogs'),
('Grain Free Dog Food', 68.00, 1, 'Nutritious food and treats for dogs'),
('Turkey Rice Formula', 79.00, 1, 'Nutritious food and treats for dogs'),
('Duck Sweet Potato Meal', 25.00, 1, 'Nutritious food and treats for dogs'),
('Venison Protein Blend', 36.00, 1, 'Nutritious food and treats for dogs'),
('Healthy Weight Dog Food', 48.00, 1, 'Nutritious food and treats for dogs'),
('Sensitive Stomach Kibble', 62.00, 1, 'Nutritious food and treats for dogs'),
('High Energy Dog Food', 72.00, 1, 'Nutritious food and treats for dogs'),
('Organic Dog Biscuits', 18.00, 1, 'Nutritious food and treats for dogs'),
('Peanut Butter Dog Treats', 33.00, 1, 'Nutritious food and treats for dogs'),
('Dental Chew Sticks', 38.00, 1, 'Nutritious food and treats for dogs'),
('Training Treat Bites', 48.00, 1, 'Nutritious food and treats for dogs'),
('Soft Chicken Treats', 57.00, 1, 'Nutritious food and treats for dogs'),
('Pumpkin Fiber Treats', 70.00, 1, 'Nutritious food and treats for dogs'),
('Joint Support Biscuits', 14.00, 1, 'Nutritious food and treats for dogs'),
('Mini Breed Dinner', 17.00, 1, 'Nutritious food and treats for dogs'),
('Farmhouse Dog Meal', 30.00, 1, 'Nutritious food and treats for dogs'),
('Feather Teaser Wand', 30.00, 2, 'Play items for active cats'),
('Catnip Mouse Toy', 24.00, 2, 'Play items for active cats'),
('Jingle Ball Set', 18.00, 2, 'Play items for active cats'),
('Scratching Post Small', 6.00, 2, 'Play items for active cats'),
('Crinkle Tunnel', 31.00, 2, 'Play items for active cats'),
('Laser Pointer Toy', 6.00, 2, 'Play items for active cats'),
('Plush Fish Toy', 19.00, 2, 'Play items for active cats'),
('Spring Coil Pack', 20.00, 2, 'Play items for active cats'),
('Hanging Door Toy', 12.00, 2, 'Play items for active cats'),
('Interactive Puzzle Toy', 22.00, 2, 'Play items for active cats'),
('Catnip Kicker Toy', 20.00, 2, 'Play items for active cats'),
('Rolling Bell Ball', 20.00, 2, 'Play items for active cats'),
('Ribbon Chase Toy', 19.00, 2, 'Play items for active cats'),
('Mini Plush Mouse', 21.00, 2, 'Play items for active cats'),
('Treat Dispensing Ball', 16.00, 2, 'Play items for active cats'),
('Double Pom Toy', 12.00, 2, 'Play items for active cats'),
('Window Perch Toy', 10.00, 2, 'Play items for active cats'),
('Scratch Pad Refill', 8.00, 2, 'Play items for active cats'),
('Rainbow Wand Toy', 23.00, 2, 'Play items for active cats'),
('Carpet Scratcher', 23.00, 2, 'Play items for active cats'),
('Bird Perch Set', 27.00, 3, 'Care supplies for pet birds'),
('Parakeet Seed Mix', 40.00, 3, 'Care supplies for pet birds'),
('Canary Food Blend', 53.00, 3, 'Care supplies for pet birds'),
('Mineral Cuttlebone', 57.00, 3, 'Care supplies for pet birds'),
('Bird Ladder Toy', 68.00, 3, 'Care supplies for pet birds'),
('Mirror Bell Combo', 80.00, 3, 'Care supplies for pet birds'),
('Clip On Food Cup', 92.00, 3, 'Care supplies for pet birds'),
('Bird Cage Liner Pack', 108.00, 3, 'Care supplies for pet birds'),
('Nesting Material Pack', 121.00, 3, 'Care supplies for pet birds'),
('Treat Spray Millet', 8.00, 3, 'Care supplies for pet birds'),
('Wooden Swing Perch', 22.00, 3, 'Care supplies for pet birds'),
('Foraging Ball Toy', 32.00, 3, 'Care supplies for pet birds'),
('Cage Cleaning Spray', 47.00, 3, 'Care supplies for pet birds'),
('Parrot Rope Perch', 54.00, 3, 'Care supplies for pet birds'),
('Bird Bath Dish', 54.00, 3, 'Care supplies for pet birds'),
('Songbird Vitamin Drops', 78.00, 3, 'Care supplies for pet birds'),
('Aquarium Filter Cartridge', 36.00, 4, 'Essential aquarium equipment and accessories'),
('Decorative Aquarium Gravel', 49.00, 4, 'Essential aquarium equipment and accessories'),
('Fish Net Medium', 34.00, 4, 'Essential aquarium equipment and accessories'),
('Water Conditioner', 45.00, 4, 'Essential aquarium equipment and accessories'),
('Aquarium Thermometer', 59.00, 4, 'Essential aquarium equipment and accessories'),
('LED Tank Light', 67.00, 4, 'Essential aquarium equipment and accessories'),
('Air Stone Pack', 76.00, 4, 'Essential aquarium equipment and accessories'),
('Aquarium Heater 50W', 92.00, 4, 'Essential aquarium equipment and accessories'),
('Aquarium Heater 100W', 106.00, 4, 'Essential aquarium equipment and accessories'),
('Fish Flake Food', 95.00, 4, 'Essential aquarium equipment and accessories'),
('Algae Scraper', 105.00, 4, 'Essential aquarium equipment and accessories'),
('Aquarium Plant Set', 122.00, 4, 'Essential aquarium equipment and accessories'),
('Bubble Curtain Kit', 136.00, 4, 'Essential aquarium equipment and accessories'),
('Breeder Box Insert', 149.00, 4, 'Essential aquarium equipment and accessories'),
('Filter Sponge Pack', 164.00, 4, 'Essential aquarium equipment and accessories'),
('Aquarium Background Roll', 183.00, 4, 'Essential aquarium equipment and accessories'),
('Glass Lid Clips', 174.00, 4, 'Essential aquarium equipment and accessories'),
('Submersible Pump', 191.00, 4, 'Essential aquarium equipment and accessories'),
('Hamster Bedding Pack', 50.00, 5, 'Supplies for small pets'),
('Rabbit Hay Bundle', 49.00, 5, 'Supplies for small pets'),
('Guinea Pig Pellets', 15.00, 5, 'Supplies for small pets'),
('Small Pet Water Bottle', 31.00, 5, 'Supplies for small pets'),
('Hamster Hideout Hut', 40.00, 5, 'Supplies for small pets'),
('Chew Stick Bundle', 48.00, 5, 'Supplies for small pets'),
('Rabbit Litter Tray', 58.00, 5, 'Supplies for small pets'),
('Exercise Ball Large', 68.00, 5, 'Supplies for small pets'),
('Small Pet Food Bowl', 20.00, 5, 'Supplies for small pets'),
('Timothy Hay Cubes', 28.00, 5, 'Supplies for small pets'),
('Guinea Pig Tunnel', 38.00, 5, 'Supplies for small pets'),
('Hamster Nesting Fluff', 47.00, 5, 'Supplies for small pets'),
('Rabbit Grooming Brush', 60.00, 5, 'Supplies for small pets'),
('Small Pet Carrier', 7.00, 5, 'Supplies for small pets'),
('Hay Rack Feeder', 11.00, 5, 'Supplies for small pets'),
('Wooden Chew Blocks', 27.00, 5, 'Supplies for small pets');
INSERT INTO productSupplier (supId, prodId, cost)
SELECT CASE MOD(p.prodId - 7, 5)
WHEN 0 THEN 1
WHEN 1 THEN 2
WHEN 2 THEN 3
WHEN 3 THEN 4
ELSE 5
END,
p.prodId,
ROUND(p.prodPrice * (0.62 + (MOD(p.prodId - 7, 5) * 0.03)), 2)
FROM product p
WHERE p.prodId >= 7
AND NOT EXISTS (
SELECT 1 FROM productSupplier ps WHERE ps.prodId = p.prodId
);
INSERT INTO inventory (prodId, quantity)
SELECT p.prodId,
CASE p.categoryId
WHEN 1 THEN 120 + MOD((p.prodId - 7) * 17, 60)
WHEN 2 THEN 180 + MOD((p.prodId - 7) * 17, 60)
WHEN 3 THEN 70 + MOD((p.prodId - 7) * 17, 60)
WHEN 4 THEN 45 + MOD((p.prodId - 7) * 17, 60)
ELSE 95 + MOD((p.prodId - 7) * 17, 60)
END
FROM product p
WHERE p.prodId >= 7
AND NOT EXISTS (
SELECT 1 FROM inventory i WHERE i.prodId = p.prodId
);

View File

@@ -26,6 +26,22 @@ public class DropdownApi {
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> getProductCategories() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/product-categories");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from product categories endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getPetSpecies() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/pet-species");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from pet species endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getProducts() throws Exception { public List<DropdownOption> getProducts() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/products"); String response = apiClient.getRawResponse("/api/v1/dropdowns/products");
if (response == null || response.isEmpty()) { if (response == null || response.isEmpty()) {

View File

@@ -24,11 +24,17 @@ public class PetApi {
return INSTANCE; 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"; String path = "/api/v1/pets?page=0&size=1000";
if (query != null && !query.isEmpty()) { if (query != null && !query.isEmpty()) {
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); 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); String response = apiClient.getRawResponse(path);
PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue( PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue(
response, response,
@@ -40,6 +46,10 @@ public class PetApi {
return pageResponse.getContent(); return pageResponse.getContent();
} }
public List<PetResponse> listPets(String query) throws Exception {
return listPets(query, null, null);
}
public PetResponse createPet(PetRequest request) throws Exception { public PetResponse createPet(PetRequest request) throws Exception {
return apiClient.post("/api/v1/pets", request, PetResponse.class); return apiClient.post("/api/v1/pets", request, PetResponse.class);
} }

View File

@@ -24,11 +24,14 @@ public class ProductApi {
return INSTANCE; 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"; String path = "/api/v1/products?page=0&size=1000";
if (query != null && !query.isEmpty()) { if (query != null && !query.isEmpty()) {
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
} }
if (categoryId != null) {
path += "&categoryId=" + categoryId;
}
String response = apiClient.getRawResponse(path); String response = apiClient.getRawResponse(path);
PageResponse<ProductResponse> pageResponse = apiClient.getObjectMapper().readValue( PageResponse<ProductResponse> pageResponse = apiClient.getObjectMapper().readValue(
response, response,
@@ -40,6 +43,10 @@ public class ProductApi {
return pageResponse.getContent(); return pageResponse.getContent();
} }
public List<ProductResponse> listProducts(String query) throws Exception {
return listProducts(query, null);
}
public ProductResponse createProduct(ProductRequest request) throws Exception { public ProductResponse createProduct(ProductRequest request) throws Exception {
return apiClient.post("/api/v1/products", request, ProductResponse.class); return apiClient.post("/api/v1/products", request, ProductResponse.class);
} }

View File

@@ -15,6 +15,8 @@ import javafx.scene.layout.StackPane;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.pet.PetResponse; import org.example.petshopdesktop.api.dto.pet.PetResponse;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.api.endpoints.PetApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController; import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.models.Pet;
@@ -64,6 +66,12 @@ public class PetController {
@FXML @FXML
private TableView<Pet> tvPets; private TableView<Pet> tvPets;
@FXML
private ComboBox<String> cbSpeciesFilter;
@FXML
private ComboBox<String> cbStatusFilter;
@FXML @FXML
private TextField txtSearch; private TextField txtSearch;
@@ -150,6 +158,11 @@ public class PetController {
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice")); colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
configureImageColumn(colPetImage); configureImageColumn(colPetImage);
loadSpeciesFilter();
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending"));
cbStatusFilter.getSelectionModel().selectFirst();
displayPets(); displayPets();
tvPets.getSelectionModel().selectedItemProperty().addListener( tvPets.getSelectionModel().selectedItemProperty().addListener(
@@ -159,9 +172,12 @@ public class PetController {
}); });
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { 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 //EventListener for DELETE key
tvPets.setOnKeyPressed(event -> { tvPets.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
@@ -173,12 +189,14 @@ public class PetController {
} }
private void displayFilteredPet(String filter) { 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(); displayPets();
} else { } else {
new Thread(() -> { new Thread(() -> {
try { try {
List<PetResponse> pets = PetApi.getInstance().listPets(filter); List<PetResponse> pets = PetApi.getInstance().listPets(filter, species, status);
List<Pet> petList = pets.stream() List<Pet> petList = pets.stream()
.map(this::mapToPet) .map(this::mapToPet)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -203,7 +221,7 @@ public class PetController {
private void displayPets() { private void displayPets() {
new Thread(() -> { new Thread(() -> {
try { try {
List<PetResponse> pets = PetApi.getInstance().listPets(null); List<PetResponse> pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus());
List<Pet> petList = pets.stream() List<Pet> petList = pets.stream()
.map(this::mapToPet) .map(this::mapToPet)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -224,6 +242,40 @@ public class PetController {
}).start(); }).start();
} }
private void applyFilters() {
displayFilteredPet(txtSearch.getText());
}
private void loadSpeciesFilter() {
new Thread(() -> {
try {
List<String> values = DropdownApi.getInstance().getPetSpecies().stream()
.map(DropdownOption::getLabel)
.collect(Collectors.toList());
values.add(0, "All Species");
Platform.runLater(() -> {
cbSpeciesFilter.setItems(FXCollections.observableArrayList(values));
cbSpeciesFilter.getSelectionModel().selectFirst();
});
} catch (Exception e) {
Platform.runLater(() -> ActivityLogger.getInstance().logException(
"PetController.loadSpeciesFilter",
e,
"Loading species filter options"));
}
}).start();
}
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){ private void openDialog(Pet pet, String mode){
//Get new view //Get new view
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml")); FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml"));

View File

@@ -16,6 +16,8 @@ import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductDTO; import org.example.petshopdesktop.DTOs.ProductDTO;
import org.example.petshopdesktop.api.dto.product.ProductResponse; 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.api.endpoints.ProductApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController; import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
@@ -62,6 +64,9 @@ public class ProductController {
@FXML @FXML
private TableView<ProductDTO> tvProducts; private TableView<ProductDTO> tvProducts;
@FXML
private ComboBox<DropdownOption> cbCategoryFilter;
@FXML @FXML
private TextField txtSearch; private TextField txtSearch;
@@ -87,6 +92,7 @@ public class ProductController {
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName")); colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc")); colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc"));
configureImageColumn(colProductImage); configureImageColumn(colProductImage);
loadCategoryFilter();
displayProduct(); displayProduct();
@@ -100,9 +106,11 @@ public class ProductController {
//EventListener to search when text is changed on searchbar //EventListener to search when text is changed on searchbar
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
displayFilteredProduct(newValue); applyFilters();
}); });
cbCategoryFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters());
//EventListener for DELETE key press //EventListener for DELETE key press
tvProducts.setOnKeyPressed(event -> { tvProducts.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
@@ -120,7 +128,7 @@ public class ProductController {
private void displayProduct(){ private void displayProduct(){
new Thread(() -> { new Thread(() -> {
try { try {
List<ProductResponse> products = ProductApi.getInstance().listProducts(null); List<ProductResponse> products = ProductApi.getInstance().listProducts(null, selectedCategoryId());
List<ProductDTO> productDTOs = products.stream() List<ProductDTO> productDTOs = products.stream()
.map(this::mapToProductDTO) .map(this::mapToProductDTO)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -222,12 +230,12 @@ public class ProductController {
* @param filter word to filter table * @param filter word to filter table
*/ */
private void displayFilteredProduct(String filter){ private void displayFilteredProduct(String filter){
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ if ((txtSearch.getText() == null || txtSearch.getText().isEmpty()) && selectedCategoryId() == null){
displayProduct(); displayProduct();
} else { } else {
new Thread(() -> { new Thread(() -> {
try { try {
List<ProductResponse> products = ProductApi.getInstance().listProducts(filter); List<ProductResponse> products = ProductApi.getInstance().listProducts(filter, selectedCategoryId());
List<ProductDTO> productDTOs = products.stream() List<ProductDTO> productDTOs = products.stream()
.map(this::mapToProductDTO) .map(this::mapToProductDTO)
.collect(Collectors.toList()); .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().getProductCategories());
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 * Function to open the new Dialog for edit or adding
* depending on the mode given * depending on the mode given

View File

@@ -89,7 +89,7 @@ public class ProductDialogController {
//Set up combobox for selecting category //Set up combobox for selecting category
try { try {
List<DropdownOption> categories = DropdownApi.getInstance().getCategories(); List<DropdownOption> categories = DropdownApi.getInstance().getProductCategories();
if (categories != null) { if (categories != null) {
ObservableList<DropdownOption> categoriesObs = FXCollections.observableArrayList(categories); ObservableList<DropdownOption> categoriesObs = FXCollections.observableArrayList(categories);
cbProdCategory.setItems(categoriesObs); cbProdCategory.setItems(categoriesObs);

View File

@@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?> <?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?> <?import javafx.scene.control.TableView?>
@@ -59,13 +60,15 @@
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" /> <Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding> </padding>
<children> <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"> <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>
<Font size="15.0" /> <Font size="15.0" />
</font> </font>
</TextField> </TextField>
</children> <ComboBox fx:id="cbSpeciesFilter" prefWidth="150.0" promptText="Species" />
</HBox> <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"> <TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colPetId" prefWidth="55.0" text="ID" /> <TableColumn fx:id="colPetId" prefWidth="55.0" text="ID" />

View File

@@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?> <?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?> <?import javafx.scene.control.TableView?>
@@ -58,13 +59,14 @@
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" /> <Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding> </padding>
<children> <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"> <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>
<Font size="15.0" /> <Font size="15.0" />
</font> </font>
</TextField> </TextField>
</children> <ComboBox fx:id="cbCategoryFilter" prefWidth="180.0" promptText="Category" />
</HBox> </children>
</HBox>
<TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colProductId" prefWidth="55.0" text="ID" /> <TableColumn fx:id="colProductId" prefWidth="55.0" text="ID" />