Implementieren der PWA, Multi-Instanzen-Passwort-Schutz und Kassen-Löschfunktion
This commit is contained in:
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>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-Type" content="text/html; utf-8" />
|
||||
<meta http-equiv="Pragma" content="cache" />
|
||||
<meta name="robots" content="INDEX,FOLLOW" />
|
||||
<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" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<title>erdbeerhannah 🍓💶</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">
|
||||
<style>
|
||||
body, html {
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.table td {
|
||||
height: calc(100vh / 6); /* Höhe der Zeilen dynamisch anpassen */
|
||||
height: calc(100vh / 6);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%; /* Button füllt die Zelle */
|
||||
height: 100%; /* Button füllt die Zelle */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.bold-row {
|
||||
font-weight: bold;
|
||||
font-size: 250%;
|
||||
}
|
||||
.large-font {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
.custom-btn-size {
|
||||
font-size: 180%;
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
.custom-btn-size-med {
|
||||
font-size: 150%;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column; /* Ändert die Richtung der Flex-Elemente zu Spalten */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.input-container input {
|
||||
margin-bottom: 10px;
|
||||
width: 80%; /* Setzt die Breite des Eingabefelds */
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.input-container button {
|
||||
width: 80%; /* Setzt die Breite des Buttons */
|
||||
width: 80%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid table-container">
|
||||
<table class="table table-bordered">
|
||||
<form method="post">
|
||||
<tbody>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
{% 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] }})
|
||||
</button>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
{% 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 data-toggle="tooltip" data-placement="top" title="Summe" class="bold-row">🫰 {{ gesamtwert }}€</td>
|
||||
<td title="Summe" class="bold-row">🫰 {{ gesamtwert.replace('.', ',') }}€</td>
|
||||
<td>
|
||||
<div class="input-container">
|
||||
<input type="text" class="form-control" name="given" placeholder="{{ 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>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
<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 }} ({{ g.request_time() }}), <a href="https://gitea.elpatron.me/elpatron/erdbeerhannah/src/branch/main/README.md" target="_blank">Infos</a></td>
|
||||
<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] }}...
|
||||
<button type="button" onclick="shareInstance()"
|
||||
class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tbody>
|
||||
</form>
|
||||
</table>
|
||||
</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>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||
// Save instance to localStorage for the landing page
|
||||
const instanceId = "{{ instance_id }}";
|
||||
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>
|
||||
</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