From aa0de2fe4a5512ba51feee7788b69711617ced2a Mon Sep 17 00:00:00 2001 From: Bjoern Welker Date: Mon, 9 Feb 2026 08:28:33 +0100 Subject: [PATCH] feat: add payment tracking for orders - Add payment method selection (PayPal/Bar) to order form - Store payment_method and payment_status in database - Add payment status badges in admin orders view - Add "mark as paid" functionality for admins - PayPal account configurable via PAYPAL_ACCOUNT env variable - Frontend loads PayPal account dynamically from /wawi/config endpoint - Update email notifications to include payment method Co-Authored-By: Claude Sonnet 4.5 --- README.md | 7 ++++- index.html | 30 ++++++++++++++++++++ wawi/app.py | 57 ++++++++++++++++++++++++++++++++++++-- wawi/static/style.css | 19 +++++++++++++ wawi/templates/orders.html | 24 ++++++++++++++-- 5 files changed, 131 insertions(+), 6 deletions(-) mode change 100644 => 100755 wawi/static/style.css diff --git a/README.md b/README.md index 21b610b..e9426af 100755 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Für Bestellungen per Mail zusätzlich: Optional: - `APP_API_KEY` – Schutz fuer `/wawi/api/bestand` und `/wawi/order` - `COOKIE_SECURE` – `0` fuer http lokal, `1` fuer https +- `PAYPAL_ACCOUNT` – PayPal-E-Mail für Bestellformular (z.B. `paypal@beispiel.de`) ## Benutzerverwaltung Beim ersten Start wird **ein Admin** aus ENV erzeugt: @@ -59,10 +60,12 @@ python import_from_html.py /pfad/zu/hellas_bestand.html --truncate ## Live‑Shop‑Ansicht (index.html) `index.html` lädt den Bestand aus der WaWi‑App: - API‑Proxy: `/wawi/proxy/bestand` +- Konfiguration: `/wawi/config` (lädt PayPal-Konto aus ENV) Wenn du diese Datei auf einem Webserver auslieferst, stelle sicher, dass die WaWi‑App unter `/wawi` erreichbar ist. Wenn du Bestellungen direkt aus `index.html` abschickst, muss der `X-Order-Key` bzw. `?key=` dem `APP_API_KEY` entsprechen. -Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""` (neben der Formularlogik). Trage dort deinen Key ein. +Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""`. Trage dort deinen Key ein (oder leer lassen, wenn kein API-Key erforderlich). +Das PayPal-Konto wird automatisch aus der ENV-Variable `PAYPAL_ACCOUNT` geladen. ## Umgebungsvariablen (ENV) **Pflicht/Empfohlen für Produktion** @@ -80,6 +83,7 @@ Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""` ( **Optional** - `APP_API_KEY` – gemeinsamer API‑Key für `/wawi/api/bestand` **und** `/wawi/order` - `COOKIE_SECURE` – `1` (default) setzt Secure‑Cookie, `0` deaktiviert für http +- `PAYPAL_ACCOUNT` – PayPal-E-Mail-Adresse für Bestellungen (wird im Bestellformular angezeigt) ## Deployment (systemd + Gunicorn) 1) App nach `/var/www/hellas/wawi` kopieren @@ -112,6 +116,7 @@ Environment="SMTP_PASS=dein_pass" Environment="SMTP_FROM=bestand@example.com" Environment="ORDER_TO=admin@example.com, zweite@example.com" Environment="APP_API_KEY=api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +Environment="PAYPAL_ACCOUNT=dein-paypal@beispiel.de" ExecStart=/var/www/hellas/wawi/.venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 app:app Restart=always diff --git a/index.html b/index.html index 40dd72d..9e69334 100755 --- a/index.html +++ b/index.html @@ -428,6 +428,22 @@ +
+ Zahlungsart wählen: +
+ +
+ Bitte sende den Betrag über PayPal an: +
+ +
+
@@ -568,6 +584,20 @@ const form = document.getElementById("orderForm"); const setField = (id, v) => document.getElementById(id).value = v || ""; const ORDER_KEY = ""; +// PayPal-Konto vom Backend laden +fetch("/wawi/config") + .then(res => res.json()) + .then(config => { + if (config.paypal_account) { + document.getElementById("paypalAccount").textContent = config.paypal_account; + } else { + document.getElementById("paypalAccount").textContent = "Nicht konfiguriert"; + } + }) + .catch(() => { + document.getElementById("paypalAccount").textContent = "Fehler beim Laden"; + }); + document.addEventListener("click", (e) => { const imgBtn = e.target.closest(".thumb-btn"); if (imgBtn) { diff --git a/wawi/app.py b/wawi/app.py index 7148c51..4d09226 100755 --- a/wawi/app.py +++ b/wawi/app.py @@ -167,6 +167,7 @@ def init_db() -> None: ensure_price_column(db) ensure_image_column(db) ensure_orders_columns(db) + ensure_payment_columns(db) ensure_indexes(db) ensure_admin_user(db) @@ -208,6 +209,22 @@ def ensure_orders_columns(db: sqlite3.Connection) -> None: db.commit() +def ensure_payment_columns(db: sqlite3.Connection) -> None: + """Fügt Zahlungs-Spalten nachträglich zur orders-Tabelle hinzu.""" + cols = db.execute("PRAGMA table_info(orders)").fetchall() + names = {c["name"] for c in cols} + if "payment_method" not in names: + db.execute("ALTER TABLE orders ADD COLUMN payment_method TEXT DEFAULT 'bar'") + if "payment_status" not in names: + db.execute("ALTER TABLE orders ADD COLUMN payment_status TEXT DEFAULT 'unpaid'") + if "paid_at" not in names: + db.execute("ALTER TABLE orders ADD COLUMN paid_at TEXT") + if "paid_by" not in names: + db.execute("ALTER TABLE orders ADD COLUMN paid_by TEXT") + db.commit() + logger.info("Zahlungs-Spalten für orders überprüft/erstellt") + + def ensure_indexes(db: sqlite3.Connection) -> None: """Erstellt Indizes für bessere Query-Performance (idempotent).""" # Index für items.artikel (häufig gesucht/gefiltert) @@ -648,6 +665,14 @@ def proxy_bestand(): return jsonify(build_bestand()) +@bp.route("/config", methods=["GET"]) +def config(): + """Öffentliche Konfiguration für Frontend (z. B. PayPal-Konto).""" + return jsonify({ + "paypal_account": os.environ.get("PAYPAL_ACCOUNT", "") + }) + + def build_bestand() -> list[dict]: """Aggregiert DB‑Zeilen in die Struktur der Live‑Bestand Ansicht.""" rows = get_db().execute( @@ -724,11 +749,15 @@ def order(): if int(data.get("menge") or 0) <= 0: return jsonify({"error": "Menge muss größer als 0 sein."}), 400 + payment_method = (data.get("payment_method") or "bar").strip().lower() + if payment_method not in ("paypal", "bar"): + payment_method = "bar" + db = get_db() db.execute( """ - INSERT INTO orders (name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL) + INSERT INTO orders (name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid') """, ( data.get("name"), @@ -739,6 +768,7 @@ def order(): int(data.get("menge") or 0), data.get("notiz"), now_iso(), + payment_method, ), ) db.commit() @@ -759,6 +789,7 @@ def order(): msg["From"] = smtp_from recipients = [a.strip() for a in to_addr.split(",") if a.strip()] msg["To"] = ", ".join(recipients) + payment_label = "PayPal (Familie & Freunde)" if payment_method == "paypal" else "Bar/Abholung" body = ( "Neue Bestellung:\n" f"Name: {data.get('name')}\n" @@ -767,6 +798,7 @@ def order(): f"Artikel: {data.get('artikel')}\n" f"Größe: {data.get('groesse')}\n" f"Menge: {data.get('menge')}\n" + f"Zahlungsart: {payment_label}\n" f"Notiz: {data.get('notiz') or '-'}\n" "WaWi: https://hellas.welker.me/wawi\n" ) @@ -799,7 +831,7 @@ def orders(): """Bestellliste in der Verwaltung.""" rows = get_db().execute( """ - SELECT id, name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at + SELECT id, name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status, paid_at, paid_by FROM orders ORDER BY id DESC LIMIT 500 @@ -861,6 +893,25 @@ def complete_order(order_id: int): return redirect(url_for("bp.orders")) +@bp.route("/orders/mark_paid/", methods=["POST"]) +@login_required +def mark_paid(order_id: int): + """Bestellung als bezahlt markieren.""" + user = session.get("user") or "unknown" + db = get_db() + db.execute( + """ + UPDATE orders + SET payment_status = 'paid', paid_by = ?, paid_at = ? + WHERE id = ? AND payment_status != 'paid' + """, + (user, now_iso(), order_id), + ) + db.commit() + logger.info(f"Bestellung #{order_id} als bezahlt markiert von {user}") + return redirect(url_for("bp.orders")) + + @bp.route("/orders/cancel/", methods=["POST"]) @login_required def cancel_order(order_id: int): diff --git a/wawi/static/style.css b/wawi/static/style.css old mode 100644 new mode 100755 index b82b9b1..757872b --- a/wawi/static/style.css +++ b/wawi/static/style.css @@ -198,6 +198,25 @@ input[type="text"], input[type="number"] { } .to-top:hover { transform: translateY(-2px); } +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + white-space: nowrap; +} +.badge.success { + background: rgba(123, 213, 141, 0.2); + color: var(--ok); + border: 1px solid rgba(123, 213, 141, 0.4); +} +.badge.warning { + background: rgba(243, 213, 42, 0.2); + color: var(--accent); + border: 1px solid rgba(243, 213, 42, 0.4); +} + @media (max-width: 720px) { .actions { display: flex; gap: 6px; flex-wrap: wrap; } .btn.small { padding: 6px 8px; } diff --git a/wawi/templates/orders.html b/wawi/templates/orders.html index 3f1868b..f685da1 100755 --- a/wawi/templates/orders.html +++ b/wawi/templates/orders.html @@ -20,6 +20,8 @@ Artikel Größe Menge + Zahlungsart + Zahlung Notiz Status Aktion @@ -35,6 +37,16 @@ {{ o.artikel }} {{ o.groesse }} {{ o.menge }} + + {% if o.payment_method == "paypal" %}PayPal + {% else %}Bar + {% endif %} + + + {% if o.payment_status == "paid" %}Bezahlt + {% else %}Unbezahlt + {% endif %} + {{ o.notiz or "–" }} {% if o.canceled %}Storniert @@ -48,6 +60,12 @@ + {% if o.payment_status != "paid" %} +
+ + +
+ {% endif %}
@@ -58,12 +76,14 @@ - +
Historie
Abgeschlossen von: {{ o.completed_by or "–" }}
Abgeschlossen am: {{ o.completed_at or "–" }}
+
Bezahlt markiert von: {{ o.paid_by or "–" }}
+
Bezahlt markiert am: {{ o.paid_at or "–" }}
Storniert von: {{ o.canceled_by or "–" }}
Storniert am: {{ o.canceled_at or "–" }}
@@ -72,7 +92,7 @@ {% else %} - Keine Bestellungen. + Keine Bestellungen. {% endfor %}