replaced vue by alpine

This commit is contained in:
Thomas Girod 2023-01-10 03:50:35 +01:00
parent 99827e005b
commit bfe90b50c8
7 changed files with 274 additions and 277 deletions

File diff suppressed because one or more lines are too long

View File

@ -1224,7 +1224,7 @@ u, .underline {
text-decoration: underline;
}
#bar_ui {
#bar-ui {
padding: 0.4em;
display: flex;
flex-wrap: wrap;

View File

@ -0,0 +1,69 @@
document.addEventListener('alpine:init', () => {
Alpine.data('counter', () => ({
basket: basket,
errors: [],
sum_basket() {
if (!this.basket || Object.keys(this.basket).length === 0) {
return 0;
}
const total = Object.values(this.basket)
.reduce((acc, cur) => acc + cur["qty"] * cur["price"], 0);
return total / 100;
},
async handle_action(event) {
const payload = $(event.target).serialize();
let request = new Request(click_api_url, {
method: "POST",
body: payload,
headers: {
'Accept': 'application/json',
'X-CSRFToken': csrf_token,
}
})
const response = await fetch(request);
const json = await response.json();
this.basket = json["basket"]
this.errors = json["errors"]
$('form.code_form #code_field').val("").focus();
}
}))
})
$(function () {
/* Autocompletion in the code field */
const code_field = $("#code_field");
let quantity = "";
let search = "";
code_field.autocomplete({
select: function (event, ui) {
event.preventDefault();
code_field.val(quantity + ui.item.value);
},
focus: function (event, ui) {
event.preventDefault();
code_field.val(quantity + ui.item.value);
},
source: function (request, response) {
// by the dark magic of JS, parseInt("123abc") === 123
quantity = parseInt(request.term);
search = request.term.slice(quantity.toString().length)
let matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
response($.grep(products_autocomplete, function (value) {
value = value.tags;
return matcher.test(value);
}));
},
});
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: () => $(".focus").focus(),
});
$("#products").tabs();
code_field.focus();
});

View File

