From 81a1ed7eef3949de447cf7a3e8234373baabaa30 Mon Sep 17 00:00:00 2001 From: Bjoern Welker Date: Fri, 30 Jan 2026 08:55:14 +0100 Subject: [PATCH] Initial commit --- .DS_Store | Bin 0 -> 10244 bytes README.md | 56 +++ hellas_bestand Kopie.html | 397 +++++++++++++++++++ hellas_bestand.html | 397 +++++++++++++++++++ index.html | 621 ++++++++++++++++++++++++++++++ logo.png | Bin 0 -> 28328 bytes wawi/app.py | 699 ++++++++++++++++++++++++++++++++++ wawi/import_from_html.py | 88 +++++ wawi/static/logo.png | Bin 0 -> 28328 bytes wawi/static/style.css | 204 ++++++++++ wawi/templates/ausbuchen.html | 23 ++ wawi/templates/base.html | 37 ++ wawi/templates/edit.html | 46 +++ wawi/templates/index.html | 88 +++++ wawi/templates/login.html | 24 ++ wawi/templates/orders.html | 79 ++++ wawi/templates/users.html | 65 ++++ 17 files changed, 2824 insertions(+) create mode 100644 .DS_Store create mode 100644 README.md create mode 100644 hellas_bestand Kopie.html create mode 100644 hellas_bestand.html create mode 100644 index.html create mode 100644 logo.png create mode 100644 wawi/app.py create mode 100644 wawi/import_from_html.py create mode 100644 wawi/static/logo.png create mode 100644 wawi/static/style.css create mode 100644 wawi/templates/ausbuchen.html create mode 100644 wawi/templates/base.html create mode 100644 wawi/templates/edit.html create mode 100644 wawi/templates/index.html create mode 100644 wawi/templates/login.html create mode 100644 wawi/templates/orders.html create mode 100644 wawi/templates/users.html diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c4b411b4405dbaa1c46f2057c8143106b9bdc60b GIT binary patch literal 10244 zcmeHMTWl0n82fupg<{f3%zhzXLqLUgy~F~ncY$d zTAwrmCitN70=^OP0k5d&i-|W()CYn@i3W^`Pb6L<_+Y&JXU^=hEz~y>66Z8?{_~&n z|L5|Z??1C=3ILeMYPA4S01)Y6Ql3EBZ4zr|&n-y^Y9b_&`~g&OAq8pBNrovzAVeTU zAVeTUAVlD9M1bDeEQ##>9i|Y05P=YZVg%Uv5T%F7cp!(k^p6h8{3`&-QmXb7mFXVf z`$Pj759AP+zAN>~Zx0x_VsOPk?oRDdW==95$RRGdJ0N!l3`WM_hJya+TcA7b)<_BdKE3G!W3)XR7plg&Vf1EY149I-LN!! zAYo@M&Gmhgyg`&iDJ54PI#gG?q^>$zyS(mjb@b566;xhH<-?LVCAN5VYigg_XW2(+ z@{}+*U11{E3;9th6&7S0V|%lv|_$Z~#3()h@PiT6yB%gQI0Rg_JcGG*$t>6J6? ztxU<2(`sg4kHe-~Z=1H0Fthru9>>s<{c6UrdOAG)fR{85>M6N=glVg;?Y&vmHrP)# z6*nvV?lw5OZu$MSba%VD*HKjNOv#hnjANVTHrq8k!?rqiy99KTr}s5k-8PMwBamDA zS(Cd{cU+RU_;lNZDbUB=e^!eNZX zH|GoUc|nF4FVK(hihL=M82iwS`(>%kvMoIp-^N=cdQ&bW#x}!F*arvUIGljf@By5M z@8M_o3&)|1vvDCV#WlDNH)0cZVFooc(BT~j`At^1p3jkevny<}>E`M=YfF;(O}>l) zYiWxk6P5Ok&aPd%Q?i_O^;X?fJtL<#sGjaJ+!(euL1@p|X133wma>d$X0l|guQyGG zm~TCNXNq_*V$(XhT-)(zx2dxt%x|-Il62;tX;t^nf3R-F!|}$Z=Is|5BO?`&sqHz# zHPWWOW5Ce7-cHrc=$1z8>!P@^Jfqvt9jOef33jzlzgW(=J5y0cTIYM|$fk|`S;HHY zW1`5%n5D!d`K-8GqeV8`8tYN>Ta;PKJW<@m>gyeya$X%-pj1n;Dz>=<2k&BWq*jt$ zscjzxH+NkcSuV<&w8JMLuUZvZEy|mv_8ey`udIly6Qz!f-PcEpad}-rX%OY};$AlJ z7S;7y`gJQmrG&Ck6uaDB)zMo9=xE7%Y!O9&8>ybTG-`%tJ3bm)XDLc2>uSBG=^8D1 z#`MZ^C8_KXa2;;IpID0Hu>xn}9Gr*q@d2#ECAbWiV?A!c zX51QtLO1rHiCN6y0UX3f@dp-V^!y?jB1P@@-QaS{eeD-N~b zJQdE|KGdoqdblUu6U7I&zc>nlE+ZUaCPoHQpZxbaCKpIRh2AIlybGEwkjf* zPz*}G@3LxAF;G@L6p6^C6pg%XtwJgUN}`^+VX-CNDVBc@7vLMX3fG9`e`5)XI1QsX z7psZYYcWpDZoo!j_BL$AB(~umOreTtI`U1lupjrMgNN`iK88d1II;XF9>b^cIeea& z{t|Kh6uyF|@l|{a&*HoI9-i|#{{?=Hm+?o=*#e83i#bfc7Je^gaW3gtrfv05UH%0# z+&t7m1VRKt1VRMv5(G;4JfiFyK;!8D|G!IS8%`rcAVlC!Ab_O}$p-fMYP8>N0<}fy zd4wL8nB5SUz6)i{gw^|><4Iwk + + + + + Hellas Bestand – Übersicht + + + +
+
+
+
+ Hellas 1899 Logo +
+

Vereinskleidung – Übersicht

+
Erstellt: 2026-01-28 11:25 · Quelle: Hellas_Bestand_neu.xlsx
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+ +
+
+ + + + diff --git a/hellas_bestand.html b/hellas_bestand.html new file mode 100644 index 0000000..c3ac80d --- /dev/null +++ b/hellas_bestand.html @@ -0,0 +1,397 @@ + + + + + + Hellas Bestand – Übersicht + + + +
+
+
+
+ Hellas 1899 Logo +
+

Vereinskleidung – Übersicht

+
Erstellt: 2026-01-28 11:25 · Quelle: Hellas_Bestand_neu.xlsx
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+ +
+
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..101f711 --- /dev/null +++ b/index.html @@ -0,0 +1,621 @@ + + + + + + Hellas – Shop + + + +
+
+
+
+ Hellas 1899 Logo +
+

Shop

+
Stand: –
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..26241ed6018c744cb23545cdcf6c5d473507536c GIT binary patch literal 28328 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 sqlite3.Connection: + if "db" not in g: + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + g.db = conn + return g.db + + +@app.teardown_appcontext +def close_db(exc: Exception | None) -> None: + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_db() -> None: + db = get_db() + db.executescript( + """ + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + artikel TEXT NOT NULL, + groesse TEXT NOT NULL, + preis REAL NOT NULL DEFAULT 0, + bild_url TEXT, + soll INTEGER NOT NULL DEFAULT 0, + gezaehlt INTEGER NOT NULL DEFAULT 0, + verkaeufe INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS ausbuchungen ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item_id INTEGER NOT NULL, + menge INTEGER NOT NULL, + grund TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY(item_id) REFERENCES items(id) + ); + + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + handy TEXT NOT NULL, + mannschaft TEXT NOT NULL, + artikel TEXT NOT NULL, + groesse TEXT NOT NULL, + menge INTEGER NOT NULL, + notiz TEXT, + created_at TEXT NOT NULL, + done INTEGER NOT NULL DEFAULT 0, + completed_by TEXT, + completed_at TEXT, + canceled INTEGER NOT NULL DEFAULT 0, + canceled_by TEXT, + canceled_at TEXT + ); + """ + ) + db.commit() + ensure_price_column(db) + ensure_image_column(db) + ensure_orders_columns(db) + ensure_admin_user(db) + + +def ensure_price_column(db: sqlite3.Connection) -> None: + # Fügt die Preisspalte nachträglich hinzu, falls DB älter ist. + cols = db.execute("PRAGMA table_info(items)").fetchall() + if any(c["name"] == "preis" for c in cols): + return + db.execute("ALTER TABLE items ADD COLUMN preis REAL NOT NULL DEFAULT 0") + db.commit() + + +def ensure_image_column(db: sqlite3.Connection) -> None: + cols = db.execute("PRAGMA table_info(items)").fetchall() + if any(c["name"] == "bild_url" for c in cols): + return + db.execute("ALTER TABLE items ADD COLUMN bild_url TEXT") + db.commit() + + +def ensure_orders_columns(db: sqlite3.Connection) -> None: + cols = db.execute("PRAGMA table_info(orders)").fetchall() + names = {c["name"] for c in cols} + if "done" not in names: + db.execute("ALTER TABLE orders ADD COLUMN done INTEGER NOT NULL DEFAULT 0") + if "completed_by" not in names: + db.execute("ALTER TABLE orders ADD COLUMN completed_by TEXT") + if "completed_at" not in names: + db.execute("ALTER TABLE orders ADD COLUMN completed_at TEXT") + if "canceled" not in names: + db.execute("ALTER TABLE orders ADD COLUMN canceled INTEGER NOT NULL DEFAULT 0") + if "canceled_by" not in names: + db.execute("ALTER TABLE orders ADD COLUMN canceled_by TEXT") + if "canceled_at" not in names: + db.execute("ALTER TABLE orders ADD COLUMN canceled_at TEXT") + db.commit() + + +@app.before_request +def ensure_db() -> None: + # Erstellt Tabellen, wenn sie fehlen (bei jedem Request idempotent). + init_db() + + +def now_iso() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M") + + +def save_upload(file) -> str | None: + if not file or not getattr(file, "filename", ""): + return None + ext = os.path.splitext(file.filename)[1].lower() + if ext not in ALLOWED_EXT: + return None + name = secure_filename(Path(file.filename).stem) or "image" + safe_name = f"{name}-{uuid4().hex}{ext}" + dest = UPLOAD_DIR / safe_name + file.save(dest) + return f"{URL_PREFIX}/static/uploads/{safe_name}" if URL_PREFIX else f"/static/uploads/{safe_name}" + + +def ensure_admin_user(db: sqlite3.Connection) -> None: + # Legt einen Admin‑User an, wenn noch kein Benutzer existiert. + row = db.execute("SELECT COUNT(*) AS c FROM users").fetchone() + if row and row["c"] > 0: + return + user = os.environ.get("APP_USER", "admin") + password = os.environ.get("APP_PASSWORD", "admin") + db.execute( + """ + INSERT INTO users (username, password_hash, created_at) + VALUES (?, ?, ?) + """, + (user, generate_password_hash(password), now_iso()), + ) + db.commit() + + +def login_required(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if not session.get("user"): + return redirect(url_for("bp.login", next=request.path)) + return fn(*args, **kwargs) + + return wrapper + + +@app.context_processor +def inject_auth(): + return {"logged_in": bool(session.get("user"))} + + +def api_key_required(fn): + # Schützt API‑Endpoints per X-API-Key oder ?key= Parameter. + @wraps(fn) + def wrapper(*args, **kwargs): + expected = os.environ.get("APP_API_KEY", "") + if not expected: + return jsonify({"error": "API key not configured"}), 500 + provided = request.headers.get("X-API-Key") or request.args.get("key") or "" + if provided != expected: + return jsonify({"error": "Unauthorized"}), 401 + return fn(*args, **kwargs) + + return wrapper + + +@bp.route("/") +@login_required +def index(): + q = (request.args.get("q") or "").strip() + sort = (request.args.get("sort") or "gezaehlt").strip().lower() + direction = (request.args.get("dir") or "desc").strip().lower() + + allowed = {"artikel", "groesse", "soll", "gezaehlt", "verkaeufe"} + if sort not in allowed: + sort = "gezaehlt" + if direction not in {"asc", "desc"}: + direction = "desc" + + params: list[Any] = [] + where = "" + if q: + where = "WHERE artikel LIKE ? OR groesse LIKE ?" + like = f"%{q}%" + params.extend([like, like]) + + sql = f""" + SELECT * + FROM items + {where} + ORDER BY {sort} {direction}, artikel ASC, groesse ASC + """ + rows = get_db().execute(sql, params).fetchall() + + grouped = {} + for r in rows: + key = r["artikel"] or "" + grouped.setdefault(key, []).append(r) + groups = [{"artikel": k, "rows": v} for k, v in grouped.items()] + + total = get_db().execute("SELECT COUNT(*) AS c FROM items").fetchone()["c"] + total_bestand = get_db().execute("SELECT COALESCE(SUM(gezaehlt), 0) AS s FROM items").fetchone()["s"] + open_orders = get_db().execute("SELECT COUNT(*) AS c FROM orders WHERE done = 0 AND canceled = 0").fetchone()["c"] + return render_template( + "index.html", + groups=groups, + q=q, + sort=sort, + direction=direction, + total=total, + total_bestand=total_bestand, + open_orders=open_orders, + ) + + +@bp.route("/new", methods=["GET", "POST"]) +@login_required +def new_item(): + if request.method == "POST": + artikel = (request.form.get("artikel") or "").strip() + groesse = (request.form.get("groesse") or "").strip() + preis = float(request.form.get("preis") or 0) + bild_url = (request.form.get("bild_url") or "").strip() + uploaded = save_upload(request.files.get("bild_file")) + if uploaded: + bild_url = uploaded + soll = int(request.form.get("soll") or 0) + gezaehlt = int(request.form.get("gezaehlt") or 0) + verkaeufe = int(request.form.get("verkaeufe") or 0) + + if artikel and groesse: + db = get_db() + db.execute( + """ + INSERT INTO items (artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, now_iso(), now_iso()), + ) + db.execute("UPDATE items SET preis = ?, bild_url = ? WHERE artikel = ?", (preis, bild_url, artikel)) + db.commit() + return redirect(url_for("bp.index")) + + return render_template("edit.html", item=None) + + +@bp.route("/edit/", methods=["GET", "POST"]) +@login_required +def edit_item(item_id: int): + db = get_db() + item = db.execute("SELECT * FROM items WHERE id = ?", (item_id,)).fetchone() + if item is None: + return redirect(url_for("bp.index")) + + if request.method == "POST": + artikel = (request.form.get("artikel") or "").strip() + groesse = (request.form.get("groesse") or "").strip() + preis = float(request.form.get("preis") or 0) + bild_url = (request.form.get("bild_url") or "").strip() + uploaded = save_upload(request.files.get("bild_file")) + if uploaded: + bild_url = uploaded + soll = int(request.form.get("soll") or 0) + gezaehlt = int(request.form.get("gezaehlt") or 0) + verkaeufe = int(request.form.get("verkaeufe") or 0) + + db.execute( + """ + UPDATE items + SET artikel = ?, groesse = ?, preis = ?, bild_url = ?, soll = ?, gezaehlt = ?, verkaeufe = ?, updated_at = ? + WHERE id = ? + """, + (artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, now_iso(), item_id), + ) + db.execute("UPDATE items SET preis = ?, bild_url = ? WHERE artikel = ?", (preis, bild_url, artikel)) + db.commit() + return redirect(url_for("bp.index")) + + return render_template("edit.html", item=item) + + +@bp.route("/delete/", methods=["POST"]) +@login_required +def delete_item(item_id: int): + db = get_db() + db.execute("DELETE FROM items WHERE id = ?", (item_id,)) + db.commit() + return redirect(url_for("bp.index")) + + +@bp.route("/ausbuchen/", methods=["GET", "POST"]) +@login_required +def ausbuchen(item_id: int): + db = get_db() + item = db.execute("SELECT * FROM items WHERE id = ?", (item_id,)).fetchone() + if item is None: + return redirect(url_for("bp.index")) + + if request.method == "POST": + menge = int(request.form.get("menge") or 0) + grund = (request.form.get("grund") or "").strip() or None + if menge > 0: + neue_gezaehlt = max(int(item["gezaehlt"]) - menge, 0) + neue_verkaeufe = int(item["verkaeufe"]) + menge + db.execute( + """ + UPDATE items + SET gezaehlt = ?, verkaeufe = ?, updated_at = ? + WHERE id = ? + """, + (neue_gezaehlt, neue_verkaeufe, now_iso(), item_id), + ) + db.execute( + """ + INSERT INTO ausbuchungen (item_id, menge, grund, created_at) + VALUES (?, ?, ?, ?) + """, + (item_id, menge, grund, now_iso()), + ) + db.commit() + return redirect(url_for("bp.index")) + + return render_template("ausbuchen.html", item=item) + + +@bp.route("/verkauf/", methods=["POST"]) +@login_required +def verkauf(item_id: int): + db = get_db() + item = db.execute("SELECT * FROM items WHERE id = ?", (item_id,)).fetchone() + if item is None: + return redirect(url_for("bp.index")) + + menge = 1 + neue_gezaehlt = max(int(item["gezaehlt"]) - menge, 0) + neue_verkaeufe = int(item["verkaeufe"]) + menge + db.execute( + """ + UPDATE items + SET gezaehlt = ?, verkaeufe = ?, updated_at = ? + WHERE id = ? + """, + (neue_gezaehlt, neue_verkaeufe, now_iso(), item_id), + ) + db.execute( + """ + INSERT INTO ausbuchungen (item_id, menge, grund, created_at) + VALUES (?, ?, ?, ?) + """, + (item_id, menge, "Verkauf", now_iso()), + ) + db.commit() + return redirect(url_for("bp.index")) + + +@bp.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + user = (request.form.get("user") or "").strip() + password = request.form.get("password") or "" + row = get_db().execute( + "SELECT id, username, password_hash FROM users WHERE username = ?", + (user,), + ).fetchone() + if row and check_password_hash(row["password_hash"], password): + session["user"] = user + nxt = request.args.get("next") or url_for("bp.index") + return redirect(nxt) + return render_template("login.html", error=True) + return render_template("login.html", error=False) + + +@bp.route("/logout") +def logout(): + session.clear() + return redirect(url_for("bp.login")) + + +@bp.route("/users", methods=["GET", "POST"]) +@login_required +def users(): + db = get_db() + error = None + if request.method == "POST": + username = (request.form.get("username") or "").strip() + password = request.form.get("password") or "" + if not username or not password: + error = "Benutzer und Passwort sind erforderlich." + else: + try: + db.execute( + """ + INSERT INTO users (username, password_hash, created_at) + VALUES (?, ?, ?) + """, + (username, generate_password_hash(password), now_iso()), + ) + db.commit() + except sqlite3.IntegrityError: + error = "Benutzername existiert bereits." + + rows = db.execute("SELECT id, username, created_at FROM users ORDER BY username").fetchall() + return render_template("users.html", rows=rows, error=error) + + +@bp.route("/users/delete/", methods=["POST"]) +@login_required +def delete_user(user_id: int): + db = get_db() + count = db.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] + if count <= 1: + return redirect(url_for("bp.users")) + db.execute("DELETE FROM users WHERE id = ?", (user_id,)) + db.commit() + return redirect(url_for("bp.users")) + + +@bp.route("/api/bestand", methods=["GET"]) +@api_key_required +def api_bestand(): + return jsonify(build_bestand()) + + +@bp.route("/proxy/bestand", methods=["GET"]) +def proxy_bestand(): + # Server‑Proxy ohne API‑Key (z. B. für öffentliche Anzeige). + return jsonify(build_bestand()) + + +def build_bestand() -> list[dict]: + # Aggregiert DB‑Zeilen in die Struktur der Live‑Bestand Ansicht. + rows = get_db().execute( + """ + SELECT artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe + FROM items + ORDER BY artikel, groesse + """ + ).fetchall() + + data: dict[str, dict] = {} + for r in rows: + artikel = (r["artikel"] or "").strip() + if not artikel: + continue + item = data.setdefault( + artikel, + {"artikel": artikel, "preis": 0, "bild_url": "", "rows": [], "totals": {"soll": 0, "gezaehlt": 0, "abweichung": 0, "fehlbestand": 0, "verkaeufe": 0}}, + ) + if not item["preis"]: + item["preis"] = float(r["preis"] or 0) + if not item["bild_url"]: + item["bild_url"] = (r["bild_url"] or "").strip() + soll = int(r["soll"] or 0) + gezaehlt = int(r["gezaehlt"] or 0) + verkaeufe = int(r["verkaeufe"] or 0) + abw = gezaehlt - soll + fehl = max(soll - gezaehlt, 0) + item["rows"].append( + { + "groesse": r["groesse"], + "soll": soll, + "gezaehlt": gezaehlt, + "abweichung": abw, + "fehlbestand": fehl if fehl > 0 else None, + "verkaeufe": verkaeufe, + } + ) + t = item["totals"] + t["soll"] += soll + t["gezaehlt"] += gezaehlt + t["abweichung"] += abw + t["fehlbestand"] += fehl + t["verkaeufe"] += verkaeufe + + result: list[dict] = [] + for artikel, item in data.items(): + t = item["totals"] + if t["fehlbestand"] == 0: + t["fehlbestand"] = None + result.append(item) + + return result + + +@bp.route("/order", methods=["POST"]) +def order(): + data = request.get_json(silent=True) or request.form + required = ["name", "handy", "mannschaft", "artikel", "groesse", "menge"] + if any(not (data.get(k) or "").strip() for k in required): + return jsonify({"error": "Pflichtfelder fehlen."}), 400 + + 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) + """, + ( + data.get("name"), + data.get("handy"), + data.get("mannschaft"), + data.get("artikel"), + data.get("groesse"), + int(data.get("menge") or 0), + data.get("notiz"), + now_iso(), + ), + ) + db.commit() + + to_addr = os.environ.get("ORDER_TO", "bjoern@welker.me") + smtp_host = os.environ.get("SMTP_HOST") + smtp_user = os.environ.get("SMTP_USER") + smtp_pass = os.environ.get("SMTP_PASS") + smtp_port = int(os.environ.get("SMTP_PORT", "587")) + smtp_from = os.environ.get("SMTP_FROM", smtp_user or "no-reply@localhost") + + if not smtp_host or not smtp_user or not smtp_pass: + return jsonify({"error": "Mailversand nicht konfiguriert."}), 500 + + msg = EmailMessage() + msg["Subject"] = "Neue Bestellung (Hellas Bestand)" + msg["From"] = smtp_from + recipients = [a.strip() for a in to_addr.split(",") if a.strip()] + msg["To"] = ", ".join(recipients) + body = ( + "Neue Bestellung:\n" + f"Name: {data.get('name')}\n" + f"Handy: {data.get('handy')}\n" + f"Mannschaft: {data.get('mannschaft')}\n" + f"Artikel: {data.get('artikel')}\n" + f"Größe: {data.get('groesse')}\n" + f"Menge: {data.get('menge')}\n" + f"Notiz: {data.get('notiz') or '-'}\n" + "WaWi: https://hellas.welker.me/wawi\n" + ) + msg.set_content(body) + + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.send_message(msg, to_addrs=recipients) + + return jsonify({"ok": True}) + + +@bp.route("/orders") +@login_required +def orders(): + 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 + FROM orders + ORDER BY id DESC + LIMIT 500 + """ + ).fetchall() + return render_template("orders.html", rows=rows) + + +@bp.route("/orders/complete/", methods=["POST"]) +@login_required +def complete_order(order_id: int): + user = session.get("user") or "unknown" + db = get_db() + order = db.execute( + """ + SELECT id, artikel, groesse, menge, done, canceled + FROM orders + WHERE id = ? + """, + (order_id,), + ).fetchone() + if order is None: + return redirect(url_for("bp.orders")) + if not order["done"] and not order["canceled"]: + menge = int(order["menge"] or 0) + item = db.execute( + """ + SELECT gezaehlt + FROM items + WHERE artikel = ? AND groesse = ? + """, + (order["artikel"], order["groesse"]), + ).fetchone() + current = int(item["gezaehlt"] or 0) if item else 0 + if current < menge: + flash("Bestand reicht nicht aus, um die Bestellung abzuschließen.") + return redirect(url_for("bp.orders")) + db.execute( + """ + UPDATE items + SET gezaehlt = CASE WHEN gezaehlt - ? < 0 THEN 0 ELSE gezaehlt - ? END, + verkaeufe = verkaeufe + ?, + updated_at = ? + WHERE artikel = ? AND groesse = ? + """, + (menge, menge, menge, now_iso(), order["artikel"], order["groesse"]), + ) + db.execute( + """ + UPDATE orders + SET done = 1, completed_by = ?, completed_at = ? + WHERE id = ? + """, + (user, now_iso(), order_id), + ) + db.commit() + return redirect(url_for("bp.orders")) + + +@bp.route("/orders/cancel/", methods=["POST"]) +@login_required +def cancel_order(order_id: int): + user = session.get("user") or "unknown" + db = get_db() + db.execute( + """ + UPDATE orders + SET canceled = 1, canceled_by = ?, canceled_at = ? + WHERE id = ? AND done = 0 + """, + (user, now_iso(), order_id), + ) + db.commit() + return redirect(url_for("bp.orders")) + + +@bp.route("/users/reset/", methods=["POST"]) +@login_required +def reset_user_password(user_id: int): + db = get_db() + user = db.execute("SELECT id, username FROM users WHERE id = ?", (user_id,)).fetchone() + if user is None: + return redirect(url_for("bp.users")) + new_password = secrets.token_urlsafe(8).replace("-", "").replace("_", "")[:12] + db.execute( + "UPDATE users SET password_hash = ? WHERE id = ?", + (generate_password_hash(new_password), user_id), + ) + db.commit() + flash(f"Neues Passwort für {user['username']}: {new_password}") + return redirect(url_for("bp.users")) + + +app.register_blueprint(bp, url_prefix=URL_PREFIX or "") + +if __name__ == "__main__": + app.run(debug=True) diff --git a/wawi/import_from_html.py b/wawi/import_from_html.py new file mode 100644 index 0000000..264a86a --- /dev/null +++ b/wawi/import_from_html.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import argparse +import json +import re +import sqlite3 +from pathlib import Path +from datetime import datetime + +BASE_DIR = Path(__file__).resolve().parent +DB_PATH = BASE_DIR / "hellas.db" + + +def now_iso() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M") + + +def ensure_db(conn: sqlite3.Connection) -> None: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + artikel TEXT NOT NULL, + groesse TEXT NOT NULL, + preis REAL NOT NULL DEFAULT 0, + soll INTEGER NOT NULL DEFAULT 0, + gezaehlt INTEGER NOT NULL DEFAULT 0, + verkaeufe INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + """ + ) + conn.commit() + + +def extract_data(html_text: str) -> list[dict]: + # Liest den const DATA = [...] Block aus der alten HTML‑Datei. + match = re.search(r"const\s+DATA\s*=\s*(\[[\s\S]*?\]);", html_text) + if not match: + raise ValueError("DATA-Block nicht gefunden.") + raw = match.group(1) + return json.loads(raw) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Import aus hellas_bestand.html in SQLite.") + parser.add_argument("html", type=Path, help="Pfad zur hellas_bestand.html") + parser.add_argument("--truncate", action="store_true", help="Vor Import alle Items löschen.") + args = parser.parse_args() + + html_text = args.html.read_text(encoding="utf-8") + data = extract_data(html_text) + + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + ensure_db(conn) + + if args.truncate: + conn.execute("DELETE FROM items") + conn.commit() + + now = now_iso() + insert_sql = """ + INSERT INTO items (artikel, groesse, soll, gezaehlt, verkaeufe, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """ + + rows_added = 0 + for item in data: + artikel = (item.get("artikel") or "").strip() + for row in item.get("rows") or []: + groesse = (row.get("groesse") or "").strip() + soll = int(row.get("soll") or 0) + gezaehlt = int(row.get("gezaehlt") or 0) + verkaeufe = int(row.get("verkaeufe") or 0) + if not artikel or not groesse: + continue + conn.execute(insert_sql, (artikel, groesse, soll, gezaehlt, verkaeufe, now, now)) + rows_added += 1 + + conn.commit() + print(f"Import fertig. Zeilen eingefügt: {rows_added}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/wawi/static/logo.png b/wawi/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..26241ed6018c744cb23545cdcf6c5d473507536c GIT binary patch literal 28328 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 +

Ausbuchen: {{ item.artikel }} ({{ item.groesse }})

+
Aktueller Bestand: {{ item.gezaehlt }}
+
+
+ + +
+
+
+ +{% endblock %} diff --git a/wawi/templates/base.html b/wawi/templates/base.html new file mode 100644 index 0000000..e29ef63 --- /dev/null +++ b/wawi/templates/base.html @@ -0,0 +1,37 @@ + + + + + + {{ title or "Hellas Artikelverwaltung" }} + + + +
+
+
+ Hellas 1899 Logo +
+

Hellas 1899 · Artikelverwaltung

+
Bestand & Ausbuchungen
+
+
+ +
+
+
+
+ {% block content %}{% endblock %} +
+
+ + + diff --git a/wawi/templates/edit.html b/wawi/templates/edit.html new file mode 100644 index 0000000..2190485 --- /dev/null +++ b/wawi/templates/edit.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block content %} +
+

