diff --git a/counter/static/bundled/counter/components/counter-product-select-index.ts b/counter/static/bundled/counter/components/counter-product-select-index.ts new file mode 100644 index 00000000..20aa23a5 --- /dev/null +++ b/counter/static/bundled/counter/components/counter-product-select-index.ts @@ -0,0 +1,61 @@ +import { AutoCompleteSelectBase } from "#core:core/components/ajax-select-base"; +import { registerComponent } from "#core:utils/web-components"; +import type { RecursivePartial, TomSettings } from "tom-select/dist/types/types"; + +const productParsingRegex = /^(\d+x)?(.*)/i; + +function parseProduct(query: string): [number, string] { + const parsed = productParsingRegex.exec(query); + return [Number.parseInt(parsed[1] || "1"), parsed[2]]; +} + +@registerComponent("counter-product-select") +export class CounterProductSelect extends AutoCompleteSelectBase { + public getOperationCodes(): string[] { + return ["FIN", "ANN"]; + } + + protected attachBehaviors(): void { + this.allowMultipleProducts(); + } + + private allowMultipleProducts(): void { + const search = this.widget.search; + const onOptionSelect = this.widget.onOptionSelect; + this.widget.hook("instead", "search", (query: string) => { + return search.call(this.widget, parseProduct(query)[1]); + }); + this.widget.hook( + "instead", + "onOptionSelect", + (evt: MouseEvent | KeyboardEvent, option: HTMLElement) => { + const [quantity, _] = parseProduct(this.widget.inputValue()); + const originalValue = option.getAttribute("data-value") ?? option.innerText; + + if (quantity === 1 || this.getOperationCodes().includes(originalValue)) { + return onOptionSelect.call(this.widget, evt, option); + } + + const value = `${quantity}x${originalValue}`; + const label = `${quantity}x${option.innerText}`; + this.widget.addOption({ value: value, text: label }, true); + return onOptionSelect.call( + this.widget, + evt, + this.widget.getOption(value, true), + ); + }, + ); + + this.widget.hook("after", "onOptionSelect", () => { + /* Focus the next element if it's an input */ + if (this.nextElementSibling.nodeName === "INPUT") { + (this.nextElementSibling as HTMLInputElement).focus(); + } + }); + } + protected tomSelectSettings(): RecursivePartial { + /* We disable the dropdown on focus because we're going to always autofocus the widget */ + return { ...super.tomSelectSettings(), openOnFocus: false }; + } +} diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index b8de64be..ef8ba2db 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -1,4 +1,5 @@ import { exportToHtml } from "#core:utils/globals"; +import type TomSelect from "tom-select"; interface CounterConfig { csrfToken: string; @@ -20,6 +21,12 @@ exportToHtml("loadCounter", (config: CounterConfig) => { basket: config.sessionBasket, errors: [], customerBalance: config.customerBalance, + codeField: undefined, + + init() { + this.codeField = this.$refs.codeField; + this.codeField.widget.focus(); + }, sumBasket() { if (!this.basket || Object.keys(this.basket).length === 0) { @@ -40,17 +47,19 @@ exportToHtml("loadCounter", (config: CounterConfig) => { (event.detail.target.querySelector("#id_amount") as HTMLInputElement).value, ); document.getElementById("selling-accordion").click(); + this.codeField.widget.focus(); }, async handleCode(event: SubmitEvent) { - const code = ( - $(event.target).find("#code_field").val() as string - ).toUpperCase(); - if (["FIN", "ANN"].includes(code)) { + const widget: TomSelect = this.codeField.widget; + const code = (widget.getValue() as string).toUpperCase(); + if (this.codeField.getOperationCodes().includes(code)) { $(event.target).submit(); } else { await this.handleAction(event); } + widget.clear(); + widget.focus(); }, async handleAction(event: SubmitEvent) { @@ -68,54 +77,12 @@ exportToHtml("loadCounter", (config: CounterConfig) => { const json = await response.json(); this.basket = json.basket; this.errors = json.errors; - $("form.code_form #code_field").val("").focus(); }, })); }); }); -interface Product { - value: string; - label: string; - tags: string; -} -declare global { - const productsAutocomplete: Product[]; -} - $(() => { - /* Autocompletion in the code field */ - // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery - const codeField: any = $("#code_field"); - - let quantity = ""; - codeField.autocomplete({ - // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery - select: (event: any, ui: any) => { - event.preventDefault(); - codeField.val(quantity + ui.item.value); - }, - // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery - focus: (event: any, ui: any) => { - event.preventDefault(); - codeField.val(quantity + ui.item.value); - }, - // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery - source: (request: any, response: any) => { - // biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal - const res = /^(\d+x)?(.*)/i.exec(request.term); - quantity = res[1] || ""; - const search = res[2]; - // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery - const matcher = new RegExp(($ as any).ui.autocomplete.escapeRegex(search), "i"); - response( - $.grep(productsAutocomplete, (value: Product) => { - return matcher.test(value.tags); - }), - ); - }, - }); - /* Accordion UI between basket and refills */ // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery ($("#click_form") as any).accordion({ @@ -124,6 +91,4 @@ $(() => { }); // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery ($("#products") as any).tabs(); - - codeField.focus(); }); diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 6de57147..01b38be1 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -5,8 +5,14 @@ {{ counter }} {% endblock %} +{% block additional_css %} + + +{% endblock %} + {% block additional_js %} + {% endblock %} {% block info_boxes %} @@ -40,9 +46,24 @@
{% csrf_token %} + - - + + + + + + + + {% for category in categories.keys() %} + + {% for product in categories[category] %} + + {% endfor %} + + {% endfor %} + +
@@ -174,15 +195,6 @@ }, {%- endfor -%} }; - const productsAutocomplete = [ - {% for p in products -%} - { - value: "{{ p.code }}", - label: "{{ p.name }}", - tags: "{{ p.code }} {{ p.name }}", - }, - {%- endfor %} - ]; window.addEventListener("DOMContentLoaded", () => { loadCounter({ csrfToken: "{{ csrf_token }}", diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 39f50561..3e15ddf0 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -586,7 +586,7 @@ msgstr "Classeur : " #: accounting/templates/accounting/journal_statement_accounting.jinja:30 #: core/templates/core/user_account.jinja:39 #: core/templates/core/user_account_detail.jinja:9 -#: counter/templates/counter/counter_click.jinja:31 +#: counter/templates/counter/counter_click.jinja:37 msgid "Amount: " msgstr "Montant : " @@ -1217,7 +1217,7 @@ msgid "Barman" msgstr "Barman" #: club/templates/club/club_sellings.jinja:51 -#: counter/templates/counter/counter_click.jinja:28 +#: counter/templates/counter/counter_click.jinja:34 #: counter/templates/counter/last_ops.jinja:22 #: counter/templates/counter/last_ops.jinja:47 #: counter/templates/counter/refilling_list.jinja:12 @@ -2562,7 +2562,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:100 +#: counter/templates/counter/counter_click.jinja:121 #: counter/templates/counter/fragments/delete_student_card.jinja:12 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" @@ -3276,7 +3276,7 @@ msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" #: core/templates/core/user_preferences.jinja:39 -#: counter/templates/counter/counter_click.jinja:123 +#: counter/templates/counter/counter_click.jinja:144 msgid "Student card" msgstr "Carte étudiante" @@ -3918,12 +3918,28 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:35 +#: counter/templates/counter/counter_click.jinja:41 #: launderette/templates/launderette/launderette_admin.jinja:8 msgid "Selling" msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:46 +#: counter/templates/counter/counter_click.jinja:52 +msgid "Select a product..." +msgstr "Sélectionnez un produit…" + +#: counter/templates/counter/counter_click.jinja:54 +msgid "Operations" +msgstr "Opérations" + +#: counter/templates/counter/counter_click.jinja:55 +msgid "Confirm (FIN)" +msgstr "Confirmer (FIN)" + +#: counter/templates/counter/counter_click.jinja:56 +msgid "Cancel (ANN)" +msgstr "Annuler (ANN)" + +#: counter/templates/counter/counter_click.jinja:67 #: counter/templates/counter/fragments/create_refill.jinja:8 #: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 @@ -3934,21 +3950,21 @@ msgstr "Vente" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:53 +#: counter/templates/counter/counter_click.jinja:74 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:94 +#: counter/templates/counter/counter_click.jinja:115 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:104 +#: counter/templates/counter/counter_click.jinja:125 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" -#: counter/templates/counter/counter_click.jinja:114 +#: counter/templates/counter/counter_click.jinja:135 msgid "" "As a barman, you are not able to refill any account on your own. An admin " "should be connected on this counter for that. The customer can refill by "