From a843b90c2f163120507908df8675a9baac19c83a Mon Sep 17 00:00:00 2001 From: Joep de Jong Date: Sat, 21 Sep 2024 00:11:48 +0200 Subject: [PATCH 1/6] Add parent products to share stock --- .../admin/controller/DashboardController.java | 3 + .../DashboardProductController.java | 11 ++- .../events/admin/utils/ProductTemplate.java | 10 +-- .../events/core/model/product/Product.java | 71 +++++++++++++++++-- .../core/service/product/ProductService.java | 7 ++ .../service/product/ProductServiceImpl.java | 20 +++++- .../utils/dev/data/EventTestDataRunner.java | 23 +++--- .../utils/dev/data/ProductTestDataRunner.java | 6 ++ src/main/resources/dev/data/events.json | 12 ++-- src/main/resources/dev/data/products.json | 12 +++- .../resources/static/js/products/script.js | 13 ++++ .../static/js/products/script.min.js | 2 +- .../templates/admin/products/index.html | 6 +- .../templates/admin/products/product.html | 52 +++++++++++++- .../templates/admin/products/view.html | 9 +++ 15 files changed, 219 insertions(+), 38 deletions(-) diff --git a/src/main/java/ch/wisv/events/admin/controller/DashboardController.java b/src/main/java/ch/wisv/events/admin/controller/DashboardController.java index f3ce242e..39862c7e 100644 --- a/src/main/java/ch/wisv/events/admin/controller/DashboardController.java +++ b/src/main/java/ch/wisv/events/admin/controller/DashboardController.java @@ -35,6 +35,9 @@ abstract class DashboardController { /** Object key product. */ static final String OBJ_PRODUCT = "product"; + /** Object key parentProducts. */ + static final String OBJ_PARENT_PRODUCTS = "parentProducts"; + /** Object key tasks. */ static final String OBJ_TASKS = "tasks"; diff --git a/src/main/java/ch/wisv/events/admin/controller/DashboardProductController.java b/src/main/java/ch/wisv/events/admin/controller/DashboardProductController.java index a78e6ded..84d3b7ee 100644 --- a/src/main/java/ch/wisv/events/admin/controller/DashboardProductController.java +++ b/src/main/java/ch/wisv/events/admin/controller/DashboardProductController.java @@ -140,12 +140,19 @@ public String create(RedirectAttributes redirect, @ModelAttribute Product produc * * @return thymeleaf template path */ - @GetMapping("/edit/{key}") + @GetMapping({"/edit/{key}","/edit/{key}/"}) public String edit(Model model, RedirectAttributes redirect, @PathVariable String key) { try { if (!model.containsAttribute(OBJ_PRODUCT)) { model.addAttribute(OBJ_PRODUCT, productService.getByKey(key)); } + if (!model.containsAttribute(OBJ_PARENT_PRODUCTS)) { + model.addAttribute(OBJ_PARENT_PRODUCTS, + productService.getPossibleParentProductsByProduct( + (Product) model.getAttribute(OBJ_PRODUCT) + ) + ); + } return "admin/products/product"; } catch (ProductNotFoundException e) { @@ -164,7 +171,7 @@ public String edit(Model model, RedirectAttributes redirect, @PathVariable Strin * * @return String */ - @PostMapping("/edit/{key}") + @PostMapping({"/edit/{key}","/edit/{key}/"}) public String update(RedirectAttributes redirect, @ModelAttribute Product product, @PathVariable String key) { try { product.setKey(key); diff --git a/src/main/java/ch/wisv/events/admin/utils/ProductTemplate.java b/src/main/java/ch/wisv/events/admin/utils/ProductTemplate.java index 8932c60e..492f16ad 100644 --- a/src/main/java/ch/wisv/events/admin/utils/ProductTemplate.java +++ b/src/main/java/ch/wisv/events/admin/utils/ProductTemplate.java @@ -36,7 +36,7 @@ public enum ProductTemplate { /** Product maxSoldperCustomer. */ @Getter - private final int maxSolPerCustomer; + private final int maxSoldPerCustomer; /** Product chOnly. */ @Getter @@ -53,7 +53,7 @@ public enum ProductTemplate { * @param title of type String * @param cost of type double * @param vatRate of type VatRate - * @param maxSolPerCustomer of type int + * @param maxSoldPerCustomer of type int * @param chOnly of type boolean * @param reservable of type boolean */ @@ -61,7 +61,7 @@ public enum ProductTemplate { String templateName, String title, double cost, VatRate vatRate, - int maxSolPerCustomer, + int maxSoldPerCustomer, boolean chOnly, boolean reservable ) { @@ -69,7 +69,7 @@ public enum ProductTemplate { this.title = title; this.cost = cost; this.vatRate = vatRate; - this.maxSolPerCustomer = maxSolPerCustomer; + this.maxSoldPerCustomer = maxSoldPerCustomer; this.chOnly = chOnly; this.reservable = reservable; } @@ -85,7 +85,7 @@ public String toJson() { object.put("title", this.getTitle()); object.put("cost", this.getCost()); object.put("vatRate", this.getVatRate()); - object.put("maxSoldPerCustomer", this.getMaxSolPerCustomer()); + object.put("maxSoldPerCustomer", this.getMaxSoldPerCustomer()); object.put("chOnly", this.isChOnly()); object.put("reservable", this.isReservable()); diff --git a/src/main/java/ch/wisv/events/core/model/product/Product.java b/src/main/java/ch/wisv/events/core/model/product/Product.java index 93f630ca..b844f5f2 100644 --- a/src/main/java/ch/wisv/events/core/model/product/Product.java +++ b/src/main/java/ch/wisv/events/core/model/product/Product.java @@ -6,6 +6,10 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import ch.wisv.events.core.model.event.Event; @@ -102,12 +106,28 @@ public class Product { @ManyToOne @JoinTable(name = "event_products", joinColumns = @JoinColumn(name = "products_id")) + @JsonIgnore public Event event; + /** + * Parent Product. Shares the product availability. + */ + @ManyToOne + @JoinColumn(name = "parent_product_id") + public Product parentProduct; + + /** + * Field childProducts. + */ + @OneToMany(mappedBy = "parentProduct", cascade = CascadeType.MERGE, targetEntity = Product.class, fetch = FetchType.LAZY) + @JsonIgnore + public List childProducts; + /** * Field productList. */ @OneToMany(cascade = CascadeType.MERGE, targetEntity = Product.class, fetch = FetchType.EAGER) + @JsonIgnore public List products; /** @@ -196,13 +216,13 @@ public Product( * @return progress of event */ public double calcProgress() { - if (this.maxSold == null) { + if (this.getMaxSold() == null) { return 100.d; - } else if (this.sold == 0) { + } else if (this.getTotalSold() == 0) { return 0.d; } - return Math.round((((double) this.sold / (double) this.maxSold) * 100.d) * 100.d) / 100.d; + return Math.round((((double) this.getTotalSold() / (double) this.getMaxSold()) * 100.d) * 100.d) / 100.d; } /** @@ -229,7 +249,50 @@ public void increaseReserved(int amount) { * @return boolean */ public boolean isSoldOut() { - return this.maxSold != null && this.sold >= this.maxSold; + return this.getMaxSold() != null && this.getTotalSold() >= this.getMaxSold(); + } + + public Integer getMaxSold() { + // If the product has a parent product, return the maxSold of the parent product. + if (this.parentProduct != null) { + return this.parentProduct.getMaxSold(); + } + + return this.maxSold; } + public Integer getMaxSoldPerCustomer() { + // If the product has a parent product, return the maxSoldPerCustomer of the parent product. + if (this.parentProduct != null) { + return this.parentProduct.getMaxSoldPerCustomer(); + } + + return this.maxSoldPerCustomer; + } + + public Integer getTotalSold() { + // If the product has a parent product, return the total sold of the parent product. + if (this.parentProduct != null) { + return this.parentProduct.getTotalSold(); + } + + int totalSold = this.sold; + if (this.childProducts != null) { + for (Product childProduct : this.childProducts) { + totalSold += childProduct.getSold(); + } + } + + return totalSold; + } + + // Custom toString + @Override + public String toString() { + return "Product{" + + "id=" + id + + ", title='" + title + '\'' + + ", description='" + description + '\'' + + '}'; + } } diff --git a/src/main/java/ch/wisv/events/core/service/product/ProductService.java b/src/main/java/ch/wisv/events/core/service/product/ProductService.java index f5b4331b..fa495f06 100644 --- a/src/main/java/ch/wisv/events/core/service/product/ProductService.java +++ b/src/main/java/ch/wisv/events/core/service/product/ProductService.java @@ -26,6 +26,13 @@ public interface ProductService { */ List getAvailableProducts(); + /** + * Get possible parent products. + * + * @return Collection of Products + */ + List getPossibleParentProductsByProduct(Product product); + /** * Get Product by Key. * diff --git a/src/main/java/ch/wisv/events/core/service/product/ProductServiceImpl.java b/src/main/java/ch/wisv/events/core/service/product/ProductServiceImpl.java index e0a2fbb9..0208d3b2 100644 --- a/src/main/java/ch/wisv/events/core/service/product/ProductServiceImpl.java +++ b/src/main/java/ch/wisv/events/core/service/product/ProductServiceImpl.java @@ -53,10 +53,25 @@ public List getAllProducts() { public List getAvailableProducts() { return productRepository.findAllBySellStartBefore(LocalDateTime.now()).stream() .filter(product -> (product.getSellEnd() == null || !product.getSellEnd() - .isBefore(LocalDateTime.now())) && (product.getMaxSold() == null || product.getSold() < product.getMaxSold())) + .isBefore(LocalDateTime.now())) && (product.getMaxSold() == null || product.getTotalSold() < product.getMaxSold())) .collect(Collectors.toCollection(ArrayList::new)); } + /** + * Get possible parent products. + * + * @return Collection of Products + */ + @Override + public List getPossibleParentProductsByProduct(Product product) { + if (product.getEvent() == null) { + return new ArrayList<>(); + } + return product.getEvent().getProducts().stream() + .filter(p -> p.getParentProduct() == null && !p.getKey().equals(product.getKey())) + .collect(Collectors.toList()); + } + /** * Get Product by Key. * @@ -125,6 +140,7 @@ public void update(Product product) throws ProductNotFoundException, ProductInva model.setMaxSoldPerCustomer(product.getMaxSoldPerCustomer()); model.setChOnly(product.isChOnly()); model.setReservable(product.isReservable()); + model.setParentProduct(product.getParentProduct()); if (product.getSold() != 0) { model.setSold(product.getSold()); @@ -197,7 +213,7 @@ private void assertIsValidProduct(Product product) throws ProductInvalidExceptio throw new ProductInvalidException("It is not possible to add the same product twice or more!"); } - if (product.getMaxSoldPerCustomer() == null || product.getMaxSoldPerCustomer() < 1 || product.getMaxSoldPerCustomer() > 25) { + if (product.getParentProduct() == null && (product.getMaxSoldPerCustomer() == null || product.getMaxSoldPerCustomer() < 1 || product.getMaxSoldPerCustomer() > 25)) { throw new ProductInvalidException("Max sold per customer should be between 1 and 25!"); } } diff --git a/src/main/java/ch/wisv/events/utils/dev/data/EventTestDataRunner.java b/src/main/java/ch/wisv/events/utils/dev/data/EventTestDataRunner.java index 5ce92afa..3642e5bc 100644 --- a/src/main/java/ch/wisv/events/utils/dev/data/EventTestDataRunner.java +++ b/src/main/java/ch/wisv/events/utils/dev/data/EventTestDataRunner.java @@ -41,24 +41,25 @@ public EventTestDataRunner(EventRepository eventRepository, ProductRepository pr } /** - * Method addProduct. + * Method addProducts. * * @param event of type Event * @param jsonObject of type JSONObject * * @return Event */ - private Event addProduct(Event event, JSONObject jsonObject) { - if (jsonObject.get("productNumber") != null) { - int productNumber = ((Long) jsonObject.get("productNumber")).intValue(); - Optional optional = this.productRepository.findById(productNumber); + private Event addProducts(Event event, JSONObject jsonObject) { + if (jsonObject.get("productNumbers") != null) { + for (Object productNumber : (Iterable) jsonObject.get("productNumbers")) { + Optional optional = this.productRepository.findById(((Long) productNumber).intValue()); - optional.ifPresent(product -> { - product.setLinked(true); - this.productRepository.saveAndFlush(product); + optional.ifPresent(product -> { + product.setLinked(true); + this.productRepository.saveAndFlush(product); - event.addProduct(product); - }); + event.addProduct(product); + }); + } } return event; @@ -101,7 +102,7 @@ private Event createEvent(JSONObject jsonObject) { @Override protected void loop(JSONObject jsonObject) { Event event = this.createEvent(jsonObject); - event = this.addProduct(event, jsonObject); + event = this.addProducts(event, jsonObject); this.eventRepository.save(event); } diff --git a/src/main/java/ch/wisv/events/utils/dev/data/ProductTestDataRunner.java b/src/main/java/ch/wisv/events/utils/dev/data/ProductTestDataRunner.java index 6cd4c1bf..e279e18b 100644 --- a/src/main/java/ch/wisv/events/utils/dev/data/ProductTestDataRunner.java +++ b/src/main/java/ch/wisv/events/utils/dev/data/ProductTestDataRunner.java @@ -4,6 +4,7 @@ import ch.wisv.events.core.repository.ProductRepository; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.Optional; import ch.wisv.events.core.util.VatRate; import org.json.simple.JSONObject; @@ -51,6 +52,11 @@ private Product createProduct(JSONObject jsonObject) { ); product.setKey((String) jsonObject.get("key")); product.setMaxSoldPerCustomer(((Long) jsonObject.get("maxSoldPerCustomer")).intValue()); + if (jsonObject.get("parentProductNumber") != null) { + Optional optional = this.productRepository.findById(((Long) jsonObject.get("parentProductNumber")).intValue()); + + optional.ifPresent(product::setParentProduct); + } return product; } diff --git a/src/main/resources/dev/data/events.json b/src/main/resources/dev/data/events.json index 3be9c65c..60536fa5 100644 --- a/src/main/resources/dev/data/events.json +++ b/src/main/resources/dev/data/events.json @@ -7,7 +7,7 @@ "target": 50, "maxSold": 100, "imageUrl": "https://ch.tudelft.nl/wp-content/uploads/2017/01/pt-boards.jpg", - "productNumber": 1 + "productNumbers": [1, 6] }, { "title": "Symposium", @@ -17,7 +17,7 @@ "target": 50, "maxSold": 100, "imageUrl": "https://ch.tudelft.nl/wp-content/uploads/2017/01/pt-boards.jpg", - "productNumber": 2 + "productNumbers": [2] }, { "title": "MatCH CH vision Music Festival", @@ -27,7 +27,7 @@ "target": 50, "maxSold": 100, "imageUrl": "https://ch.tudelft.nl/wp-content/uploads/2017/01/pt-boards.jpg", - "productNumber": 4 + "productNumbers": [4] }, { "title": "WiFi Rally", @@ -37,7 +37,7 @@ "target": 50, "maxSold": 100, "imageUrl": "https://ch.tudelft.nl/wp-content/uploads/2017/01/pt-boards.jpg", - "productNumber": 3 + "productNumbers": [3] }, { "title": "Annu drinks", @@ -47,6 +47,6 @@ "target": 50, "maxSold": 100, "imageUrl": "https://ch.tudelft.nl/wp-content/uploads/2017/01/pt-boards.jpg", - "productNumber": 5 + "productNumbers": [5] } -] \ No newline at end of file +] diff --git a/src/main/resources/dev/data/products.json b/src/main/resources/dev/data/products.json index 5cad3010..a51518d6 100644 --- a/src/main/resources/dev/data/products.json +++ b/src/main/resources/dev/data/products.json @@ -43,5 +43,15 @@ "maxSold": 250, "maxSoldPerCustomer": 25, "key": "7a5b9158-a9e4-40f5-ad7f-586092274e63" + }, + { + "title": "T.U.E.S.Day lecture ticket zonder eten", + "description": "Ticket for the T.U.E.S.Day lecture. Zonder eten.", + "cost": 0.00, + "vatRate": "VAT_HIGH", + "maxSold": 100, + "maxSoldPerCustomer": 4, + "parentProductNumber": 1, + "key": "dd729c96-dacf-4eab-a2e8-19d2a1ad917d" } -] \ No newline at end of file +] diff --git a/src/main/resources/static/js/products/script.js b/src/main/resources/static/js/products/script.js index 6d8a0633..45d97f21 100644 --- a/src/main/resources/static/js/products/script.js +++ b/src/main/resources/static/js/products/script.js @@ -62,6 +62,19 @@ $(document).ready(function () { } }); + hasParentFields($('#parentProduct').val() !== ''); + $('#parentProduct').on('change', function () { + const hasParent = $(this).val() !== ''; + hasParentFields(hasParent); + }); + + function hasParentFields(hasParent) { + $('#maxSold').prop('disabled', hasParent); + $('#maxSoldPerCustomer').prop('disabled', hasParent); + $('#productAvailability').toggle(!hasParent); + $('#parentProductHint').toggle(hasParent); + } + $('.remove-product').on('click', function (e) { e.preventDefault(); diff --git a/src/main/resources/static/js/products/script.min.js b/src/main/resources/static/js/products/script.min.js index f12b8c22..5549ed95 100644 --- a/src/main/resources/static/js/products/script.min.js +++ b/src/main/resources/static/js/products/script.min.js @@ -1 +1 @@ -var TemplateSelector;function format(t,e){return $.each(e,(function(e,a){t=t.replace(new RegExp("\\{"+e+"\\}","g"),a)})),t}!function(t){TemplateSelector={init:function(){TemplateSelector.binds()},binds:function(){t(".product-template-item a").on("click",TemplateSelector.__setProductTemplateValues)},__setProductTemplateValues:function(e){e.preventDefault();var a=t(e.target).parent().data("template");t.each(a,(function(e,a){"chOnly"===e&&(e+="1");var r=t("#"+e);r.val(a),"chOnly1"===e&&!0===a?r.attr("checked","checked"):r.removeAttr("checked")}))}}}(jQuery),$(document).ready((function(){TemplateSelector.init();var t={enableTime:!0,altInput:!0,altFormat:"F j, Y H:i",dateFormat:"Y-m-dTH:i:S",time_24hr:!0,locale:{firstDayOfWeek:1}};$("#sellStart").flatpickr(t),$("#sellEnd").flatpickr(t),$("#q").autocomplete({serviceUrl:"/events/api/v1/products/search/unused",onSelect:function(t){!function(t,e){const a='',r=" {0}";var n=$("#products"),o=n.children().length;$("#productsTable").append(format(r,[e,t])),n.append(format(a,[o,o,t])),$("#noProducts").remove(),$((function(){$('[data-toggle="tooltip"]').tooltip()}))}(t.data,t.value),$(this).val(""),$("#addProduct").modal("hide")}}),$(".remove-product").on("click",(function(t){t.preventDefault();var e=$("#products"),a=e.children().length,r=$(this).data("product-id"),n=e.find(":input[value='"+r+"']"),o=n.attr("id").replace("products","");n.remove(),$(this).parent().parent().remove();for(var c=o;c {0}",[a,e])),r.append(format('',[o,o,e])),$("#noProducts").remove(),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(this).val(""),$("#addProduct").modal("hide")}}),e(""!==$("#parentProduct").val()),$("#parentProduct").on("change",function(){let t=""!==$(this).val();e(t)}),$(".remove-product").on("click",function(t){t.preventDefault();var e=$("#products"),a=e.children().length,r=$(this).data("product-id"),o=e.find(":input[value='"+r+"']"),n=o.attr("id").replace("products","");o.remove(),$(this).parent().parent().remove();for(var l=n;lProducts - +
-
diff --git a/src/main/resources/templates/admin/products/product.html b/src/main/resources/templates/admin/products/product.html index 487ee726..dfc3cfeb 100644 --- a/src/main/resources/templates/admin/products/product.html +++ b/src/main/resources/templates/admin/products/product.html @@ -143,11 +143,28 @@
Information
-
+ +
+
+ + + The availability and max sold per customer will be inherited from the parent product. +
+
+
@@ -155,7 +172,8 @@
Information
+ th:disabled="${product.parentProduct != null}" + th:required="${product.parentProduct == null}">
@@ -267,6 +285,34 @@

Search product

+ + + diff --git a/src/main/resources/templates/admin/products/view.html b/src/main/resources/templates/admin/products/view.html index 75149a62..dc1d9b21 100644 --- a/src/main/resources/templates/admin/products/view.html +++ b/src/main/resources/templates/admin/products/view.html @@ -112,6 +112,15 @@
Information
+
+
+

Warning The stock and availability of this product is linked to the parent product.

+ + + +
+
-
+