Compare commits
14 Commits
7ffad354fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dbe22cd175 | |||
| c94e313672 | |||
| 83afc3c2f1 | |||
| c0abf76643 | |||
| 67489bb46b | |||
| 73cb0c7777 | |||
| c9ea1c924a | |||
| c523085405 | |||
| bd4664f23b | |||
| 5c8c4a947e | |||
| ef36d63aa7 | |||
| c23f08fdd9 | |||
| 9f3951e6ef | |||
| 8d8dd115c5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.venv/
|
||||
log/
|
||||
__pycache__/
|
||||
*.db
|
||||
|
||||
12
README.md
12
README.md
@@ -66,9 +66,19 @@ touch kasse.db
|
||||
docker run -p 9090:90 -v $(pwd)/kasse.db:/app/kasse.db -t erdbeerhannah:latest
|
||||
```
|
||||
|
||||
### Nginx Reverse-Proxy
|
||||
|
||||
Bei 502 Bad Gateway hinter nginx:
|
||||
|
||||
1. **Proxy-Header**: Nginx muss `X-Forwarded-For`, `X-Forwarded-Proto`, `Host` setzen (die App nutzt `ProxyFix`).
|
||||
2. **Upstream-Erreichbarkeit**: `proxy_pass` muss den Container erreichen:
|
||||
- nginx auf gleichem Host wie Docker: `http://127.0.0.1:9090`
|
||||
- nginx auf anderem Host: `http://<Docker-Host-IP>:9090`
|
||||
3. **Beispielkonfiguration**: Siehe `nginx-proxy.example.conf`
|
||||
|
||||
## Privatsphäre
|
||||
|
||||
Die App *erdbeerhannah* protokolliert keinerlei Daten. Keine IP-Adressen, keine Klicks, keine Benutzer-Eingaben und auch sonst nichts. Es werden keine Cookies genutzt, es findet kein Tracking statt und es gibt keine Werbung.
|
||||
Die App *erdbeerhannah* protokolliert keine personenbezogenen Daten. Keine IP-Adressen und keine persönlichen Benutzer-Eingaben. Es werden keine Cookies genutzt. Zur reinen Nutzungsstatistik (Seitenaufrufe) verwenden wir das datenschutzfreundliche, Cookie-lose [Plausible Analytics](https://plausible.io/). Es gibt keine Werbung.
|
||||
|
||||
## Lizenz
|
||||
|
||||
|
||||
87
app.py
87
app.py
@@ -1,13 +1,16 @@
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import sqlite3
|
||||
import uuid
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from flask import Flask, render_template, request, session, send_from_directory, g, redirect, url_for
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
|
||||
app.config['SECRET_KEY'] = 'j69ol5mcHLsEtLg4Y/+myd9wWD4pp56E'
|
||||
|
||||
# setup logging
|
||||
@@ -21,7 +24,7 @@ app.logger.addHandler(handler)
|
||||
|
||||
Bootstrap(app)
|
||||
|
||||
version = "1.1.0/2026-02-24"
|
||||
version = "1.1.1/2026-03-22"
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('kasse.db')
|
||||
@@ -110,7 +113,7 @@ def index(instance_id):
|
||||
gesamtwert = session.get(f'{session_prefix}gesamtwert', 0.0)
|
||||
change = session.get(f'{session_prefix}change', "0.00")
|
||||
givenfloat = session.get(f'{session_prefix}given', 0.0)
|
||||
items = {i: session.get(f'{session_prefix}item{i}', 0) for i in range(1, 7)}
|
||||
items = {p: session.get(f'{session_prefix}item{p}', 0) for p in products}
|
||||
|
||||
background = "bg-white"
|
||||
|
||||
@@ -122,7 +125,7 @@ def index(instance_id):
|
||||
gesamtwert = 0.0
|
||||
change = "0.00"
|
||||
givenfloat = 0.0
|
||||
items = {i: 0 for i in range(1, 7)}
|
||||
items = {p: 0 for p in products}
|
||||
background = "bg-white"
|
||||
elif action == "calculate_change":
|
||||
given = request.form.get('given', "0", type=float)
|
||||
@@ -138,15 +141,21 @@ def index(instance_id):
|
||||
session[f'{session_prefix}gesamtwert'] = round(gesamtwert, 2)
|
||||
session[f'{session_prefix}change'] = change
|
||||
session[f'{session_prefix}given'] = round(givenfloat, 2)
|
||||
for i in range(1, 7):
|
||||
session[f'{session_prefix}item{i}'] = items[i]
|
||||
for p in products:
|
||||
session[f'{session_prefix}item{p}'] = items[p]
|
||||
|
||||
# Update our local variables after processing so they reflect what is rendered
|
||||
gesamtwert = round(gesamtwert, 2)
|
||||
|
||||
n = len(products)
|
||||
grid_cols = math.ceil(math.sqrt(n)) if n else 1
|
||||
grid_rows = math.ceil(n / grid_cols) if n else 1
|
||||
|
||||
return render_template("index.html",
|
||||
instance_id=instance_id,
|
||||
products=products,
|
||||
grid_cols=grid_cols,
|
||||
grid_rows=grid_rows,
|
||||
gesamtwert=f"{gesamtwert:.2f}",
|
||||
change=change,
|
||||
given=f"{givenfloat:.2f}" if givenfloat > 0 else "0",
|
||||
@@ -165,9 +174,21 @@ def admin(instance_id):
|
||||
|
||||
auth_key = f'admin_auth_{instance_id}'
|
||||
|
||||
# Check if instance has no password (empty or None)
|
||||
stored_password = instance['password']
|
||||
has_no_password = (
|
||||
stored_password is None or
|
||||
check_password_hash(stored_password, '')
|
||||
)
|
||||
|
||||
# Handle Login Submission
|
||||
if request.method == "POST" and 'admin_password' in request.form:
|
||||
if check_password_hash(instance['password'], request.form['admin_password']):
|
||||
entered = request.form['admin_password']
|
||||
if has_no_password and entered == '':
|
||||
session[auth_key] = True
|
||||
conn.close()
|
||||
return redirect(url_for('admin', instance_id=instance_id))
|
||||
elif not has_no_password and check_password_hash(stored_password, entered):
|
||||
session[auth_key] = True
|
||||
conn.close()
|
||||
return redirect(url_for('admin', instance_id=instance_id))
|
||||
@@ -191,18 +212,56 @@ def admin(instance_id):
|
||||
session.pop(auth_key, None)
|
||||
return redirect(url_for('landing'))
|
||||
|
||||
# Additional safety: Ensure that the post request isn't processed if not authenticated
|
||||
# (Though we checked above, this is for the product update form submission)
|
||||
for i in range(1, 7):
|
||||
name = request.form.get(f'name_{i}')
|
||||
price = request.form.get(f'price_{i}', type=float)
|
||||
icon = request.form.get(f'icon_{i}')
|
||||
color = request.form.get(f'color_{i}')
|
||||
for key in request.form:
|
||||
if key.startswith('delete_') and request.form[key]:
|
||||
pos = int(key.split('_')[1])
|
||||
conn.execute('DELETE FROM products WHERE instance_id = ? AND position = ?',
|
||||
(instance_id, pos))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return redirect(url_for('admin', instance_id=instance_id))
|
||||
|
||||
if action == "add_product":
|
||||
name = request.form.get('add_name', '').strip()
|
||||
price_str = request.form.get('add_price', '').replace(',', '.').strip()
|
||||
try:
|
||||
price = float(price_str) if price_str else None
|
||||
except ValueError:
|
||||
price = None
|
||||
icon = request.form.get('add_icon', '🛒').strip() or '🛒'
|
||||
color = request.form.get('add_color', 'btn-primary')
|
||||
if name and price is not None:
|
||||
max_pos = conn.execute(
|
||||
'SELECT COALESCE(MAX(position), 0) FROM products WHERE instance_id = ?',
|
||||
(instance_id,)
|
||||
).fetchone()[0]
|
||||
conn.execute(
|
||||
'INSERT INTO products (instance_id, position, name, price, icon, color_class) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(instance_id, max_pos + 1, name, price, icon, color)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return redirect(url_for('admin', instance_id=instance_id))
|
||||
|
||||
# Update existing products
|
||||
products_rows = conn.execute(
|
||||
'SELECT position FROM products WHERE instance_id = ?', (instance_id,)
|
||||
).fetchall()
|
||||
for row in products_rows:
|
||||
pos = row['position']
|
||||
name = request.form.get(f'name_{pos}')
|
||||
price_str = request.form.get(f'price_{pos}', '').replace(',', '.').strip()
|
||||
try:
|
||||
price = float(price_str) if price_str else None
|
||||
except ValueError:
|
||||
price = None
|
||||
icon = request.form.get(f'icon_{pos}')
|
||||
color = request.form.get(f'color_{pos}')
|
||||
if name is not None and price is not None:
|
||||
conn.execute('''
|
||||
UPDATE products SET name = ?, price = ?, icon = ?, color_class = ?
|
||||
WHERE instance_id = ? AND position = ?
|
||||
''', (name, price, icon, color, instance_id, i))
|
||||
''', (name, price, icon, color, instance_id, pos))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return redirect(url_for('index', instance_id=instance_id))
|
||||
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Vor dem ersten Start: touch kasse.db (bzw. echo. > kasse.db unter Windows)
|
||||
# damit Docker keine Verzeichnis-Datei erstellt
|
||||
services:
|
||||
erdbeerkasse:
|
||||
build: .
|
||||
container_name: erdbeerkasse
|
||||
ports:
|
||||
- "9090:90"
|
||||
volumes:
|
||||
- ./kasse.db:/app/kasse.db
|
||||
29
nginx-proxy.example.conf
Normal file
29
nginx-proxy.example.conf
Normal file
@@ -0,0 +1,29 @@
|
||||
# Beispiel nginx Reverse-Proxy Konfiguration für erdbeerhannah
|
||||
# Pfad anpassen, z.B. /etc/nginx/sites-available/erdbeerhannah
|
||||
#
|
||||
# Upstream-URL je nach Deployment:
|
||||
# - nginx auf gleichem Host wie Docker: http://127.0.0.1:9090
|
||||
# - nginx auf anderem Host: http://10.7.0.5:9090 (Docker-Host-IP)
|
||||
# - Beide im gleichen docker-compose: http://erdbeerkasse:90
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name erdbeerhannah.elpatron.me;
|
||||
|
||||
# SSL-Zertifikate
|
||||
ssl_certificate /pfad/zu/cert.pem;
|
||||
ssl_certificate_key /pfad/zu/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:9090;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#dc3545">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
@@ -30,21 +31,26 @@
|
||||
<th>Preis (€)</th>
|
||||
<th>Icon/Emoji</th>
|
||||
<th>Farbe (Bootstrap Klasse)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(1, 7) %}
|
||||
{% set prod = products[i] %}
|
||||
{% if not products %}
|
||||
<tr>
|
||||
<td class="align-middle">{{ i }}</td>
|
||||
<td><input type="text" class="form-control" name="name_{{ i }}" value="{{ prod['name'] }}"
|
||||
<td colspan="6" class="text-muted text-center py-4">Keine Artikel. Füge unten einen neuen hinzu.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for pos, prod in products|dictsort %}
|
||||
<tr>
|
||||
<td class="align-middle">{{ pos }}</td>
|
||||
<td><input type="text" class="form-control" name="name_{{ pos }}" value="{{ prod['name'] }}"
|
||||
required></td>
|
||||
<td><input type="number" step="0.01" class="form-control" name="price_{{ i }}"
|
||||
value="{{ prod['price'] }}" required></td>
|
||||
<td><input type="text" class="form-control" name="icon_{{ i }}" value="{{ prod['icon'] }}"
|
||||
<td><input type="text" inputmode="decimal" class="form-control" name="price_{{ pos }}"
|
||||
value="{{ '{:.2f}'.format(prod['price']).replace('.', ',') }}" required></td>
|
||||
<td><input type="text" class="form-control" name="icon_{{ pos }}" value="{{ prod['icon'] }}"
|
||||
required></td>
|
||||
<td>
|
||||
<select class="form-control" name="color_{{ i }}">
|
||||
<select class="form-control" name="color_{{ pos }}">
|
||||
<option value="btn-primary" {% if prod['color_class']=='btn-primary' %}selected{% endif
|
||||
%}>Blau (Primary)</option>
|
||||
<option value="btn-secondary" {% if prod['color_class']=='btn-secondary' %}selected{%
|
||||
@@ -61,6 +67,12 @@
|
||||
Schwarz (Dark)</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{% if products|length > 1 %}
|
||||
<button type="submit" name="delete_{{ pos }}" value="1" class="btn btn-outline-danger btn-sm"
|
||||
onclick="return confirm('Artikel wirklich löschen?');">Löschen</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -76,6 +88,49 @@
|
||||
<button type="submit" name="action" value="save" class="btn btn-success">Speichern & Zur Kasse</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" class="bg-white p-4 shadow-sm rounded mt-4">
|
||||
<input type="hidden" name="action" value="add_product">
|
||||
<h5 class="mb-3">Neuer Artikel hinzufügen</h5>
|
||||
<div class="form-row align-items-end">
|
||||
<div class="form-group col-md-3">
|
||||
<label for="add_name">Name</label>
|
||||
<input type="text" class="form-control" name="add_name" id="add_name" required
|
||||
placeholder="z.B. 500g Brombeeren">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label for="add_price">Preis (€)</label>
|
||||
<input type="text" inputmode="decimal" class="form-control" name="add_price" id="add_price" required
|
||||
placeholder="4,50">
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<label for="add_icon">Icon</label>
|
||||
<input type="text" class="form-control" name="add_icon" id="add_icon" placeholder="🫐"
|
||||
maxlength="4">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label for="add_color">Farbe</label>
|
||||
<select class="form-control" name="add_color" id="add_color">
|
||||
<option value="btn-primary">Blau</option>
|
||||
<option value="btn-secondary">Grau</option>
|
||||
<option value="btn-success">Grün</option>
|
||||
<option value="btn-danger">Rot</option>
|
||||
<option value="btn-warning">Gelb</option>
|
||||
<option value="btn-info">Hellblau</option>
|
||||
<option value="btn-dark">Schwarz</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<button type="submit" class="btn btn-primary">Artikel hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center text-muted pb-4">
|
||||
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
|
||||
target="_blank">Kiel Strawberry City.</a><br>
|
||||
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...</small>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
@@ -12,137 +12,162 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#dc3545">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
height: 100%;
|
||||
.kasse-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.kasse-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.article-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat({{ grid_cols }}, 1fr);
|
||||
grid-template-rows: repeat({{ grid_rows }}, 1fr);
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.article-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.article-tile .btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: normal;
|
||||
font-size: clamp(0.9rem, 4vw, 1.5rem);
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
table-layout: fixed;
|
||||
.article-tile .btn .article-icon {
|
||||
font-size: min(12vw, 11vh);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.table td {
|
||||
height: calc(100vh / 6);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.bold-row {
|
||||
.kasse-sum-row {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 250%;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-btn-size {
|
||||
font-size: 180%;
|
||||
.kasse-reset {
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-btn-size-med {
|
||||
font-size: 150%;
|
||||
.kasse-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.input-container input {
|
||||
margin-bottom: 10px;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.input-container button {
|
||||
width: 80%;
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid table-container">
|
||||
<table class="table table-bordered">
|
||||
<form method="post">
|
||||
<tbody>
|
||||
<tr class="bold-row">
|
||||
<td colspan="2">erdbeerrechner 🍓💶</td>
|
||||
<td><a href="{{ url_for('admin', instance_id=instance_id) }}"
|
||||
class="btn btn-outline-secondary custom-btn-size-med">⚙️ Setup</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for i in range(1, 4) %}
|
||||
{% set prod = products[i] %}
|
||||
<td>
|
||||
<button type="submit" name="position" value="{{ i }}" title="{{ prod['name'] }}"
|
||||
class="btn btn-xl {{ prod['color_class'] }} custom-btn-size">
|
||||
{{ prod['icon'] }} <br> {{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€ <br>
|
||||
({{ items[i] }})
|
||||
<form method="post" class="kasse-container container-fluid">
|
||||
<div class="kasse-header">
|
||||
<span>erdbeerrechner 🍓💶</span>
|
||||
<a href="{{ url_for('admin', instance_id=instance_id) }}"
|
||||
class="btn btn-outline-secondary">⚙️ Setup</a>
|
||||
</div>
|
||||
|
||||
<div class="article-grid">
|
||||
{% if not products %}
|
||||
<div class="empty-state">
|
||||
Keine Artikel. Bitte im <a href="{{ url_for('admin', instance_id=instance_id) }}">Admin</a> hinzufügen.
|
||||
</div>
|
||||
{% else %}
|
||||
{% for pos, prod in products|dictsort %}
|
||||
<div class="article-tile">
|
||||
<button type="submit" name="position" value="{{ pos }}" title="{{ prod['name'] }}"
|
||||
class="btn {{ prod['color_class'] }}">
|
||||
<span class="article-icon">{{ prod['icon'] }}</span>
|
||||
<span>{{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€</span>
|
||||
<span>({{ items.get(pos, 0) }})</span>
|
||||
</button>
|
||||
</td>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
{% for i in range(4, 7) %}
|
||||
{% set prod = products[i] %}
|
||||
<td>
|
||||
<button type="submit" name="position" value="{{ i }}" title="{{ prod['name'] }}"
|
||||
class="btn btn-xl {{ prod['color_class'] }} custom-btn-size">
|
||||
{{ prod['icon'] }} <br> {{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€ <br>
|
||||
({{ items[i] }})
|
||||
</button>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td title="Summe" class="bold-row">🫰 {{ gesamtwert.replace('.', ',') }}€</td>
|
||||
<td>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="kasse-sum-row">
|
||||
<span title="Summe">🫰 {{ gesamtwert.replace('.', ',') }}€</span>
|
||||
<div class="input-container">
|
||||
<input type="number" step="0.01" class="form-control" name="given"
|
||||
placeholder="{{ given }}" value="{% if given != '0' %}{{ given }}{% endif %}">
|
||||
<button type="submit" name="action" value="calculate_change"
|
||||
title="Wechselgeld berechnen" class="btn btn-xl btn-primary custom-btn-size-med">🧾
|
||||
Berechnen</button>
|
||||
title="Wechselgeld berechnen" class="btn btn-primary btn-sm">🧾 Berechnen</button>
|
||||
</div>
|
||||
</td>
|
||||
<td title="Wechselgeld" class="bold-row {{ background }}">🪙 {{ change.replace('.', ',') }}€
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button type="submit" name="action" value="reset" id="reset"
|
||||
class="btn btn-xl btn-dark custom-btn-size">Reset 🦭</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">Made with ♥️, marmalade and zero knowledge in <a
|
||||
href="https://kiel-sailing-city.de/" target="_blank">Kiel Strawberry City.</a><br>
|
||||
<span title="Wechselgeld" class="{{ background }}">🪙 {{ change.replace('.', ',') }}€</span>
|
||||
</div>
|
||||
|
||||
<div class="kasse-reset">
|
||||
<button type="submit" name="action" value="reset" class="btn btn-dark btn-block">Reset 🦭</button>
|
||||
</div>
|
||||
|
||||
<div class="kasse-footer">
|
||||
Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
|
||||
target="_blank">Kiel Strawberry City.</a><br>
|
||||
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...
|
||||
<button type="button" onclick="shareInstance()"
|
||||
class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</form>
|
||||
</table>
|
||||
<button type="button" onclick="shareInstance()" class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#dc3545">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -45,8 +46,10 @@
|
||||
<ul class="list-group shadow-sm" id="saved-instances-list">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-5 text-muted">
|
||||
<small>Version: {{ version }}</small>
|
||||
<div class="mt-5 text-muted pb-4">
|
||||
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
|
||||
target="_blank">Kiel Strawberry City.</a><br>
|
||||
Version: {{ version }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#dc3545">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
@@ -21,14 +22,19 @@
|
||||
|
||||
<form method="post" class="bg-white p-4 shadow-sm rounded">
|
||||
<div class="form-group">
|
||||
<label for="admin_password">Passworteingabe erforderlich</label>
|
||||
<input type="password" class="form-control" name="admin_password" id="admin_password" required
|
||||
autofocus>
|
||||
<label for="admin_password">Passwort (bei passwortloser Kasse leer lassen)</label>
|
||||
<input type="password" class="form-control" name="admin_password" id="admin_password"
|
||||
autofocus placeholder="Leer lassen wenn keine Kasse mit Passwort">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Einloggen</button>
|
||||
<a href="{{ url_for('index', instance_id=instance_id) }}" class="btn btn-secondary w-100 mt-2">Zurück zur
|
||||
Kasse</a>
|
||||
</form>
|
||||
<div class="mt-4 text-center text-muted pb-4">
|
||||
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
|
||||
target="_blank">Kiel Strawberry City.</a><br>
|
||||
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...</small>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
Reference in New Issue
Block a user