diff --git a/app/main.py b/app/main.py
index 63261cf..29250ba 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,6 +1,7 @@
import os
import re
import secrets
+import urllib.parse
from datetime import datetime, timedelta, date
from typing import Optional, Dict, List, Set
from decimal import Decimal
@@ -130,7 +131,14 @@ def seed_demo_users_if_needed(tenant: Tenant, db: Session):
@app.get("/", response_class=HTMLResponse)
-def index(request: Request, event_id: Optional[int] = None, token: Optional[str] = None, db: Session = Depends(get_db)):
+def index(
+ request: Request,
+ event_id: Optional[int] = None,
+ token: Optional[str] = None,
+ success: Optional[str] = None,
+ error: Optional[str] = None,
+ db: Session = Depends(get_db)
+):
tenant = resolve_tenant(request, db)
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
@@ -207,6 +215,8 @@ def index(request: Request, event_id: Optional[int] = None, token: Optional[str]
"existing_order": existing_order,
"user_items": user_items,
"order_count": order_count,
+ "success": success,
+ "error": error,
"now": now
})
@@ -456,6 +466,11 @@ async def place_order(
).first()
if existing_order:
+ if existing_order.is_paid:
+ raise HTTPException(
+ status_code=400,
+ detail="Diese Bestellung wurde bereits als bezahlt verbucht und kann nicht mehr geändert werden. Bitte wende dich an deinen Organisator."
+ )
db.query(OrderItem).filter(OrderItem.order_id == existing_order.id).delete()
existing_order.total_price = total_price
existing_order.guest_display_name = guest_name_str
@@ -484,6 +499,43 @@ async def place_order(
return RedirectResponse(url=f"/order/{order_id}/success", status_code=status.HTTP_303_SEE_OTHER)
+@app.post("/order/cancel")
+def cancel_order(
+ request: Request,
+ event_id: int = Form(...),
+ db: Session = Depends(get_db)
+):
+ tenant = resolve_tenant(request, db)
+ current_user = get_current_user(request, db)
+ if not current_user:
+ return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
+
+ event = db.query(Event).filter(Event.id == event_id, Event.tenant_id == tenant.id).first()
+ if not event:
+ raise HTTPException(status_code=404, detail="Event nicht gefunden")
+
+ if event.status != "open" or event.deadline < datetime.now():
+ raise HTTPException(status_code=400, detail="Die Frist ist abgelaufen. Bestellungen können nicht mehr storniert werden.")
+
+ order = db.query(Order).filter(
+ Order.tenant_id == tenant.id,
+ Order.event_id == event.id,
+ Order.user_id == current_user.id
+ ).first()
+
+ if not order:
+ return RedirectResponse(url=f"/?event_id={event.id}", status_code=status.HTTP_303_SEE_OTHER)
+
+ if order.is_paid:
+ raise HTTPException(status_code=400, detail="Bereits bezahlte Bestellungen können nur vom Organisator storniert werden.")
+
+ db.delete(order)
+ db.commit()
+
+ success_msg = urllib.parse.quote("Deine Bestellung wurde erfolgreich storniert")
+ return RedirectResponse(url=f"/?event_id={event.id}&success={success_msg}", status_code=status.HTTP_303_SEE_OTHER)
+
+
@app.get("/order/{order_id}/success", response_class=HTMLResponse)
def order_success(order_id: int, request: Request, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
@@ -1522,6 +1574,45 @@ def toggle_paid(order_id: int, request: Request, db: Session = Depends(get_db)):
return RedirectResponse(url=f"/admin?event_id={ev_id}&tab=orders", status_code=status.HTTP_303_SEE_OTHER)
+@app.post("/admin/orders/{order_id}/delete")
+def admin_delete_order(
+ order_id: int,
+ request: Request,
+ db: Session = Depends(get_db)
+):
+ tenant = resolve_tenant(request, db)
+ actor = require_organizer_or_admin(request, db)
+
+ order = db.query(Order).filter(Order.id == order_id, Order.tenant_id == tenant.id).first()
+ if not order:
+ raise HTTPException(status_code=404, detail="Bestellung nicht gefunden")
+
+ ev_id = order.event_id
+
+ # Falls per Guthaben bezahlt, Betrag automatisch zurückerstatten!
+ if order.is_paid and order.payment_method == "balance" and order.user:
+ order.user.balance += order.total_price
+ ev_title = order.event.title if order.event else "Brotzeit"
+ tx = BalanceTransaction(
+ tenant_id=tenant.id,
+ user_id=order.user.id,
+ order_id=order.id,
+ amount=order.total_price,
+ transaction_type="refund",
+ description=f"Rückerstattung wegen Stornierung: {ev_title}",
+ created_by_id=actor.id
+ )
+ db.add(tx)
+
+ db.delete(order)
+ db.commit()
+
+ referer = request.headers.get("referer", "")
+ if "/einkauf" in referer:
+ return RedirectResponse(url=f"/einkauf?event_id={ev_id}&success=Bestellung erfolgreich storniert und ggf. erstattet", status_code=status.HTTP_303_SEE_OTHER)
+ return RedirectResponse(url=f"/admin?event_id={ev_id}&tab=orders&success=Bestellung erfolgreich storniert und ggf. erstattet", status_code=status.HTTP_303_SEE_OTHER)
+
+
@app.get("/health")
def health():
return {"status": "ok", "app": "Schmankerl"}
diff --git a/app/static/style.css b/app/static/style.css
index db008f6..a1fe693 100644
--- a/app/static/style.css
+++ b/app/static/style.css
@@ -133,12 +133,29 @@ body {
transform: translateY(-1px);
}
+.hamburger-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 3.5px;
+ width: 16px;
+ height: 14px;
+ flex-shrink: 0;
+}
+
+.hamburger-icon .bar,
.menu-toggle-btn .bar {
display: block;
- width: 16px;
+ width: 100%;
height: 2px;
background-color: var(--text-main);
border-radius: 2px;
+ transition: all 0.2s ease;
+}
+
+.menu-toggle-btn:hover .hamburger-icon .bar,
+.menu-toggle-btn:hover .bar {
+ background-color: var(--accent);
}
.menu-btn-text {
diff --git a/app/templates/admin.html b/app/templates/admin.html
index 6062547..4498b5f 100644
--- a/app/templates/admin.html
+++ b/app/templates/admin.html
@@ -295,15 +295,25 @@
{% if event.sponsor_name %}
von Sponsor{% endif %}