mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
Merge pull request #955 from ae-utbm/counter-click-step-3
Use TomSelect for product selection on counter
This commit is contained in:
commit
4f233538e0
@ -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<TomSettings> {
|
||||||
|
/* We disable the dropdown on focus because we're going to always autofocus the widget */
|
||||||
|
return { ...super.tomSelectSettings(), openOnFocus: false };
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { exportToHtml } from "#core:utils/globals";
|
import { exportToHtml } from "#core:utils/globals";
|
||||||
|
import type TomSelect from "tom-select";
|
||||||
|
|
||||||
interface CounterConfig {
|
interface CounterConfig {
|
||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
@ -20,6 +21,12 @@ exportToHtml("loadCounter", (config: CounterConfig) => {
|
|||||||
basket: config.sessionBasket,
|
basket: config.sessionBasket,
|
||||||
errors: [],
|
errors: [],
|
||||||
customerBalance: config.customerBalance,
|
customerBalance: config.customerBalance,
|
||||||
|
codeField: undefined,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.codeField = this.$refs.codeField;
|
||||||
|
this.codeField.widget.focus();
|
||||||
|
},
|
||||||
|
|
||||||
sumBasket() {
|
sumBasket() {
|
||||||
if (!this.basket || Object.keys(this.basket).length === 0) {
|
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,
|
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
|
||||||
);
|
);
|
||||||
document.getElementById("selling-accordion").click();
|
document.getElementById("selling-accordion").click();
|
||||||
|
this.codeField.widget.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleCode(event: SubmitEvent) {
|
async handleCode(event: SubmitEvent) {
|
||||||
const code = (
|
const widget: TomSelect = this.codeField.widget;
|
||||||
$(event.target).find("#code_field").val() as string
|
const code = (widget.getValue() as string).toUpperCase();
|
||||||
).toUpperCase();
|
if (this.codeField.getOperationCodes().includes(code)) {
|
||||||
if (["FIN", "ANN"].includes(code)) {
|
|
||||||
$(event.target).submit();
|
$(event.target).submit();
|
||||||
} else {
|
} else {
|
||||||
await this.handleAction(event);
|
await this.handleAction(event);
|
||||||
}
|
}
|
||||||
|
widget.clear();
|
||||||
|
widget.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleAction(event: SubmitEvent) {
|
async handleAction(event: SubmitEvent) {
|
||||||
@ -68,54 +77,12 @@ exportToHtml("loadCounter", (config: CounterConfig) => {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
this.basket = json.basket;
|
this.basket = json.basket;
|
||||||
this.errors = json.errors;
|
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 */
|
/* Accordion UI between basket and refills */
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
($("#click_form") as any).accordion({
|
($("#click_form") as any).accordion({
|
||||||
@ -124,6 +91,4 @@ $(() => {
|
|||||||
});
|
});
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
($("#products") as any).tabs();
|
($("#products") as any).tabs();
|
||||||
|
|
||||||
codeField.focus();
|
|
||||||
});
|
});
|
||||||
|
@ -5,8 +5,14 @@
|
|||||||
{{ counter }}
|
{{ counter }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block additional_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ static('bundled/core/components/ajax-select-index.css') }}" defer></link>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ static('core/components/ajax-select.scss') }}" defer></link>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_js %}
|
{% block additional_js %}
|
||||||
<script type="module" src="{{ static('bundled/counter/counter-click-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/counter/counter-click-index.ts') }}"></script>
|
||||||
|
<script type="module" src="{{ static('bundled/counter/components/counter-product-select-index.ts') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block info_boxes %}
|
{% block info_boxes %}
|
||||||
@ -40,9 +46,24 @@
|
|||||||
<form method="post" action=""
|
<form method="post" action=""
|
||||||
class="code_form" @submit.prevent="handleCode">
|
class="code_form" @submit.prevent="handleCode">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<input type="hidden" name="action" value="code">
|
<input type="hidden" name="action" value="code">
|
||||||
<label for="code_field"></label>
|
|
||||||
<input type="text" name="code" value="" class="focus" id="code_field"/>
|
<counter-product-select name="code" x-ref="codeField" autofocus required placeholder="{% trans %}Select a product...{% endtrans %}">
|
||||||
|
<option value=""></option>
|
||||||
|
<optgroup label="{% trans %}Operations{% endtrans %}">
|
||||||
|
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
||||||
|
<option value="ANN">{% trans %}Cancel (ANN){% endtrans %}</option>
|
||||||
|
</optgroup>
|
||||||
|
{% for category in categories.keys() %}
|
||||||
|
<optgroup label="{{ category }}">
|
||||||
|
{% for product in categories[category] %}
|
||||||
|
<option value="{{ product.code }}">{{ product }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
{% endfor %}
|
||||||
|
</counter-product-select>
|
||||||
|
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -174,15 +195,6 @@
|
|||||||
},
|
},
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
};
|
};
|
||||||
const productsAutocomplete = [
|
|
||||||
{% for p in products -%}
|
|
||||||
{
|
|
||||||
value: "{{ p.code }}",
|
|
||||||
label: "{{ p.name }}",
|
|
||||||
tags: "{{ p.code }} {{ p.name }}",
|
|
||||||
},
|
|
||||||
{%- endfor %}
|
|
||||||
];
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
loadCounter({
|
loadCounter({
|
||||||
csrfToken: "{{ csrf_token }}",
|
csrfToken: "{{ csrf_token }}",
|
||||||
|
@ -586,7 +586,7 @@ msgstr "Classeur : "
|
|||||||
#: accounting/templates/accounting/journal_statement_accounting.jinja:30
|
#: accounting/templates/accounting/journal_statement_accounting.jinja:30
|
||||||
#: core/templates/core/user_account.jinja:39
|
#: core/templates/core/user_account.jinja:39
|
||||||
#: core/templates/core/user_account_detail.jinja:9
|
#: core/templates/core/user_account_detail.jinja:9
|
||||||
#: counter/templates/counter/counter_click.jinja:31
|
#: counter/templates/counter/counter_click.jinja:37
|
||||||
msgid "Amount: "
|
msgid "Amount: "
|
||||||
msgstr "Montant : "
|
msgstr "Montant : "
|
||||||
|
|
||||||
@ -1217,7 +1217,7 @@ msgid "Barman"
|
|||||||
msgstr "Barman"
|
msgstr "Barman"
|
||||||
|
|
||||||
#: club/templates/club/club_sellings.jinja:51
|
#: 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:22
|
||||||
#: counter/templates/counter/last_ops.jinja:47
|
#: counter/templates/counter/last_ops.jinja:47
|
||||||
#: counter/templates/counter/refilling_list.jinja:12
|
#: counter/templates/counter/refilling_list.jinja:12
|
||||||
@ -2562,7 +2562,7 @@ msgstr "Confirmation"
|
|||||||
|
|
||||||
#: core/templates/core/delete_confirm.jinja:20
|
#: core/templates/core/delete_confirm.jinja:20
|
||||||
#: core/templates/core/file_delete_confirm.jinja:46
|
#: 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
|
#: counter/templates/counter/fragments/delete_student_card.jinja:12
|
||||||
#: sas/templates/sas/ask_picture_removal.jinja:20
|
#: sas/templates/sas/ask_picture_removal.jinja:20
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
@ -3276,7 +3276,7 @@ msgid "Go to my Trombi tools"
|
|||||||
msgstr "Allez à mes outils de Trombi"
|
msgstr "Allez à mes outils de Trombi"
|
||||||
|
|
||||||
#: core/templates/core/user_preferences.jinja:39
|
#: core/templates/core/user_preferences.jinja:39
|
||||||
#: counter/templates/counter/counter_click.jinja:123
|
#: counter/templates/counter/counter_click.jinja:144
|
||||||
msgid "Student card"
|
msgid "Student card"
|
||||||
msgstr "Carte étudiante"
|
msgstr "Carte étudiante"
|
||||||
|
|
||||||
@ -3918,12 +3918,28 @@ msgstr "oui"
|
|||||||
msgid "There is no cash register summary in this website."
|
msgid "There is no cash register summary in this website."
|
||||||
msgstr "Il n'y a pas de relevé de caisse dans ce site web."
|
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
|
#: launderette/templates/launderette/launderette_admin.jinja:8
|
||||||
msgid "Selling"
|
msgid "Selling"
|
||||||
msgstr "Vente"
|
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_refill.jinja:8
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:10
|
#: counter/templates/counter/fragments/create_student_card.jinja:10
|
||||||
#: counter/templates/counter/invoices_call.jinja:16
|
#: counter/templates/counter/invoices_call.jinja:16
|
||||||
@ -3934,21 +3950,21 @@ msgstr "Vente"
|
|||||||
msgid "Go"
|
msgid "Go"
|
||||||
msgstr "Valider"
|
msgstr "Valider"
|
||||||
|
|
||||||
#: counter/templates/counter/counter_click.jinja:53
|
#: counter/templates/counter/counter_click.jinja:74
|
||||||
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:19
|
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:19
|
||||||
msgid "Basket: "
|
msgid "Basket: "
|
||||||
msgstr "Panier : "
|
msgstr "Panier : "
|
||||||
|
|
||||||
#: counter/templates/counter/counter_click.jinja:94
|
#: counter/templates/counter/counter_click.jinja:115
|
||||||
msgid "Finish"
|
msgid "Finish"
|
||||||
msgstr "Terminer"
|
msgstr "Terminer"
|
||||||
|
|
||||||
#: counter/templates/counter/counter_click.jinja:104
|
#: counter/templates/counter/counter_click.jinja:125
|
||||||
#: counter/templates/counter/refilling_list.jinja:9
|
#: counter/templates/counter/refilling_list.jinja:9
|
||||||
msgid "Refilling"
|
msgid "Refilling"
|
||||||
msgstr "Rechargement"
|
msgstr "Rechargement"
|
||||||
|
|
||||||
#: counter/templates/counter/counter_click.jinja:114
|
#: counter/templates/counter/counter_click.jinja:135
|
||||||
msgid ""
|
msgid ""
|
||||||
"As a barman, you are not able to refill any account on your own. An admin "
|
"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 "
|
"should be connected on this counter for that. The customer can refill by "
|
||||||
|
Loading…
Reference in New Issue
Block a user