Initial commit: Schmankerl v2 - Multi-tenant FastAPI Brotzeitportal
This commit is contained in:
@@ -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 %}
|
||||
@@ -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>© 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>
|
||||
@@ -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> |
|
||||
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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user