feat: Compact 1-page print view, event lifecycle & conflict warnings, bulk email invites, guest pass link and post-event guest cleanup

This commit is contained in:
2026-09-21 13:29:15 +02:00
parent e5ca9c3cbf
commit 913aa646ea
5 changed files with 741 additions and 263 deletions
+168 -32
View File
@@ -1,4 +1,5 @@
import os
import re
import secrets
from datetime import datetime, timedelta, date
from typing import Optional, Dict, List
@@ -98,7 +99,7 @@ def seed_demo_users_if_needed(tenant: Tenant, db: Session):
@app.get("/", response_class=HTMLResponse)
def index(request: Request, db: Session = Depends(get_db)):
def index(request: Request, event_id: Optional[int] = None, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
@@ -106,16 +107,25 @@ def index(request: Request, db: Session = Depends(get_db)):
current_user = get_current_user(request, db)
now = datetime.now()
# Aktuelle offene Runde suchen
event = db.query(Event).filter(
# Alle offenen Events laden
open_events = db.query(Event).filter(
Event.tenant_id == tenant.id,
Event.status == "open"
).order_by(Event.deadline.asc()).first()
).order_by(Event.event_date.asc(), Event.deadline.asc()).all()
# Wenn keine offene, das neueste anzeigen
# Gewünschtes Event wählen
event = None
if event_id:
event = db.query(Event).filter(Event.id == event_id, Event.tenant_id == tenant.id).first()
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")
# Wenn nicht eingeloggt und Event erlaubt keine Gäste -> Redirect zu Login
if not current_user:
if not event or not event.allow_guests:
@@ -157,6 +167,8 @@ def index(request: Request, db: Session = Depends(get_db)):
"tenant": tenant,
"current_user": current_user,
"event": event,
"open_events": open_events,
"is_guest_mode": is_guest_mode,
"is_closed": is_closed,
"seconds_remaining": seconds_remaining,
"products": products,
@@ -177,7 +189,6 @@ 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)
# Prüfen, ob aktives Event Gäste erlaubt
active_event = db.query(Event).filter(
Event.tenant_id == tenant.id,
Event.status == "open",
@@ -190,6 +201,7 @@ def login_page(request: Request, db: Session = Depends(get_db), error: Optional[
"error": error,
"success": success,
"allow_guests": allow_guests,
"active_event": active_event,
"current_user": None
})
@@ -328,7 +340,6 @@ def process_join(
)
db.add(new_user)
# Nur Einmal-Links als verwendet markieren; wiederverwendbare Links bleiben offen!
if not invitation.is_reusable:
invitation.used_at = datetime.now()
@@ -360,16 +371,17 @@ async def place_order(
if event.deadline < datetime.now():
raise HTTPException(status_code=400, detail="Die Bestellfrist für diese Runde ist leider abgelaufen!")
# Falls nicht eingeloggt -> Gast-Bestellung prüfen
guest_name_str = None
if not current_user:
if not event.allow_guests:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
if not guest_first_name or not guest_first_name.strip():
raise HTTPException(status_code=400, detail="Bitte gib als Gast deinen Vornamen an!")
raise HTTPException(status_code=400, detail="Bitte gib als Gast deinen Namen an!")
fname = guest_first_name.strip()
lname = guest_last_name.strip() if guest_last_name else "(Gast)"
guest_name_str = f"{fname} {lname}"
clean_email = (guest_email.strip().lower() if guest_email and guest_email.strip() else f"gast_{secrets.token_hex(4)}@{tenant.slug}.local")
guest_user = User(
@@ -386,6 +398,8 @@ async def place_order(
db.refresh(guest_user)
current_user = guest_user
request.session["user_id"] = guest_user.id
else:
guest_name_str = f"{current_user.first_name} {current_user.last_name}"
form_data = await request.form()
order_items = []
@@ -411,7 +425,6 @@ async def place_order(
if not order_items:
raise HTTPException(status_code=400, detail="Bitte wähle mindestens einen Artikel aus!")
# Bestehende Bestellung des Users in diesem Event aktualisieren oder neu anlegen
existing_order = db.query(Order).filter(
Order.tenant_id == tenant.id,
Order.event_id == event.id,
@@ -421,6 +434,7 @@ async def place_order(
if existing_order:
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
for item in order_items:
item.order_id = existing_order.id
db.add(item)
@@ -431,6 +445,7 @@ async def place_order(
tenant_id=tenant.id,
event_id=event.id,
user_id=current_user.id,
guest_display_name=guest_name_str,
total_price=total_price,
is_paid=False
)
@@ -470,6 +485,9 @@ def admin_dashboard(
request: Request,
event_id: Optional[int] = None,
tab: str = "orders",
warn_active_id: Optional[int] = None,
success: Optional[str] = None,
bulk_count: Optional[int] = None,
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
@@ -483,14 +501,21 @@ def admin_dashboard(
raise HTTPException(status_code=403, detail="Zugriff nur für Administratoren")
# Alle Events des Mandanten
all_events = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.id.desc()).all()
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
# Ausgewähltes Event bestimmen: Wenn event_id übergeben, nehmen wir das, sonst das erste offene, sonst das neueste
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 and all_events:
selected_event = all_events[0]
if not selected_event:
# Bevorzuge aktives offenes Event
for ev in all_events:
if ev.status == "open":
selected_event = ev
break
if not selected_event and all_events:
selected_event = all_events[0]
orders = []
item_sums = {}
@@ -511,19 +536,33 @@ def admin_dashboard(
item_sums[pname] = {"quantity": 0, "unit": unit}
item_sums[pname]["quantity"] += it.quantity
# Teammitglieder & Einladungen laden
team_members = db.query(User).filter(User.tenant_id == tenant.id).order_by(User.role.asc(), User.first_name.asc()).all()
# Offene Einladungen (entweder unbegrenzt oder expires_at in der Zukunft)
# Teammitglieder laden (Gäste filtern oder am Ende aufführen)
team_members = db.query(User).filter(
User.tenant_id == tenant.id,
User.role != "guest"
).order_by(User.role.asc(), User.first_name.asc()).all()
# Aktive Gäste laden
active_guests = db.query(User).filter(
User.tenant_id == tenant.id,
User.role == "guest"
).all()
# Offene Einladungen
now = datetime.now()
pending_invitations = db.query(Invitation).filter(
Invitation.tenant_id == tenant.id,
(Invitation.expires_at == None) | (Invitation.expires_at > now)
).order_by(Invitation.created_at.desc()).all()
# Produkte des Mandanten laden
# Produkte laden
products = db.query(Product).filter(Product.tenant_id == tenant.id).order_by(Product.sort_order.asc(), Product.id.asc()).all()
# Warnung bei existierendem aktivem Event
active_warning_event = None
if warn_active_id:
active_warning_event = db.query(Event).filter(Event.id == warn_active_id, Event.tenant_id == tenant.id).first()
return templates.TemplateResponse(request, "admin.html", context={
"tenant": tenant,
"current_user": current_user,
@@ -534,9 +573,13 @@ def admin_dashboard(
"total_revenue": total_revenue,
"total_paid": total_paid,
"team_members": team_members,
"active_guests": active_guests,
"pending_invitations": pending_invitations,
"products": products,
"active_tab": tab,
"warn_active_event": active_warning_event,
"success": success,
"bulk_count": bulk_count,
"now": now
})
@@ -580,20 +623,25 @@ 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}"
ord_obj.user_id = None
db.delete(user)
db.commit()
return RedirectResponse(url="/admin?tab=team", status_code=status.HTTP_303_SEE_OTHER)
# --- ADMIN INVITATIONS ---
# --- ADMIN INVITATIONS (EINZELN & BULK) ---
@app.post("/admin/invitations/create")
def admin_create_invitation(
request: Request,
email: Optional[str] = Form(None),
role: str = Form("user"),
valid_days: str = Form("14"),
valid_days: str = Form("unlimited"),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
@@ -602,7 +650,6 @@ def admin_create_invitation(
token = secrets.token_urlsafe(20)
clean_email = email.strip().lower() if email and email.strip() else None
# Unbegrenzt prüfen
if valid_days in ["0", "unlimited", "forever"]:
expires_at = None
is_reusable = True
@@ -626,6 +673,50 @@ def admin_create_invitation(
return RedirectResponse(url="/admin?tab=team", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/admin/invitations/bulk")
def admin_bulk_invitations(
request: Request,
emails_text: str = Form(...),
role: str = Form("user"),
valid_days: str = Form("14"),
db: Session = Depends(get_db)
):
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)]))
if not found_emails:
raise HTTPException(status_code=400, detail="Keine gültigen E-Mail-Adressen gefunden.")
expires_at = None
is_reusable = False
if valid_days in ["0", "unlimited", "forever"]:
is_reusable = True
else:
expires_at = datetime.now() + timedelta(days=int(valid_days))
count = 0
for em in found_emails:
token = secrets.token_urlsafe(20)
inv = Invitation(
tenant_id=tenant.id,
token=token,
email=em,
role=role,
created_by=admin.id,
expires_at=expires_at,
is_reusable=is_reusable
)
db.add(inv)
count += 1
db.commit()
return RedirectResponse(url=f"/admin?tab=team&bulk_count={count}", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/admin/invitations/{inv_id}/revoke")
def admin_revoke_invitation(
inv_id: int,
@@ -654,6 +745,7 @@ def admin_create_event(
deadline_time: str = Form(...),
sponsor_name: Optional[str] = Form(None),
allow_guests: bool = Form(True),
force: bool = Form(False),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
@@ -666,6 +758,20 @@ 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,
Event.status == "open",
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
)
new_event = Event(
tenant_id=tenant.id,
title=title.strip(),
@@ -679,7 +785,7 @@ def admin_create_event(
db.commit()
db.refresh(new_event)
return RedirectResponse(url=f"/admin?event_id={new_event.id}&tab=event", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url=f"/admin?event_id={new_event.id}&tab=orders", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/admin/events/{event_id}/edit")
@@ -720,8 +826,40 @@ def admin_edit_event(
return RedirectResponse(url=f"/admin?event_id={event.id}&tab=event", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/admin/events/{event_id}/toggle-status")
def admin_toggle_event_status(
@app.post("/admin/events/{event_id}/complete-and-cleanup")
def admin_complete_event_and_cleanup_guests(
event_id: int,
request: Request,
db: Session = Depends(get_db)
):
"""Schließt das Event ab und bereinigt temporäre Gast-Konten."""
tenant = resolve_tenant(request, db)
require_admin(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")
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:
if ord_obj.user and ord_obj.user.role == "guest":
ord_obj.guest_display_name = f"{ord_obj.user.first_name} {ord_obj.user.last_name}"
guest_user = ord_obj.user
ord_obj.user_id = None
db.delete(guest_user)
cleaned_count += 1
db.commit()
msg = f"Event erfolgreich abgeschlossen. {cleaned_count} Gastkonten bereinigt."
return RedirectResponse(url=f"/admin?event_id={event.id}&tab=orders&success={msg}", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/admin/events/{event_id}/delete")
def admin_delete_event(
event_id: int,
request: Request,
db: Session = Depends(get_db)
@@ -730,13 +868,11 @@ def admin_toggle_event_status(
require_admin(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")
if event:
db.delete(event)
db.commit()
event.status = "closed" if event.status == "open" else "open"
db.commit()
return RedirectResponse(url=f"/admin?event_id={event.id}", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/admin?tab=event", status_code=status.HTTP_303_SEE_OTHER)
# --- ADMIN PRODUCT CATALOG MANAGEMENT ---
@@ -885,7 +1021,7 @@ def toggle_paid(order_id: int, request: Request, db: Session = Depends(get_db)):
if order:
order.is_paid = not order.is_paid
db.commit()
return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url=f"/admin?event_id={order.event_id}", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/health")
+9 -2
View File
@@ -35,7 +35,7 @@ class User(Base):
created_at = Column(DateTime, default=func.now())
tenant = relationship("Tenant", back_populates="users")
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
orders = relationship("Order", back_populates="user")
class Invitation(Base):
@@ -93,7 +93,8 @@ class Order(Base):
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
event_id = Column(Integer, ForeignKey("events.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
guest_display_name = Column(String(150), nullable=True)
total_price = Column(Numeric(10, 2), nullable=False, default=0.00)
is_paid = Column(Boolean, default=False)
created_at = Column(DateTime, default=func.now())
@@ -103,6 +104,12 @@ class Order(Base):
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
@property
def display_name(self) -> str:
if self.user:
return f"{self.user.first_name} {self.user.last_name}"
return self.guest_display_name or "Gast"
class OrderItem(Base):
__tablename__ = "order_items"
+269 -86
View File
@@ -139,6 +139,47 @@ body {
padding: 36px 0;
}
/* Multi-Event Banner on Order Page */
.multi-event-banner {
display: flex;
align-items: center;
gap: 12px;
background: rgba(56, 189, 248, 0.08);
border: 1px solid rgba(56, 189, 248, 0.2);
border-radius: var(--radius-md);
padding: 10px 16px;
margin-bottom: 18px;
font-size: 0.85rem;
flex-wrap: wrap;
}
.multi-event-chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.mini-chip {
padding: 4px 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-main);
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.mini-chip:hover {
border-color: var(--accent);
}
.mini-chip.active {
background: var(--accent);
color: #0b0f19;
border-color: var(--accent);
}
/* Banner */
.banner {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8) 0%, rgba(15, 23, 42, 0.9) 100%);
@@ -656,6 +697,131 @@ body {
margin-bottom: 18px;
}
.alert-warning-card {
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 24px;
}
.alert-warning-card h3 {
color: var(--warning);
font-size: 1.1rem;
margin-bottom: 6px;
}
/* Events Ribbon in Admin */
.events-ribbon {
display: flex;
align-items: center;
gap: 14px;
background: rgba(11, 15, 25, 0.6);
border: 1px solid var(--border-color);
padding: 12px 18px;
border-radius: var(--radius-md);
margin-bottom: 24px;
overflow-x: auto;
}
.events-ribbon-title {
font-weight: 700;
font-size: 0.9rem;
color: var(--text-muted);
white-space: nowrap;
}
.events-ribbon-list {
display: flex;
gap: 10px;
overflow-x: auto;
}
.event-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-main);
text-decoration: none;
font-size: 0.85rem;
transition: all 0.2s;
white-space: nowrap;
}
.event-chip:hover {
border-color: var(--accent);
}
.event-chip.active {
background: rgba(56, 189, 248, 0.15);
border-color: var(--accent);
}
.event-chip.completed {
opacity: 0.6;
}
.chip-date {
font-weight: 700;
color: var(--accent);
}
.chip-badge {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
.chip-badge.open {
background: rgba(34, 197, 94, 0.15);
color: #86efac;
}
.chip-badge.closed {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
}
.chip-badge.comp {
background: rgba(148, 163, 184, 0.2);
color: #94a3b8;
}
/* Event Action Bar */
.event-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(11, 15, 25, 0.6);
border: 1px solid var(--border-color);
padding: 16px 20px;
border-radius: var(--radius-md);
margin-bottom: 24px;
flex-wrap: wrap;
gap: 12px;
}
.btn-success-small {
padding: 8px 14px;
background: rgba(34, 197, 94, 0.2);
border: 1px solid rgba(34, 197, 94, 0.4);
color: #86efac;
border-radius: 6px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-success-small:hover {
background: rgba(34, 197, 94, 0.3);
}
/* Admin Dashboard Tabs */
.admin-header {
display: flex;
@@ -669,6 +835,7 @@ body {
.admin-actions-top {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.admin-sub {
@@ -703,26 +870,6 @@ body {
border-bottom-color: var(--accent);
}
.event-selector-bar {
display: flex;
align-items: center;
gap: 12px;
background: rgba(11, 15, 25, 0.6);
border: 1px solid var(--border-color);
padding: 10px 16px;
border-radius: var(--radius-md);
margin-bottom: 20px;
}
.event-selector-bar select {
background: rgba(11, 15, 25, 0.9);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 6px 12px;
border-radius: 6px;
font-size: 0.9rem;
}
.sponsor-highlight-card {
display: flex;
align-items: center;
@@ -882,6 +1029,20 @@ body {
color: #c084fc;
}
/* Bulk Invite Box */
.bulk-invite-box {
background: rgba(11, 15, 25, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 24px;
}
.bulk-invite-box h3 {
font-size: 1.05rem;
margin-bottom: 4px;
}
/* Invitations List */
.invitations-list {
display: flex;
@@ -986,35 +1147,6 @@ body {
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
}
/* Setup Wizard Steps */
.setup-step-card {
display: flex;
gap: 16px;
background: rgba(11, 15, 25, 0.6);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 18px;
}
.step-num {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--accent);
color: #0b0f19;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-content h3 {
font-size: 1.05rem;
margin-bottom: 6px;
}
/* Footer */
.footer {
border-top: 1px solid var(--border-color);
@@ -1047,19 +1179,32 @@ body {
margin-bottom: 16px;
}
/* --- PRINT MEDIA QUERY --- */
/* ================= PRINT MEDIA QUERY (1-PAGE A4) ================= */
.print-only {
display: none;
}
@page {
size: A4 portrait;
margin: 8mm 10mm;
}
@media print {
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
body {
background: #ffffff !important;
color: #000000 !important;
font-size: 11pt;
font-size: 8.5pt !important;
line-height: 1.2 !important;
margin: 0 !important;
padding: 0 !important;
}
.navbar, .footer, .no-print, .btn-primary, .btn-secondary, .badge-btn {
.navbar, .footer, .no-print, .btn-primary, .btn-secondary, .badge-btn, .events-ribbon, .admin-nav-tabs, .admin-header {
display: none !important;
}
@@ -1071,47 +1216,85 @@ body {
display: block !important;
}
.print-header {
/* Single-line condensed header */
.print-header-singleline {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 2px solid #000;
padding-bottom: 10px;
margin-bottom: 20px;
}
.print-header h1 {
font-size: 18pt;
margin-bottom: 6px;
}
.print-sponsor-box {
margin-top: 8px;
padding: 6px;
border: 1px dashed #000;
font-size: 10pt;
}
.print-section h3 {
font-size: 13pt;
margin-bottom: 8px;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
}
.print-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.print-table th, .print-table td {
border: 1px solid #ddd;
padding: 6px 10px;
text-align: left;
margin-bottom: 6px;
font-size: 10pt;
}
.print-logo {
font-weight: 800;
font-size: 11pt;
margin-right: 6px;
}
.print-sponsor {
font-weight: bold;
color: #000;
}
/* Compact Sammel-Box */
.print-sammel-box {
background: #f1f5f9 !important;
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 5px 8px;
margin-bottom: 8px;
font-size: 8.5pt;
line-height: 1.4;
}
.print-sammel-title {
font-weight: 700;
margin-right: 8px;
text-decoration: underline;
}
.print-item-pill {
display: inline-block;
background: #ffffff !important;
border: 1px solid #94a3b8;
border-radius: 3px;
padding: 1px 6px;
margin: 1px 3px 1px 0;
font-size: 8pt;
}
/* Compact Ausgabeliste */
.print-table {
width: 100%;
border-collapse: collapse;
font-size: 8.5pt;
margin-top: 4px;
}
.print-table th, .print-table td {
border: 1px solid #64748b;
padding: 2.5px 5px !important;
text-align: left;
font-size: 8.5pt !important;
line-height: 1.15 !important;
color: #000 !important;
}
.print-table th {
background-color: #f2f2f2 !important;
background-color: #e2e8f0 !important;
font-weight: bold;
font-size: 8.5pt;
}
.print-items-cell {
font-size: 8pt !important;
}
.guest-tag {
font-size: 7.5pt;
font-style: italic;
color: #475569;
}
}
+272 -135
View File
@@ -3,91 +3,87 @@
{% block content %}
<div class="container">
<!-- Printable distribution sheet for the kitchen (hidden on screen, visible in print) -->
<!-- ================= COMPACT PRINT VIEW (1-PAGE A4) ================= -->
<div class="print-only">
<div class="print-header">
<h1>🥨 Schmankerl – Brotzeit-Verteilliste</h1>
<p>
<strong>Event:</strong> {{ event.title if event else 'Brotzeitrunde' }} &nbsp;|&nbsp;
<strong>Datum:</strong> {{ event.event_date.strftime('%d.%m.%Y') if event else '' }} &nbsp;|&nbsp;
<strong>Team:</strong> {{ tenant.name }}
</p>
{% if event and event.sponsor_name %}
<div class="print-sponsor-box">
🎉 <strong>GESPONSERTE RUNDE:</strong> Ausgegeben von <strong>{{ event.sponsor_name }}</strong> – Gesamtrechnung: <strong>{{ "%.2f"|format(total_revenue) }} €</strong>
<!-- Single-line condensed header -->
<div class="print-header-singleline">
<div class="print-left">
<span class="print-logo">🥨 Schmankerl</span>
<strong>{{ event.title if event else 'Brotzeitrunde' }}</strong>
<span class="print-date">({{ event.event_date.strftime('%d.%m.%Y') if event else '' }})</span>
</div>
<div class="print-right">
<span>Teilnehmer: <strong>{{ orders|length }}</strong></span>
<span> | </span>
<span>Einkauf: <strong>{{ "%.2f"|format(total_revenue) }} €</strong></span>
{% if event and event.sponsor_name %}
<span> | </span>
<span class="print-sponsor">🎉 Ausgegeben von: <strong>{{ event.sponsor_name }}</strong></span>
{% endif %}
</div>
{% endif %}
</div>
<div class="print-section">
<h3>🛒 Sammelbestellung (Metzger & Bäcker)</h3>
<table class="print-table">
<thead>
<tr>
<th>Artikel</th>
<th style="text-align: right;">Gesamtmenge</th>
</tr>
</thead>
<tbody>
{% for name, data in item_sums.items() %}
<tr>
<td>{{ name }}</td>
<td style="text-align: right; font-weight: bold;">{{ data.quantity }} {{ data.unit }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Compact Sammelliste (Horizontal / 2-Column) -->
<div class="print-sammel-box">
<span class="print-sammel-title">🛒 Einkauf (Bäcker & Metzger):</span>
{% for name, data in item_sums.items() %}
<span class="print-item-pill">
<strong>{{ data.quantity }}x</strong> {{ name }}
</span>
{% else %}
<span>Keine Artikel</span>
{% endfor %}
</div>
<div class="print-section" style="margin-top: 1.5rem;">
<h3>👥 Ausgabe- & Strichliste (zum Abhaken)</h3>
<table class="print-table">
<thead>
<tr>
<th style="width: 40px; text-align: center;">Abgehakt</th>
<th>Kollege / Name</th>
<th>Bestellte Artikel</th>
<th style="text-align: right;">Betrag</th>
<th style="text-align: center;">Status</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td style="text-align: center; font-size: 1.2rem;">☐</td>
<td>
<strong>{{ order.user.first_name }} {{ order.user.last_name }}</strong>
{% if order.user.role == 'guest' %}<small>(Gast)</small>{% endif %}
</td>
<td>
{% for item in order.items %}
{{ item.quantity }}x {{ item.product.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
</td>
<td style="text-align: right; font-weight: bold;">
{% if event and event.sponsor_name %}
<span style="color: #666; font-size: 9pt;">Gesponsert</span>
{% else %}
{{ "%.2f"|format(order.total_price) }} €
{% endif %}
</td>
<td style="text-align: center;">
{% if event and event.sponsor_name %}
Übernommen ✓
{% elif order.is_paid %}
Bezahlt ✓
{% else %}
Offen
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Ausgabeliste zum Abhaken -->
<table class="print-table">
<thead>
<tr>
<th style="width: 28px; text-align: center;">☐</th>
<th style="width: 25%;">Kollege / Name</th>
<th>Bestellte Artikel</th>
<th style="width: 70px; text-align: right;">Betrag</th>
<th style="width: 85px; text-align: center;">Kasse</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td style="text-align: center; font-size: 10pt;">☐</td>
<td>
<strong>{{ order.display_name }}</strong>
{% if order.user and order.user.role == 'guest' %}<small class="guest-tag">(Gast)</small>{% endif %}
</td>
<td class="print-items-cell">
{% for item in order.items %}
<span>{{ item.quantity }}x {{ item.product.name }}</span>{% if not loop.last %}, {% endif %}
{% endfor %}
</td>
<td style="text-align: right; font-weight: bold;">
{% if event and event.sponsor_name %}
<span style="font-size: 8pt; color: #555;">Sponsor</span>
{% else %}
{{ "%.2f"|format(order.total_price) }} €
{% endif %}
</td>
<td style="text-align: center; font-size: 8pt;">
{% if event and event.sponsor_name %}
Übernommen
{% elif order.is_paid %}
Bezahlt ✓
{% else %}
Offen
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" style="text-align: center;">Keine Bestellungen vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Normal Screen Content -->
<!-- ================= NORMAL SCREEN VIEW ================= -->
<div class="no-print">
<div class="admin-header">
<div>
@@ -97,11 +93,46 @@
<div class="admin-actions-top">
{% if event %}
<button onclick="copyShoppingList()" class="btn-primary">📋 Einkaufsliste kopieren</button>
<button onclick="window.print()" class="btn-secondary">🖨️ Verteilliste drucken</button>
<button onclick="copyGuestPassLink('{{ event.id }}')" class="btn-secondary" title="Gast-Link für Dienstleister">🔗 Dienstleister-Gastlink</button>
<button onclick="window.print()" class="btn-secondary">🖨️ Liste drucken (A4)</button>
{% endif %}
</div>
</div>
{% if success %}
<div class="alert-success" style="margin-bottom: 20px;">
✓ {{ success }}
</div>
{% endif %}
{% if bulk_count %}
<div class="alert-success" style="margin-bottom: 20px;">
✓ Erfolgreich <strong>{{ bulk_count }}</strong> Einladungen generiert! Die Links sind unten aufgeführt.
</div>
{% endif %}
<!-- EVENT-LEISTE (Zeigt alle Events transparent an) -->
<div class="events-ribbon">
<div class="events-ribbon-title">
<span>📅 Brotzeitrunden:</span>
</div>
<div class="events-ribbon-list">
{% for ev in all_events %}
<a href="?tab={{ active_tab }}&event_id={{ ev.id }}" class="event-chip {% if event and event.id == ev.id %}active{% endif %} {% if ev.status == 'completed' %}completed{% endif %}">
<span class="chip-date">{{ ev.event_date.strftime('%d.%m.') }}</span>
<span class="chip-title">{{ ev.title }}</span>
{% if ev.status == 'open' %}
<span class="chip-badge open">🟢 Offen</span>
{% elif ev.status == 'completed' %}
<span class="chip-badge comp">🏁 Abgeschlossen</span>
{% else %}
<span class="chip-badge closed">🔴 Geschlossen</span>
{% endif %}
</a>
{% endfor %}
</div>
</div>
<!-- Tab Navigation -->
<div class="admin-nav-tabs">
<a href="?tab=orders{% if event %}&event_id={{ event.id }}{% endif %}" class="admin-tab-btn {% if active_tab == 'orders' %}active{% endif %}">
@@ -111,30 +142,34 @@
📅 Event- & Frist-Planung
</a>
<a href="?tab=products{% if event %}&event_id={{ event.id }}{% endif %}" class="admin-tab-btn {% if active_tab == 'products' %}active{% endif %}">
🥨 Produkte & Sortiment ({{ products|length }})
🥨 Sortiment ({{ products|length }})
</a>
<a href="?tab=team{% if event %}&event_id={{ event.id }}{% endif %}" class="admin-tab-btn {% if active_tab == 'team' %}active{% endif %}">
👥 Team & Einladungen ({{ team_members|length }})
👥 Team & Massen-Einladung ({{ team_members|length }})
</a>
</div>
<!-- TAB 1: BESTELLUNGEN & KASSE -->
{% if active_tab == 'orders' %}
{% if all_events|length > 1 %}
<div class="event-selector-bar">
<label for="eventSelect">Runde auswählen:</label>
<select id="eventSelect" onchange="window.location.href='?tab=orders&event_id=' + this.value;">
{% for ev in all_events %}
<option value="{{ ev.id }}" {% if event and event.id == ev.id %}selected{% endif %}>
{{ ev.title }} ({{ ev.event_date.strftime('%d.%m.%Y') }}) – {% if ev.status == 'open' %}🟢 Offen{% else %}🔴 Geschlossen{% endif %}
</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if event %}
<!-- Event Action Bar -->
<div class="event-action-bar">
<div class="event-info-left">
<h2>{{ event.title }}</h2>
<span class="text-muted">Termin: <strong>{{ event.event_date.strftime('%d.%m.%Y') }}</strong> | Bestellschluss: <strong>{{ event.deadline.strftime('%d.%m. um %H:%M Uhr') }}</strong></span>
</div>
<div class="event-actions-right">
{% if event.status != 'completed' %}
<form action="/admin/events/{{ event.id }}/complete-and-cleanup" method="POST" style="margin: 0;" onsubmit="return confirm('Möchtest du dieses Event abschließen und die temporären Gastkonten bereinigen?');">
<button type="submit" class="btn-success-small">✅ Event abschließen & Gastkonten aufräumen</button>
</form>
{% else %}
<span class="badge-role" style="background: rgba(34, 197, 94, 0.2); color: #86efac; font-size: 0.85rem;">🏁 Abgeschlossen & Gastkonten archiviert</span>
{% endif %}
</div>
</div>
{% if event.sponsor_name %}
<div class="sponsor-highlight-card">
<div class="sponsor-icon">🎁</div>
@@ -208,7 +243,7 @@
<table class="data-table">
<thead>
<tr>
<th>Kollege</th>
<th>Kollege / Gast</th>
<th>Bestellung</th>
<th>Betrag</th>
<th>Status</th>
@@ -218,12 +253,15 @@
{% for order in orders %}
<tr>
<td>
<strong>{{ order.user.first_name }} {{ order.user.last_name }}</strong>
{% if order.user.role == 'guest' %}
<strong>{{ order.display_name }}</strong>
{% if order.user and order.user.role == 'guest' %}
<span class="badge-role guest">Gast</span>
{% elif not order.user and order.guest_display_name %}
<span class="badge-role" style="background: rgba(148, 163, 184, 0.15); color: #cbd5e1;">Archivierter Gast</span>
{% endif %}
{% if order.user %}
<br><small class="text-muted">{{ order.user.email }}</small>
{% endif %}
<br>
<small class="text-muted">{{ order.user.email }}</small>
</td>
<td>
<ul class="mini-items-list">
@@ -259,19 +297,41 @@
{% else %}
<div class="empty-state">
<h2>Keine offene Bestellrunde gefunden</h2>
<p>Erstelle im Reiter <strong>Event- & Frist-Planung</strong> eine neue Runde.</p>
<h2>Keine Runde ausgewählt</h2>
<p>Wähle oben eine Runde aus oder erstelle eine neue.</p>
</div>
{% endif %}
<!-- TAB 2: EVENT- & FRIST-PLANUNG -->
{% elif active_tab == 'event' %}
{% if warn_active_event %}
<div class="alert-warning-card">
<h3>⚠️ Aktives Event läuft bereits!</h3>
<p>
Es gibt aktuell noch das geplante Event <strong>{{ warn_active_event.title }}</strong> am <strong>{{ warn_active_event.event_date.strftime('%d.%m.%Y') }}</strong>.
Möchtest du das bisherige Event bearbeiten oder wirklich ein neues, paralleles Event starten?
</p>
<div style="display: flex; gap: 12px; margin-top: 12px;">
<a href="?tab=orders&event_id={{ warn_active_event.id }}" class="btn-primary">Zum bisherigen Event</a>
<button type="button" class="btn-secondary" onclick="document.getElementById('forceInput').value='true'; document.getElementById('newEventForm').submit();">Trotzdem neues Event anlegen</button>
</div>
</div>
{% endif %}
<div class="admin-columns">
<!-- Edit current event -->
{% if event %}
<div class="admin-card">
<h2>✏️ Aktives Event bearbeiten</h2>
<p class="card-hint">Passe Titel, Frist, Gast-Erlaubnis und Sponsor an:</p>
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h2>✏️ Event bearbeiten</h2>
<p class="card-hint">Aktuelle Runde: <strong>{{ event.title }}</strong></p>
</div>
<form action="/admin/events/{{ event.id }}/delete" method="POST" onsubmit="return confirm('Event und zugehörige Bestellungen wirklich löschen?');">
<button type="submit" class="btn-revoke-small" style="font-size: 0.8rem;" title="Event löschen">🗑️ Löschen</button>
</form>
</div>
<form action="/admin/events/{{ event.id }}/edit" method="POST" class="admin-form">
<div class="input-group">
@@ -295,12 +355,11 @@
<div class="input-group">
<label for="edit_sponsor">🎉 Sponsor / Anlass (Optional)</label>
<input type="text" id="edit_sponsor" name="sponsor_name" value="{{ event.sponsor_name or '' }}" placeholder="z. B. Maxi (Geburtstag) oder Firma übernimmt">
<small class="form-hint">Falls ausgefüllt, übernimmt der Sponsor die Gesamtrechnung statt Einzelabrechnung.</small>
</div>
<div class="input-group-checkbox">
<label>
<input type="checkbox" name="allow_guests" value="true" {% if event.allow_guests %}checked{% endif %}>
<strong>Gäste-Bestellungen erlauben</strong> (Mitbestellen ohne Registrierung)
<strong>Gäste- & Dienstleister-Bestellungen erlauben</strong>
</label>
</div>
<div class="input-group" style="margin-top: 14px;">
@@ -308,6 +367,7 @@
<select id="edit_status" name="status_val">
<option value="open" {% if event.status == 'open' %}selected{% endif %}>🟢 Geöffnet (Bestellungen möglich)</option>
<option value="closed" {% if event.status == 'closed' %}selected{% endif %}>🔴 Geschlossen (Bestellungen gesperrt)</option>
<option value="completed" {% if event.status == 'completed' %}selected{% endif %}>🏁 Abgeschlossen (Archiviert)</option>
</select>
</div>
<button type="submit" class="btn-primary" style="margin-top: 1rem;">Änderungen speichern</button>
@@ -320,7 +380,8 @@
<h2>➕ Neue Brotzeitrunde planen</h2>
<p class="card-hint">Erstelle einen neuen Termin für dein Team:</p>
<form action="/admin/events/new" method="POST" class="admin-form">
<form action="/admin/events/new" method="POST" id="newEventForm" class="admin-form">
<input type="hidden" id="forceInput" name="force" value="false">
<div class="input-group">
<label for="new_title">Titel</label>
<input type="text" id="new_title" name="title" placeholder="z. B. Weißwurstfrühstück am Freitag" required>
@@ -346,7 +407,7 @@
<div class="input-group-checkbox">
<label>
<input type="checkbox" name="allow_guests" value="true" checked>
<strong>Gäste-Bestellungen erlauben</strong>
<strong>Gäste- & Dienstleister-Bestellungen erlauben</strong>
</label>
</div>
<button type="submit" class="btn-primary" style="margin-top: 1rem;">Runde jetzt starten</button>
@@ -357,7 +418,6 @@
<!-- TAB 3: PRODUKTE & SORTIMENT -->
{% elif active_tab == 'products' %}
<div class="admin-columns">
<!-- Product List -->
<div class="admin-card" style="grid-column: 1 / -1;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div>
@@ -442,11 +502,10 @@
<button type="submit" class="btn-primary" style="margin-top: 10px;">Artikel speichern</button>
</form>
</div>
</div>
</div>
<!-- Edit Product Modal (Simple JS) -->
<!-- Edit Product Modal -->
<div id="editProductModal" class="modal-backdrop" style="display: none;">
<div class="modal-card">
<h3>✏️ Produkt bearbeiten</h3>
@@ -481,51 +540,83 @@
</div>
</div>
<!-- TAB 4: TEAM & EINLADUNGEN -->
<!-- TAB 4: TEAM & MASSEN-EINLADUNG -->
{% elif active_tab == 'team' %}
<div class="admin-columns">
<!-- Generate invite link -->
<!-- Einladungs-Bereich (Einzeln & Massen-Einladung) -->
<div class="admin-card">
<h2>✉️ Teammitglied einladen</h2>
<p class="card-hint">Erzeuge einen Einladungslink (einmalig oder unbegrenzt dauerhaft):</p>
<h2>✉️ Team einladen</h2>
<!-- Bulk Invite Form -->
<div class="bulk-invite-box">
<h3>⚡ Massen-Einladung (Mehrere E-Mails)</h3>
<p class="card-hint">Füge mehrere Adressen kommagetrennt oder zeilenweise ein (z. B. aus Outlook/Slack kopiert):</p>
<form action="/admin/invitations/bulk" method="POST" class="admin-form">
<div class="input-group">
<textarea name="emails_text" rows="3" placeholder="anna@firma.de, max@firma.de, lisa@firma.de" required style="width: 100%; padding: 10px; background: rgba(11, 15, 25, 0.8); border: 1px solid var(--border-color); border-radius: var(--radius-md); color: #fff;"></textarea>
</div>
<div class="form-row-2">
<div class="input-group">
<label>Rolle</label>
<select name="role">
<option value="user" selected>👤 Mitarbeiter</option>
<option value="admin">👨‍🍳 Admin</option>
</select>
</div>
<div class="input-group">
<label>Gültigkeit</label>
<select name="valid_days">
<option value="14" selected>14 Tage</option>
<option value="30">30 Tage</option>
<option value="unlimited">∞ Unbegrenzt</option>
</select>
</div>
</div>
<button type="submit" class="btn-primary" style="margin-top: 6px;">✉️ Alle einladen</button>
</form>
</div>
<hr class="divider">
<!-- Single / Open Invite Link Form -->
<h3>🔗 Offenen Einladungslink generieren</h3>
<p class="card-hint">Dauerhafter Link für das ganze Team (z. B. Pinned Message im Chat):</p>
<form action="/admin/invitations/create" method="POST" class="admin-form">
<div class="input-group">
<label for="inv_email">E-Mail-Adresse (Optional)</label>
<input type="email" id="inv_email" name="email" placeholder="kollege@firma.de (optional)">
<small class="form-hint">Freilassen für einen offenen Link, den der Kollege selbst ausfüllt.</small>
</div>
<div class="form-row-2">
<div class="input-group">
<label for="inv_role">Rolle</label>
<select id="inv_role" name="role">
<option value="user" selected>👤 Mitarbeiter (Besteller)</option>
<option value="admin">👨‍🍳 Organisator (Admin)</option>
<label>Rolle</label>
<select name="role">
<option value="user" selected>👤 Mitarbeiter</option>
<option value="admin">👨‍🍳 Admin</option>
</select>
</div>
<div class="input-group">
<label for="inv_valid">Gültigkeit</label>
<select id="inv_valid" name="valid_days">
<label>Gültigkeit</label>
<select name="valid_days">
<option value="unlimited" selected>∞ Unbegrenzt (Dauerhafter Team-Link)</option>
<option value="7">7 Tage</option>
<option value="14">14 Tage</option>
<option value="30">30 Tage</option>
</select>
</div>
</div>
<button type="submit" class="btn-primary" style="margin-top: 1rem;">🔗 Einladungslink generieren</button>
<button type="submit" class="btn-secondary">🔗 Offenen Link generieren</button>
</form>
{% if pending_invitations %}
<h3 style="margin-top: 2rem;">Aktive Einladungslinks</h3>
<div class="invitations-list">
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2rem;">
<h3>Aktive Einladungslinks ({{ pending_invitations|length }})</h3>
<button type="button" class="btn-copy-small" onclick="copyAllInviteLinks()">📋 Alle Links kopieren</button>
</div>
<div class="invitations-list" id="invitationsListContainer">
{% for inv in pending_invitations %}
<div class="invite-item">
<div class="invite-item" data-token="{{ inv.token }}" data-email="{{ inv.email or 'Team' }}">
<div class="invite-info">
<strong>{% if inv.email %}{{ inv.email }}{% else %}Offener Team-Link{% endif %}</strong>
<strong>{% if inv.email %}{{ inv.email }}{% else %}Dauerhafter Team-Link{% endif %}</strong>
<span class="badge-role">{{ inv.role }}</span>
{% if inv.is_reusable %}
<span class="badge-role" style="background: rgba(34, 197, 94, 0.2); color: #86efac;">Dauerhaft wiederverwendbar</span>
<span class="badge-role" style="background: rgba(34, 197, 94, 0.2); color: #86efac;">Dauerhaft</span>
{% endif %}
<br>
<small class="text-muted">
@@ -547,7 +638,7 @@
<!-- Current team members list -->
<div class="admin-card">
<h2>👥 Registrierte Teammitglieder ({{ team_members|length }})</h2>
<p class="card-hint">Alle aktiven Konten dieses Portals:</p>
<p class="card-hint">Feste Teamkonten dieses Portals:</p>
<div class="orders-table-wrapper">
<table class="data-table">
@@ -568,8 +659,6 @@
<td>
{% if member.role == 'admin' %}
<span class="badge-role admin">Admin</span>
{% elif member.role == 'guest' %}
<span class="badge-role guest">Gast</span>
{% else %}
<span class="badge-role">Mitarbeiter</span>
{% endif %}
@@ -589,6 +678,32 @@
</tbody>
</table>
</div>
{% if active_guests %}
<h3 style="margin-top: 2rem;">👤 Aktive Gäste (für anstehende Events)</h3>
<p class="card-hint">Werden beim Klick auf „Event abschließen & aufräumen“ automatisch archiviert:</p>
<div class="orders-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>E-Mail / Kennung</th>
<th>Rolle</th>
</tr>
</thead>
<tbody>
{% for guest in active_guests %}
<tr>
<td><strong>{{ guest.first_name }} {{ guest.last_name }}</strong></td>
<td><small class="text-muted">{{ guest.email }}</small></td>
<td><span class="badge-role guest">Gast</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
@@ -634,7 +749,29 @@
const origin = window.location.origin;
const url = origin + '/join?token=' + token;
navigator.clipboard.writeText(url).then(() => {
alert("Einladungslink kopiert!\n\n" + url + "\n\nDu kannst ihn jetzt an das Team senden.");
alert("Einladungslink kopiert!\n\n" + url + "\n\nDu kannst ihn jetzt an den Kollegen senden.");
});
}
function copyGuestPassLink(eventId) {
const origin = window.location.origin;
const url = origin + '/?event_id=' + eventId + '&guest=1';
navigator.clipboard.writeText(url).then(() => {
alert("Dienstleister-Gastlink kopiert!\n\n" + url + "\n\nExterne können damit für diesen Termin mitbestellen, ohne ein Konto anzulegen.");
});
}
function copyAllInviteLinks() {
const origin = window.location.origin;
const items = document.querySelectorAll('#invitationsListContainer .invite-item');
let text = "🥨 Einladungslinks für Schmankerl:\n\n";
items.forEach(el => {
const email = el.dataset.email;
const token = el.dataset.token;
text += email + ": " + origin + "/join?token=" + token + "\n";
});
navigator.clipboard.writeText(text).then(() => {
alert("Alle Einladungslinks in die Zwischenablage kopiert!");
});
}
+23 -8
View File
@@ -3,6 +3,20 @@
{% block content %}
<div class="container">
<!-- Multi-Event Picker (wenn mehrere anstehende Runden offen sind) -->
{% if open_events|length > 1 %}
<div class="multi-event-banner">
<span>📅 Weitere offene Termine:</span>
<div class="multi-event-chips">
{% for ev in open_events %}
<a href="/?event_id={{ ev.id }}{% if is_guest_mode %}&guest=1{% endif %}" class="mini-chip {% if event and event.id == ev.id %}active{% endif %}">
{{ ev.event_date.strftime('%d.%m.') }} – {{ ev.title }}
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Header Banner -->
{% if event %}
<div class="banner {% if is_closed %}banner-closed{% endif %}">
@@ -76,9 +90,9 @@
<!-- Right: User Details & Summary -->
<div class="summary-section">
<div class="summary-card">
<h2>2. Bestellung & Angaben</h2>
<h2>2. Angaben & Übersicht</h2>
{% if current_user %}
{% if current_user and not is_guest_mode %}
<div class="logged-in-badge">
<div class="user-avatar">👤</div>
<div>
@@ -92,18 +106,19 @@
<!-- Gast Angaben -->
<div class="guest-order-box">
<div class="privacy-note" style="margin-bottom: 12px;">
👋 <strong>Gast-Bestellung:</strong> Du bestellst ohne dauerhaftes Benutzerkonto für diesen Termin mit.
👋 <strong>Gast- & Dienstleister-Bestellung:</strong><br>
Du bestellst ohne dauerhaftes Konto für diesen Termin ({{ event.event_date.strftime('%d.%m.') }}) mit.
</div>
<div class="input-group">
<label for="g_first_name">Dein Vorname *</label>
<input type="text" id="g_first_name" name="guest_first_name" required placeholder="z. B. Christian">
<label for="g_first_name">Vorname / Name *</label>
<input type="text" id="g_first_name" name="guest_first_name" required placeholder="z. B. Tobias">
</div>
<div class="input-group">
<label for="g_last_name">Nachname (optional)</label>
<input type="text" id="g_last_name" name="guest_last_name" placeholder="z. B. Bauer">
<label for="g_last_name">Firma / Abteilung (optional)</label>
<input type="text" id="g_last_name" name="guest_last_name" placeholder="z. B. IT-Service Müller">
</div>
<div class="input-group">
<label for="g_email">E-Mail (optional für Bestätigung)</label>
<label for="g_email">E-Mail (optional)</label>
<input type="email" id="g_email" name="guest_email" placeholder="name@firma.de">
</div>
</div>