/** * Copyright since 2007 PrestaShop SA and Contributors * PrestaShop is an International Registered Trademark & Property of PrestaShop SA * * NOTICE OF LICENSE * * This source file is subject to the Open Software License (OSL 3.0) * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: * https://opensource.org/licenses/OSL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * * DISCLAIMER * * Do not edit or add to this file if you wish to upgrade PrestaShop to newer * versions in the future. If you wish to customize PrestaShop for your * needs please refer to https://devdocs.prestashop.com/ for more information. * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ import CartEditor, {CartProduct} from '@pages/order/create/cart-editor'; import createOrderMap from '@pages/order/create/create-order-map'; import eventMap from '@pages/order/create/event-map'; import {EventEmitter} from '@components/event-emitter'; import ProductRenderer from '@pages/order/create/product-renderer'; import Router from '@components/router'; const {$} = window; /* eslint-disable */ interface SearchParams { currency_id?: string; search_phrase: string; } /* eslint-enable */ /** * Product component Object for "Create order" page */ export default class ProductManager { products: Array>; selectedProduct: null | Record; selectedCombinationId: null | string | number; activeSearchRequest: JQuery.jqXHR | null; productRenderer: ProductRenderer; router: Router; cartEditor: CartEditor; constructor() { this.products = []; this.selectedProduct = null; this.selectedCombinationId = null; this.activeSearchRequest = null; this.productRenderer = new ProductRenderer(); this.router = new Router(); this.cartEditor = new CartEditor(); this.initListeners(); } addProductToCart(cartId: number): void { this.cartEditor.addProduct(cartId, this.getProductData()); } removeProductFromCart(cartId: number, product: CartProduct): void { this.cartEditor.removeProductFromCart(cartId, product); } changeProductPrice( cartId: number, customerId: number, updatedProduct: CartProduct, ): void { this.cartEditor.changeProductPrice(cartId, customerId, updatedProduct); } changeProductQty(cartId: number, updatedProduct: CartProduct): void { this.cartEditor.changeProductQty(cartId, updatedProduct); } /** * Initializes event listeners * * @private */ private initListeners(): void { $(createOrderMap.productSelect).on('change', (e: JQueryEventObject) => this.initProductSelect(e), ); $(createOrderMap.combinationsSelect).on('change', (e: JQueryEventObject) => this.initCombinationSelect(e), ); this.onProductSearch(); this.onAddProductToCart(); this.onRemoveProductFromCart(); this.onProductPriceChange(); this.onProductQtyChange(); } /** * Listens for product search event * * @private */ private onProductSearch(): void { EventEmitter.on(eventMap.productSearched, (response) => { this.products = response.products; this.productRenderer.renderSearchResults(this.products); this.selectFirstResult(); }); } /** * Listens for add product to cart event * * @private */ private onAddProductToCart(): void { // on success EventEmitter.on(eventMap.productAddedToCart, (cartInfo) => { this.productRenderer.cleanCartBlockAlerts(); this.updateStockOnProductAdd(); EventEmitter.emit(eventMap.cartLoaded, cartInfo); }); // on failure EventEmitter.on(eventMap.productAddToCartFailed, (errorMessage) => { this.productRenderer.renderCartBlockErrorAlert(errorMessage); }); } /** * Listens for remove product from cart event * * @private */ private onRemoveProductFromCart(): void { EventEmitter.on(eventMap.productRemovedFromCart, (data) => { this.updateStockOnProductRemove(data.product); EventEmitter.emit(eventMap.cartLoaded, data.cartInfo); }); } /** * Listens for product price change in cart event * * @private */ private onProductPriceChange(): void { EventEmitter.on(eventMap.productPriceChanged, (cartInfo) => { this.productRenderer.cleanCartBlockAlerts(); EventEmitter.emit(eventMap.cartLoaded, cartInfo); }); } /** * Listens for product quantity change in cart success/failure event * * @private */ private onProductQtyChange(): void { const enableQtyInputs = () => { const inputsQty = >( document.querySelectorAll(createOrderMap.listedProductQtyInput) ); inputsQty.forEach((inputQty) => { // eslint-disable-next-line inputQty.disabled = false; }); }; // on success EventEmitter.on(eventMap.productQtyChanged, (data) => { this.productRenderer.cleanCartBlockAlerts(); this.updateStockOnQtyChange(data.product); $(createOrderMap.createOrderButton).prop('disabled', false); EventEmitter.emit(eventMap.cartLoaded, data.cartInfo); enableQtyInputs(); }); // on failure EventEmitter.on(eventMap.productQtyChangeFailed, (e) => { this.productRenderer.renderCartBlockErrorAlert(e.responseJSON.message); $(createOrderMap.createOrderButton).prop('disabled', true); enableQtyInputs(); }); } /** * Initializes product select * * @param event * * @private */ private initProductSelect(event: JQueryEventObject): void { const productId = Number( $(event.currentTarget) .find(':selected') .val(), ); this.selectProduct(productId); } /** * Initializes combination select * * @param event * * @private */ private initCombinationSelect(event: JQueryEventObject): void { const combinationId = Number( $(event.currentTarget) .find(':selected') .val(), ); this.selectCombination(combinationId); } /** * Searches for product */ search(searchPhrase: string): void { // Search only if the search phrase length is greater than 2 characters if (searchPhrase.length < 2) { return; } this.productRenderer.renderSearching(); if (this.activeSearchRequest !== null) { this.activeSearchRequest.abort(); } const params: SearchParams = { search_phrase: searchPhrase, }; if ( $(createOrderMap.cartCurrencySelect).data('selectedCurrencyId') !== undefined ) { params.currency_id = $(createOrderMap.cartCurrencySelect).data( 'selectedCurrencyId', ); } const $searchRequest = $.get( this.router.generate('admin_orders_products_search'), params, ); this.activeSearchRequest = $searchRequest; $searchRequest .then((response) => { EventEmitter.emit(eventMap.productSearched, response); }) .catch((response: JQuery.jqXHR) => { if (response.statusText === 'abort') { return; } window.showErrorMessage(response.responseJSON.message); }); } /** * Initiate first result dataset after search * * @private */ private selectFirstResult(): void { this.unsetProduct(); if (this.products.length !== 0) { this.selectProduct(this.products[0].productId); } } /** * Handles use case when product is selected from search results * * @private * * @param {Number} productId */ private selectProduct(productId: number): Record { this.unsetCombination(); const selectedProduct = Object.values(this.products).find( (product) => product.productId === productId, ); if (selectedProduct) { this.selectedProduct = selectedProduct; } this.productRenderer.renderProductMetadata( > this.selectedProduct, ); // if product has combinations select the first else leave it null if (this.selectedProduct?.combinations.length !== 0) { this.selectCombination( Object.keys(this.selectedProduct?.combinations)[0], ); } return > this.selectedProduct; } /** * Handles use case when new combination is selected * * @param combinationId * * @private */ private selectCombination( combinationId: number | string, ): Record { const combination = this.selectedProduct?.combinations[combinationId]; this.selectedCombinationId = combinationId; this.productRenderer.renderStock( $(createOrderMap.inStockCounter), $(createOrderMap.quantityInput), combination.stock, this.selectedProduct?.availableOutOfStock || combination.stock <= 0, ); return combination; } /** * Sets the selected combination id to null * * @private */ private unsetCombination(): void { this.selectedCombinationId = null; } /** * Sets the selected product to null * * @private */ private unsetProduct(): void { this.selectedProduct = null; } /** * Retrieves product data from product search result block fields * * @returns {Object} * * @private */ private getProductData(): Record { const $fileInputs = $(createOrderMap.productCustomizationContainer).find( 'input[type="file"]', ); const formData = new FormData( document.querySelector(createOrderMap.productAddForm), ); const fileSizes: Record = {}; // adds key value pairs {input name: file size} of each file in separate object // in case formData size exceeds server settings. $.each($fileInputs, (key: number, input: any) => { if (input.files.length !== 0) { fileSizes[$(input).data('customization-field-id')] = input.files[0].size; } }); return { product: formData, fileSizes, }; } /** * Updates the stock when the product is added to cart in "create new order" page * * @private */ private updateStockOnProductAdd(): void { const {productId} = > this.selectedProduct; const attributeId = this.selectedCombinationId; const qty = -Number($(createOrderMap.quantityInput).val()); this.updateStock(productId, attributeId, qty); } /** * Updates the stock when the product is removed from cart in Orders/"create new order page" * * @private */ private updateStockOnProductRemove(product: Record): void { const {productId, attributeId, qtyToRemove} = product; const qty = qtyToRemove; this.updateStock(productId, attributeId, qty); } /** * Updates the stock when the quantity of product is changed from cart in Orders/"create new order page" * * @private */ private updateStockOnQtyChange(product: Record): void { const { productId, attributeId, prevQty, newQty, } = product; const qty = prevQty - newQty; this.updateStock(productId, attributeId, qty); } /** * Updates the stock in products object and renders the new stock * * @private */ private updateStock( productId: number, attributeId: string | number, qty: number, ): void { const productKeys = Object.keys(this.products); const productValues = Object.values(this.products); for (let i = 0; i < productKeys.length; i += 1) { if (productValues[i].productId === productId) { const $template = this.productRenderer.cloneProductTemplate( productValues[i], ); // Update the stock value in products object productValues[i].stock += qty; // Update the stock also for combination */ if (attributeId && attributeId > 0) { productValues[i].combinations[attributeId].stock += qty; } // Render the new stock value if (this.selectedProduct?.productId === productId) { if (this.selectedProduct.combinations.length === 0) { this.productRenderer.renderStock( $template.find(createOrderMap.listedProductQtyStock), $template.find(createOrderMap.listedProductQtyInput), productValues[i].stock, productValues[i].availableOutOfStock || productValues[i].availableStock <= 0, ); } else if ( attributeId && Number(this.selectedCombinationId) === Number(attributeId) ) { this.productRenderer.renderStock( $template.find(createOrderMap.listedProductQtyStock), $template.find(createOrderMap.listedProductQtyInput), productValues[i].combinations[attributeId].stock, productValues[i].availableOutOfStock || productValues[i].availableStock <= 0, ); } } break; } } } }