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/repository/TicketRepository.java b/src/main/java/ch/wisv/events/core/repository/TicketRepository.java index de4d6cce..1a693319 100644 --- a/src/main/java/ch/wisv/events/core/repository/TicketRepository.java +++ b/src/main/java/ch/wisv/events/core/repository/TicketRepository.java @@ -23,6 +23,16 @@ public interface TicketRepository extends JpaRepository { */ List findAllByProductAndOwner(Product product, Customer owner); + /** + * Find all Ticket by Products and Customer. + * + * @param products of type List + * @param owner of type Customer + * + * @return List + */ + List findAllByProductInAndOwner(List products, Customer owner); + /** * Find all Ticket by Product. * diff --git a/src/main/java/ch/wisv/events/core/service/order/OrderValidationServiceImpl.java b/src/main/java/ch/wisv/events/core/service/order/OrderValidationServiceImpl.java index ba6601ba..bf77002c 100644 --- a/src/main/java/ch/wisv/events/core/service/order/OrderValidationServiceImpl.java +++ b/src/main/java/ch/wisv/events/core/service/order/OrderValidationServiceImpl.java @@ -13,6 +13,7 @@ import ch.wisv.events.core.model.product.Product; import ch.wisv.events.core.repository.OrderRepository; import ch.wisv.events.core.service.event.EventService; +import ch.wisv.events.core.service.product.ProductService; import ch.wisv.events.core.service.ticket.TicketService; import java.time.LocalDateTime; import java.util.List; @@ -31,6 +32,9 @@ public class OrderValidationServiceImpl implements OrderValidationService { /** TicketService. */ private final TicketService ticketService; + /** ProductService. */ + private final ProductService productService; + /** EventService. */ private final EventService eventService; @@ -42,9 +46,10 @@ public class OrderValidationServiceImpl implements OrderValidationService { * @param eventService of type EventService */ @Autowired - public OrderValidationServiceImpl(OrderRepository orderRepository, TicketService ticketService, EventService eventService) { + public OrderValidationServiceImpl(OrderRepository orderRepository, TicketService ticketService, ProductService productService, EventService eventService) { this.orderRepository = orderRepository; this.ticketService = ticketService; + this.productService = productService; this.eventService = eventService; } @@ -80,16 +85,23 @@ public void assertOrderIsValidForCustomer(Order order, Customer customer) throws continue; } - int ticketSold = ticketService.getAllByProductAndCustomer(orderProduct.getProduct(), customer).size(); + List relatedProducts = productService.getRelatedProducts(orderProduct.getProduct()); + + int ticketSold = ticketService.getAllByProductsAndCustomer(relatedProducts, customer).size(); ticketSold += reservationOrders.stream() .mapToInt(reservationOrder -> reservationOrder.getOrderProducts().stream() - .filter(reservationOrderProduct -> orderProduct.getProduct().equals(reservationOrderProduct.getProduct())) + .filter(reservationOrderProduct -> relatedProducts.contains(reservationOrderProduct.getProduct())) .mapToInt(reservationOrderProduct -> reservationOrderProduct.getAmount().intValue()) .sum() ).sum(); - if (ticketSold + orderProduct.getAmount() > maxSoldPerCustomer) { + int tryingToOrder = order.getOrderProducts().stream() + .filter(relatedOrderProduct -> relatedProducts.contains(relatedOrderProduct.getProduct())) + .mapToInt(relatedOrderProduct -> relatedOrderProduct.getAmount().intValue()) + .sum(); + + if (ticketSold + tryingToOrder > maxSoldPerCustomer) { throw new OrderExceedCustomerLimitException(maxSoldPerCustomer - ticketSold); } } 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..591e9673 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,20 @@ public interface ProductService { */ List getAvailableProducts(); + /** + * Get possible parent products. + * + * @return Collection of Products + */ + List getPossibleParentProductsByProduct(Product product); + + /** + * Get the parent, children, and sibling products, including the original product + * + * @return Collection of Products + */ + List getRelatedProducts(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..aa71e95f 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 @@ -8,9 +8,7 @@ import ch.wisv.events.core.model.product.Product; import ch.wisv.events.core.repository.ProductRepository; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -53,10 +51,47 @@ 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 the parent, children, and sibling products, including the original product + * + * @return Collection of Products + */ + @Override + public List getRelatedProducts(Product product) { + List result = new ArrayList<>(); + Product parent = product; + while (parent.getParentProduct() != null) { + parent = parent.getParentProduct(); + } + Queue q = new LinkedList<>(); + q.add(parent); + while (!q.isEmpty()) { + Product p = q.remove(); + result.add(p); + q.addAll(p.getChildProducts()); + } + return result; + } + /** * Get Product by Key. * @@ -125,6 +160,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,9 +233,13 @@ 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!"); } + + if (product.getParentProduct() != null && product.getChildProducts() != null && product.getChildProducts().size() > 0) { + throw new ProductInvalidException("A product can not have a parent product and be a parent product at the same time!"); + } } /** diff --git a/src/main/java/ch/wisv/events/core/service/ticket/TicketService.java b/src/main/java/ch/wisv/events/core/service/ticket/TicketService.java index efb3cb54..42ef94bd 100644 --- a/src/main/java/ch/wisv/events/core/service/ticket/TicketService.java +++ b/src/main/java/ch/wisv/events/core/service/ticket/TicketService.java @@ -51,6 +51,16 @@ public interface TicketService { */ List getAllByProductAndCustomer(Product product, Customer customer); + /** + * Get all Ticket for one of multiple products and Customer. + * + * @param products of type List + * @param customer of type Customer + * + * @return List of Tickets + */ + List getAllByProductsAndCustomer(List products, Customer customer); + /** * Get all Ticket by a Product. * diff --git a/src/main/java/ch/wisv/events/core/service/ticket/TicketServiceImpl.java b/src/main/java/ch/wisv/events/core/service/ticket/TicketServiceImpl.java index 17da9240..adaadaf8 100644 --- a/src/main/java/ch/wisv/events/core/service/ticket/TicketServiceImpl.java +++ b/src/main/java/ch/wisv/events/core/service/ticket/TicketServiceImpl.java @@ -101,6 +101,19 @@ public List getAllByProductAndCustomer(Product product, Customer custome return ticketRepository.findAllByProductAndOwner(product, customer); } + /** + * Get all Ticket for one of multiple products and Customer. + * + * @param products of type List + * @param customer of type Customer + * + * @return List of Tickets + */ + @Override + public List getAllByProductsAndCustomer(List products, Customer customer) { + return ticketRepository.findAllByProductInAndOwner(products, customer); + } + /** * Get all Ticket by a Product. * 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/java/db/migration/V202409024__Add_product_parent_id.java b/src/main/java/db/migration/V202409024__Add_product_parent_id.java new file mode 100644 index 00000000..14354f8b --- /dev/null +++ b/src/main/java/db/migration/V202409024__Add_product_parent_id.java @@ -0,0 +1,30 @@ +package db.migration; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +import java.sql.Statement; + + +/** + * DB migration which adds parent_product_id to products + */ +public class V202409024__Add_product_parent_id extends BaseJavaMigration { + + /** + * Executes this migration. The execution will automatically take place within a transaction, when the underlying + * database supports it. + * + * @param context of type Context + * @throws Exception when something is wrong + */ + public void migrate(Context context) throws Exception { + try (Statement select = context.getConnection().createStatement()) { + select.execute("ALTER TABLE public.product ADD parent_product_id INTEGER"); + + select.execute("ALTER TABLE public.product ADD CONSTRAINT FK_PRODUCT_ON_PARENT_PRODUCT FOREIGN KEY " + + "(parent_product_id) REFERENCES public.product (id)"); + } + } + +} 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..5a46a014 100644 --- a/src/main/resources/static/js/products/script.js +++ b/src/main/resources/static/js/products/script.js @@ -62,6 +62,20 @@ $(document).ready(function () { } }); + // Check if parentProduct exists + hasParentFields($('#parentProduct').val() !== '' && $('#parentProduct').val() !== undefined); + $('#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..3db78e1a 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()&&void 0!==$("#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..0458151b 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.

+ + + +
+