{{ "Artikel bearbeiten" if item else "Neuen Artikel anlegen" }}

+
+
+ + + + + + + + +
+
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/wawi/templates/index.html b/wawi/templates/index.html new file mode 100644 index 0000000..c822e55 --- /dev/null +++ b/wawi/templates/index.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block content %} +
+ +
+
Artikel gesamt
+
{{ total }}
+
+
+
Bestand gesamt
+
{{ total_bestand }}
+
+ +
Offene Bestellungen
+
{{ open_orders }}
+
+
+ +
+ + + + + + + + + + + + + + + + {% if groups %} + {% for g in groups %} + + + + {% for r in g.rows %} + {% set diff = (r.gezaehlt or 0) - (r.soll or 0) %} + {% set fehl = (r.soll or 0) - (r.gezaehlt or 0) %} + + + + + + + + + + + + {% endfor %} + {% endfor %} + {% else %} + + + + {% endif %} + +
ArtikelGrößePreisSollBestandAbweichungFehlbestandVerkäufeAktionen
{{ g.artikel }}
{{ r.groesse }}{{ "%.2f"|format(r.preis or 0) }} €{{ r.soll }}{{ r.gezaehlt }}{{ diff }}{{ fehl if fehl > 0 else "–" }}{{ r.verkaeufe }} + +
+ +
+ +
+ +
+
Keine Treffer.
+
+{% endblock %} diff --git a/wawi/templates/login.html b/wawi/templates/login.html new file mode 100644 index 0000000..63afd36 --- /dev/null +++ b/wawi/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block content %} +
+