@ -2,266 +2,192 @@
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
{% block title %}
{{ counter }}
{{ counter }}
{% endblock %}
{% block additional_js %}
<script src="{{ static('counter/js/counter_click.js') }}" defer></script>
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h4 id="click_interface">{{ counter }}</h4>
<h4 id="click_interface">{{ counter }}</h4>
<div id="bar_ui">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" />
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
{% raw %}
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
{% endraw %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul>
<li v-for="p_info,p_id in basket">
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> - </button>
</form>
{{ p_info["qty"] + p_info["bonus_qty"] }}
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> + </button>
</form>
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
</li>
</ul>
<p>
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
</p>
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
{% endraw %}
<div id="bar-ui" x-data="counter">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
</form>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
</form>
</div>
{% if (counter.type == 'BAR' and barmens_can_refill) %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul>
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
{% for category in categories.keys() -%}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% else %}
{% set file = static('core/img/na.gif') %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
<input type="text" name="student_card_uid"/>
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %}
<form method="post" action=""
class="code_form" @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
<input type="hidden" name="action" value="code">
<label for="code_field"></label>
<input type="text" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<template x-for="error in errors">
<div class="alert alert-red" x-text="error">
</div>
</template>
<p>{% trans %}Basket: {% endtrans %}</p>
<ul>
<template x-for="[id, item] in Object.entries(basket)">
<div>
<form method="post" action="" class="inline del_product_form"
@submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" :value="id">
<input type="submit" value="-"/>
</form>
<span x-text="item['qty'] + item['bonus_qty']"></span>
<form method="post" action="" class="inline add_product_form"
@submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" :value="id">
<input type="submit" value="+">
</form>
<span x-text="products[id].name"></span> :
<span x-text="(item['qty'] * item['price'] / 100)
.toLocaleString(undefined, { minimumFractionDigits: 2 })">
</span> €
<template x-if="item['bonus_qty'] > 0">P</template>
</div>
</template>
</ul>
<p>
<strong>Total: </strong>
<strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong>
<strong> €</strong>
</p>
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}"/>
</form>
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
</form>
</div>
{% if (counter.type == 'BAR' and barmens_can_refill) %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul>
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
{% for category in categories.keys() -%}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
class="form_button add_product_form" @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit">
<strong>{{ p.name }}</strong>
{% if p.icon %}
<img src="{{ p.icon.url }}" alt="image de {{ p.name }}"/>
{% else %}
<img src="{{ static('core/img/na.gif') }}" alt="image de {{ p.name }}"/>
{% endif %}
<span>{{ p.price }} €<br>{{ p.code }}</span>
</button>
</form>
{%- endfor %}
</div>
{%- endfor %}
</div>
{%- endfor %}
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script>
$( function() {
/* Vue.JS dynamic form */
const click_form_vue = Vue.createApp({
data() {
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}",
special_selling_price: "{{ p.special_selling_price }}",
},
{%- endfor %}
},
basket: {{ request.session["basket"]|tojson }},
errors: [],
}
},
methods: {
sum_basket() {
var vm = this;
var total = 0;
for(idx in vm.basket) {
var item = vm.basket[idx];
console.log(item);
total += item["qty"] * item["price"];
}
return total / 100;
},
handle_code(event) {
var vm = this;
var code = $(event.target).find("#code_field").val().toUpperCase();
console.log("Code:");
console.log(code);
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
$(event.target).submit();
} else {
vm.handle_action(event);
}
},
handle_action(event) {
var vm = this;
var payload = $(event.target).serialize();
$.ajax({
type: 'post',
dataType: 'json',
data: payload,
success: function(response) {
vm.basket = response.basket;
vm.errors = [];
},
error: function(error) {
vm.basket = error.responseJSON.basket;
vm.errors = error.responseJSON.errors;
}
});
$('form.code_form #code_field').val("").focus();
}
}
}).mount('#bar_ui');
/* Autocompletion in the code field */
var products_autocomplete = [
{% for p in products -%}
{
value: "{{ p.code }}",
label: "{{ p.name }}",
tags: "{{ p.code }} {{ p.name }}",
},
{%- endfor %}
];
var quantity = "";
var search = "";
var pattern = /^(\d+x)?(.*)/i;
$( "#code_field" ).autocomplete({
select: function (event, ui) {
event.preventDefault();
$("#code_field").val(quantity + ui.item.value);
},
focus: function (event, ui) {
event.preventDefault();
$("#code_field").val(quantity + ui.item.value);
},
source: function( request, response ) {
var res = pattern.exec(request.term);
quantity = res[1] || "";
search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products_autocomplete, function( value ) {
value = value.tags;
return matcher.test( value );
}));
},
});
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: function(event, ui){
$(".focus").focus();
}
});
$("#products").tabs();
$("#code_field").focus();
});
</script>
{{ super() }}
<script>
const csrf_token = "{{ csrf_token }}";
const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const basket = {{ request.session["basket"]|tojson }};
const products = {
{%- for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
price: {{ p.price }},
},
{%- endfor -%}
};
const products_autocomplete = [
{% for p in products -%}
{
value: "{{ p.code }}",
label: "{{ p.name }}",
tags: "{{ p.code }} {{ p.name }}",
},
{%- endfor %}
];
</script>
{% endblock %}

View File

