Compare commits
6 Commits
12eece0226
...
fd3f49a2e1
| Author | SHA1 | Date | |
|---|---|---|---|
| fd3f49a2e1 | |||
| a40b028a0e | |||
| 450a4b062f | |||
| a8b26a25da | |||
| 630595bce9 | |||
| e062a1e836 |
81
index.html
Normal file → Executable file
81
index.html
Normal file → Executable file
@@ -313,6 +313,62 @@
|
|||||||
.order-btn { width: 100%; justify-content: center; }
|
.order-btn { width: 100%; justify-content: center; }
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* Toast Notifications */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(12, 31, 51, 0.98);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4), 0 0 1px rgba(255, 255, 255, 0.3);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
pointer-events: all;
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
}
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.toast.success {
|
||||||
|
border-left: 4px solid var(--ok);
|
||||||
|
background: linear-gradient(90deg, rgba(123, 213, 141, 0.15), rgba(12, 31, 51, 0.98));
|
||||||
|
}
|
||||||
|
.toast.error {
|
||||||
|
border-left: 4px solid var(--bad);
|
||||||
|
background: linear-gradient(90deg, rgba(255, 107, 125, 0.15), rgba(12, 31, 51, 0.98));
|
||||||
|
}
|
||||||
|
.toast.info {
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
background: linear-gradient(90deg, rgba(243, 213, 42, 0.15), rgba(12, 31, 51, 0.98));
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.toast-container {
|
||||||
|
top: auto;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 16px;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -403,7 +459,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Toast Notification System
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const container = document.getElementById('toastContainer');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation
|
||||||
|
setTimeout(() => toast.classList.add('show'), 10);
|
||||||
|
|
||||||
|
// Auto-remove after 4 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
setTimeout(() => container.removeChild(toast), 300);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
// Proxy der WaWi‑App (kein API‑Key im Browser nötig).
|
// Proxy der WaWi‑App (kein API‑Key im Browser nötig).
|
||||||
const API_URL = "/wawi/proxy/bestand";
|
const API_URL = "/wawi/proxy/bestand";
|
||||||
let DATA = [];
|
let DATA = [];
|
||||||
@@ -563,13 +640,13 @@ form.addEventListener("submit", async (e) => {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert("Bestellung gesendet.");
|
showToast("Bestellung erfolgreich gesendet! Wir melden uns bei dir.", "success");
|
||||||
modal.classList.remove("open");
|
modal.classList.remove("open");
|
||||||
document.getElementById("detailModal").classList.remove("open");
|
document.getElementById("detailModal").classList.remove("open");
|
||||||
form.reset();
|
form.reset();
|
||||||
setField("fMenge", 1);
|
setField("fMenge", 1);
|
||||||
} else {
|
} else {
|
||||||
alert("Fehler beim Senden der Bestellung.");
|
showToast("Fehler beim Senden der Bestellung. Bitte versuche es erneut.", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
5
requirements.txt
Executable file
5
requirements.txt
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
Flask>=3.0.0
|
||||||
|
Flask-WTF>=1.2.0
|
||||||
|
Werkzeug>=3.0.0
|
||||||
|
gunicorn>=21.0.0
|
||||||
|
Pillow>=10.0.0
|
||||||
139
wawi/app.py
139
wawi/app.py
@@ -14,6 +14,7 @@ import secrets
|
|||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import logging
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -21,7 +22,15 @@ from pathlib import Path
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
HAS_PIL = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PIL = False
|
||||||
|
logger.warning("Pillow nicht installiert - Bild-Optimierung deaktiviert")
|
||||||
|
|
||||||
from flask import Flask, Blueprint, g, flash, jsonify, redirect, render_template, request, session, url_for
|
from flask import Flask, Blueprint, g, flash, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@@ -30,17 +39,42 @@ DB_PATH = BASE_DIR / "hellas.db"
|
|||||||
UPLOAD_DIR = BASE_DIR / "static" / "uploads"
|
UPLOAD_DIR = BASE_DIR / "static" / "uploads"
|
||||||
ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
||||||
|
|
||||||
|
# Logging konfigurieren
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Optionaler Prefix (z. B. /wawi), wenn die App hinter einem Sub‑Pfad läuft.
|
# Optionaler Prefix (z. B. /wawi), wenn die App hinter einem Sub‑Pfad läuft.
|
||||||
URL_PREFIX = os.environ.get("URL_PREFIX", "").strip().rstrip("/")
|
URL_PREFIX = os.environ.get("URL_PREFIX", "").strip().rstrip("/")
|
||||||
STATIC_URL_PATH = f"{URL_PREFIX}/static" if URL_PREFIX else "/static"
|
STATIC_URL_PATH = f"{URL_PREFIX}/static" if URL_PREFIX else "/static"
|
||||||
|
|
||||||
app = Flask(__name__, static_url_path=STATIC_URL_PATH)
|
app = Flask(__name__, static_url_path=STATIC_URL_PATH)
|
||||||
|
|
||||||
# Session‑Secret für Login‑Cookies (in Produktion unbedingt setzen).
|
# Session‑Secret für Login‑Cookies (in Produktion unbedingt setzen).
|
||||||
app.secret_key = os.environ.get("SECRET_KEY", "change-me")
|
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me")
|
||||||
|
|
||||||
|
# Validierung: SECRET_KEY muss in Produktion gesetzt sein
|
||||||
|
if SECRET_KEY == "change-me":
|
||||||
|
import sys
|
||||||
|
if not app.debug and "pytest" not in sys.modules:
|
||||||
|
raise RuntimeError(
|
||||||
|
"SECURITY ERROR: SECRET_KEY ist nicht gesetzt!\n"
|
||||||
|
"Setze die Umgebungsvariable SECRET_KEY mit einem sicheren Wert.\n"
|
||||||
|
"Beispiel: export SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.secret_key = SECRET_KEY
|
||||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||||
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "1") == "1"
|
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "1") == "1"
|
||||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||||
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
# CSRF-Schutz aktivieren
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
bp = Blueprint("bp", __name__)
|
bp = Blueprint("bp", __name__)
|
||||||
|
|
||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -133,6 +167,7 @@ def init_db() -> None:
|
|||||||
ensure_price_column(db)
|
ensure_price_column(db)
|
||||||
ensure_image_column(db)
|
ensure_image_column(db)
|
||||||
ensure_orders_columns(db)
|
ensure_orders_columns(db)
|
||||||
|
ensure_indexes(db)
|
||||||
ensure_admin_user(db)
|
ensure_admin_user(db)
|
||||||
|
|
||||||
|
|
||||||
@@ -173,6 +208,26 @@ def ensure_orders_columns(db: sqlite3.Connection) -> None:
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_indexes(db: sqlite3.Connection) -> None:
|
||||||
|
"""Erstellt Indizes für bessere Query-Performance (idempotent)."""
|
||||||
|
# Index für items.artikel (häufig gesucht/gefiltert)
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_items_artikel ON items(artikel)")
|
||||||
|
|
||||||
|
# Index für items(artikel, groesse) für eindeutige Zuordnung
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_items_artikel_groesse ON items(artikel, groesse)")
|
||||||
|
|
||||||
|
# Index für orders.done und orders.canceled (Filter "offene Bestellungen")
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_orders_done ON orders(done)")
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_orders_canceled ON orders(canceled)")
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(done, canceled)")
|
||||||
|
|
||||||
|
# Index für ausbuchungen.item_id (Foreign Key, JOINs)
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS idx_ausbuchungen_item_id ON ausbuchungen(item_id)")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info("Datenbank-Indizes überprüft/erstellt")
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def ensure_db() -> None:
|
def ensure_db() -> None:
|
||||||
"""Stellt sicher, dass DB/Schema einmal pro Worker initialisiert ist."""
|
"""Stellt sicher, dass DB/Schema einmal pro Worker initialisiert ist."""
|
||||||
@@ -188,7 +243,7 @@ def now_iso() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def save_upload(file) -> str | None:
|
def save_upload(file) -> str | None:
|
||||||
"""Speichert einen Bild‑Upload und gibt die öffentliche URL zurück."""
|
"""Speichert einen Bild‑Upload mit automatischer Optimierung und Thumbnail-Erstellung."""
|
||||||
if not file or not getattr(file, "filename", ""):
|
if not file or not getattr(file, "filename", ""):
|
||||||
return None
|
return None
|
||||||
ext = os.path.splitext(file.filename)[1].lower()
|
ext = os.path.splitext(file.filename)[1].lower()
|
||||||
@@ -196,11 +251,61 @@ def save_upload(file) -> str | None:
|
|||||||
return None
|
return None
|
||||||
if not (file.mimetype or "").startswith("image/"):
|
if not (file.mimetype or "").startswith("image/"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
name = secure_filename(Path(file.filename).stem) or "image"
|
name = secure_filename(Path(file.filename).stem) or "image"
|
||||||
safe_name = f"{name}-{uuid4().hex}{ext}"
|
unique_id = uuid4().hex
|
||||||
dest = UPLOAD_DIR / safe_name
|
|
||||||
file.save(dest)
|
# Original speichern
|
||||||
return f"{URL_PREFIX}/static/uploads/{safe_name}" if URL_PREFIX else f"/static/uploads/{safe_name}"
|
original_name = f"{name}-{unique_id}{ext}"
|
||||||
|
original_path = UPLOAD_DIR / original_name
|
||||||
|
file.save(original_path)
|
||||||
|
|
||||||
|
# Bild optimieren (falls Pillow verfügbar)
|
||||||
|
if HAS_PIL:
|
||||||
|
try:
|
||||||
|
with Image.open(original_path) as img:
|
||||||
|
# EXIF-Orientierung korrigieren
|
||||||
|
try:
|
||||||
|
from PIL import ImageOps
|
||||||
|
img = ImageOps.exif_transpose(img)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Konvertiere RGBA zu RGB (für JPEG)
|
||||||
|
if img.mode in ('RGBA', 'LA', 'P'):
|
||||||
|
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||||
|
if img.mode == 'P':
|
||||||
|
img = img.convert('RGBA')
|
||||||
|
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
||||||
|
img = background
|
||||||
|
|
||||||
|
# Optimiertes Hauptbild (max 800x800)
|
||||||
|
optimized_name = f"{name}-{unique_id}-opt.jpg"
|
||||||
|
optimized_path = UPLOAD_DIR / optimized_name
|
||||||
|
img_copy = img.copy()
|
||||||
|
img_copy.thumbnail((800, 800), Image.Resampling.LANCZOS)
|
||||||
|
img_copy.save(optimized_path, 'JPEG', quality=85, optimize=True)
|
||||||
|
|
||||||
|
# Thumbnail (max 400x400)
|
||||||
|
thumb_name = f"{name}-{unique_id}-thumb.jpg"
|
||||||
|
thumb_path = UPLOAD_DIR / thumb_name
|
||||||
|
img.thumbnail((400, 400), Image.Resampling.LANCZOS)
|
||||||
|
img.save(thumb_path, 'JPEG', quality=80, optimize=True)
|
||||||
|
|
||||||
|
# Original löschen (verwenden nur optimierte Versionen)
|
||||||
|
original_path.unlink()
|
||||||
|
|
||||||
|
logger.info(f"Bild optimiert: {optimized_name} & {thumb_name}")
|
||||||
|
|
||||||
|
# Thumbnail-URL zurückgeben (wird als Standard verwendet)
|
||||||
|
return f"{URL_PREFIX}/static/uploads/{thumb_name}" if URL_PREFIX else f"/static/uploads/{thumb_name}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Bild-Optimierung: {e}")
|
||||||
|
# Fallback: Original verwenden
|
||||||
|
|
||||||
|
# Fallback wenn Pillow fehlt oder Fehler aufgetreten ist
|
||||||
|
return f"{URL_PREFIX}/static/uploads/{original_name}" if URL_PREFIX else f"/static/uploads/{original_name}"
|
||||||
|
|
||||||
|
|
||||||
def ensure_admin_user(db: sqlite3.Connection) -> None:
|
def ensure_admin_user(db: sqlite3.Connection) -> None:
|
||||||
@@ -472,8 +577,10 @@ def login():
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
if row and check_password_hash(row["password_hash"], password):
|
if row and check_password_hash(row["password_hash"], password):
|
||||||
session["user"] = user
|
session["user"] = user
|
||||||
|
logger.info(f"Erfolgreicher Login: {user}")
|
||||||
nxt = request.args.get("next") or url_for("bp.index")
|
nxt = request.args.get("next") or url_for("bp.index")
|
||||||
return redirect(nxt)
|
return redirect(nxt)
|
||||||
|
logger.warning(f"Fehlgeschlagener Login-Versuch: {user}")
|
||||||
return render_template("login.html", error=True)
|
return render_template("login.html", error=True)
|
||||||
return render_template("login.html", error=False)
|
return render_template("login.html", error=False)
|
||||||
|
|
||||||
@@ -597,6 +704,7 @@ def build_bestand() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/order", methods=["POST"])
|
@bp.route("/order", methods=["POST"])
|
||||||
|
@csrf.exempt # JSON API ohne CSRF-Schutz (nutzt API-Key stattdessen)
|
||||||
def order():
|
def order():
|
||||||
"""Erstellt eine Bestellung (optional API‑Key) und versendet Mail."""
|
"""Erstellt eine Bestellung (optional API‑Key) und versendet Mail."""
|
||||||
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "unknown").split(",")[0].strip()
|
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "unknown").split(",")[0].strip()
|
||||||
@@ -634,6 +742,7 @@ def order():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
logger.info(f"Neue Bestellung: {data.get('artikel')} ({data.get('groesse')}) x{data.get('menge')} von {data.get('name')}")
|
||||||
|
|
||||||
to_addr = os.environ.get("ORDER_TO", "bjoern@welker.me")
|
to_addr = os.environ.get("ORDER_TO", "bjoern@welker.me")
|
||||||
smtp_host = os.environ.get("SMTP_HOST")
|
smtp_host = os.environ.get("SMTP_HOST")
|
||||||
@@ -664,10 +773,19 @@ def order():
|
|||||||
msg.set_content(body)
|
msg.set_content(body)
|
||||||
|
|
||||||
def _send():
|
def _send():
|
||||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
"""Sendet Bestellungs-Email asynchron mit Error Handling."""
|
||||||
server.starttls()
|
try:
|
||||||
server.login(smtp_user, smtp_pass)
|
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||||
server.send_message(msg, to_addrs=recipients)
|
server.starttls()
|
||||||
|
server.login(smtp_user, smtp_pass)
|
||||||
|
server.send_message(msg, to_addrs=recipients)
|
||||||
|
logger.info(f"Bestellungs-Email erfolgreich versendet an {', '.join(recipients)}")
|
||||||
|
except smtplib.SMTPAuthenticationError as e:
|
||||||
|
logger.error(f"SMTP-Authentifizierung fehlgeschlagen: {e}")
|
||||||
|
except smtplib.SMTPException as e:
|
||||||
|
logger.error(f"SMTP-Fehler beim Email-Versand: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unerwarteter Fehler beim Email-Versand: {e}", exc_info=True)
|
||||||
|
|
||||||
threading.Thread(target=_send, daemon=True).start()
|
threading.Thread(target=_send, daemon=True).start()
|
||||||
|
|
||||||
@@ -739,6 +857,7 @@ def complete_order(order_id: int):
|
|||||||
(user, now_iso(), order_id),
|
(user, now_iso(), order_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
logger.info(f"Bestellung #{order_id} abgeschlossen von {user}")
|
||||||
return redirect(url_for("bp.orders"))
|
return redirect(url_for("bp.orders"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
wawi/templates/ausbuchen.html
Normal file → Executable file
1
wawi/templates/ausbuchen.html
Normal file → Executable file
@@ -4,6 +4,7 @@
|
|||||||
<h2>Ausbuchen: {{ item.artikel }} ({{ item.groesse }})</h2>
|
<h2>Ausbuchen: {{ item.artikel }} ({{ item.groesse }})</h2>
|
||||||
<div class="note">Aktueller Bestand: <strong>{{ item.gezaehlt }}</strong></div>
|
<div class="note">Aktueller Bestand: <strong>{{ item.gezaehlt }}</strong></div>
|
||||||
<form method="post" onsubmit="return confirm('Wirklich ausbuchen?');">
|
<form method="post" onsubmit="return confirm('Wirklich ausbuchen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Menge
|
Menge
|
||||||
|
|||||||
1
wawi/templates/edit.html
Normal file → Executable file
1
wawi/templates/edit.html
Normal file → Executable file
@@ -3,6 +3,7 @@
|
|||||||
<div class="card form-card">
|
<div class="card form-card">
|
||||||
<h2>{{ "Artikel bearbeiten" if item else "Neuen Artikel anlegen" }}</h2>
|
<h2>{{ "Artikel bearbeiten" if item else "Neuen Artikel anlegen" }}</h2>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Artikel
|
Artikel
|
||||||
|
|||||||
2
wawi/templates/index.html
Normal file → Executable file
2
wawi/templates/index.html
Normal file → Executable file
@@ -67,10 +67,12 @@
|
|||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a class="btn icon" href="{{ url_for('bp.edit_item', item_id=r.id) }}" title="Bearbeiten" aria-label="Bearbeiten"><span>✎</span></a>
|
<a class="btn icon" href="{{ url_for('bp.edit_item', item_id=r.id) }}" title="Bearbeiten" aria-label="Bearbeiten"><span>✎</span></a>
|
||||||
<form method="post" action="{{ url_for('bp.verkauf', item_id=r.id) }}" onsubmit="return confirm('Wirklich 1 Stück als verkauft buchen?');">
|
<form method="post" action="{{ url_for('bp.verkauf', item_id=r.id) }}" onsubmit="return confirm('Wirklich 1 Stück als verkauft buchen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn icon" type="submit" title="Verkauf +1" aria-label="Verkauf +1"><span>🛒</span></button>
|
<button class="btn icon" type="submit" title="Verkauf +1" aria-label="Verkauf +1"><span>🛒</span></button>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn icon" href="{{ url_for('bp.ausbuchen', item_id=r.id) }}" title="Ausbuchen" aria-label="Ausbuchen"><span>⇩</span></a>
|
<a class="btn icon" href="{{ url_for('bp.ausbuchen', item_id=r.id) }}" title="Ausbuchen" aria-label="Ausbuchen"><span>⇩</span></a>
|
||||||
<form method="post" action="{{ url_for('bp.delete_item', item_id=r.id) }}" onsubmit="return confirm('Wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.');">
|
<form method="post" action="{{ url_for('bp.delete_item', item_id=r.id) }}" onsubmit="return confirm('Wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn icon danger" type="submit" title="Löschen" aria-label="Löschen"><span>🗑</span></button>
|
<button class="btn icon danger" type="submit" title="Löschen" aria-label="Löschen"><span>🗑</span></button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
1
wawi/templates/login.html
Normal file → Executable file
1
wawi/templates/login.html
Normal file → Executable file
@@ -6,6 +6,7 @@
|
|||||||
<div class="note">Benutzername oder Passwort ist falsch.</div>
|
<div class="note">Benutzername oder Passwort ist falsch.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Benutzer
|
Benutzer
|
||||||
|
|||||||
2
wawi/templates/orders.html
Normal file → Executable file
2
wawi/templates/orders.html
Normal file → Executable file
@@ -45,9 +45,11 @@
|
|||||||
<td class="actions">
|
<td class="actions">
|
||||||
{% if not o.done and not o.canceled %}
|
{% if not o.done and not o.canceled %}
|
||||||
<form method="post" action="{{ url_for('bp.complete_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung als erledigt markieren?');">
|
<form method="post" action="{{ url_for('bp.complete_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung als erledigt markieren?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small" type="submit">Erledigt</button>
|
<button class="btn small" type="submit">Erledigt</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small danger" type="submit">Stornieren</button>
|
<button class="btn small danger" type="submit">Stornieren</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
3
wawi/templates/users.html
Normal file → Executable file
3
wawi/templates/users.html
Normal file → Executable file
@@ -15,6 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Benutzername
|
Benutzername
|
||||||
@@ -47,9 +48,11 @@
|
|||||||
<td>{{ u.created_at }}</td>
|
<td>{{ u.created_at }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<form method="post" action="{{ url_for('bp.reset_user_password', user_id=u.id) }}" onsubmit="return confirm('Passwort für diesen Benutzer wirklich zurücksetzen?');">
|
<form method="post" action="{{ url_for('bp.reset_user_password', user_id=u.id) }}" onsubmit="return confirm('Passwort für diesen Benutzer wirklich zurücksetzen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small" type="submit">Passwort neu</button>
|
<button class="btn small" type="submit">Passwort neu</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="{{ url_for('bp.delete_user', user_id=u.id) }}" onsubmit="return confirm('Benutzer wirklich löschen?');">
|
<form method="post" action="{{ url_for('bp.delete_user', user_id=u.id) }}" onsubmit="return confirm('Benutzer wirklich löschen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small danger" type="submit">Löschen</button>
|
<button class="btn small danger" type="submit">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user