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/
|
.venv/
|
||||||
log/
|
log/
|
||||||
__pycache__/
|
__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
|
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
|
## 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
|
## Lizenz
|
||||||
|
|
||||||
|
|||||||
87
app.py
87
app.py
@@ -1,13 +1,16 @@
|
|||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import uuid
|
import uuid
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
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 import Flask, render_template, request, session, send_from_directory, g, redirect, url_for
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap
|
||||||
|
|
||||||
app = Flask(__name__)
|
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'
|
app.config['SECRET_KEY'] = 'j69ol5mcHLsEtLg4Y/+myd9wWD4pp56E'
|
||||||
|
|
||||||
# setup logging
|
# setup logging
|
||||||
@@ -21,7 +24,7 @@ app.logger.addHandler(handler)
|
|||||||
|
|
||||||
Bootstrap(app)
|
Bootstrap(app)
|
||||||
|
|
||||||
version = "1.1.0/2026-02-24"
|
version = "1.1.1/2026-03-22"
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
conn = sqlite3.connect('kasse.db')
|
conn = sqlite3.connect('kasse.db')
|
||||||
@@ -110,7 +113,7 @@ def index(instance_id):
|
|||||||
gesamtwert = session.get(f'{session_prefix}gesamtwert', 0.0)
|
gesamtwert = session.get(f'{session_prefix}gesamtwert', 0.0)
|
||||||
change = session.get(f'{session_prefix}change', "0.00")
|
change = session.get(f'{session_prefix}change', "0.00")
|
||||||
givenfloat = session.get(f'{session_prefix}given', 0.0)
|
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"
|
background = "bg-white"
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ def index(instance_id):
|
|||||||
gesamtwert = 0.0
|
gesamtwert = 0.0
|
||||||
change = "0.00"
|
change = "0.00"
|
||||||
givenfloat = 0.0
|
givenfloat = 0.0
|
||||||
items = {i: 0 for i in range(1, 7)}
|
items = {p: 0 for p in products}
|
||||||
background = "bg-white"
|
background = "bg-white"
|
||||||
elif action == "calculate_change":
|
elif action == "calculate_change":
|
||||||
given = request.form.get('given', "0", type=float)
|
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}gesamtwert'] = round(gesamtwert, 2)
|
||||||
session[f'{session_prefix}change'] = change
|
session[f'{session_prefix}change'] = change
|
||||||
session[f'{session_prefix}given'] = round(givenfloat, 2)
|
session[f'{session_prefix}given'] = round(givenfloat, 2)
|
||||||
for i in range(1, 7):
|
for p in products:
|
||||||
session[f'{session_prefix}item{i}'] = items[i]
|
session[f'{session_prefix}item{p}'] = items[p]
|
||||||
|
|
||||||
# Update our local variables after processing so they reflect what is rendered
|
# Update our local variables after processing so they reflect what is rendered
|
||||||
gesamtwert = round(gesamtwert, 2)
|
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",
|
return render_template("index.html",
|
||||||
instance_id=instance_id,
|
instance_id=instance_id,
|
||||||
products=products,
|
products=products,
|
||||||
|
grid_cols=grid_cols,
|
||||||
|
grid_rows=grid_rows,
|
||||||
gesamtwert=f"{gesamtwert:.2f}",
|
gesamtwert=f"{gesamtwert:.2f}",
|
||||||
change=change,
|
change=change,
|
||||||
given=f"{givenfloat:.2f}" if givenfloat > 0 else "0",
|
given=f"{givenfloat:.2f}" if givenfloat > 0 else "0",
|
||||||
@@ -165,9 +174,21 @@ def admin(instance_id):
|
|||||||
|
|
||||||
auth_key = f'admin_auth_{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
|
# Handle Login Submission
|
||||||
if request.method == "POST" and 'admin_password' in request.form:
|
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
|
session[auth_key] = True
|
||||||
conn.close()
|
conn.close()
|
||||||
return redirect(url_for('admin', instance_id=instance_id))
|
return redirect(url_for('admin', instance_id=instance_id))
|
||||||
@@ -191,18 +212,56 @@ def admin(instance_id):
|
|||||||
session.pop(auth_key, None)
|
session.pop(auth_key, None)
|
||||||
return redirect(url_for('landing'))
|
return redirect(url_for('landing'))
|
||||||
|
|
||||||
# Additional safety: Ensure that the post request isn't processed if not authenticated
|
for key in request.form:
|
||||||
# (Though we checked above, this is for the product update form submission)
|
if key.startswith('delete_') and request.form[key]:
|
||||||
for i in range(1, 7):
|
pos = int(key.split('_')[1])
|
||||||
name = request.form.get(f'name_{i}')
|
conn.execute('DELETE FROM products WHERE instance_id = ? AND position = ?',
|
||||||
price = request.form.get(f'price_{i}', type=float)
|
(instance_id, pos))
|
||||||
icon = request.form.get(f'icon_{i}')
|
conn.commit()
|
||||||
color = request.form.get(f'color_{i}')
|
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:
|
if name is not None and price is not None:
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
UPDATE products SET name = ?, price = ?, icon = ?, color_class = ?
|
UPDATE products SET name = ?, price = ?, icon = ?, color_class = ?
|
||||||
WHERE instance_id = ? AND position = ?
|
WHERE instance_id = ? AND position = ?
|
||||||
''', (name, price, icon, color, instance_id, i))
|
''', (name, price, icon, color, instance_id, pos))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return redirect(url_for('index', instance_id=instance_id))
|
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">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#dc3545">
|
<meta name="theme-color" content="#dc3545">
|
||||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
@@ -30,21 +31,26 @@
|
|||||||
<th>Preis (€)</th>
|
<th>Preis (€)</th>
|
||||||
<th>Icon/Emoji</th>
|
<th>Icon/Emoji</th>
|
||||||
<th>Farbe (Bootstrap Klasse)</th>
|
<th>Farbe (Bootstrap Klasse)</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for i in range(1, 7) %}
|
{% if not products %}
|
||||||
{% set prod = products[i] %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">{{ i }}</td>
|
<td colspan="6" class="text-muted text-center py-4">Keine Artikel. Füge unten einen neuen hinzu.</td>
|
||||||
<td><input type="text" class="form-control" name="name_{{ i }}" value="{{ prod['name'] }}"
|
</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>
|
required></td>
|
||||||
<td><input type="number" step="0.01" class="form-control" name="price_{{ i }}"
|
<td><input type="text" inputmode="decimal" class="form-control" name="price_{{ pos }}"
|
||||||
value="{{ prod['price'] }}" required></td>
|
value="{{ '{:.2f}'.format(prod['price']).replace('.', ',') }}" required></td>
|
||||||
<td><input type="text" class="form-control" name="icon_{{ i }}" value="{{ prod['icon'] }}"
|
<td><input type="text" class="form-control" name="icon_{{ pos }}" value="{{ prod['icon'] }}"
|
||||||
required></td>
|
required></td>
|
||||||
<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
|
<option value="btn-primary" {% if prod['color_class']=='btn-primary' %}selected{% endif
|
||||||
%}>Blau (Primary)</option>
|
%}>Blau (Primary)</option>
|
||||||
<option value="btn-secondary" {% if prod['color_class']=='btn-secondary' %}selected{%
|
<option value="btn-secondary" {% if prod['color_class']=='btn-secondary' %}selected{%
|
||||||
@@ -61,6 +67,12 @@
|
|||||||
Schwarz (Dark)</option>
|
Schwarz (Dark)</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -76,6 +88,49 @@
|
|||||||
<button type="submit" name="action" value="save" class="btn btn-success">Speichern & Zur Kasse</button>
|
<button type="submit" name="action" value="save" class="btn btn-success">Speichern & Zur Kasse</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
@@ -12,137 +12,162 @@
|
|||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#dc3545">
|
<meta name="theme-color" content="#dc3545">
|
||||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
<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>
|
<style>
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.kasse-container {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
display: flex;
|
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;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.article-tile .btn .article-icon {
|
||||||
width: 100%;
|
font-size: min(12vw, 11vh);
|
||||||
height: 100%;
|
line-height: 1.1;
|
||||||
table-layout: fixed;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
.kasse-sum-row {
|
||||||
height: calc(100vh / 6);
|
flex-shrink: 0;
|
||||||
vertical-align: middle;
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
}
|
justify-content: space-around;
|
||||||
|
padding: 0.5rem;
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold-row {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 250%;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-btn-size {
|
.kasse-reset {
|
||||||
font-size: 180%;
|
flex-shrink: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-btn-size-med {
|
.kasse-footer {
|
||||||
font-size: 150%;
|
flex-shrink: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 0.25rem;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container input {
|
.input-container input {
|
||||||
margin-bottom: 10px;
|
|
||||||
width: 80%;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container button {
|
.empty-state {
|
||||||
width: 80%;
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid table-container">
|
<form method="post" class="kasse-container container-fluid">
|
||||||
<table class="table table-bordered">
|
<div class="kasse-header">
|
||||||
<form method="post">
|
<span>erdbeerrechner 🍓💶</span>
|
||||||
<tbody>
|
<a href="{{ url_for('admin', instance_id=instance_id) }}"
|
||||||
<tr class="bold-row">
|
class="btn btn-outline-secondary">⚙️ Setup</a>
|
||||||
<td colspan="2">erdbeerrechner 🍓💶</td>
|
</div>
|
||||||
<td><a href="{{ url_for('admin', instance_id=instance_id) }}"
|
|
||||||
class="btn btn-outline-secondary custom-btn-size-med">⚙️ Setup</a></td>
|
<div class="article-grid">
|
||||||
</tr>
|
{% if not products %}
|
||||||
<tr>
|
<div class="empty-state">
|
||||||
{% for i in range(1, 4) %}
|
Keine Artikel. Bitte im <a href="{{ url_for('admin', instance_id=instance_id) }}">Admin</a> hinzufügen.
|
||||||
{% set prod = products[i] %}
|
</div>
|
||||||
<td>
|
{% else %}
|
||||||
<button type="submit" name="position" value="{{ i }}" title="{{ prod['name'] }}"
|
{% for pos, prod in products|dictsort %}
|
||||||
class="btn btn-xl {{ prod['color_class'] }} custom-btn-size">
|
<div class="article-tile">
|
||||||
{{ prod['icon'] }} <br> {{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€ <br>
|
<button type="submit" name="position" value="{{ pos }}" title="{{ prod['name'] }}"
|
||||||
({{ items[i] }})
|
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>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
{% endif %}
|
||||||
<tr>
|
</div>
|
||||||
{% for i in range(4, 7) %}
|
|
||||||
{% set prod = products[i] %}
|
<div class="kasse-sum-row">
|
||||||
<td>
|
<span title="Summe">🫰 {{ gesamtwert.replace('.', ',') }}€</span>
|
||||||
<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>
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="number" step="0.01" class="form-control" name="given"
|
<input type="number" step="0.01" class="form-control" name="given"
|
||||||
placeholder="{{ given }}" value="{% if given != '0' %}{{ given }}{% endif %}">
|
placeholder="{{ given }}" value="{% if given != '0' %}{{ given }}{% endif %}">
|
||||||
<button type="submit" name="action" value="calculate_change"
|
<button type="submit" name="action" value="calculate_change"
|
||||||
title="Wechselgeld berechnen" class="btn btn-xl btn-primary custom-btn-size-med">🧾
|
title="Wechselgeld berechnen" class="btn btn-primary btn-sm">🧾 Berechnen</button>
|
||||||
Berechnen</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<span title="Wechselgeld" class="{{ background }}">🪙 {{ change.replace('.', ',') }}€</span>
|
||||||
<td title="Wechselgeld" class="bold-row {{ background }}">🪙 {{ change.replace('.', ',') }}€
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<div class="kasse-reset">
|
||||||
<tr>
|
<button type="submit" name="action" value="reset" class="btn btn-dark btn-block">Reset 🦭</button>
|
||||||
<td colspan="3">
|
</div>
|
||||||
<button type="submit" name="action" value="reset" id="reset"
|
|
||||||
class="btn btn-xl btn-dark custom-btn-size">Reset 🦭</button>
|
<div class="kasse-footer">
|
||||||
</td>
|
Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
|
||||||
</tr>
|
target="_blank">Kiel Strawberry City.</a><br>
|
||||||
<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>
|
|
||||||
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...
|
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...
|
||||||
<button type="button" onclick="shareInstance()"
|
<button type="button" onclick="shareInstance()" class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
|
||||||
class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</form>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#dc3545">
|
<meta name="theme-color" content="#dc3545">
|
||||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -45,8 +46,10 @@
|
|||||||
<ul class="list-group shadow-sm" id="saved-instances-list">
|
<ul class="list-group shadow-sm" id="saved-instances-list">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 text-muted">
|
<div class="mt-5 text-muted pb-4">
|
||||||
<small>Version: {{ version }}</small>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#dc3545">
|
<meta name="theme-color" content="#dc3545">
|
||||||
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
@@ -21,14 +22,19 @@
|
|||||||
|
|
||||||
<form method="post" class="bg-white p-4 shadow-sm rounded">
|
<form method="post" class="bg-white p-4 shadow-sm rounded">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="admin_password">Passworteingabe erforderlich</label>
|
<label for="admin_password">Passwort (bei passwortloser Kasse leer lassen)</label>
|
||||||
<input type="password" class="form-control" name="admin_password" id="admin_password" required
|
<input type="password" class="form-control" name="admin_password" id="admin_password"
|
||||||
autofocus>
|
autofocus placeholder="Leer lassen wenn keine Kasse mit Passwort">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary w-100">Einloggen</button>
|
<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
|
<a href="{{ url_for('index', instance_id=instance_id) }}" class="btn btn-secondary w-100 mt-2">Zurück zur
|
||||||
Kasse</a>
|
Kasse</a>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
Reference in New Issue
Block a user