@ -22,8 +22,10 @@
#
#
import json
from urllib.parse import parse_qs
from django.contrib.auth.decorators import login_required
from django.db.models import F
from django.shortcuts import get_object_or_404
from django.http import Http404
from django.core.exceptions import PermissionDenied
@ -300,7 +302,10 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests
if len(self.request.POST) == 0 and len(self.request.body) != 0:
# when using the fetch API, the django request.POST dict is empty
# this is but a wretched contrivance which must be replaced as soon as possible
# by a proper separation between the api endpoints of the counter
response = {"errors": []}
status = HTTPStatus.OK
@ -395,42 +400,39 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["not_valid_student_card_uid"] = False
if self.object.type != "BAR":
self.operator = request.user
elif self.is_barman_price():
elif self.customer_is_barman():
self.operator = self.customer.user
else:
self.operator = self.object.get_random_barman()
action = parse_qs(request.body.decode())["action"][0]
if "add_product" in request.POST["action"]:
if action == "add_product":
self.add_product(request)
elif "add_student_card" in request.POST["action"]:
elif action == "add_student_card":
self.add_student_card(request)
elif "del_product" in request.POST["action"]:
elif action == "del_product":
self.del_product(request)
elif "refill" in request.POST["action"]:
elif action == "refill":
self.refill(request)
elif "code" in request.POST["action"]:
elif action == "code":
return self.parse_code(request)
elif "cancel" in request.POST["action"]:
elif action == "cancel":
return self.cancel(request)
elif "finish" in request.POST["action"]:
elif action == "finish":
return self.finish(request)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def is_barman_price(self):
if self.object.type == "BAR" and self.customer.user.id in [
s.id for s in self.object.get_barmen_list()
]:
return True
else:
return False
def customer_is_barman(self) -> bool:
barmen = self.object.barmen_list
return self.object.type == "BAR" and self.customer.user in barmen
def get_product(self, pid):
return Product.objects.filter(pk=int(pid)).first()
def get_price(self, pid):
p = self.get_product(pid)
if self.is_barman_price():
if self.customer_is_barman():
price = p.special_selling_price
else:
price = p.selling_price
@ -481,7 +483,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
q is the quantity passed as integer
p is the product id, passed as an integer
"""
pid = p or request.POST["product_id"]
pid = p or parse_qs(request.body.decode())["product_id"][0]
pid = str(pid)
price = self.get_price(pid)
total = self.sum_basket(request)
@ -563,7 +565,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def del_product(self, request):
"""Delete a product from the basket"""
pid = str(request.POST["product_id"])
pid = parse_qs(request.body.decode())["product_id"][0]
product = self.get_product(pid)
if pid in request.session["basket"]:
if (
@ -581,25 +583,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session.modified = True
def parse_code(self, request):
"""Parse the string entered by the barman"""
string = str(request.POST["code"]).upper()
if string == _("END"):
return self.finish(request)
elif string == _("CAN"):
return self.cancel(request)
"""
Parse the string entered by the barman
This can be of two forms :
- <str>, where the string is the code of the product
- <int>X<str>, where the integer is the quantity and str the code
"""
string = parse_qs(request.body.decode())["code"][0].upper()
regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
m = regex.match(string)
if m is not None:
nb = m.group("nb")
code = m.group("code")
if nb is None:
nb = 1
else:
nb = int(nb)
nb = int(nb) if nb is not None else 1
p = self.object.products.filter(code=code).first()
if p is not None:
while nb > 0 and not self.add_product(request, nb, p.id):
nb -= 1
self.add_product(request, nb, p.id)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
@ -613,7 +612,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
for pid, infos in request.session["basket"].items():
# This duplicates code for DB optimization (prevent to load many times the same object)
p = Product.objects.filter(pk=pid).first()
if self.is_barman_price():
if self.customer_is_barman():
uprice = p.special_selling_price
else:
uprice = p.selling_price
@ -680,7 +679,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def get_context_data(self, **kwargs):
"""Add customer to the context"""
kwargs = super(CounterClick, self).get_context_data(**kwargs)
kwargs["products"] = self.object.products.select_related("product_type")
products = self.object.products.select_related("product_type")
if self.customer_is_barman():
products = products.annotate(price=F("special_selling_price"))
else:
products = products.annotate(price=F("selling_price"))
kwargs["products"] = products
kwargs["categories"] = {}
for product in kwargs["products"]:
if product.product_type:

View File

@ -111,7 +111,7 @@
<i class="fa fa-2x fa-picture-o product-image" ></i>
{% endif %}
<div class="product-description">
<h4>{{ p.name }}</strong></h4>
<h4>{{ p.name }}</h4>
<p>{{ p.selling_price }} €</p>
</div>
</button>

View File

@ -689,6 +689,5 @@ SITH_FRONT_DEP_VERSIONS = {
"https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
"https://github.com/vuejs/vue-next": "3.2.18",
"https://github.com/alpinejs/alpine": "3.10.5",
}