Compare commits

...

6 commits

Author SHA1 Message Date
76ac07742e
Allow anonymous order creation 2021-07-08 12:02:50 +02:00
f0561bcd71
Remove pills introduced in previous commit 2021-06-21 02:08:32 +02:00
d7dc926992
Aggregate comments 2021-06-21 02:05:11 +02:00
d20ce9803e
Improve design
Remove main CSS from shop view page. Make it theme independent and
maximize contrast.

Improve spacing in "add item" list.

Refactor old term "showcase" to "shop_view" in code.
2021-06-21 00:46:29 +02:00
d0699b3716
HLDS: change :: to double space and require it
Require double space before tags and price, like in the plain text
accounting format of ledger. This makes it easier to differentiate
between prices mentioned in descriptions and the price for the dish.
2021-06-20 23:57:50 +02:00
a1a68a3fd6
Add space at bottom in show view
On big orders the bottom of the page was awkward.
2021-06-20 21:47:24 +02:00
12 changed files with 184 additions and 126 deletions

View file

@ -37,15 +37,12 @@ class OrderForm(Form):
def populate(self) -> None: def populate(self) -> None:
"Fill in the options for courier for an Order" "Fill in the options for courier for an Order"
if current_user.is_admin():
self.courier_id.choices = [(0, None)] + [ self.courier_id.choices = [(0, None)] + (
(u.id, u.username) for u in User.query.order_by("username") [(u.id, u.username) for u in User.query.order_by("username")] if current_user.is_admin()
] else [(current_user.id, current_user.username)] if current_user.is_authenticated()
else: else []
self.courier_id.choices = [ )
(0, None),
(current_user.id, current_user.username),
]
self.location_id.choices = [(l.id, l.name) for l in location_definitions] self.location_id.choices = [(l.id, l.name) for l in location_definitions]
if self.stoptime.data is None: if self.stoptime.data is None:
self.stoptime.data = datetime.now() + timedelta(hours=1) self.stoptime.data = datetime.now() + timedelta(hours=1)

View file

@ -29,10 +29,12 @@ location = >location_header items:{ block } ;
attributes = attributes =
name:/[^\n#]*?(?= +-- | +:: | +€ | *\n| *#)/ name:/[^\n#]*?(?= +-- | | *\n| *#)/
[ s '--' ~ s description:/[^\n#]*?(?= +:: | +€ | *\n| *#)/ ] [ s '--' ~ s description:/[^\n#]*?(?= | *\n| *#)/ ]
[ s '::' {s ('{' tags+:identifier '}')} ] [ / {2,}/ ~
[ [ s '::' ~ ] s price:price ] [ {[ s ] ('{' tags+:identifier '}')} / +|$/ ]
[ price:price ]
]
; ;

View file

@ -1,6 +1,7 @@
"Script for everything Order related in the database" "Script for everything Order related in the database"
import typing import typing
from datetime import datetime from datetime import datetime
from collections import defaultdict
from utils import first from utils import first
from hlds.definitions import location_definitions from hlds.definitions import location_definitions
@ -69,23 +70,27 @@ class Order(db.Model):
return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or ""))) return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or "")))
def group_by_dish(self) -> typing.List[typing.Tuple[str, typing.List]]: def group_by_dish(self) \
-> typing.List[typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]]:
"Group items of an Order by dish" "Group items of an Order by dish"
group: typing.Dict[str, typing.List] = dict() group: typing.Dict[str, typing.Dict[str, typing.List]] = \
defaultdict(lambda: defaultdict(list))
for item in self.items: for item in self.items:
if item.dish_name not in group: group[item.dish_name][item.comment].append(item)
group[item.dish_name] = []
group[item.dish_name].append(item) return sorted(
(
for _dish_name, order_items in group.items(): dish_name,
order_items.sort(key=lambda order_item: ( # Amount of items of this dish
(order_item.comment or " No comment") + sum(map(len, comment_group.values())),
(order_item.for_name or "") sorted(
)) (comment, sorted(items, key=lambda x: (x.for_name or "")))
for comment, items in comment_group.items()
return list(sorted(group.items())) )
)
for dish_name, comment_group in group.items()
)
def is_closed(self) -> bool: def is_closed(self) -> bool:
return self.stoptime and datetime.now() > self.stoptime return self.stoptime and datetime.now() > self.stoptime

