feat: Paket 1 - Token-based invites, event & deadline management with live countdown, printable distribution sheet

This commit is contained in:
2026-09-21 11:16:44 +02:00
parent 4f5ef164c3
commit 83393e363e
7 changed files with 1203 additions and 358 deletions
+367 -102
View File
@@ -3,121 +3,378 @@
{% 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>
<!-- Printable distribution sheet for the kitchen (hidden in screen, visible in print) -->
<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>
</div>
<div class="admin-actions-top">
<button onclick="copyShoppingList()" class="btn-primary">📋 Einkaufsliste kopieren</button>
<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>
</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></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;">{{ "%.2f"|format(order.total_price) }} €</td>
<td style="text-align: center;">{% if order.is_paid %}Bezahlt ✓{% else %}Offen{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
<!-- Normal Screen Content -->
<div class="no-print">
<div class="admin-header">
<div>
<h1>👨‍🍳 Einkäufer- & Admin-Cockpit</h1>
<p class="admin-sub">Portal: <strong>{{ tenant.name }}</strong></p>
</div>
<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>
{% endif %}
</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>
<!-- 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 %}">
🛒 Bestellungen & Kasse
</a>
<a href="?tab=event{% if event %}&event_id={{ event.id }}{% endif %}" class="admin-tab-btn {% if active_tab == 'event' %}active{% endif %}">
📅 Event- & Frist-Planung
</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 }})
</a>
</div>
<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>
<!-- 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 %}
<!-- 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 }} Kollegen</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">Aufsummierte Mengen für den Einkauf:</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 bezahlt/offen umzuschalten:</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>Erstelle im Reiter <strong>Event- & Frist-Planung</strong> eine neue Runde.</p>
</div>
{% endif %}
<!-- TAB 2: EVENT- & FRIST-PLANUNG -->
{% elif active_tab == 'event' %}
<div class="admin-columns">
<!-- Edit current event -->
{% if event %}
<div class="admin-card">
<h2>✏️ Aktives Event bearbeiten</h2>
<p class="card-hint">Passe Titel, Termin und die Bestellfrist sekundengenau an:</p>
<form action="/admin/events/{{ event.id }}/edit" method="POST" class="admin-form">
<div class="input-group">
<label for="edit_title">Titel der Runde</label>
<input type="text" id="edit_title" name="title" value="{{ event.title }}" required>
</div>
<div class="input-group">
<label for="edit_event_date">Brotzeit-Tag (Datum)</label>
<input type="date" id="edit_event_date" name="event_date" value="{{ event.event_date.strftime('%Y-%m-%d') }}" required>
</div>
<div class="form-row-2">
<div class="input-group">
<label for="edit_deadline_date">Bestellschluss (Tag)</label>
<input type="date" id="edit_deadline_date" name="deadline_date" value="{{ event.deadline.strftime('%Y-%m-%d') }}" required>
</div>
<div class="input-group">
<label for="edit_deadline_time">Bestellschluss (Uhrzeit)</label>
<input type="time" id="edit_deadline_time" name="deadline_time" value="{{ event.deadline.strftime('%H:%M') }}" required>
</div>
</div>
<div class="input-group">
<label for="edit_status">Status der Runde</label>
<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>
</select>
</div>
<button type="submit" class="btn-primary" style="margin-top: 1rem;">Änderungen speichern</button>
</form>
</div>
{% endif %}
<!-- Create new event -->
<div class="admin-card">
<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">
<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>
</div>
<div class="input-group">
<label for="new_event_date">Brotzeit-Tag (Datum)</label>
<input type="date" id="new_event_date" name="event_date" required>
</div>
<div class="form-row-2">
<div class="input-group">
<label for="new_deadline_date">Bestellschluss (Tag)</label>
<input type="date" id="new_deadline_date" name="deadline_date" required>
</div>
<div class="input-group">
<label for="new_deadline_time">Bestellschluss (Uhrzeit)</label>
<input type="time" id="new_deadline_time" name="deadline_time" value="08:30" required>
</div>
</div>
<button type="submit" class="btn-primary" style="margin-top: 1rem;">Runde jetzt starten</button>
</form>
</div>
</div>
<!-- TAB 3: TEAM & EINLADUNGEN -->
{% elif active_tab == 'team' %}
<div class="admin-columns">
<!-- Generate invite link -->
<div class="admin-card">
<h2>✉️ Teammitglied einladen</h2>
<p class="card-hint">Erzeuge einen sicheren Einladungslink zum Teilen per Chat (Slack, Teams, WhatsApp):</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>
</select>
</div>
<div class="input-group">
<label for="inv_valid">Gültigkeit</label>
<select id="inv_valid" name="valid_days">
<option value="7">7 Tage</option>
<option value="14" selected>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>
</form>
{% if pending_invitations %}
<h3 style="margin-top: 2rem;">Offene Einladungslinks</h3>
<div class="invitations-list">
{% for inv in pending_invitations %}
<div class="invite-item">
<div class="invite-info">
<strong>{% if inv.email %}{{ inv.email }}{% else %}Offener Link{% endif %}</strong>
<span class="badge-role">{{ inv.role }}</span>
<br>
<small class="text-muted">Gültig bis {{ inv.expires_at.strftime('%d.%m.%Y') }}</small>
</div>
<div class="invite-actions">
<button type="button" class="btn-copy-small" onclick="copyInviteUrl('{{ inv.token }}')">📋 Kopieren</button>
<form action="/admin/invitations/{{ inv.id }}/revoke" method="POST" style="margin: 0; display: inline;">
<button type="submit" class="btn-revoke-small" title="Link widerrufen">✕</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="4" class="empty-cell">Noch keine Bestellungen vorhanden.</td></tr>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<!-- 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>
<div class="orders-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>Rolle</th>
</tr>
</thead>
<tbody>
{% for member in team_members %}
<tr>
<td><strong>{{ member.first_name }} {{ member.last_name }}</strong></td>
<td><small class="text-muted">{{ member.email }}</small></td>
<td>
{% if member.role == 'admin' %}
<span class="badge-role admin">Admin</span>
{% else %}
<span class="badge-role">Mitarbeiter</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="empty-state">
<h2>Keine offene Bestellrunde gefunden</h2>
<p>Aktuell gibt es kein aktives Event.</p>
</div>
{% endif %}
</div>
<script>
@@ -132,5 +389,13 @@
alert("Einkaufsliste in die Zwischenablage kopiert! Kann direkt in WhatsApp oder Slack eingefügt werden.");
});
}
function copyInviteUrl(token) {
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 den Kollegen senden.");
});
}
</script>
{% endblock %}