Close chat #169

Closed
RecentRunner wants to merge 291 commits from close-chat into main
21 changed files with 491 additions and 62 deletions
Showing only changes of commit 4a68c99c4c - Show all commits

View File

@@ -33,6 +33,7 @@ public class DevStackApplication {
docker.ensureDockerAvailable();
docker.startDatabase();
context = new SpringApplicationBuilder(BackendApplication.class)
.profiles("local")
.initializers(new FlywayContextInitializer())
.run(args);
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
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}")

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")
public ResponseEntity<List<DropdownOption>> getStores() {
return ResponseEntity.ok(

View File

@@ -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}")

View File

@@ -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}")

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

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

View File

@@ -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(),

View File

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

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>>() {});
}
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 {
String response = apiClient.getRawResponse("/api/v1/dropdowns/products");
if (response == null || response.isEmpty()) {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -15,6 +15,8 @@ import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
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.controllers.dialogcontrollers.PetDialogController;
import org.example.petshopdesktop.models.Pet;
@@ -64,6 +66,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 +158,11 @@ public class PetController {
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
configureImageColumn(colPetImage);
loadSpeciesFilter();
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending"));
cbStatusFilter.getSelectionModel().selectFirst();
displayPets();
tvPets.getSelectionModel().selectedItemProperty().addListener(
@@ -159,9 +172,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 +189,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 +221,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 +242,40 @@ public class PetController {
}).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){
//Get new view
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 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().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
* depending on the mode given

View File

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

View File

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

View File

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