From 635b32dd5c025eb09f45c7657e3c186e5e4aee0d Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 13 Jun 2025 13:15:40 -0600 Subject: [PATCH 01/22] feat: updated product model --- .../com/inventory/backend/model/Product.java | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/model/Product.java b/backend/src/main/java/com/inventory/backend/model/Product.java index 32a9197..7695688 100644 --- a/backend/src/main/java/com/inventory/backend/model/Product.java +++ b/backend/src/main/java/com/inventory/backend/model/Product.java @@ -1,6 +1,5 @@ package com.inventory.backend.model; -import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @@ -9,9 +8,94 @@ public class Product { private UUID id; private String name; private String category; - private BigDecimal number; - private LocalDate expirationDate; + private double unitPrice; private int quantityInStock; + private LocalDate expirationDate; private LocalDateTime createdAt; private LocalDateTime updatedAt; + + public Product(){} + + public Product(UUID id, String name, String category, double unitPrice, int quantityInStock, LocalDate expirationDate) { + this.id = id; + this.name = name; + this.category = category; + this.unitPrice = unitPrice; + this.quantityInStock = quantityInStock; + this.expirationDate = expirationDate; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(double unitPrice) { + this.unitPrice = unitPrice; + } + + public int getQuantityInStock() { + return quantityInStock; + } + + public void setQuantityInStock(int quantityInStock) { + this.quantityInStock = quantityInStock; + } + + public LocalDate getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(LocalDate expirationDate) { + this.expirationDate = expirationDate; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public boolean isInStock() { + return quantityInStock > 0; + } + + public double getTotalValue() { + return unitPrice * quantityInStock; + } } \ No newline at end of file From 675523cb151eed1cf51a5ca3ca6f3ed6016925c2 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 16 Jun 2025 09:44:37 -0600 Subject: [PATCH 02/22] feat: added ProductDTO --- backend/pom.xml | 9 +++ .../com/inventory/backend/dto/ProductDTO.java | 62 +++++++++++++++++++ .../backend/model/InventoryMetrics.java | 0 .../backend/repository/ProductRepository.java | 0 4 files changed, 71 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/dto/ProductDTO.java create mode 100644 backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java create mode 100644 backend/src/main/java/com/inventory/backend/repository/ProductRepository.java diff --git a/backend/pom.xml b/backend/pom.xml index 7abf8a1..ed6dde8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -46,6 +46,15 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + diff --git a/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java b/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java new file mode 100644 index 0000000..fe61fc8 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java @@ -0,0 +1,62 @@ +package com.inventory.backend.dto; + +import jakarta.validation.constraints.*; +import java.time.LocalDate; + +public class ProductDTO { + @NotBlank(message = "Name is required") + @Size(max = 120, message = "Name must not exceed 120 characters") + private String name; + + @NotBlank(message = "Category is required") + private String category; + + @NotNull(message = "Unit price is required") + @PositiveOrZero(message = "Unit price must be 0 or more") + private Double unitPrice; + + @PositiveOrZero(message = "Stock must be 0 or more") + private Integer quantityInStock; + + private LocalDate expirationDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public Double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(Double unitPrice) { + this.unitPrice = unitPrice; + } + + public Integer getQuantityInStock() { + return quantityInStock; + } + + public void setQuantityInStock(Integer quantityInStock) { + this.quantityInStock = quantityInStock; + } + + public LocalDate getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(LocalDate expirationDate) { + this.expirationDate = expirationDate; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java new file mode 100644 index 0000000..e69de29 From 77174ee1e06f0bcf740b15062b6f4f7821c47763 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 16 Jun 2025 14:03:45 -0600 Subject: [PATCH 03/22] feat: added metrics to backend --- .../backend/model/InventoryMetrics.java | 124 ++++++++++++++++++ .../backend/repository/ProductRepository.java | 49 +++++++ .../backend/service/ProductService.java | 122 +++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/service/ProductService.java diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java index e69de29..b3b064b 100644 --- a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java +++ b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java @@ -0,0 +1,124 @@ +package com.inventory.backend.model; + +import java.util.*; +import java.util.stream.Collectors; + +public class InventoryMetrics { + private int totalInStock; + private double totalValue; + private double averagePrice; + + private Map byCategory; + + public InventoryMetrics() {} + + public InventoryMetrics(int totalInStock, double totalValue, double averagePrice, Map byCategory) { + this.totalInStock = totalInStock; + this.totalValue = totalValue; + this.averagePrice = averagePrice; + this.byCategory = byCategory; + } + + public static InventoryMetrics fromProductList(List products) { + int totalStock = products.stream().mapToInt(Product::getQuantityInStock).sum(); + + double totalValue = products.stream() + .maptoDouble(Product::getTotalValue) + .sum(); + + List inStock = products.stream() + .filter(p -> p.getQuantityInStock() > 0) + .collect(Collectors.toList()); + + double averagePrice = inStock.isEmpty() ? 0: + inStock.stream().mapToDouble(Product::getUnitPrice).average().orElse(0); + + Map categoryMetrics = products.stream() + .collect(Collectors.groupingBy( + Product::getCategory, + Collectors.collectingAndThen(Collectors.toList(), CategoryMetrics::fromProductList) + )); + return new InventoryMetrics(totalStock, totalValue, averagePrice, categoryMetrics); + } + + public int getTotalInStock() { + return totalInStock; + } + + public void setTotalInStock(int totalInStock) { + this.totalInStock = totalInStock; + } + + public double getTotalValue() { + return totalValue; + } + + public void setTotalValue(double totalValue) { + this.totalValue = totalValue; + } + + public double getAveragePrice() { + return averagePrice; + } + + public void setAveragePrice(double averagePrice) { + this.averagePrice = averagePrice; + } + + public Map getByCategory() { + return byCategory; + } + + public void setByCategory(Map byCategory) { + this.byCategory = byCategory; + } + + public static class CategoryMetrics { + private int inStock; + private double totalValue; + private double averagePrice; + + public static CategoryMetrics fromProductList(List products) { + int stock = products.stream().mapToInt(Product::getQuantityInStock).sum(); + double value = products.stream().mapToDouble(Product::getTotalValue).sum(); + + List inStock = products.stream().filter(p -> p.getQuantityInStock() > 0).toList(); + double avg = inStock.isEmpty() ? 0 : + inStock.stream().mapToDouble(Product::getUnitPrice).average().orElse(0); + + return new CategoryMetrics(stock, value, avg); + } + + public CategoryMetrics() {} + + public CategoryMetrics(int inStock, double totalValue, double averagePrice) { + this.inStock = inStock; + this.totalValue = totalValue; + this.averagePrice = averagePrice; + } + + public double getTotalValue() { + return totalValue; + } + + public void setTotalValue(double totalValue) { + this.totalValue = totalValue; + } + + public double getAveragePrice() { + return averagePrice; + } + + public void setAveragePrice(double averagePrice) { + this.averagePrice = averagePrice; + } + + public int getInStock() { + return inStock; + } + + public void setInStock(int inStock) { + this.inStock = inStock; + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index e69de29..477e225 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -0,0 +1,49 @@ +package com.inventory.backend.repository; + +import com.inventory.backend.model.Product; +import org.springframework.stereotype.Repository; + +import java.util.*; +import java.util.stream.Collectors; + +@Repository +public class ProductRepository { + private final Map products = new HashMap<>(); + + public List findAll(){ + return new ArrayList<>(products.values()); + } + + public Optional findById(UUID id) { + return Optional.ofNullable(products) + } + + public Product save(Product product) { + products.put(product.getId(), product); + return product; + } + + public void delete(UUID id) { + products.remove(id); + } + + public boolean existsById(UUID id) { + return products.containsKey(id); + } + + public void clear() { + products.clear(); + } + + public List findByNameOrCategoryOrAvailability( + Optional nameFilter, + Optional> categoryFilter, + Optional availability, + ) { + return products.values().stream() + .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) + .filter(p -> categoryFilter.map(categories -> categories.contains(p.category())).orElse(true)) + .filter(p -> availability.map(avail -> avail == p.isInStock()).orElse(true)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/service/ProductService.java b/backend/src/main/java/com/inventory/backend/service/ProductService.java new file mode 100644 index 0000000..555a1f7 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/service/ProductService.java @@ -0,0 +1,122 @@ +package com.inventory.backend.service; + +import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.model.Product; +import com.inventory.backend.model.InventoryMetrics; +import com.inventory.backend.repository.ProductRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class ProductService { + private final ProductRepository repository; + + public ProductService(ProductRepository repository) { + this.repository = repository; + } + + public List getFilteredAndSortedProducts( + Optional nameFilter, + Optional> categoryFilter, + Optional availability, + Optional sortBy, + Optional sortBy2, + boolean ascending, + boolean ascending2, + int page, + int size + ){ + List filtered = repository.findByNameOrCategoryOrAvailability(nameFilter, categoryFilter,availability); + + Comparator comparator = getComparator(sortBy.orElse(null), ascending); + if(sortBy2.isPresent()) { + comparator = comparator.thenComparing(getComparator(sortBy2.get(), ascending2)); + } + + return filtered.stream() + .sorted(comparator) + .skip((long) page * size) + .limit(size) + .collect(Collectors.toList()); + } + + private Comparator getComparator(String sortField, boolean ascending) { + Comparator comparator; + + switch (sortField) { + case "name": + comparator = Comparator.comparing(Product::getName, String.CASE_INSENSITIVE_ORDER); + break; + case "category": + comparator = Comparator.comparing(Product::getCategory, String.CASE_INSENSITIVE_ORDER); + break; + case "unitPrice": + comparator = Comparator.comparingDouble(Product::getUnitPrice); + break; + case "quantityInStock": + comparator = Comparator.comparingInt(Product::getQuantityInStock); + break; + case "expirationDate": + comparator = Comparator.comparing(p -> Optional.ofNullable(p.getExpirationDate()).orElse(null), + Comparator.nullsLast(Comparator.naturalOrder())); + break; + default: + comparator = Comparator.comparing(Product::getName); + } + return ascending ? comparator : comparator.reversed(); + } + + public Product createProduct(ProductDTO dto) { + Product product = new Product( + UUID.randomUUID(), + dto.getName(), + dto.getCategory(), + dto.getUnitPrice(), + dto.getQuantityInStock() != null ? dto.getQuantityInStock() : 0, + dto.getExpirationDate() + ); + return repository.save(product); + } + + public Product updateProduct(UUID id, ProductDTO dto) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + + product.setName(dto.getName()); + product.setCategory(dto.getCategory()); + product.setUnitPrice(dto.getUnitPrice()); + product.setQuantityInStock(dto.getQuantityInStock()); + product.setExpirationDate(dto.getExpirationDate()); + product.setUpdatedAt(LocalDateTime.now()); + + return repository.save(product); + } + + public void deleteProduct(UUID id) { + if(!repository.existsById(id)) { + throw new NoSuchElementException("Product not found"); + } + repository.delete(id); + } + + public void markOutOfStock(UUID id) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + product.setQuantityInStock(0); + product.setUpdatedAt(LocalDateTime.now()); + repository.save(product); + } + + public void markInStock(UUID id, int defaultQuantity) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + product.setQuantityInStock(defaultQuantity); + product.setUpdatedAt(LocalDateTime.now()); + repository.save(product); + } + + public InventoryMetrics getMetrics() { + List all = repository.findAll(); + return InventoryMetrics.fromProductList(all); + } +} \ No newline at end of file From 66f3018f006d95932b2aa6c4b791bb59ba9e356e Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:17:31 -0600 Subject: [PATCH 04/22] feat: added Controller --- .../backend/controller/ProductController.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/controller/ProductController.java diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java new file mode 100644 index 0000000..9bf349b --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -0,0 +1,84 @@ +package com.inventory.backend.controller; + +import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.model.InventoryMetrics; +import com.inventory.backend.model.Product; +import com.inventory.backend.service.ProductService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +@RequestMapping("/products") +@CrossOrigin(origins = "http://localhost:8080") +public class ProductController { + private final ProductService service; + + public ProductController(ProductService service) { + this.service = service; + } + + // GET /products + @GetMapping + public List getProducts( + @RequestParam Optional name, + @RequestParam Optional> category, + @RequestParam Optional availability, + @RequestParam Optional sortBy, + @RequestParam Optional sortBy2, + @RequestParam(defaultValue = "true") boolean asc, + @RequestParam(defaultValue = "true") boolean asc2, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + ){ + Set categorySet = category.map(HashSet::new).orElse(null); + return service.getFilteredAndSortedProducts( + name, + Optional.ofNullable(categorySet), + availability, + sortBy, + sortBy2, + asc, + asc2, + page, + size + ); + } + + // POST /products + @PostMapping + public Product createProduct(@RequestBody @Valid ProductDTO dto) { + return service.createProduct(id, dto); + } + + // PUT /products/{id} + @PutMapping("/{id}") + public Product updateProduct(@PathVariable UUID id, @RequestBody @Valid ProductDTO dto) { + return service.updateProduct(id,dto); + } + + // DELETE /products/{id} + @DelteMapping("/{id}") + public void deleteProduct(@PathVariable UUID id) { + service.deleteProduct(id); + } + + // POST /products/{id}/outofstock + @PostMapping("/{id}/outofstock") + public void markOutOfStock(@PathVariable UUID id) { + service.markOutOfStock(id); + } + + // PUT /products/{id}/instock?defaultQuantity=10 + @PutMapping("/{id}/instock") + public void markInStock(@PathVariable UUID id, @RequestParam(defaultValue = "10") int defaultQuantity) { + service.markInStock(id, defaultQuantity); + } + + // GET /products/metrics + @GetMapping("/metrics") + public InventoryMetrics getMetrics() { + return service.getMetrics(); + } +} \ No newline at end of file From fedb3d04fb157d784e30d7364d9b86570b04b953 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:34:06 -0600 Subject: [PATCH 05/22] fix: added missing ; --- .../com/inventory/backend/controller/ProductController.java | 2 +- .../com/inventory/backend/repository/ProductRepository.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 9bf349b..939a4d0 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -30,7 +30,7 @@ public List getProducts( @RequestParam(defaultValue = "true") boolean asc, @RequestParam(defaultValue = "true") boolean asc2, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "10") int size ){ Set categorySet = category.map(HashSet::new).orElse(null); return service.getFilteredAndSortedProducts( diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index 477e225..16517ca 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -15,7 +15,7 @@ public List findAll(){ } public Optional findById(UUID id) { - return Optional.ofNullable(products) + return Optional.ofNullable(products); } public Product save(Product product) { @@ -38,7 +38,7 @@ public void clear() { public List findByNameOrCategoryOrAvailability( Optional nameFilter, Optional> categoryFilter, - Optional availability, + Optional availability ) { return products.values().stream() .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) From 773f52b29ac29738500dbd7260dbbd6949d6d297 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:36:40 -0600 Subject: [PATCH 06/22] fix: corrected typo --- .../com/inventory/backend/controller/ProductController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 939a4d0..56b2819 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -59,7 +59,7 @@ public Product updateProduct(@PathVariable UUID id, @RequestBody @Valid ProductD } // DELETE /products/{id} - @DelteMapping("/{id}") + @DeleteMapping("/{id}") public void deleteProduct(@PathVariable UUID id) { service.deleteProduct(id); } From 1da6dbab9cc8215b1e0ddd8c8f26b0efbe79acf7 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:48:01 -0600 Subject: [PATCH 07/22] fix: corrected typos --- .../com/inventory/backend/controller/ProductController.java | 2 +- .../java/com/inventory/backend/model/InventoryMetrics.java | 2 +- .../com/inventory/backend/repository/ProductRepository.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 56b2819..f13502e 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -49,7 +49,7 @@ public List getProducts( // POST /products @PostMapping public Product createProduct(@RequestBody @Valid ProductDTO dto) { - return service.createProduct(id, dto); + return service.createProduct(dto); } // PUT /products/{id} diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java index b3b064b..a4c1f58 100644 --- a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java +++ b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java @@ -23,7 +23,7 @@ public static InventoryMetrics fromProductList(List products) { int totalStock = products.stream().mapToInt(Product::getQuantityInStock).sum(); double totalValue = products.stream() - .maptoDouble(Product::getTotalValue) + .mapToDouble(Product::getTotalValue) .sum(); List inStock = products.stream() diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index 16517ca..d6ea2e2 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -15,7 +15,7 @@ public List findAll(){ } public Optional findById(UUID id) { - return Optional.ofNullable(products); + return Optional.ofNullable(products.get(id)); } public Product save(Product product) { @@ -41,7 +41,7 @@ public List findByNameOrCategoryOrAvailability( Optional availability ) { return products.values().stream() - .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) + .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLowerCase())).orElse(true)) .filter(p -> categoryFilter.map(categories -> categories.contains(p.category())).orElse(true)) .filter(p -> availability.map(avail -> avail == p.isInStock()).orElse(true)) .collect(Collectors.toList()); From 3307eff9e7e05f9111aa8830610978bfbd14ceb2 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 13:39:56 -0600 Subject: [PATCH 08/22] fix: should be running now --- .../com/inventory/backend/repository/ProductRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index d6ea2e2..d61cd93 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -42,7 +42,7 @@ public List findByNameOrCategoryOrAvailability( ) { return products.values().stream() .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLowerCase())).orElse(true)) - .filter(p -> categoryFilter.map(categories -> categories.contains(p.category())).orElse(true)) + .filter(p -> categoryFilter.map(categories -> categories.contains(p.getCategory())).orElse(true)) .filter(p -> availability.map(avail -> avail == p.isInStock()).orElse(true)) .collect(Collectors.toList()); } From c388c03b79ca0e34f0ca97ac15e8a29c90f5791d Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 14:00:13 -0600 Subject: [PATCH 09/22] feat: added a basic test --- .../backend/service/ProductServiceTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 backend/src/test/java/com/inventory/backend/service/ProductServiceTest.java diff --git a/backend/src/test/java/com/inventory/backend/service/ProductServiceTest.java b/backend/src/test/java/com/inventory/backend/service/ProductServiceTest.java new file mode 100644 index 0000000..6f80f6a --- /dev/null +++ b/backend/src/test/java/com/inventory/backend/service/ProductServiceTest.java @@ -0,0 +1,55 @@ +package com.inventory.backend.service; + +import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.model.Product; +import com.inventory.backend.repository.ProductRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + + +public class ProductServiceTest { + private ProductRepository repository; + private ProductService service; + + @BeforeEach + void setUp() { + repository = new ProductRepository(); + service = new ProductService(repository); + } + + @Test + void createProduct_shouldAddProduct() { + ProductDTO dto = new ProductDTO(); + dto.setName("Keyboard"); + dto.setCategory("Electronics"); + dto.setUnitPrice(95.99); + dto.setQuantityInStock(2); + + Product created = service.createProduct(dto); + + assertNotNull(created.getId()); + assertEquals("Keyboard", created.getName()); + assertEquals(1, repository.findAll().size()); + } + + @Test + void markOutOfStock_shouldSetQuantityToZero() { + ProductDTO dto = new ProductDTO(); + dto.setName("Mouse"); + dto.setCategory("Electronics"); + dto.setUnitPrice(10.50); + dto.setQuantityInStock(10); + + Product created = service.createProduct(dto); + UUID id = created.getId(); + + service.markOutOfStock(id); + + Product updated = repository.findById(id).orElseThrow(); + assertEquals(0, updated.getQuantityInStock()); + } +} From d091ac755d024f5fa3d0e14e7ba3c9ff14b7635b Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Wed, 18 Jun 2025 15:21:21 -0600 Subject: [PATCH 10/22] feat: basic test + nonworking swagger --- backend/pom.xml | 5 +- .../backend/config/OpenAPIConfig.java | 19 ++ frontend/package-lock.json | 171 ++++++++++++------ frontend/package.json | 9 +- 4 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java diff --git a/backend/pom.xml b/backend/pom.xml index ed6dde8..41b87d5 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -34,7 +34,10 @@ org.springframework.boot spring-boot-starter-web - + + org.springdoc + springdoc-openapi-starter-webmvc-ui + org.springframework.boot spring-boot-devtools diff --git a/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java b/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java new file mode 100644 index 0000000..308da29 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java @@ -0,0 +1,19 @@ +package com.inventory.backend.config; + +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.OpenAPI; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAPIConfig { + + @Bean + public OpenAPI inventoryOpenAPI(){ + return new OpenAPI() + .info(new Info() + .title("Inventory Manager API") + .description("API for management") + .version("1.0")); + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f526601..1988c92 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,11 +16,18 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "autoprefixer": "^10.4.21", + "axios": "^1.10.0", + "postcss": "^8.5.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", + "tailwindcss": "^4.1.10", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/uuid": "^10.0.0" } }, "node_modules/@adobe/css-tools": { @@ -3932,6 +3939,13 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -4926,6 +4940,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5312,9 +5353,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8106,9 +8147,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12247,9 +12288,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -13679,6 +13720,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -14043,6 +14090,55 @@ } } }, + "node_modules/react-scripts/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/react-scripts/node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15649,9 +15745,9 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -15909,53 +16005,10 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.2", diff --git a/frontend/package.json b/frontend/package.json index e06d41a..b6a7f6d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,13 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "autoprefixer": "^10.4.21", + "axios": "^1.10.0", + "postcss": "^8.5.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", + "tailwindcss": "^4.1.10", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, @@ -42,7 +46,7 @@ "last 1 safari version" ] }, - "jest": { + "jest": { "collectCoverageFrom": [ "src/**/*.tsx" ], @@ -55,5 +59,8 @@ "html", "text" ] + }, + "devDependencies": { + "@types/uuid": "^10.0.0" } } From 0931c961c936f0567d63bc4a151439bdee797651 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Wed, 18 Jun 2025 15:22:13 -0600 Subject: [PATCH 11/22] feat: added services --- frontend/src/services/api.ts | 7 +++++ frontend/src/services/productService.ts | 34 +++++++++++++++++++++++++ frontend/src/types/Product.ts | 31 ++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/services/productService.ts create mode 100644 frontend/src/types/Product.ts diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..6172bda --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,7 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "http://localhost:9090", +}); + +export default api; \ No newline at end of file diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts new file mode 100644 index 0000000..2e2bb29 --- /dev/null +++ b/frontend/src/services/productService.ts @@ -0,0 +1,34 @@ +import api from "./api"; +import { Product, ProductDTO, InventoryMetrics } from "../types/Product"; + +export const getProducts = async (params: any): Promise => { + const response = await api.get("/products", {params}); + return response.data; +}; + +export const createProduct = async (product: ProductDTO): Promise => { + const response = await api.post("/products", product); + return response.data; +}; + +export const updateProduct = async (id: string, product: ProductDTO): Promise => { + const response = await api.put(`/products/${id}`, product); + return response.data; +}; + +export const deleteProduct = async (id: string): Promise => { + await api.delete(`/products/${id}`); +}; + +export const markOutOfStock = async (id: string): Promise => { + await api.post(`/products/${id}/outofstock`); +}; + +export const markInStock = async (id: string, quantity: number = 10): Promise => { + await api.post(`/products/${id}/instock?defaultQuantity=${quantity}`); +}; + +export const getMetrics = async (): Promise => { + const response = await api.get("/products/metrics"); + return response.data; +}; \ No newline at end of file diff --git a/frontend/src/types/Product.ts b/frontend/src/types/Product.ts new file mode 100644 index 0000000..abae3f5 --- /dev/null +++ b/frontend/src/types/Product.ts @@ -0,0 +1,31 @@ +export interface Product { + id: string; + name: string; + category: string; + unitPrice: number; + quantityInStock: number; + expirationDate?: string; + createdAt: string; + updatedAt: string; +} + +export interface ProductDTO { + name: string; + category: string; + unitPrice: number; + quantityInStock: number; + expirationDate?: string; +} + +export interface InventoryMetrics { + totalInStock: number; + totalValue: number; + averagePrice: number; + byCategory: { + [category: string]: { + inStock: number; + totalValue: number; + averagePrice: number; + }; + }; +} \ No newline at end of file From b82dac3181eb26223659c4d3025edf6427863ecc Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Thu, 19 Jun 2025 14:36:31 -0600 Subject: [PATCH 12/22] feat: added basic table --- frontend/package-lock.json | 45 +++++++++- frontend/package.json | 8 +- frontend/postcss.config.js | 6 ++ frontend/src/index.css | 4 + frontend/src/pages/InventoryPage.tsx | 128 +++++++++++++++++++++++++++ frontend/tailwind.config.js | 9 ++ 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/pages/InventoryPage.tsx create mode 100644 frontend/tailwind.config.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f526601..079887b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,11 +16,17 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } }, "node_modules/@adobe/css-tools": { @@ -4926,6 +4932,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -12247,9 +12280,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -13679,6 +13712,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e06d41a..76f4adf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", @@ -42,7 +43,7 @@ "last 1 safari version" ] }, - "jest": { + "jest": { "collectCoverageFrom": [ "src/**/*.tsx" ], @@ -55,5 +56,10 @@ "html", "text" ] + }, + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/index.css b/frontend/src/index.css index ec2585e..7eb87c9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,7 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx new file mode 100644 index 0000000..fe0b302 --- /dev/null +++ b/frontend/src/pages/InventoryPage.tsx @@ -0,0 +1,128 @@ +import React, {useEffect, useState} from "react"; +import { + getProducts, + deleteProduct, + markOutOfStock, + markInStock, + getMetrics, +}from "../services/productService" +import { Product, InventoryMetrics } from "../types/Product"; + +const InventoryPage = () => { + const [products, setProducts] = useState([]); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(0); + + const fetchData = async () => { + setLoading(true); + try { + const data = await getProducts({page, size: 10}); + const m = await getMetrics(); + setProducts(data); + setMetrics(m); + }catch (err) { + console.log("Error loading products", err); + } + setLoading(false); + }; + + useEffect(() => { + fetchData(); + }, [page]); + + const handleToggleStock = async (product: Product) => { + if(product.quantityInStock === 0) { + await markInStock(product.id, 10); + }else { + await markOutOfStock(product.id); + } + fetchData(); + }; + + const handleDelete = async (id: string) => { + await deleteProduct(id); + fetchData(); + }; + + return ( +
+

Inventory

+ + {metrics && ( +
+

Total in stock: {metrics.totalInStock}

+

Total value: ${metrics.totalValue.toFixed(2)}

+

Average Price: ${metrics.averagePrice.toFixed(2)}

+
+ )} + + {loading ? ( +

Loading...

+ ) : ( + + + + + + + + + + + + + + {products.map((prod) => ( + + + + + + + + + + ))} + +
In StockNameCategoryPriceStockExpirationActions
+ 0} onChange={() => handleToggleStock(prod)} /> + {prod.name}{prod.category}${prod.unitPrice} 10 + ? "" + : prod.quantityInStock >= 5 + ? "text-orange-500" + : prod.quantityInStock > 0 + ? "text-red-500" + : "" + } + > + {prod.quantityInStock} + {prod.expirationDate || "-"} + + +
+ )} + +
+ + Page {page + 1} + +
+
+ ); +}; + +export default InventoryPage \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..ee52cec --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} + From cce5f298b8ad86e28f14745a300e7aef3c6f6c1e Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 14:55:03 -0600 Subject: [PATCH 13/22] feat: added filters --- .../src/components/PaginationControls.tsx | 0 frontend/src/components/ProductFilters.tsx | 86 +++++++++++++++ frontend/src/components/ProductTable.tsx | 100 ++++++++++++++++++ frontend/src/pages/ProductListPage.tsx | 79 ++++++++++++++ frontend/src/services/productService.ts | 7 +- 5 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/PaginationControls.tsx create mode 100644 frontend/src/components/ProductFilters.tsx create mode 100644 frontend/src/components/ProductTable.tsx create mode 100644 frontend/src/pages/ProductListPage.tsx diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ProductFilters.tsx b/frontend/src/components/ProductFilters.tsx new file mode 100644 index 0000000..d02f6fb --- /dev/null +++ b/frontend/src/components/ProductFilters.tsx @@ -0,0 +1,86 @@ +import React, {useState, useEffect} from "react"; +import { getCategories } from "../services/productService"; + +interface Props { + filters: any; + setFilters: (filters: any) => void; +} + +const ProductFilters: React.FC = ({filters, setFilters}) => { + const [name, setName] = useState(filters.name || ""); + const [category, setCategory] = useState(filters.category || []); + const [availability, setAvailability] = useState(filters.availability || ""); + + const [allCategories, setAllCategories] = useState ([]); + + useEffect(() => { + getCategories().then(setAllCategories).catch(err => { + console.error("Error getting categories"); + setAllCategories([]); + }); + }, []); + + const handleApply = () => { + setFilters({ + ...filters, + name, + category, + availability: availability === "" ? "" : availability === "in", + page: 0, + }); + }; + + const toggleCategory = (value: string) => { + setCategory(prev => + prev.includes(value) + ? prev.filter(c => c !== value) + : [...prev, value] + ); + }; + + useEffect(() => { + handleApply(); + }, [name, category, availability]); + + return ( +
+
+ + setName(e.target.value)} + className="w-full p-2 border rounded" + placeholder="Product name..." /> +
+ +
+ +
+ {allCategories.map(cat => ( + + ))} +
+
+ +
+ + +
+
+ ); +}; + +export default ProductFilters \ No newline at end of file diff --git a/frontend/src/components/ProductTable.tsx b/frontend/src/components/ProductTable.tsx new file mode 100644 index 0000000..cf81979 --- /dev/null +++ b/frontend/src/components/ProductTable.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { Product } from "../types/Product"; +import { markOutOfStock, markInStock, deleteProduct } from "../services/productService"; + +interface Props{ + products: Product[]; + filters: any; + setFilters: (f: any) => void; +} + +const ProductTable: React.FC = ({products, filters, setFilters}) => { + const handleSort = (field: string, secondary = false) => { + if(secondary) { + setFilters({ ...filters, sortBy2: field, asc2: !filters.asc2}); + }else { + setFilters({...filters, sortBy: field, asc: !filters.asc}); + } + }; + + const toggleStock = async (product: Product) => { + if(product.quantityInStock === 0) { + await markInStock(product.id); + } else { + await markOutOfStock(product.id); + } + setFilters({ ...filters}); + } + + const handleDelete = async (id:string) => { + if(window.confirm("Are you sure you want to delete this product?")) { + await deleteProduct(id); + setFilters({...filters}); + } + }; + + const getExpirationColor = (exp?: string) => { + if(!exp) return ""; + const now = new Date(); + const date = new Date(exp); + const diff = (date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24); + if ( diff < 7) return "bg-red-100"; + if (diff <14) return "bg-yellow-100"; + return "bg-green-100"; + }; + + const getStockColor = (stock: number) => { + if(stock === 0) return "text-gray-400 line-through"; + if(stock < 5) return "text-red-600"; + if(stock <= 10) return "text-yellow-500"; + return ""; + }; + + return( + + + + + + + + + + + + + + {products.map(p => ( + + + + + + + + + + ))} + {products.length === 0 && ( + + )} + +
handleSort("name")} className="cursor-pointer">Name handleSort("category")} className="cursor-pointer">Category handleSort("unitPrice")} className="cursor-pointer">Price handleSort("quantityInStock")} className="cursor-pointer">Stock handleSort("expirationDate")} className="cursor-pointer">Expiration dateActions
+ toggleStock(p)}/> + {p.name}{p.category}${p.unitPrice.toFixed(2)}{p.quantityInStock}{p.expirationDate ?? "-"} + + +
No available products :C
+ ); +}; + +export default ProductTable; \ No newline at end of file diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx new file mode 100644 index 0000000..a96da01 --- /dev/null +++ b/frontend/src/pages/ProductListPage.tsx @@ -0,0 +1,79 @@ +import React, {useEffect, useState} from "react"; +import { getProducts, getMetrics } from "../services/productService"; +import { Product, InventoryMetrics } from "../types/Product"; +import ProductTable from "../components/ProductTable"; +import ProductFilters from "../components/ProductFilters"; +import PaginationControls from "../components/PaginationControls"; + +const ProductListPage: React.FC = () => { + const [products, setProducts] = useState([]); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(false); + + const [filters, setFilters] = useState({ + name: "", + category: [] as string[], + availability: "", + sortBy: "name", + sortBy2: "", + asc: true, + asc2: true, + page: 0, + size: 10, + }); + + const fetchData = async () => { + setLoading(true); + try { + const data = await getProducts(filters); + setProducts(data); + const m = await getMetrics(); + setMetrics(m); + }catch (err) { + console.log("Error loading products", err); + } finally {setLoading(false);} + }; + + useEffect(() => { + fetchData(); + }, [filters]); + + return( +
+

Inventory

+ + + +
+ +
+ + {loading ? ( +

Loading...

+ ) : ( + <> + + + + )} + + {metrics && ( +
+

General Metrics

+

Total in stock: {metrics.totalInStock}

+

Total Value: {metrics.totalValue.toFixed(2)}

+

Average price: ${metrics.averagePrice.toFixed(2)}

+
+ )} +
+ ); +}; + +export default ProductListPage; \ No newline at end of file diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts index 2e2bb29..27d17f7 100644 --- a/frontend/src/services/productService.ts +++ b/frontend/src/services/productService.ts @@ -31,4 +31,9 @@ export const markInStock = async (id: string, quantity: number = 10): Promise => { const response = await api.get("/products/metrics"); return response.data; -}; \ No newline at end of file +}; + +export const getCategories = async (): Promise => { + const res = await api.get("/products/categories") + return res.data; +} \ No newline at end of file From 3176528659ef1ed57ea8a2772ca00afd7f3625be Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:08:03 -0600 Subject: [PATCH 14/22] feat: added new endpoint to get categories --- backend/pom.xml | 4 ---- .../backend/config/OpenAPIConfig.java | 19 ------------------- .../backend/controller/ProductController.java | 5 +++++ .../backend/service/ProductService.java | 7 +++++++ 4 files changed, 12 insertions(+), 23 deletions(-) delete mode 100644 backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java diff --git a/backend/pom.xml b/backend/pom.xml index 41b87d5..535e021 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -34,10 +34,6 @@ org.springframework.boot spring-boot-starter-web
- - org.springdoc - springdoc-openapi-starter-webmvc-ui - org.springframework.boot spring-boot-devtools diff --git a/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java b/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java deleted file mode 100644 index 308da29..0000000 --- a/backend/src/main/java/com/inventory/backend/config/OpenAPIConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.inventory.backend.config; - -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.OpenAPI; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OpenAPIConfig { - - @Bean - public OpenAPI inventoryOpenAPI(){ - return new OpenAPI() - .info(new Info() - .title("Inventory Manager API") - .description("API for management") - .version("1.0")); - } -} diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index f13502e..ef74f17 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -81,4 +81,9 @@ public void markInStock(@PathVariable UUID id, @RequestParam(defaultValue = "10" public InventoryMetrics getMetrics() { return service.getMetrics(); } + + @GetMapping("/categories") + public Set getCategories(){ + return service.getAllCategories(); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/service/ProductService.java b/backend/src/main/java/com/inventory/backend/service/ProductService.java index 555a1f7..faeb9be 100644 --- a/backend/src/main/java/com/inventory/backend/service/ProductService.java +++ b/backend/src/main/java/com/inventory/backend/service/ProductService.java @@ -119,4 +119,11 @@ public InventoryMetrics getMetrics() { List all = repository.findAll(); return InventoryMetrics.fromProductList(all); } + + public Set getAllCategories() { + return repository.findAll().stream() + .map(Product::getCategory) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(TreeSet::new)); + } } \ No newline at end of file From 714bc41dd14994435466136534c8b2c35a3c3493 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:24:34 -0600 Subject: [PATCH 15/22] feat: added pagination --- .../src/components/PaginationControls.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index e69de29..13f533e 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +interface Props { + filters: any; + setFilters: (filters: any) => void; +} + +const PaginationControls: React.FC = ({filters, setFilters}) => { + const page = filters.page || 0; + + const goToPage = (newPage: number) => { + setFilters({...filters, page: newPage}); + }; + + return ( +
+ + + Page {page + 1} + + +
+ ); +}; + +export default PaginationControls; \ No newline at end of file From 7d5d07e7960c22378b2a0c328b7b8a332187c570 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:40:48 -0600 Subject: [PATCH 16/22] feat: added new endpoint to get all available pages --- .../backend/controller/ProductController.java | 5 ++-- .../inventory/backend/dto/ProductPage.java | 30 +++++++++++++++++++ .../backend/service/ProductService.java | 30 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/com/inventory/backend/dto/ProductPage.java diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index ef74f17..bd4f766 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -1,6 +1,7 @@ package com.inventory.backend.controller; import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.dto.ProductPage; import com.inventory.backend.model.InventoryMetrics; import com.inventory.backend.model.Product; import com.inventory.backend.service.ProductService; @@ -21,7 +22,7 @@ public ProductController(ProductService service) { // GET /products @GetMapping - public List getProducts( + public ProductPage getProducts( @RequestParam Optional name, @RequestParam Optional> category, @RequestParam Optional availability, @@ -33,7 +34,7 @@ public List getProducts( @RequestParam(defaultValue = "10") int size ){ Set categorySet = category.map(HashSet::new).orElse(null); - return service.getFilteredAndSortedProducts( + return service.getFilteredProductsPage( name, Optional.ofNullable(categorySet), availability, diff --git a/backend/src/main/java/com/inventory/backend/dto/ProductPage.java b/backend/src/main/java/com/inventory/backend/dto/ProductPage.java new file mode 100644 index 0000000..5dacf88 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/dto/ProductPage.java @@ -0,0 +1,30 @@ +package com.inventory.backend.dto; + +import com.inventory.backend.model.Product; +import java.util.List; + +public class ProductPage { + private List items; + private long total; + + public ProductPage(List items, long total) { + this.items = items; + this.total = total; + } + + public List getItems(){ + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } +} diff --git a/backend/src/main/java/com/inventory/backend/service/ProductService.java b/backend/src/main/java/com/inventory/backend/service/ProductService.java index faeb9be..23ac353 100644 --- a/backend/src/main/java/com/inventory/backend/service/ProductService.java +++ b/backend/src/main/java/com/inventory/backend/service/ProductService.java @@ -1,6 +1,7 @@ package com.inventory.backend.service; import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.dto.ProductPage; import com.inventory.backend.model.Product; import com.inventory.backend.model.InventoryMetrics; import com.inventory.backend.repository.ProductRepository; @@ -42,6 +43,35 @@ public List getFilteredAndSortedProducts( .limit(size) .collect(Collectors.toList()); } + public ProductPage getFilteredProductsPage( + Optional nameFilter, + Optional> categoryFilter, + Optional availability, + Optional sortBy, + Optional sortBy2, + boolean ascending, + boolean ascending2, + int page, + int size + ){ + List filtered = repository.findByNameOrCategoryOrAvailability(nameFilter, categoryFilter,availability); + + Comparator comparator = getComparator(sortBy.orElse(null), ascending); + if(sortBy2.isPresent()) { + comparator = comparator.thenComparing(getComparator(sortBy2.get(), ascending2)); + } + + long total = filtered.size(); + + List pageItems = filtered.stream() + .sorted(comparator) + .skip((long) page * size) + .limit(size) + .toList(); + + return new ProductPage(pageItems, total); + + } private Comparator getComparator(String sortField, boolean ascending) { Comparator comparator; From 6a944c3cdcf27ced63b41312ab8ca51b6d7e3f89 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:50:58 -0600 Subject: [PATCH 17/22] fix: updated pagination --- frontend/src/App.tsx | 19 ++----------------- .../src/components/PaginationControls.tsx | 12 ++++++++---- frontend/src/pages/InventoryPage.tsx | 2 +- frontend/src/pages/ProductListPage.tsx | 6 ++++-- frontend/src/services/productService.ts | 2 +- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a53698a..6003503 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,25 +1,10 @@ import React from 'react'; -import logo from './logo.svg'; import './App.css'; +import ProductListPage from './pages/ProductListPage'; function App() { return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
+ ); } diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index 13f533e..a762910 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -3,10 +3,13 @@ import React from "react"; interface Props { filters: any; setFilters: (filters: any) => void; + total: number; } -const PaginationControls: React.FC = ({filters, setFilters}) => { +const PaginationControls: React.FC = ({filters, setFilters, total}) => { const page = filters.page || 0; + const size = filters.size || 10; + const totalPages = Math.ceil(total / size); const goToPage = (newPage: number) => { setFilters({...filters, page: newPage}); @@ -20,11 +23,12 @@ const PaginationControls: React.FC = ({filters, setFilters}) => { Return - Page {page + 1} + Page {page + 1} of {totalPages} diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index fe0b302..302309e 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -19,7 +19,7 @@ const InventoryPage = () => { try { const data = await getProducts({page, size: 10}); const m = await getMetrics(); - setProducts(data); + setProducts(data.items); setMetrics(m); }catch (err) { console.log("Error loading products", err); diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index a96da01..442a3f9 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -9,6 +9,7 @@ const ProductListPage: React.FC = () => { const [products, setProducts] = useState([]); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); const [filters, setFilters] = useState({ name: "", @@ -26,7 +27,8 @@ const ProductListPage: React.FC = () => { setLoading(true); try { const data = await getProducts(filters); - setProducts(data); + setProducts(data.items); + setTotal(data.total); const m = await getMetrics(); setMetrics(m); }catch (err) { @@ -60,7 +62,7 @@ const ProductListPage: React.FC = () => { filters={filters} setFilters={setFilters} /> - + )} diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts index 27d17f7..18c9c77 100644 --- a/frontend/src/services/productService.ts +++ b/frontend/src/services/productService.ts @@ -1,7 +1,7 @@ import api from "./api"; import { Product, ProductDTO, InventoryMetrics } from "../types/Product"; -export const getProducts = async (params: any): Promise => { +export const getProducts = async (params: any): Promise<{items: Product[], total: number}> => { const response = await api.get("/products", {params}); return response.data; }; From 0f7937f1c24ef4f812c04451bbc19f941902d01f Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:25:13 -0600 Subject: [PATCH 18/22] feat: added creation and edit modal --- frontend/src/components/ProductFormModal.tsx | 151 +++++++++++++++++++ frontend/src/components/ProductTable.tsx | 5 +- frontend/src/pages/InventoryPage.tsx | 128 ---------------- frontend/src/pages/ProductListPage.tsx | 20 ++- 4 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/ProductFormModal.tsx delete mode 100644 frontend/src/pages/InventoryPage.tsx diff --git a/frontend/src/components/ProductFormModal.tsx b/frontend/src/components/ProductFormModal.tsx new file mode 100644 index 0000000..e7041ae --- /dev/null +++ b/frontend/src/components/ProductFormModal.tsx @@ -0,0 +1,151 @@ +import React, {useEffect, useState} from "react"; +import { Product, ProductDTO } from "../types/Product"; +import { createProduct, updateProduct } from "../services/productService"; + +interface Props{ + isOpen: boolean; + onClose:() => void; + onSucces: () => void; + initialData?: Product; +} + +const ProductFormModal: React.FC = ({isOpen, onClose, onSucces, initialData}) => { + const [form, setForm] = useState({ + name: "", + category: "", + unitPrice: 0, + quantityInStock: 0, + expirationDate: "", + }); + + const [errors, setErrors] = useState>({}); + + useEffect(() => { + if(initialData) { + setForm({ + name: initialData.name, + category: initialData.category, + unitPrice: initialData.unitPrice, + quantityInStock: initialData.quantityInStock, + expirationDate: initialData.expirationDate || "", + }); + } else { + setForm({ + name: "", + category: "", + unitPrice: 0, + quantityInStock: 0, + expirationDate: "", + }); + } + setErrors({}); + }, [initialData, isOpen]); + + if(!isOpen) return null; + + const handleChange = (e: React.ChangeEvent) => { + const {name, value} = e.target; + setForm({...form, [name]: name ==="unitPrice" || name === "quantityInStock" ? Number(value) : value}); + }; + + const validate = (): boolean => { + const newErrors: Record ={}; + if(!form.name.trim()) newErrors.name = "Name is required"; + if(!form.category.trim()) newErrors.category = "Category is required"; + if(form.unitPrice < 0) newErrors.unitPrice = "Price can't less than 0"; + if (form.quantityInStock < 0) newErrors.quantityInStock = "Stock can't be less than 0"; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if(!validate()) return; + + try { + if(initialData) { + await updateProduct(initialData.id, form); + } else { + await createProduct(form); + } + onSucces(); + onClose(); + }catch (error) { + alert("Error saving product"); + console.error(error); + } + }; + + return ( +
+
+

+ {initialData ? "Edit product" : "New product"} +

+ +
+
+ + + {errors.name &&

{errors.name}

} +
+ +
+ + + {errors.category &&

{errors.category}

} +
+ +
+
+ + + {errors.unitPrice &&

{errors.unitPrice}

} +
+ +
+ + + {errors.category &&

{errors.category}

} +
+
+ +
+ + +
+
+ +
+ + +
+
+
+ ); +}; + +export default ProductFormModal; \ No newline at end of file diff --git a/frontend/src/components/ProductTable.tsx b/frontend/src/components/ProductTable.tsx index cf81979..01b6753 100644 --- a/frontend/src/components/ProductTable.tsx +++ b/frontend/src/components/ProductTable.tsx @@ -6,9 +6,10 @@ interface Props{ products: Product[]; filters: any; setFilters: (f: any) => void; + onEdit: (product: Product) => void; } -const ProductTable: React.FC = ({products, filters, setFilters}) => { +const ProductTable: React.FC = ({products, filters, setFilters, onEdit}) => { const handleSort = (field: string, secondary = false) => { if(secondary) { setFilters({ ...filters, sortBy2: field, asc2: !filters.asc2}); @@ -81,7 +82,7 @@ const ProductTable: React.FC = ({products, filters, setFilters}) => { {p.expirationDate ?? "-"} - Page {page + 1} - - - - ); -}; - -export default InventoryPage \ No newline at end of file diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 442a3f9..84ce880 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -4,12 +4,15 @@ import { Product, InventoryMetrics } from "../types/Product"; import ProductTable from "../components/ProductTable"; import ProductFilters from "../components/ProductFilters"; import PaginationControls from "../components/PaginationControls"; +import ProductFormModal from "../components/ProductFormModal"; const ProductListPage: React.FC = () => { const [products, setProducts] = useState([]); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); + const [modalOpen, setModalOpen] = useState(false); + const [editingProduct, setEditingProducts] = useState(); const [filters, setFilters] = useState({ name: "", @@ -35,6 +38,11 @@ const ProductListPage: React.FC = () => { console.log("Error loading products", err); } finally {setLoading(false);} }; + + const handleEdit = (product: Product) => { + setEditingProducts(product); + setModalOpen(true); + } useEffect(() => { fetchData(); @@ -48,11 +56,20 @@ const ProductListPage: React.FC = () => {
+ setModalOpen(false)} + onSucces={fetchData} + initialData={editingProduct} /> + {loading ? (

Loading...

) : ( @@ -61,6 +78,7 @@ const ProductListPage: React.FC = () => { products={products} filters={filters} setFilters={setFilters} + onEdit={handleEdit} /> From 6f9997fe7ad2478c4fc7ddfe55155d109c0437ef Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:29:22 -0600 Subject: [PATCH 19/22] fix: fixed tailwind config --- frontend/tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ee52cec..2616547 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{tsx}"], + content: ["./src/**/*.tsx"], theme: { extend: {}, }, From 2fc697dd625859b770c00a26be3eff6717964b23 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:33:37 -0600 Subject: [PATCH 20/22] fix: added an .env --- frontend/.env | 1 + frontend/src/services/api.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/.env diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..d63aadf --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +REACT_APP_API_URL=http://localhost:9090 \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 6172bda..2b456f0 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,7 @@ import axios from "axios"; const api = axios.create({ - baseURL: "http://localhost:9090", + baseURL: process.env.REACT_APP_API_URL, }); export default api; \ No newline at end of file From 68212396f052368039cdddc277a97d5846f8f12c Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 15:28:57 -0600 Subject: [PATCH 21/22] fix: fixed warnings --- frontend/src/components/ProductFilters.tsx | 14 +++++++------- .../src/components/__tests__/ProductFlow.test.tsx | 0 frontend/src/pages/ProductListPage.tsx | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/__tests__/ProductFlow.test.tsx diff --git a/frontend/src/components/ProductFilters.tsx b/frontend/src/components/ProductFilters.tsx index d02f6fb..86ffb28 100644 --- a/frontend/src/components/ProductFilters.tsx +++ b/frontend/src/components/ProductFilters.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from "react"; +import React, {useState, useEffect, useCallback} from "react"; import { getCategories } from "../services/productService"; interface Props { @@ -20,15 +20,15 @@ const ProductFilters: React.FC = ({filters, setFilters}) => { }); }, []); - const handleApply = () => { - setFilters({ - ...filters, + const handleApply = useCallback( () => { + setFilters((prevFilters: any) =>({ + ...prevFilters, name, category, availability: availability === "" ? "" : availability === "in", page: 0, - }); - }; + })); + }, [setFilters, name, category, availability]); const toggleCategory = (value: string) => { setCategory(prev => @@ -40,7 +40,7 @@ const ProductFilters: React.FC = ({filters, setFilters}) => { useEffect(() => { handleApply(); - }, [name, category, availability]); + }, [handleApply]); return (
diff --git a/frontend/src/components/__tests__/ProductFlow.test.tsx b/frontend/src/components/__tests__/ProductFlow.test.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 84ce880..e488842 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from "react"; +import React, {useCallback, useEffect, useState} from "react"; import { getProducts, getMetrics } from "../services/productService"; import { Product, InventoryMetrics } from "../types/Product"; import ProductTable from "../components/ProductTable"; @@ -26,7 +26,7 @@ const ProductListPage: React.FC = () => { size: 10, }); - const fetchData = async () => { + const fetchData = useCallback( async () => { setLoading(true); try { const data = await getProducts(filters); @@ -37,7 +37,7 @@ const ProductListPage: React.FC = () => { }catch (err) { console.log("Error loading products", err); } finally {setLoading(false);} - }; + }, [filters]); const handleEdit = (product: Product) => { setEditingProducts(product); @@ -46,7 +46,7 @@ const ProductListPage: React.FC = () => { useEffect(() => { fetchData(); - }, [filters]); + }, [fetchData]); return(
From db72b3d762a125b6de0bfcfb298d96877ef216e9 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 15:52:45 -0600 Subject: [PATCH 22/22] fix: changed running port --- backend/src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 3ca17a4..661d54c 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=backend +server.port=9090