import { AlertMessage } from "#core:utils/alert-message"; import { BasketItem } from "#counter:counter/basket"; import type { CounterConfig, CounterItem, ProductFormula, } from "#counter:counter/types"; import type { CounterProductSelect } from "./components/counter-product-select-index"; document.addEventListener("alpine:init", () => { Alpine.data("counter", (config: CounterConfig) => ({ basket: {} as Record, customerBalance: config.customerBalance, codeField: null as CounterProductSelect | null, alertMessage: new AlertMessage({ defaultDuration: 2000 }), init() { // Fill the basket with the initial data for (const entry of config.formInitial) { if (entry.id !== undefined && entry.quantity !== undefined) { this.addToBasket(entry.id, entry.quantity); this.basket[entry.id].errors = entry.errors ?? []; } } this.codeField = this.$refs.codeField as CounterProductSelect; this.codeField.widget.hook("after", "onOptionSelect", () => { this.handleCode(); }); this.codeField.widget.focus(); // It's quite tricky to manually apply attributes to the management part // of a formset so we dynamically apply it here this.$refs.basketManagementForm .querySelector("#id_form-TOTAL_FORMS") ?.setAttribute(":value", "getBasketSize()"); }, removeFromBasket(id: string) { delete this.basket[id]; }, addToBasket(id: string, quantity: number) { const item: BasketItem = this.basket[id] || new BasketItem(config.products[id], 0); const oldQty = item.quantity; item.quantity += quantity; if (item.quantity <= 0) { delete this.basket[id]; return; } this.basket[id] = item; this.checkFormulas(); if (this.sumBasket() > this.customerBalance) { item.quantity = oldQty; if (item.quantity === 0) { delete this.basket[id]; } this.alertMessage.display(gettext("Not enough money"), { success: false }); } }, checkFormulas() { // Try to find a formula. // A formula is found if all its elements are already in the basket const products = new Set( Object.values(this.basket).map((item: BasketItem) => item.product.productId), ); const formula = config.formulas.find((f: ProductFormula) => { return f.products.every((p: number) => products.has(p)); }); if (formula === undefined) { return; } // Now that the formula is found, remove the items composing it from the basket for (const product of formula.products) { const item = Object.entries(this.basket).find( ([_, i]: [string, BasketItem]) => i.product.productId === product, ); if (item === undefined) { continue; } const key = item[0]; this.basket[key].quantity -= 1; if (this.basket[key].quantity <= 0) { this.removeFromBasket(key); } } // Then add the result product of the formula to the basket const result = Object.values(config.products) .filter((item: CounterItem) => item.productId === formula.result) .reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr)); this.addToBasket(result.price.id.toString(), 1); this.alertMessage.display( interpolate( gettext("Formula %(formula)s applied"), { formula: result.name }, true, ), { success: true }, ); }, getBasketSize() { return Object.keys(this.basket).length; }, sumBasket() { if (this.getBasketSize() === 0) { return 0; } const total = Object.values(this.basket).reduce( (acc: number, cur: BasketItem) => acc + cur.sum(), 0, ) as number; return Math.round(total * 100) / 100; }, onRefillingSuccess(event: CustomEvent) { if ( event.type !== "htmx:after-swap" || event.detail.failed || event.detail.elt.querySelector(".errorlist") ) { return; } this.customerBalance += Number.parseFloat( (event.detail.target.querySelector("#id_amount") as HTMLInputElement).value, ); document.getElementById("selling-accordion")?.setAttribute("open", ""); this.codeField?.widget.focus(); }, finish() { if (this.getBasketSize() === 0) { this.alertMessage.display(gettext("You can't send an empty basket."), { success: false, }); return; } (this.$refs.basketForm as HTMLFormElement).submit(); }, cancel() { location.href = config.cancelUrl; }, handleCode() { if (!this.codeField) throw Error("Unexpected null codeField."); const [quantity, code] = this.codeField.getSelectedProduct() as [number, string]; if (this.codeField.getOperationCodes().includes(code.toUpperCase())) { if (code === "ANN") { this.cancel(); } if (code === "FIN") { this.finish(); } } else { this.addToBasket(code, quantity); } this.codeField.widget.clear(); this.codeField.widget.setTextboxValue(""); this.codeField.widget.focus(); }, })); });