Initial commit: Schmankerl v2 - Multi-tenant FastAPI Brotzeitportal

This commit is contained in:
2026-09-21 10:05:43 +02:00
commit 8200857754
11 changed files with 1294 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="admin-header">
<div>
<h1>👨‍🍳 Einkäufer-Cockpit</h1>
<p class="admin-sub">Mandant: <strong>{{ tenant.name }}</strong> | Aktuelle Brotzeitrunde</p>
</div>
<div class="admin-actions-top">
<button onclick="copyShoppingList()" class="btn-primary">📋 Einkaufsliste kopieren</button>
</div>
</div>
{% if event %}
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Gesamtsumme Einkauf</div>
<div class="stat-value">{{ "%.2f"|format(total_revenue) }} €</div>
</div>
<div class="stat-card">
<div class="stat-label">Bereits bezahlt</div>
<div class="stat-value success">{{ "%.2f"|format(total_paid) }} €</div>
</div>
<div class="stat-card">
<div class="stat-label">Noch offen</div>
<div class="stat-value warning">{{ "%.2f"|format(total_revenue - total_paid) }} €</div>
</div>
<div class="stat-card">
<div class="stat-label">Teilnehmer</div>
<div class="stat-value">{{ orders|length }} Personen</div>
</div>
</div>
<div class="admin-columns">
<!-- Aggregated Shopping List -->
<div class="admin-card">
<h2>🛒 Sammelliste für Metzger & Bäcker</h2>
<p class="card-hint">Exakt aufsummierte Gesamtmengen aller Kollegen:</p>
<div class="shopping-table-wrapper">
<table class="data-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><strong>{{ name }}</strong></td>
<td style="text-align: right; font-size: 1.15rem; font-weight: 700; color: var(--accent);">
{{ data.quantity }} {{ data.unit }}
</td>
</tr>
{% else %}
<tr><td colspan="2" class="empty-cell">Noch keine Bestellungen eingegangen.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Individual Orders & Payment Checklist -->
<div class="admin-card">
<h2>👥 Wer hat bestellt? (Strichliste)</h2>
<p class="card-hint">Klicke auf den Status, um den Bezahlstatus zu ändern:</p>
<div class="orders-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Kollege</th>
<th>Bestellung</th>
<th>Betrag</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>
<strong>{{ order.user.first_name }} {{ order.user.last_name }}</strong><br>
<small class="text-muted">{{ order.user.email }}</small>
</td>
<td>
<ul class="mini-items-list">
{% for item in order.items %}
<li>{{ item.quantity }}x {{ item.product.name }}</li>
{% endfor %}
</ul>
</td>
<td><strong>{{ "%.2f"|format(order.total_price) }} €</strong></td>
<td>
<form action="/admin/toggle-paid/{{ order.id }}" method="POST" style="margin: 0;">
<button type="submit" class="badge-btn {% if order.is_paid %}paid{% else %}unpaid{% endif %}">
{% if order.is_paid %}✓ Bezahlt{% else %}⏳ Offen{% endif %}
</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="4" class="empty-cell">Noch keine Bestellungen vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="empty-state">
<h2>Keine offene Bestellrunde gefunden</h2>
<p>Aktuell gibt es kein aktives Event.</p>
</div>
{% endif %}
</div>
<script>
function copyShoppingList() {
let text = "🥨 Einkaufsliste für {{ event.title if event else 'Brotzeit' }}:\n\n";
{% for name, data in item_sums.items() %}
text += "• {{ name }}: {{ data.quantity }} {{ data.unit }}\n";
{% endfor %}
text += "\nGesamtsumme: {{ '%.2f'|format(total_revenue) }} €";
navigator.clipboard.writeText(text).then(() => {
alert("Einkaufsliste in die Zwischenablage kopiert! Kann direkt in WhatsApp oder Slack eingefügt werden.");
});
}
</script>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Schmankerl – Das moderne Brotzeit-Portal{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" href="https://new.dev.waiswuaschd.de/favicon.ico">
</head>
<body>
<header class="navbar">
<div class="container nav-content">
<a href="/" class="brand">
🥨 <span>Schmankerl</span> <small class="tenant-badge">{{ tenant.name if tenant else 'Portal' }}</small>
</a>
<nav class="nav-links">
<a href="/">Brotzeit bestellen</a>
<a href="/admin" class="nav-admin">👨‍🍳 Einkäufer-Cockpit</a>
<a href="https://new.dev.waiswuaschd.de" target="_blank" class="nav-home">Zur Plattform</a>
</nav>
</div>
</header>
<main class="main-content">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<div class="container footer-inner">
<p>&copy; 2026 Schmankerl by <a href="https://new.dev.waiswuaschd.de">Waiswuaschd.de</a></p>
<div class="footer-tenant-info">
Mandant: <strong>{{ tenant.slug if tenant else 'demo' }}</strong> ({{ tenant.subdomain if tenant else '' }})
</div>
</div>
</footer>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<!-- Header Banner -->
{% if event %}
<div class="banner">
<div class="banner-badge">🟢 Runde geöffnet</div>
<h1>{{ event.title }}</h1>
<p class="banner-sub">
Brotzeit-Tag: <strong>{{ event.event_date.strftime('%d.%m.%Y') }}</strong> &nbsp;|&nbsp;
Bestellschluss: <strong>{{ event.deadline.strftime('%d.%m.%Y um %H:%M Uhr') }}</strong>
</p>
<div class="banner-stats">
<span>👥 Bereits <strong>{{ order_count }}</strong> Bestellungen eingegangen</span>
</div>
</div>
<!-- Order Form -->
<form action="/order" method="POST" class="order-form" id="orderForm">
<input type="hidden" name="event_id" value="{{ event.id }}">
<div class="form-grid">
<!-- Left: Products -->
<div class="products-section">
<h2>1. Brotzeit auswählen</h2>
<div class="product-list">
{% for product in products %}
<div class="product-card" data-price="{{ product.price }}" data-id="{{ product.id }}">
<div class="product-info">
<h3>{{ product.name }}</h3>
<p class="product-desc">{{ product.description }}</p>
<div class="product-price">{{ "%.2f"|format(product.price) }} € <small>/ {{ product.unit }}</small></div>
</div>
<div class="quantity-control">
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, -1)">-</button>
<input type="number" id="qty_{{ product.id }}" name="qty_{{ product.id }}" value="0" min="0" max="50" class="qty-input" onchange="updateTotal()">
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, 1)">+</button>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Right: User Details & Summary -->
<div class="summary-section">
<div class="summary-card">
<h2>2. Deine Angaben</h2>
<div class="input-group">
<label for="first_name">Vorname</label>
<input type="text" id="first_name" name="first_name" required placeholder="z. B. Maxi">
</div>
<div class="input-group">
<label for="last_name">Nachname</label>
<input type="text" id="last_name" name="last_name" required placeholder="z. B. Muster">
</div>
<div class="input-group">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" required placeholder="maxi@beispiel.de">
</div>
<hr class="divider">
<div class="order-total-box">
<div class="total-label">Gesamtsumme:</div>
<div class="total-amount" id="totalAmount">0,00 €</div>
</div>
<button type="submit" class="btn-submit" id="submitBtn">
🥨 Bestellung verbindlich abschicken
</button>
<small class="form-hint">Du kannst deine Bestellung bis zum Stichtag jederzeit mit derselben E-Mail-Adresse ändern.</small>
</div>
</div>
</div>
</form>
{% else %}
<div class="empty-state">
<div class="empty-icon">🏖️</div>
<h2>Aktuell keine offene Bestellrunde</h2>
<p>Für dieses Team ist im Moment keine Brotzeit geplant.</p>
<a href="/admin" class="btn-primary">Neue Bestellrunde im Cockpit anlegen</a>
</div>
{% endif %}
</div>
<script>
function changeQty(productId, delta) {
const input = document.getElementById('qty_' + productId);
let val = parseInt(input.value) || 0;
val = Math.max(0, val + delta);
input.value = val;
updateTotal();
}
function updateTotal() {
let total = 0;
const cards = document.querySelectorAll('.product-card');
cards.forEach(card => {
const price = parseFloat(card.dataset.price) || 0;
const id = card.dataset.id;
const qtyInput = document.getElementById('qty_' + id);
const qty = parseInt(qtyInput.value) || 0;
total += price * qty;
});
document.getElementById('totalAmount').innerText = total.toFixed(2).replace('.', ',') + ' €';
const submitBtn = document.getElementById('submitBtn');
if (total > 0) {
submitBtn.removeAttribute('disabled');
submitBtn.style.opacity = '1';
} else {
submitBtn.style.opacity = '0.7';
}
}
document.addEventListener('DOMContentLoaded', updateTotal);
</script>
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<div class="container narrow">
<div class="empty-state">
<div class="empty-icon">🔍</div>
<h1>Mandant nicht gefunden</h1>
<p>Für diese Subdomain wurde noch kein aktives Brotzeit-Portal eingerichtet.</p>
<a href="https://new.dev.waiswuaschd.de" class="btn-primary">Zur Waiswuaschd-Hauptseite</a>
</div>
</div>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block content %}
<div class="container narrow">
<div class="success-card">
<div class="success-icon">🎉</div>
<h1>Bestellung erfolgreich erfasst!</h1>
<p class="success-sub">Vielen Dank, <strong>{{ order.user.first_name }} {{ order.user.last_name }}</strong>!</p>
<p>Deine Brotzeit für <strong>{{ order.event.title }}</strong> ist fest reserviert.</p>
<div class="order-receipt">
<h3>Deine ausgewählten Schmankerl:</h3>
<ul class="receipt-list">
{% for item in order.items %}
<li>
<span><strong>{{ item.quantity }}x</strong> {{ item.product.name }}</span>
<span>{{ "%.2f"|format(item.unit_price * item.quantity) }} €</span>
</li>
{% endfor %}
</ul>
<div class="receipt-total">
<span>Gesamtbetrag:</span>
<span class="total-price">{{ "%.2f"|format(order.total_price) }} €</span>
</div>
</div>
<div class="success-actions">
<a href="/" class="btn-primary">Zurück zur Übersicht</a>
<a href="/admin" class="btn-outline">Zum Einkäufer-Cockpit</a>
</div>
</div>
</div>
{% endblock %}