Implementieren der PWA, Multi-Instanzen-Passwort-Schutz und Kassen-Löschfunktion
This commit is contained in:
288
app.py
288
app.py
@@ -1,7 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from flask import Flask, render_template, request, session, send_from_directory, g
|
import sqlite3
|
||||||
|
import uuid
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
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__)
|
||||||
@@ -18,133 +21,210 @@ app.logger.addHandler(handler)
|
|||||||
|
|
||||||
Bootstrap(app)
|
Bootstrap(app)
|
||||||
|
|
||||||
version = "1.0.9/2024-05-28"
|
version = "1.1.0/2026-02-24"
|
||||||
postcounter = 0
|
|
||||||
gesamtwert = 0
|
def get_db_connection():
|
||||||
change = 0
|
conn = sqlite3.connect('kasse.db')
|
||||||
givenfloat = 0
|
conn.row_factory = sqlite3.Row
|
||||||
sum = ""
|
return conn
|
||||||
item1 = 0
|
|
||||||
item2 = 0
|
def init_db():
|
||||||
item3 = 0
|
conn = get_db_connection()
|
||||||
item4 = 0
|
conn.execute('''
|
||||||
item5 = 0
|
CREATE TABLE IF NOT EXISTS instances (
|
||||||
item6 = 0
|
id TEXT PRIMARY KEY,
|
||||||
|
password TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
try:
|
||||||
|
conn.execute('ALTER TABLE instances ADD COLUMN password TEXT')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
instance_id TEXT,
|
||||||
|
position INTEGER,
|
||||||
|
name TEXT,
|
||||||
|
price REAL,
|
||||||
|
icon TEXT,
|
||||||
|
color_class TEXT,
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id),
|
||||||
|
PRIMARY KEY (instance_id, position)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
app.logger.info('Starting erdbeerhannah ' + version)
|
app.logger.info('Starting erdbeerhannah ' + version)
|
||||||
|
|
||||||
# https://code-maven.com/flask-display-elapsed-time
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def before_request():
|
def before_request():
|
||||||
g.request_start_time = time.time()
|
g.request_start_time = time.time()
|
||||||
g.request_time = lambda: "%.4fs" % (time.time() - g.request_start_time)
|
g.request_time = lambda: "%.4fs" % (time.time() - g.request_start_time)
|
||||||
|
|
||||||
# prevent cached responses
|
|
||||||
# https://stackoverflow.com/questions/47376744/how-to-prevent-cached-response-flask-server-using-chrome
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_header(r):
|
def add_header(r):
|
||||||
"""
|
|
||||||
Add headers to both force latest IE rendering engine or Chrome Frame,
|
|
||||||
and also to cache the rendered page for 10 minutes.
|
|
||||||
"""
|
|
||||||
r.headers["Cache-Control"] = "no-store, max-age=0"
|
r.headers["Cache-Control"] = "no-store, max-age=0"
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@app.route("/", methods=["GET", "POST"])
|
@app.route("/", methods=["GET", "POST"])
|
||||||
def index():
|
def landing():
|
||||||
global gesamtwert, item1, item2, item3, item4, item5, item6, sum, givenfloat, change, given, background, postcounter, version
|
|
||||||
background = "bg-white"
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
postcounter += 1
|
password = request.form.get('password', '')
|
||||||
# wert = float(request.form["wert"])
|
instance_id = str(uuid.uuid4())
|
||||||
wert = request.form.get('wert', "0", type=float)
|
conn = get_db_connection()
|
||||||
given = request.form.get('given', "0", type=float)
|
conn.execute('INSERT INTO instances (id, password) VALUES (?, ?)', (instance_id, generate_password_hash(password)))
|
||||||
app.logger.debug('wert: %s, given: %s', wert, given)
|
default_products = [
|
||||||
wertfloat = float(wert)
|
(instance_id, 1, "500g Erdbeeren", 4.9, "🍓", "btn-primary"),
|
||||||
givenfloat = float(given)
|
(instance_id, 2, "Marmelade groß", 4.8, "🫙🫙", "btn-danger"),
|
||||||
|
(instance_id, 3, "Marmelade klein", 3.3, "🫙", "btn-danger"),
|
||||||
# reset button
|
(instance_id, 4, "500g Kirschen", 5.0, "🍒", "btn-warning"),
|
||||||
if wertfloat == 0:
|
(instance_id, 5, "500g Himbeeren", 4.5, "🫐", "btn-warning"),
|
||||||
global gesamtwert
|
(instance_id, 6, "Tragetasche", 0.2, "🛍️", "btn-success")
|
||||||
gesamtwert = 0
|
]
|
||||||
change = 0
|
conn.executemany('INSERT INTO products (instance_id, position, name, price, icon, color_class) VALUES (?, ?, ?, ?, ?, ?)', default_products)
|
||||||
sum = "0"
|
conn.commit()
|
||||||
item1 = 0
|
conn.close()
|
||||||
item2 = 0
|
return redirect(url_for('admin', instance_id=instance_id))
|
||||||
item3 = 0
|
return render_template("landing.html", version=version)
|
||||||
item4 = 0
|
|
||||||
item5 = 0
|
@app.route("/<instance_id>", methods=["GET", "POST"])
|
||||||
item6 = 0
|
def index(instance_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
instance = conn.execute('SELECT * FROM instances WHERE id = ?', (instance_id,)).fetchone()
|
||||||
|
if not instance:
|
||||||
|
conn.close()
|
||||||
|
return "Instance not found", 404
|
||||||
|
|
||||||
|
products_rows = conn.execute('SELECT * FROM products WHERE instance_id = ? ORDER BY position', (instance_id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
products = {row['position']: dict(row) for row in products_rows}
|
||||||
|
|
||||||
|
session_prefix = f"kasse_{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)}
|
||||||
|
|
||||||
|
background = "bg-white"
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get('action')
|
||||||
|
position = request.form.get('position', type=int)
|
||||||
|
|
||||||
|
if action == "reset":
|
||||||
|
gesamtwert = 0.0
|
||||||
|
change = "0.00"
|
||||||
|
givenfloat = 0.0
|
||||||
|
items = {i: 0 for i in range(1, 7)}
|
||||||
background = "bg-white"
|
background = "bg-white"
|
||||||
# summarize value
|
elif action == "calculate_change":
|
||||||
elif wertfloat != -2:
|
given = request.form.get('given', "0", type=float)
|
||||||
gesamtwert += wertfloat
|
givenfloat = given
|
||||||
gesamtwert = round(gesamtwert, 2)
|
change_val = givenfloat - gesamtwert
|
||||||
if gesamtwert > 0:
|
change = f"{change_val:.2f}"
|
||||||
sum = str(gesamtwert) + "0"
|
background = "bg-danger" if change_val < 0 else "bg-white"
|
||||||
|
elif position and position in products:
|
||||||
|
price = products[position]['price']
|
||||||
|
gesamtwert += price
|
||||||
|
items[position] += 1
|
||||||
|
|
||||||
# summarize items
|
session[f'{session_prefix}gesamtwert'] = round(gesamtwert, 2)
|
||||||
if wertfloat == 4.9:
|
session[f'{session_prefix}change'] = change
|
||||||
item1 += 1
|
session[f'{session_prefix}given'] = round(givenfloat, 2)
|
||||||
if wertfloat == 4.8:
|
for i in range(1, 7):
|
||||||
item2 += 1
|
session[f'{session_prefix}item{i}'] = items[i]
|
||||||
if wertfloat == 3.3:
|
|
||||||
item3 += 1
|
# Update our local variables after processing so they reflect what is rendered
|
||||||
if wertfloat == 4.5:
|
gesamtwert = round(gesamtwert, 2)
|
||||||
item4 += 1
|
|
||||||
if wertfloat == 5:
|
return render_template("index.html",
|
||||||
item5 += 1
|
instance_id=instance_id,
|
||||||
if wertfloat == .2:
|
products=products,
|
||||||
item6 += 1
|
gesamtwert=f"{gesamtwert:.2f}",
|
||||||
if givenfloat > 0:
|
change=change,
|
||||||
try:
|
given=f"{givenfloat:.2f}" if givenfloat > 0 else "0",
|
||||||
gesamtwert = session['summefloat'] or 0
|
items=items,
|
||||||
sum = str(gesamtwert) + "0"
|
|
||||||
change = str(round((givenfloat - gesamtwert) * -1, 2)) + "0"
|
|
||||||
except:
|
|
||||||
app.logger.warning("Failed to read sum")
|
|
||||||
if givenfloat - gesamtwert < 0:
|
|
||||||
background = "bg-danger"
|
|
||||||
else:
|
|
||||||
background = "bg-white"
|
|
||||||
|
|
||||||
session['item1'] = item1
|
|
||||||
session['item2'] = item2
|
|
||||||
session['item3'] = item3
|
|
||||||
session['item4'] = item4
|
|
||||||
session['item5'] = item5
|
|
||||||
session['item6'] = item6
|
|
||||||
session['summefloat'] = gesamtwert
|
|
||||||
session['summestring'] = sum
|
|
||||||
session['change'] = change
|
|
||||||
session['given'] = givenfloat
|
|
||||||
|
|
||||||
app.logger.info('*** sum %s, given %s, change %s', sum, givenfloat, change)
|
|
||||||
app.logger.info('*** postcounter %s', postcounter)
|
|
||||||
|
|
||||||
return render_template("index.html", gesamtwert=session.get('summestring', 0),
|
|
||||||
change=session.get('change', 0),
|
|
||||||
given=session.get('given', 0),
|
|
||||||
item1=session.get('item1', 0),
|
|
||||||
item2=session.get('item2', 0),
|
|
||||||
item3=session.get('item3', 0),
|
|
||||||
item4=session.get('item4', 0),
|
|
||||||
item5=session.get('item5', 0),
|
|
||||||
item6=session.get('item6', 0),
|
|
||||||
background=background,
|
background=background,
|
||||||
version=version,
|
version=version
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/<instance_id>/admin", methods=["GET", "POST"])
|
||||||
|
def admin(instance_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
instance = conn.execute('SELECT * FROM instances WHERE id = ?', (instance_id,)).fetchone()
|
||||||
|
if not instance:
|
||||||
|
conn.close()
|
||||||
|
return "Instance not found", 404
|
||||||
|
|
||||||
|
auth_key = f'admin_auth_{instance_id}'
|
||||||
|
|
||||||
|
# Handle Login Submission
|
||||||
|
if request.method == "POST" and 'admin_password' in request.form:
|
||||||
|
if check_password_hash(instance['password'], request.form['admin_password']):
|
||||||
|
session[auth_key] = True
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('admin', instance_id=instance_id))
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
return render_template("login.html", instance_id=instance_id, error="Falsches Passwort", version=version)
|
||||||
|
|
||||||
|
# Require Authentication
|
||||||
|
if not session.get(auth_key):
|
||||||
|
conn.close()
|
||||||
|
return render_template("login.html", instance_id=instance_id, error=None, version=version)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
conn.execute('DELETE FROM products WHERE instance_id = ?', (instance_id,))
|
||||||
|
conn.execute('DELETE FROM instances WHERE id = ?', (instance_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
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}')
|
||||||
|
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))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('index', instance_id=instance_id))
|
||||||
|
|
||||||
|
products_rows = conn.execute('SELECT * FROM products WHERE instance_id = ? ORDER BY position', (instance_id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
products = {row['position']: dict(row) for row in products_rows}
|
||||||
|
return render_template("admin.html", instance_id=instance_id, products=products, version=version)
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static'),
|
return send_from_directory(os.path.join(app.root_path, 'static'),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
# @app.route('/about')
|
@app.route('/manifest.json')
|
||||||
# def about():
|
def manifest():
|
||||||
# return send_from_directory(os.path.join(app.root_path, 'static'),
|
return send_from_directory('static', 'manifest.json')
|
||||||
# 'about.html')
|
|
||||||
|
@app.route('/sw.js')
|
||||||
|
def service_worker():
|
||||||
|
return send_from_directory('static', 'sw.js', mimetype='application/javascript')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, host='127.0.0.1')
|
app.run(debug=True, host='127.0.0.1')
|
||||||
36
gen_icons.py
Normal file
36
gen_icons.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import os
|
||||||
|
|
||||||
|
def create_icon(size, filename):
|
||||||
|
# Create a red square
|
||||||
|
img = Image.new('RGB', (size, size), color='#dc3545')
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Try to draw a simple 'E' in the center as a fallback if no emoji font
|
||||||
|
# Calculate approximate font size
|
||||||
|
font_size = int(size * 0.6)
|
||||||
|
try:
|
||||||
|
# standard windows font
|
||||||
|
font = ImageFont.truetype("arialbd.ttf", font_size)
|
||||||
|
except IOError:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
text = "E"
|
||||||
|
# Get bounding box
|
||||||
|
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_w = right - left
|
||||||
|
text_h = bottom - top
|
||||||
|
|
||||||
|
x = (size - text_w) / 2
|
||||||
|
y = (size - text_h) / 2
|
||||||
|
|
||||||
|
draw.text((x, y), text, fill=(255, 255, 255), font=font)
|
||||||
|
|
||||||
|
# Save the image
|
||||||
|
filepath = os.path.join('static', filename)
|
||||||
|
img.save(filepath)
|
||||||
|
print(f"Saved {filepath}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_icon(192, 'icon-192x192.png')
|
||||||
|
create_icon(512, 'icon-512x512.png')
|
||||||
BIN
static/icon-192x192.png
Normal file
BIN
static/icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 B |
BIN
static/icon-512x512.png
Normal file
BIN
static/icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
21
static/manifest.json
Normal file
21
static/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "erdbeerrechner",
|
||||||
|
"short_name": "erdbeer",
|
||||||
|
"description": "Eine anpassbare Kassen-App.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f8f9fa",
|
||||||
|
"theme_color": "#dc3545",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
56
static/sw.js
Normal file
56
static/sw.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const CACHE_NAME = 'erdbeerkasse-v1';
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'/',
|
||||||
|
'/manifest.json',
|
||||||
|
'/static/icon-192x192.png',
|
||||||
|
'/static/icon-512x512.png',
|
||||||
|
'https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
||||||
|
.then(() => self.skipWaiting())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network-first strategy for dynamic instances, fallback to cache
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.method !== 'GET') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
// Clone the response before caching if it's a valid response
|
||||||
|
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
89
templates/admin.html
Normal file
89
templates/admin.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>erdbeerhannah 🍓💶 - Admin</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#dc3545">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h2 class="mb-4">Kassen-Konfiguration</h2>
|
||||||
|
<p>Dies ist die Konfiguration für deine Kasse: <strong>{{ instance_id }}</strong></p>
|
||||||
|
<p>Speichere dir diese URL ab, um später Änderungen vorzunehmen.</p>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Deine Kasse ist erreichbar unter: <a href="{{ url_for('index', instance_id=instance_id) }}">/{{ instance_id
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="bg-white p-4 shadow-sm rounded">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Preis (€)</th>
|
||||||
|
<th>Icon/Emoji</th>
|
||||||
|
<th>Farbe (Bootstrap Klasse)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in range(1, 7) %}
|
||||||
|
{% set prod = products[i] %}
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">{{ i }}</td>
|
||||||
|
<td><input type="text" class="form-control" name="name_{{ i }}" 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'] }}"
|
||||||
|
required></td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="color_{{ i }}">
|
||||||
|
<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{%
|
||||||
|
endif %}>Grau (Secondary)</option>
|
||||||
|
<option value="btn-success" {% if prod['color_class']=='btn-success' %}selected{% endif
|
||||||
|
%}>Grün (Success)</option>
|
||||||
|
<option value="btn-danger" {% if prod['color_class']=='btn-danger' %}selected{% endif
|
||||||
|
%}>Rot (Danger)</option>
|
||||||
|
<option value="btn-warning" {% if prod['color_class']=='btn-warning' %}selected{% endif
|
||||||
|
%}>Gelb (Warning)</option>
|
||||||
|
<option value="btn-info" {% if prod['color_class']=='btn-info' %}selected{% endif %}>
|
||||||
|
Hellblau (Info)</option>
|
||||||
|
<option value="btn-dark" {% if prod['color_class']=='btn-dark' %}selected{% endif %}>
|
||||||
|
Schwarz (Dark)</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('index', instance_id=instance_id) }}" class="btn btn-secondary">Zurück zur
|
||||||
|
Kasse</a>
|
||||||
|
<button type="submit" name="action" value="delete" class="btn btn-danger ml-2"
|
||||||
|
onclick="return confirm('Möchtest du diese Kasse wirklich unwiderruflich löschen? Alle zugehörigen Verkaufsdaten und Produkteinstellungen gehen verloren.');">🗑️
|
||||||
|
Kasse löschen</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="save" class="btn btn-success">Speichern & Zur Kasse</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,125 +1,176 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="content-Type" content="text/html; utf-8" />
|
<meta http-equiv="content-Type" content="text/html; utf-8" />
|
||||||
<meta http-equiv="Pragma" content="cache" />
|
<meta http-equiv="Pragma" content="cache" />
|
||||||
<meta name="robots" content="INDEX,FOLLOW" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<meta http-equiv="content-Language" content="de" />
|
|
||||||
<meta name="description" content="Viele Obststände in Deutschland haben keine elektronische Kasse. Aufgrund der geringen Zahl unterschiedlicher Artikel bietet sich diese simple Rechner-App an, die bis zu sechs verschiedene Artikel in unterschiedlicher Stückzahl summieren kann." />
|
|
||||||
<meta name="keywords" content="rechner kasse calculator bargeld artikel obst erdbeeren spargel kirschen verkaufsstand wechselgeld kostenlos opensource werbefrei" />
|
|
||||||
<meta name="author" content="Markus Busche" />
|
|
||||||
<meta name="publisher" content="Markus Busche" />
|
|
||||||
<meta name="copyright" content="2024 Markus Busche" />
|
|
||||||
<meta name="audience" content="Alle" />
|
|
||||||
<meta name="page-type" content="HTML-Formular" />
|
|
||||||
<meta name="page-topic" content="Dienstleistung" />
|
|
||||||
<meta http-equiv="Reply-to" content="m.busche@mailbox.org" />
|
|
||||||
<meta name="expires" content="" />
|
|
||||||
<meta name="revisit-after" content="2 days" />
|
|
||||||
<title>erdbeerhannah 🍓💶</title>
|
<title>erdbeerhannah 🍓💶</title>
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#dc3545">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||||
<style>
|
<style>
|
||||||
body, html {
|
body,
|
||||||
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
.table td {
|
||||||
height: calc(100vh / 6); /* Höhe der Zeilen dynamisch anpassen */
|
height: calc(100vh / 6);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 100%; /* Button füllt die Zelle */
|
width: 100%;
|
||||||
height: 100%; /* Button füllt die Zelle */
|
height: 100%;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold-row {
|
.bold-row {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 250%;
|
font-size: 250%;
|
||||||
}
|
}
|
||||||
.large-font {
|
|
||||||
font-size: 300%;
|
|
||||||
}
|
|
||||||
.custom-btn-size {
|
.custom-btn-size {
|
||||||
font-size: 180%;
|
font-size: 180%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-btn-size-med {
|
.custom-btn-size-med {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* Ändert die Richtung der Flex-Elemente zu Spalten */
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container input {
|
.input-container input {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
width: 80%; /* Setzt die Breite des Eingabefelds */
|
width: 80%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container button {
|
.input-container button {
|
||||||
width: 80%; /* Setzt die Breite des Buttons */
|
width: 80%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<script>
|
|
||||||
$(document).ready(function(){
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid table-container">
|
<div class="container-fluid table-container">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="bold-row">
|
<tr class="bold-row">
|
||||||
<td colspan="3">erdbeerrechner 🍓💶</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button type="submit" name="wert" value="4.9" data-toggle="tooltip" data-placement="top" title="500g Erdbeeren" class="btn btn-xl btn-primary custom-btn-size">🍓 4,90€ ({{ item1 }})</button></td>
|
{% for i in range(1, 4) %}
|
||||||
<td><button type="submit" name="wert" value="4.8" data-toggle="tooltip" data-placement="top" title="Marmelade groß" class="btn btn-xl btn-danger custom-btn-size">🫙🫙 4,80€ ({{ item2 }})</button></td>
|
{% set prod = products[i] %}
|
||||||
<td><button type="submit" name="wert" value="3.3" data-toggle="tooltip" data-placement="top" title="Marmelade klein" class="btn btn-xl btn-danger custom-btn-size">🫙 3,30€ ({{ item3 }})</button></td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button type="submit" name="wert" value="5.0" data-toggle="tooltip" data-placement="top" title="500g Kirschen" class="btn btn-xl btn-warning custom-btn-size">🍒 5,00€ ({{ item4 }})</button></td>
|
{% for i in range(4, 7) %}
|
||||||
<td><button type="submit" name="wert" value="4.5" data-toggle="tooltip" data-placement="top" title="500g Himbeeren" class="btn btn-xl btn-warning custom-btn-size">🫐 4,50€ ({{ item5 }})</button></td>
|
{% set prod = products[i] %}
|
||||||
<td><button type="submit" name="wert" value="0.2" data-toggle="tooltip" data-placement="top" title="Tragetasche" class="btn btn-xl btn-success custom-btn-size">🛍️ 0,20€ ({{ item6 }})</button></td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td data-toggle="tooltip" data-placement="top" title="Summe" class="bold-row">🫰 {{ gesamtwert }}€</td>
|
<td title="Summe" class="bold-row">🫰 {{ gesamtwert.replace('.', ',') }}€</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" class="form-control" name="given" placeholder="{{ given }}">
|
<input type="number" step="0.01" class="form-control" name="given"
|
||||||
<button type="submit" name="wert" value="-2" data-toggle="tooltip" data-placement="top" title="Wechselgeld berechnen" class="btn btn-xl btn-primary custom-btn-size-med">🧾</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-toggle="tooltip" data-placement="top" title="Wechselgeld" class="bold-row {{ background }}">🪙 {{ change }}€</td>
|
<td title="Wechselgeld" class="bold-row {{ background }}">🪙 {{ change.replace('.', ',') }}€
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3"><button type="submit" name="wert" value="0" id="reset" class="btn btn-xl btn-dark custom-btn-size">Reset 🦭</button></td>
|
<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>
|
||||||
<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>
|
<td colspan="3">Made with ♥️, marmalade and zero knowledge in <a
|
||||||
Version: {{ version }} ({{ g.request_time() }}), <a href="https://gitea.elpatron.me/elpatron/erdbeerhannah/src/branch/main/README.md" target="_blank">Infos</a></td>
|
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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</form>
|
</form>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
// Save instance to localStorage for the landing page
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
|
const instanceId = "{{ instance_id }}";
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
let saved = JSON.parse(localStorage.getItem('erdbeerkassen')) || [];
|
||||||
|
// Optional: remove if exists so it gets pushed to the end (most recent)
|
||||||
|
saved = saved.filter(i => i.id !== instanceId);
|
||||||
|
saved.push({ id: instanceId, date: new Date().toISOString() });
|
||||||
|
localStorage.setItem('erdbeerkassen', JSON.stringify(saved));
|
||||||
|
|
||||||
|
function shareInstance() {
|
||||||
|
const url = window.location.href;
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator.share({
|
||||||
|
title: 'erdbeerrechner 🍓💶',
|
||||||
|
url: url
|
||||||
|
}).catch(console.error);
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
alert('URL in die Zwischenablage kopiert!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
117
templates/landing.html
Normal file
117
templates/landing.html
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>erdbeerhannah 🍓💶 - Start</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 10vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#dc3545">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Willkommen beim erdbeerrechner 🍓💶</h1>
|
||||||
|
<p class="lead mb-5">Erstelle deine eigene, anpassbare Kassen-Instanz für deinen Verkaufsstand.</p>
|
||||||
|
<form method="post" class="bg-white p-4 shadow-sm rounded" style="max-width: 400px; width: 100%;">
|
||||||
|
<div class="form-group text-left">
|
||||||
|
<label for="password">Admin Passwort (optional)</label>
|
||||||
|
<input type="password" class="form-control" name="password" id="password" placeholder="Passwort...">
|
||||||
|
<small class="form-text text-muted">Dieses Passwort wird für den /admin Bereich benötigt.</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg shadow-sm w-100">Neue Kasse erstellen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-5 w-100" id="saved-instances-container" style="display: none; max-width: 500px;">
|
||||||
|
<h4 class="mb-3 text-secondary">Gespeicherte Kassen:</h4>
|
||||||
|
<ul class="list-group shadow-sm" id="saved-instances-list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 text-muted">
|
||||||
|
<small>Version: {{ version }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let saved = JSON.parse(localStorage.getItem('erdbeerkassen')) || [];
|
||||||
|
if (saved.length > 0) {
|
||||||
|
document.getElementById('saved-instances-container').style.display = 'block';
|
||||||
|
const list = document.getElementById('saved-instances-list');
|
||||||
|
|
||||||
|
[...saved].reverse().forEach(instance => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap text-left';
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '/' + instance.id;
|
||||||
|
link.textContent = 'Kasse ' + instance.id.substring(0, 8);
|
||||||
|
link.className = 'font-weight-bold flex-grow-1 text-decoration-none';
|
||||||
|
|
||||||
|
const btnGroup = document.createElement('div');
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'btn btn-sm btn-outline-danger mr-2';
|
||||||
|
deleteBtn.textContent = '🗑️';
|
||||||
|
deleteBtn.title = 'Aus Liste entfernen';
|
||||||
|
deleteBtn.onclick = () => {
|
||||||
|
saved = saved.filter(i => i.id !== instance.id);
|
||||||
|
localStorage.setItem('erdbeerkassen', JSON.stringify(saved));
|
||||||
|
li.remove();
|
||||||
|
if (list.children.length === 0) {
|
||||||
|
document.getElementById('saved-instances-container').style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareBtn = document.createElement('button');
|
||||||
|
shareBtn.className = 'btn btn-sm btn-outline-primary';
|
||||||
|
shareBtn.textContent = '📤 Teilen';
|
||||||
|
shareBtn.onclick = () => {
|
||||||
|
const url = window.location.origin + '/' + instance.id;
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator.share({
|
||||||
|
title: 'erdbeerrechner 🍓💶',
|
||||||
|
url: url
|
||||||
|
}).catch(console.error);
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
alert('URL in die Zwischenablage kopiert!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
btnGroup.appendChild(deleteBtn);
|
||||||
|
btnGroup.appendChild(shareBtn);
|
||||||
|
|
||||||
|
li.appendChild(link);
|
||||||
|
li.appendChild(btnGroup);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
42
templates/login.html
Normal file
42
templates/login.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>erdbeerhannah 🍓💶 - Login</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#dc3545">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container mt-5" style="max-width: 400px;">
|
||||||
|
<h2 class="mb-4 text-center">Admin Login</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user