Login

+ {% if error %} +
Benutzername oder Passwort ist falsch.
+ {% endif %} +
+
+ + +
+
+ +
+
+
+{% endblock %} diff --git a/wawi/templates/orders.html b/wawi/templates/orders.html new file mode 100644 index 0000000..00424d5 --- /dev/null +++ b/wawi/templates/orders.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% block content %} + {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for m in messages %} +
{{ m }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
+ + + + + + + + + + + + + + + + + {% for o in rows %} + + + + + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
DatumNameHandyMannschaftArtikelGrößeMengeNotizStatusAktion
{{ o.created_at }}{{ o.name }}{{ o.handy }}{{ o.mannschaft }}{{ o.artikel }}{{ o.groesse }}{{ o.menge }}{{ o.notiz or "–" }} + {% if o.canceled %}Storniert + {% elif o.done %}Erledigt + {% else %}Offen + {% endif %} + + {% if not o.done and not o.canceled %} +
+ +
+
+ +
+ {% else %} + – + {% endif %} +
+
+ Historie +
+
Abgeschlossen von: {{ o.completed_by or "–" }}
+
Abgeschlossen am: {{ o.completed_at or "–" }}
+
Storniert von: {{ o.canceled_by or "–" }}
+
Storniert am: {{ o.canceled_at or "–" }}
+
+
+
Keine Bestellungen.
+
+{% endblock %} diff --git a/wawi/templates/users.html b/wawi/templates/users.html new file mode 100644 index 0000000..1eda894 --- /dev/null +++ b/wawi/templates/users.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block content %} +
+

Benutzer verwalten

+ {% if error %} +
{{ error }}
+ {% endif %} + {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for m in messages %} +
{{ m }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + {% for u in rows %} + + + + + + {% else %} + + + + {% endfor %} + +
BenutzerErstelltAktion
{{ u.username }}{{ u.created_at }} +
+ +
+
+ +
+
Keine Benutzer.
+
+{% endblock %}