feat: token-secured guest invites, product freeze lock during active events, Outlook ics export and template, realistic test data
This commit is contained in:
+201
-43
@@ -2,10 +2,10 @@ import os
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, date
|
||||
from typing import Optional, Dict, List
|
||||
from typing import Optional, Dict, List, Set
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import FastAPI, Request, Depends, HTTPException, Form, status
|
||||
from fastapi import FastAPI, Request, Depends, HTTPException, Form, status, Response
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@@ -68,6 +68,19 @@ def require_admin(request: Request, db: Session) -> User:
|
||||
return current_user
|
||||
|
||||
|
||||
def is_product_locked_in_active_event(product_id: int, tenant_id: int, db: Session) -> bool:
|
||||
"""Prüft, ob ein Produkt in einer offenen Runde bereits bestellt wurde."""
|
||||
open_events = db.query(Event).filter(Event.tenant_id == tenant_id, Event.status == "open").all()
|
||||
if not open_events:
|
||||
return False
|
||||
open_ids = [e.id for e in open_events]
|
||||
has_item = db.query(OrderItem).join(Order).filter(
|
||||
Order.event_id.in_(open_ids),
|
||||
OrderItem.product_id == product_id
|
||||
).first()
|
||||
return has_item is not None
|
||||
|
||||
|
||||
def seed_demo_users_if_needed(tenant: Tenant, db: Session):
|
||||
"""Stellt sicher, dass die Standard-Accounts für die Demo existieren."""
|
||||
demo_user = db.query(User).filter(User.tenant_id == tenant.id, User.email == "maxi@waiswuaschd.de").first()
|
||||
@@ -99,7 +112,7 @@ def seed_demo_users_if_needed(tenant: Tenant, db: Session):
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request, event_id: Optional[int] = None, db: Session = Depends(get_db)):
|
||||
def index(request: Request, event_id: Optional[int] = None, token: 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={})
|
||||
@@ -120,16 +133,16 @@ def index(request: Request, event_id: Optional[int] = None, db: Session = Depend
|
||||
if not event and open_events:
|
||||
event = open_events[0]
|
||||
if not event:
|
||||
# Neuestes Event überhaupt anzeigen
|
||||
event = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.id.desc()).first()
|
||||
|
||||
# Gast-Modus
|
||||
is_guest_mode = (request.query_params.get("guest") == "1")
|
||||
# Sicherer Gast-Check: Nur wenn token übereinstimmt, ist Gast-Modus aktiv
|
||||
is_valid_guest = False
|
||||
if event and token and event.guest_token and token == event.guest_token:
|
||||
is_valid_guest = True
|
||||
|
||||
# Wenn nicht eingeloggt und Event erlaubt keine Gäste -> Redirect zu Login
|
||||
if not current_user:
|
||||
if not event or not event.allow_guests:
|
||||
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
# Wenn nicht eingeloggt und kein gültiger Gast-Token -> Redirect zu Login
|
||||
if not current_user and not is_valid_guest:
|
||||
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
# Fristprüfung & Countdown
|
||||
is_closed = True
|
||||
@@ -168,7 +181,8 @@ def index(request: Request, event_id: Optional[int] = None, db: Session = Depend
|
||||
"current_user": current_user,
|
||||
"event": event,
|
||||
"open_events": open_events,
|
||||
"is_guest_mode": is_guest_mode,
|
||||
"is_valid_guest": is_valid_guest,
|
||||
"guest_token": token or (event.guest_token if event else ""),
|
||||
"is_closed": is_closed,
|
||||
"seconds_remaining": seconds_remaining,
|
||||
"products": products,
|
||||
@@ -189,19 +203,10 @@ def login_page(request: Request, db: Session = Depends(get_db), error: Optional[
|
||||
if current_user:
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
active_event = db.query(Event).filter(
|
||||
Event.tenant_id == tenant.id,
|
||||
Event.status == "open",
|
||||
Event.deadline > datetime.now()
|
||||
).first()
|
||||
allow_guests = (active_event.allow_guests if active_event else False)
|
||||
|
||||
return templates.TemplateResponse(request, "login.html", context={
|
||||
"tenant": tenant,
|
||||
"error": error,
|
||||
"success": success,
|
||||
"allow_guests": allow_guests,
|
||||
"active_event": active_event,
|
||||
"current_user": None
|
||||
})
|
||||
|
||||
@@ -221,8 +226,7 @@ def login(
|
||||
return templates.TemplateResponse(request, "login.html", context={
|
||||
"tenant": tenant,
|
||||
"error": "Ungültige E-Mail-Adresse oder falsches Passwort.",
|
||||
"current_user": None,
|
||||
"allow_guests": False
|
||||
"current_user": None
|
||||
}, status_code=400)
|
||||
|
||||
request.session["user_id"] = user.id
|
||||
@@ -356,6 +360,7 @@ def process_join(
|
||||
async def place_order(
|
||||
request: Request,
|
||||
event_id: int = Form(...),
|
||||
guest_token: Optional[str] = Form(None),
|
||||
guest_first_name: Optional[str] = Form(None),
|
||||
guest_last_name: Optional[str] = Form(None),
|
||||
guest_email: Optional[str] = Form(None),
|
||||
@@ -373,8 +378,9 @@ async def place_order(
|
||||
|
||||
guest_name_str = None
|
||||
if not current_user:
|
||||
if not event.allow_guests:
|
||||
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
# Gast-Token prüfen
|
||||
if not guest_token or not event.guest_token or guest_token != event.guest_token:
|
||||
raise HTTPException(status_code=403, detail="Ungültiger oder fehlender Gast-Token. Bitte nutze den Einladungslink.")
|
||||
|
||||
if not guest_first_name or not guest_first_name.strip():
|
||||
raise HTTPException(status_code=400, detail="Bitte gib als Gast deinen Namen an!")
|
||||
@@ -478,6 +484,49 @@ def order_success(order_id: int, request: Request, db: Session = Depends(get_db)
|
||||
})
|
||||
|
||||
|
||||
# --- OUTLOOK & CALENDAR (.ics) ---
|
||||
|
||||
@app.get("/events/{event_id}/calendar.ics")
|
||||
def download_calendar_ics(event_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
tenant = resolve_tenant(request, db)
|
||||
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")
|
||||
|
||||
dt_start = datetime.combine(event.event_date, datetime.min.time()).replace(hour=9, minute=30)
|
||||
dt_end = dt_start + timedelta(hours=1)
|
||||
dt_stamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
||||
dt_start_str = dt_start.strftime("%Y%m%dT%H%M%S")
|
||||
dt_end_str = dt_end.strftime("%Y%m%dT%H%M%S")
|
||||
host = request.headers.get("host", "demo.dev.waiswuaschd.de")
|
||||
order_url = f"https://{host}/?event_id={event.id}"
|
||||
|
||||
ics_content = (
|
||||
"BEGIN:VCALENDAR\r\n"
|
||||
"VERSION:2.0\r\n"
|
||||
"PRODID:-//Schmankerl//Brotzeitportal//DE\r\n"
|
||||
"CALSCALE:GREGORIAN\r\n"
|
||||
"METHOD:PUBLISH\r\n"
|
||||
"BEGIN:VEVENT\r\n"
|
||||
f"UID:schmankerl-event-{event.id}@{host}\r\n"
|
||||
f"DTSTAMP:{dt_stamp}\r\n"
|
||||
f"DTSTART:{dt_start_str}\r\n"
|
||||
f"DTEND:{dt_end_str}\r\n"
|
||||
f"SUMMARY:🥨 {event.title}\r\n"
|
||||
f"DESCRIPTION:Gemeinsame Brotzeit / Weißwurstfrühstück.\\nBestellschluss: {event.deadline.strftime('%d.%m.%Y um %H:%M Uhr')}\\nBestell-Link: {order_url}\r\n"
|
||||
f"LOCATION:Kantine / Teeküche ({tenant.name})\r\n"
|
||||
"STATUS:CONFIRMED\r\n"
|
||||
"END:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n"
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=ics_content,
|
||||
media_type="text/calendar",
|
||||
headers={"Content-Disposition": f'attachment; filename="brotzeit-{event.event_date.strftime("%Y-%m-%d")}.ics"'}
|
||||
)
|
||||
|
||||
|
||||
# --- ADMIN COCKPIT (/admin) ---
|
||||
|
||||
@app.get("/admin", response_class=HTMLResponse)
|
||||
@@ -487,6 +536,7 @@ def admin_dashboard(
|
||||
tab: str = "orders",
|
||||
warn_active_id: Optional[int] = None,
|
||||
success: Optional[str] = None,
|
||||
error: Optional[str] = None,
|
||||
bulk_count: Optional[int] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -503,13 +553,12 @@ def admin_dashboard(
|
||||
# Alle Events des Mandanten
|
||||
all_events = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.event_date.desc(), Event.id.desc()).all()
|
||||
|
||||
# Ausgewähltes Event bestimmen: Wenn event_id übergeben, nehmen wir das, sonst das erste offene, sonst das neueste
|
||||
# Ausgewähltes Event bestimmen
|
||||
selected_event = None
|
||||
if event_id:
|
||||
selected_event = db.query(Event).filter(Event.id == event_id, Event.tenant_id == tenant.id).first()
|
||||
|
||||
if not selected_event:
|
||||
# Bevorzuge aktives offenes Event
|
||||
for ev in all_events:
|
||||
if ev.status == "open":
|
||||
selected_event = ev
|
||||
@@ -536,7 +585,7 @@ def admin_dashboard(
|
||||
item_sums[pname] = {"quantity": 0, "unit": unit}
|
||||
item_sums[pname]["quantity"] += it.quantity
|
||||
|
||||
# Teammitglieder laden (Gäste filtern oder am Ende aufführen)
|
||||
# Teammitglieder laden
|
||||
team_members = db.query(User).filter(
|
||||
User.tenant_id == tenant.id,
|
||||
User.role != "guest"
|
||||
@@ -558,6 +607,14 @@ def admin_dashboard(
|
||||
# Produkte laden
|
||||
products = db.query(Product).filter(Product.tenant_id == tenant.id).order_by(Product.sort_order.asc(), Product.id.asc()).all()
|
||||
|
||||
# Welche Produkte sind durch Bestellungen in offenen Events gesperrt?
|
||||
locked_product_ids: Set[int] = set()
|
||||
open_events = [e for e in all_events if e.status == "open"]
|
||||
if open_events:
|
||||
open_ev_ids = [e.id for e in open_events]
|
||||
ordered_pids = db.query(OrderItem.product_id).join(Order).filter(Order.event_id.in_(open_ev_ids)).distinct().all()
|
||||
locked_product_ids = {p[0] for p in ordered_pids}
|
||||
|
||||
# Warnung bei existierendem aktivem Event
|
||||
active_warning_event = None
|
||||
if warn_active_id:
|
||||
@@ -576,9 +633,11 @@ def admin_dashboard(
|
||||
"active_guests": active_guests,
|
||||
"pending_invitations": pending_invitations,
|
||||
"products": products,
|
||||
"locked_product_ids": locked_product_ids,
|
||||
"active_tab": tab,
|
||||
"warn_active_event": active_warning_event,
|
||||
"success": success,
|
||||
"error": error,
|
||||
"bulk_count": bulk_count,
|
||||
"now": now
|
||||
})
|
||||
@@ -606,7 +665,7 @@ def admin_reset_password(
|
||||
user.password_hash = hash_password(new_password.strip())
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=team", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=team&success=Passwort erfolgreich aktualisiert", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/admin/users/{user_id}/delete")
|
||||
@@ -623,7 +682,6 @@ def admin_delete_user(
|
||||
|
||||
user = db.query(User).filter(User.id == user_id, User.tenant_id == tenant.id).first()
|
||||
if user:
|
||||
# Eventuelle Bestellungen von user_id entkoppeln, damit Beträge im Archiv nicht verloren gehen
|
||||
orders = db.query(Order).filter(Order.user_id == user.id).all()
|
||||
for ord_obj in orders:
|
||||
ord_obj.guest_display_name = f"{user.first_name} {user.last_name}"
|
||||
@@ -631,10 +689,10 @@ def admin_delete_user(
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=team", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=team&success=Benutzer gelöscht", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
# --- ADMIN INVITATIONS (EINZELN & BULK) ---
|
||||
# --- ADMIN INVITATIONS ---
|
||||
|
||||
@app.post("/admin/invitations/create")
|
||||
def admin_create_invitation(
|
||||
@@ -684,7 +742,6 @@ def admin_bulk_invitations(
|
||||
tenant = resolve_tenant(request, db)
|
||||
admin = require_admin(request, db)
|
||||
|
||||
# E-Mails per Regex extrahieren (unterstützt Komma, Semikolon, Umbrüche etc.)
|
||||
email_pattern = r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+'
|
||||
found_emails = list(set([e.lower().strip() for e in re.findall(email_pattern, emails_text)]))
|
||||
|
||||
@@ -758,7 +815,6 @@ def admin_create_event(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Ungültiges Datumsformat: {e}")
|
||||
|
||||
# Prüfung: Gibt es bereits ein aktives offenes Event in der Zukunft?
|
||||
if not force:
|
||||
active_existing = db.query(Event).filter(
|
||||
Event.tenant_id == tenant.id,
|
||||
@@ -766,7 +822,6 @@ def admin_create_event(
|
||||
Event.event_date >= date.today()
|
||||
).first()
|
||||
if active_existing:
|
||||
# Weiterleitung mit Warn-Hinweis
|
||||
return RedirectResponse(
|
||||
url=f"/admin?tab=event&warn_active_id={active_existing.id}",
|
||||
status_code=status.HTTP_303_SEE_OTHER
|
||||
@@ -779,7 +834,8 @@ def admin_create_event(
|
||||
deadline=deadline_dt,
|
||||
status="open",
|
||||
sponsor_name=sponsor_name.strip() if sponsor_name and sponsor_name.strip() else None,
|
||||
allow_guests=allow_guests
|
||||
allow_guests=allow_guests,
|
||||
guest_token=secrets.token_urlsafe(16)
|
||||
)
|
||||
db.add(new_event)
|
||||
db.commit()
|
||||
@@ -821,9 +877,11 @@ def admin_edit_event(
|
||||
event.status = status_val
|
||||
event.sponsor_name = sponsor_name.strip() if sponsor_name and sponsor_name.strip() else None
|
||||
event.allow_guests = allow_guests
|
||||
if not event.guest_token:
|
||||
event.guest_token = secrets.token_urlsafe(16)
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url=f"/admin?event_id={event.id}&tab=event", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url=f"/admin?event_id={event.id}&tab=event&success=Event aktualisiert", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/admin/events/{event_id}/complete-and-cleanup")
|
||||
@@ -842,7 +900,6 @@ def admin_complete_event_and_cleanup_guests(
|
||||
|
||||
event.status = "completed"
|
||||
|
||||
# Gast-Bestellungen sichern und Gast-Konten entfernen
|
||||
orders = db.query(Order).filter(Order.event_id == event.id).all()
|
||||
cleaned_count = 0
|
||||
for ord_obj in orders:
|
||||
@@ -872,10 +929,10 @@ def admin_delete_event(
|
||||
db.delete(event)
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=event", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=event&success=Event gelöscht", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
# --- ADMIN PRODUCT CATALOG MANAGEMENT ---
|
||||
# --- ADMIN PRODUCT CATALOG MANAGEMENT (MIT SICHERUNGSSPERRE) ---
|
||||
|
||||
@app.post("/admin/products/new")
|
||||
def admin_create_product(
|
||||
@@ -907,7 +964,7 @@ def admin_create_product(
|
||||
db.add(product)
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=products", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=products&success=Produkt angelegt", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/admin/products/{product_id}/edit")
|
||||
@@ -933,6 +990,13 @@ def admin_edit_product(
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Ungültiger Preis")
|
||||
|
||||
# Prüfen, ob Preis gesperrt ist wegen laufender Bestellungen
|
||||
if prod.price != clean_price and is_product_locked_in_active_event(product_id, tenant.id, db):
|
||||
return RedirectResponse(
|
||||
url="/admin?tab=products&error=Preisänderung gesperrt: Dieser Artikel wurde in der laufenden Runde bereits bestellt!",
|
||||
status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
prod.name = name.strip()
|
||||
prod.description = description.strip()
|
||||
prod.price = clean_price
|
||||
@@ -940,7 +1004,7 @@ def admin_edit_product(
|
||||
prod.sort_order = sort_order
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=products", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=products&success=Produkt aktualisiert", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/admin/products/{product_id}/toggle")
|
||||
@@ -954,13 +1018,20 @@ def admin_toggle_product(
|
||||
|
||||
prod = db.query(Product).filter(Product.id == product_id, Product.tenant_id == tenant.id).first()
|
||||
if prod:
|
||||
# Wenn aktiv -> versuche zu deaktivieren: prüfen, ob in aktiver Runde bestellt
|
||||
if prod.is_active and is_product_locked_in_active_event(product_id, tenant.id, db):
|
||||
return RedirectResponse(
|
||||
url="/admin?tab=products&error=Deaktivierung gesperrt: Artikel wurde in der laufenden Runde bereits bestellt!",
|
||||
status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
prod.is_active = not prod.is_active
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=products", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
# --- ONBOARDING / SETUP WIZARD (/setup) ---
|
||||
# --- ONBOARDING / SETUP WIZARD & TESTDATEN ---
|
||||
|
||||
@app.get("/setup", response_class=HTMLResponse)
|
||||
def setup_page(request: Request, db: Session = Depends(get_db)):
|
||||
@@ -1008,7 +1079,94 @@ def seed_standard_products(request: Request, db: Session = Depends(get_db)):
|
||||
))
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse(url="/admin?tab=products", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(url="/admin?tab=products&success=Standard-Sortiment geladen", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/setup/seed-test-data")
|
||||
def seed_realistic_test_data(
|
||||
request: Request,
|
||||
event_id: Optional[int] = Form(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Spielt realistische Testdaten für das nächste Event ein."""
|
||||
tenant = resolve_tenant(request, db)
|
||||
require_admin(request, db)
|
||||
|
||||
# Event finden
|
||||
event = None
|
||||
if event_id:
|
||||
event = db.query(Event).filter(Event.tenant_id == tenant.id, Event.id == event_id).first()
|
||||
if not event:
|
||||
event = db.query(Event).filter(Event.tenant_id == tenant.id, Event.status == "open").order_by(Event.event_date.asc()).first()
|
||||
if not event:
|
||||
event = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.id.desc()).first()
|
||||
if not event:
|
||||
raise HTTPException(status_code=400, detail="Kein Event vorhanden")
|
||||
|
||||
# Produkte finden
|
||||
products = {p.name: p for p in db.query(Product).filter(Product.tenant_id == tenant.id).all()}
|
||||
if not products:
|
||||
raise HTTPException(status_code=400, detail="Bitte zuerst Standard-Produkte anlegen")
|
||||
|
||||
# Sicherstellen, dass Basisprodukte aktiv sind
|
||||
for p in products.values():
|
||||
if not p.is_active:
|
||||
p.is_active = True
|
||||
|
||||
test_users_data = [
|
||||
{"email": "anna.huber@firma.de", "fname": "Anna", "lname": "Huber", "items": [("Weißwurst", 2), ("Breze", 1), ("Senf", 1)], "paid": True},
|
||||
{"email": "stefan.meier@firma.de", "fname": "Stefan", "lname": "Meier", "items": [("Leberkas", 2), ("Bier", 1)], "paid": False},
|
||||
{"email": "laura.wagner@firma.de", "fname": "Laura", "lname": "Wagner", "items": [("Breze", 1), ("Senf", 1), ("Weißwurst", 2)], "paid": True},
|
||||
{"email": "michael.schmidt@firma.de", "fname": "Michael", "lname": "Schmidt", "items": [("Wiener", 2), ("Breze", 1)], "paid": False},
|
||||
{"email": "christian.dienstleister@extern.de", "fname": "Christian", "lname": "Bauer (IT-Service)", "items": [("Weißwurst", 4), ("Breze", 2), ("Senf", 1)], "paid": True, "role": "guest"},
|
||||
]
|
||||
|
||||
for tdata in test_users_data:
|
||||
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == tdata["email"]).first()
|
||||
if not user:
|
||||
user = User(
|
||||
tenant_id=tenant.id,
|
||||
email=tdata["email"],
|
||||
username=tdata["email"].split("@")[0],
|
||||
first_name=tdata["fname"],
|
||||
last_name=tdata["lname"],
|
||||
password_hash=hash_password("demo123"),
|
||||
role=tdata.get("role", "user")
|
||||
)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
|
||||
# Bestellung prüfen
|
||||
existing = db.query(Order).filter(Order.event_id == event.id, Order.user_id == user.id).first()
|
||||
if not existing:
|
||||
tot = Decimal("0.00")
|
||||
new_ord = Order(
|
||||
tenant_id=tenant.id,
|
||||
event_id=event.id,
|
||||
user_id=user.id,
|
||||
guest_display_name=f"{user.first_name} {user.last_name}",
|
||||
is_paid=tdata["paid"]
|
||||
)
|
||||
db.add(new_ord)
|
||||
db.flush()
|
||||
|
||||
for key_term, qty in tdata["items"]:
|
||||
prod = products.get(key_term)
|
||||
if not prod:
|
||||
term_lower = key_term.lower()
|
||||
for p_name, p_obj in products.items():
|
||||
if term_lower in p_name.lower():
|
||||
prod = p_obj
|
||||
break
|
||||
|
||||
if prod:
|
||||
tot += prod.price * qty
|
||||
db.add(OrderItem(order_id=new_ord.id, product_id=prod.id, quantity=qty, unit_price=prod.price))
|
||||
|
||||
new_ord.total_price = tot
|
||||
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/admin?event_id={event.id}&tab=orders&success=Realistische Testdaten erfolgreich eingespielt", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/admin/toggle-paid/{order_id}")
|
||||
|
||||
Reference in New Issue
Block a user