View file

@ -60,64 +60,6 @@ body {
width: 100%; width: 100%;
} }
.showcase {
font-size: 16px;
line-height: 1.2;
}
.showcase {
padding: 0;
max-width: 500px;
margin: 0 auto;
}
.showcase h1 {
font-size: 200%;
margin: 0 ;
padding: 0.4em 0 0.2em;
border-bottom: 1px dashed var(--gray1);
text-align: center;
}
.showcase h2 {
font-size: 110%;
font-weight: bold;
margin-bottom: 0.6em;
}
.showcase .open-order-warning {
font-size: 150%;
text-align: center;
padding: 1em 0.5em;
background-color: rgba(255, 0, 0, 0.1);
}
.showcase .dish {
padding: 0 0.5em;
}
@media (min-width: 500px) {
.showcase .dish {
padding: 0 1em;
}
}
.showcase .quantity {
font-size: 110%;
}
.showcase .comments {
padding-left: 2em;
}
.showcase .comments li {
margin: 0.5em 0 0;
}
.showcase .total {
border-top: 1px dashed var(--gray1);
text-align: center;
padding: 0.5em 0;
margin-top: 1.3em;
}
.time_data{ .time_data{
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -0,0 +1,109 @@
:root {
--bg: #eee;
--ticketBg: #fff;
--fg: #000;
--dashes: 1px dashed #444;
--fontFamily: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
--fontSize: 16px;
}
html, body {
margin: 0;
padding: 0;
}
body {
background: var(--bg);
color: var(--fg);
font-family: var(--fontFamily);
font-size: var(--fontSize);
line-height: 1.2;
}
.ticket {
background: var(--ticketBg);
font-size: 16px;
padding: 0;
max-width: 500px;
margin: 25px auto;
margin: 7vh auto;
box-shadow: 0 0.15em 0.3em rgba(0, 0, 0, 0.2);
}
@media (max-width: 500px) {
body {
background: var(--ticketBg);
}
.ticket {
margin: 0;
box-shadow: none;
}
}
h1 {
font-size: 200%;
margin: 0 ;
padding: 0.3em 0 0.3em;
border-bottom: var(--dashes);
text-align: center;
}
h2 {
font-size: 110%;
font-weight: bold;
margin-bottom: 0.6em;
}
.open-order-warning {
font-size: 150%;
text-align: center;
padding: 1em 0.5em;
background-color: rgba(255, 0, 0, 0.1);
}
.dish {
padding: 0 0.5em;
}
@media (min-width: 500px) {
.dish {
padding: 0 1em;
}
}
.quantity {
font-size: 110%;
}
.comments {
padding-left: 1.5em;
list-style-type: none;
}
.comments li {
margin: 0.5em 0 0;
}
.comment_part {
background-color: #f7f7f7;
border: 1px solid #ddd;
padding: 0.1em 0.5em;
margin-top: 2px;
margin-right: 0.4em;
border-radius: 1em;
display: inline-block;
}
.comment_part_separator {
display: inline-block;
width: 0;
overflow: hidden;
}
.total {
border-top: var(--dashes);
text-align: center;
padding: 0.5em 0;
margin-top: 1.3em;
}
.time_data{
display: flex;
justify-content: space-between;
}

View file

@ -230,24 +230,26 @@
<div class="box" id="per_dish"> <div class="box" id="per_dish">
<h3>Ordered dishes</h3> <h3>Ordered dishes</h3>
{% for dish_name, dish_order_items in order.group_by_dish() -%} {% for dish_name, dish_quantity, dish_comment_groups in order.group_by_dish() -%}
{% set has_comments = dish_order_items | map(attribute="comment") | any -%} {% set has_comments = dish_comment_groups | length > 1 or (dish_comment_groups | map("first") | any) -%}
<div class="dish {{ 'spacecake no_comments' if not has_comments }}"> <div class="dish {{ 'spacecake no_comments' if not has_comments }}">
<h4> <h4>
<span class="quantity">{{ dish_order_items | length }}</span> × <span class="quantity">{{ dish_quantity }}</span> ×
{{ dish_name }} {{ dish_name }}
</h4> </h4>
{% if has_comments -%} {% if has_comments -%}
<ul class="comments"> <ul class="comments">
{% for item in dish_order_items -%} {% for comment, items in dish_comment_groups -%}
<li class="spacecake">{% if item["comment"] %}<span>{{ item["comment"] }}</span> <li class="spacecake"><span class="comment">
<span class="quantity">{{ items | length }}</span> ×
{% if comment %}{{ comment }}
{% else %}<i>No comment</i> {% else %}<i>No comment</i>
{% endif %}<span class="spacer"> </span><span class="item_for">for {{ item.for_name }}</span></li> {% endif %}</span><span class="spacer"> </span><span class="item_for">for {{ items | map(attribute="for_name") | join(", ") }}</span></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<span class="spacer"> </span><span class="item_for">for {{ dish_order_items | map(attribute="for_name") | join(", ") }}</span> <span class="spacer"> </span><span class="item_for">for {{ dish_comment_groups[0][1] | map(attribute="for_name") | join(", ") }}</span>
{%- endif %} {%- endif %}
</p> </p>
@ -256,7 +258,7 @@
<div class="footer"> <div class="footer">
Total {{ order.items.count() }} items — {{ total_price|euro }} Total {{ order.items.count() }} items — {{ total_price|euro }}
&nbsp; &nbsp;
<a class="btn btn-sm" href="{{ url_for('order_bp.items_showcase', order_id=order.id) }}">Shop view</a> <a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_id=order.id) }}">Shop view</a>
</div> </div>
</div> </div>
</div> </div>
@ -313,9 +315,7 @@
<div class="footer"> <div class="footer">
On selected: On selected:
<button class="btn btn-sm">Mark paid (TODO)</button> <button class="btn btn-sm"><span class="glyphicon glyphicon-ok"></span> Mark paid (TODO)</button>
Request payment for selected:
<button class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button> <button class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button>
<button class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button> <button class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button>
</div> </div>
@ -439,6 +439,10 @@ li {
color: var(--gray2); color: var(--gray2);
text-align: right; text-align: right;
} }
#per_dish .comments {
padding-left: 1.5em;
list-style-type: none;
}
#per_person li { #per_person li {
@ -472,7 +476,8 @@ li {
summary { summary {
line-height: 1.2; line-height: 1.2;
margin: 0.6em 0; margin: 0 -10px;
padding: 4px 10px;
} }
summary .dish_name, summary:before { summary .dish_name, summary:before {
align-self: flex-start; align-self: flex-start;
@ -480,14 +485,16 @@ summary .dish_name, summary:before {
details[open] summary .dish_name { details[open] summary .dish_name {
font-weight: bold; font-weight: bold;
} }
details {
margin: 0 -10px;
padding: 0 10px;
}
details[open] { details[open] {
background-color: var(--gray5); background-color: var(--gray5);
margin-left: -10px; padding-bottom: 5px;
margin-right: -10px; }
margin-bottom: 5px; details:not([open]) summary:hover {
padding-left: 10px; background-color: var(--gray5);
padding-right: 10px;
padding-bottom: 10px;
} }
.select2-container--default .select2-selection--multiple .select2-selection__rendered { .select2-container--default .select2-selection--multiple .select2-selection__rendered {

View file

@ -7,9 +7,7 @@ Haldis - Order {{ order.id }}
{% block styles %} {% block styles %}
{{ super() }} {{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}" media="screen"> <link rel="stylesheet" href="{{ url_for('static', filename='css/shop_view.css') }}" media="screen">
<link rel="stylesheet" href="{{ url_for('general_bp.theme_css') }}" media="screen">
<link rel="stylesheet" href="{{ url_for('static', filename='css/print.css') }}" media="print">
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -23,7 +21,7 @@ Haldis - Order {{ order.id }}
{% block content -%} {% block content -%}
{{ utils.flashed_messages(container=True) }} {{ utils.flashed_messages(container=True) }}
<div class="darker showcase" id="items-ordered"> <div class="ticket" id="items-ordered">
<h1>Haldis order {{ order.id }}</h1> <h1>Haldis order {{ order.id }}</h1>
{% if not order.is_closed() %} {% if not order.is_closed() %}
@ -33,13 +31,14 @@ Haldis - Order {{ order.id }}
</div> </div>
{% endif %} {% endif %}
{% for dish_name, dish_order_items in order.group_by_dish() -%} {% for dish_name, dish_quantity, dish_comment_groups in order.group_by_dish() -%}
<div class="dish"> <div class="dish">
<h2><span class="quantity">{{ dish_order_items | length }}</span> × {{ dish_name }}</h2> <h2><span class="quantity">{{ dish_quantity }}</span> × {{ dish_name }}</h2>
{% if dish_order_items | map(attribute="comment") | any -%} {% if dish_comment_groups | map("first") | any -%}
<ul class="comments"> <ul class="comments">
{% for item in dish_order_items -%} {% for comment, items in dish_comment_groups -%}
<li>{% if item["comment"] %}{{ item["comment"] }} <li><span class="quantity">{{ items | length }}</span> ×
{% if comment %}{{ comment }}
{% else %}<i>No comment</i> {% else %}<i>No comment</i>
{% endif %}</li> {% endif %}</li>
{% endfor %} {% endfor %}

View file

@ -14,14 +14,14 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<h4>No orders available.</h4> <h4>No orders available.</h4>
{% if not current_user.is_anonymous() %} {% if form %}
To create an order, fill in the form on the right. To create an order, fill in the form on the right.
{% else %} {% else %}
Login to create an order, or ask someone else. Login to create an order, or ask someone else.
{% endif %} {% endif %}
{%- endif %} {%- endif %}
</div> </div>
{% if not current_user.is_anonymous() %} {% if form %}
<div class="col-md-push-1 col-md-6"> <div class="col-md-push-1 col-md-6">
<h3>Create new order</h3> <h3>Create new order</h3>
<div class="row darker"> <div class="row darker">

View file

@ -31,7 +31,7 @@ order_bp = Blueprint("order_bp", "order")
@order_bp.route("/") @order_bp.route("/")
def orders(form: OrderForm = None) -> str: def orders(form: OrderForm = None) -> str:
"Generate general order view" "Generate general order view"
if form is None and not current_user.is_anonymous(): if form is None:
form = OrderForm() form = OrderForm()
location_id = request.args.get("location_id") location_id = request.args.get("location_id")
form.location_id.default = location_id form.location_id.default = location_id
@ -88,7 +88,7 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
@order_bp.route("/<order_id>/items") @order_bp.route("/<order_id>/items")
def items_showcase(order_id: int) -> str: def items_shop_view(order_id: int) -> str:
"Generate order items view from id" "Generate order items view from id"
order = Order.query.filter(Order.id == order_id).first() order = Order.query.filter(Order.id == order_id).first()
if order is None: if order is None:

View file

@ -1 +1,2 @@
setlocal noexpandtab setlocal noexpandtab
setlocal softtabstop=0

View file

@ -25,8 +25,9 @@ syn keyword hldsChoiceType single_choice multi_choice nextgroup=hldsBlockIdAf
syn match hldsBlockId "^[a-z0-9_-]\+: " syn match hldsBlockId "^[a-z0-9_-]\+: "
syn match hldsBlockIdAftrKywrd "[a-z0-9_-]\+: " contained syn match hldsBlockIdAftrKywrd "[a-z0-9_-]\+: " contained
syn match hldsTag " {[a-z0-9_-]\+}" syn match _doubleSpace " \+" nextgroup=hldsTag,hldsPrice
syn match hldsPrice "€ *[0-9]\+\(\.[0-9]\+\|\)" syn match hldsTag "{[a-z0-9_-]\+}\( \|$\)" contained nextgroup=hldsTag,hldsPrice
syn match hldsPrice "€ *[0-9]\+\(\.[0-9]\+\|\)\( \|$\)" contained
syn match hldsComment "#.*$" contains=hldsTodo,@Spell syn match hldsComment "#.*$" contains=hldsTodo,@Spell
syn keyword hldsTodo FIXME NOTE NOTES TODO XXX contained syn keyword hldsTodo FIXME NOTE NOTES TODO XXX contained

View file

@ -33,13 +33,8 @@
"tags": { "tags": {
"patterns": [ "patterns": [
{ {
"match": "(::)\\s*({[a-zA-Z-_]*}\\s*)*", "match": " +( +{[a-zA-Z-_]*})*",
"name": "markup.italic", "name": "markup.italic"
"captures": {
"1": {
"name": "markup.bold"
}
}
} }
] ]
}, },