feat: Paket 1 - Token-based invites, event & deadline management with live countdown, printable distribution sheet
This commit is contained in:
+367
-102
@@ -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' }} |
|
||||
<strong>Datum:</strong> {{ event.event_date.strftime('%d.%m.%Y') if event else '' }} |
|
||||
<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 %}
|
||||
|
||||
+97
-19
@@ -5,18 +5,34 @@
|
||||
|
||||
<!-- Header Banner -->
|
||||
{% if event %}
|
||||
<div class="banner">
|
||||
<div class="banner-badge">🟢 Runde geöffnet</div>
|
||||
<div class="banner {% if is_closed %}banner-closed{% endif %}">
|
||||
<div class="banner-top-row">
|
||||
<div class="banner-badge {% if is_closed %}closed{% endif %}">
|
||||
{% if is_closed %}🔴 Runde geschlossen{% else %}🟢 Runde geöffnet{% endif %}
|
||||
</div>
|
||||
<div class="countdown-container" id="countdownContainer">
|
||||
<span class="countdown-label">Frist:</span>
|
||||
<span class="countdown-timer" id="countdownTimer">Berechne...</span>
|
||||
</div>
|
||||
</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 von Kollegen eingegangen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_closed %}
|
||||
<div class="closed-notice-bar">
|
||||
🔒 <strong>Bestellschluss erreicht:</strong> Diese Bestellrunde ist geschlossen. Änderungen oder neue Bestellungen sind nicht mehr möglich. Wende dich bei Notfällen direkt an den Organisator.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Order Form -->
|
||||
<form action="/order" method="POST" class="order-form" id="orderForm">
|
||||
<input type="hidden" name="event_id" value="{{ event.id }}">
|
||||
@@ -28,16 +44,16 @@
|
||||
<div class="product-list">
|
||||
{% for product in products %}
|
||||
{% set current_qty = user_items.get(product.id, 0) %}
|
||||
<div class="product-card" data-price="{{ product.price }}" data-id="{{ product.id }}">
|
||||
<div class="product-card {% if is_closed %}disabled{% endif %}" 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="{{ current_qty }}" min="0" max="50" class="qty-input" onchange="updateTotal()">
|
||||
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, 1)">+</button>
|
||||
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, -1)" {% if is_closed %}disabled{% endif %}>-</button>
|
||||
<input type="number" id="qty_{{ product.id }}" name="qty_{{ product.id }}" value="{{ current_qty }}" min="0" max="50" class="qty-input" onchange="updateTotal()" {% if is_closed %}disabled{% endif %}>
|
||||
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, 1)" {% if is_closed %}disabled{% endif %}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -59,7 +75,8 @@
|
||||
|
||||
{% if existing_order %}
|
||||
<div class="notice-box">
|
||||
ℹ️ Du hast bereits für diese Runde bestellt. Du kannst deine Mengen hier anpassen und aktualisieren.
|
||||
ℹ️ Du hast bereits für diese Runde bestellt ({% if existing_order.is_paid %}<strong>Bereits bezahlt ✓</strong>{% else %}Noch offen{% endif %}).
|
||||
{% if not is_closed %}Du kannst deine Mengen anpassen und aktualisieren.{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -70,14 +87,21 @@
|
||||
<div class="total-amount" id="totalAmount">0,00 €</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit" id="submitBtn">
|
||||
{% if existing_order %}
|
||||
<button type="submit" class="btn-submit" id="submitBtn" {% if is_closed %}disabled{% endif %}>
|
||||
{% if is_closed %}
|
||||
🔒 Bestellschluss vorüber
|
||||
{% elif existing_order %}
|
||||
🥨 Bestellung aktualisieren
|
||||
{% else %}
|
||||
🥨 Bestellung verbindlich abschicken
|
||||
{% endif %}
|
||||
</button>
|
||||
<small class="form-hint">Du kannst deine Bestellung bis zum Stichtag jederzeit hier im Portal anpassen.</small>
|
||||
|
||||
{% if not is_closed %}
|
||||
<small class="form-hint">Du kannst deine Bestellung bis zum Bestellschluss jederzeit hier anpassen.</small>
|
||||
{% else %}
|
||||
<small class="form-hint text-warning">Die Frist ist abgelaufen.</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +113,7 @@
|
||||
<h2>Aktuell keine offene Bestellrunde</h2>
|
||||
<p>Für dieses Team ist im Moment keine Brotzeit geplant.</p>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<a href="/admin" class="btn-primary">Neue Bestellrunde im Cockpit anlegen</a>
|
||||
<a href="/admin?tab=event" class="btn-primary" style="margin-top: 1rem;">Neue Bestellrunde im Cockpit anlegen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -99,6 +123,7 @@
|
||||
<script>
|
||||
function changeQty(productId, delta) {
|
||||
const input = document.getElementById('qty_' + productId);
|
||||
if (!input || input.disabled) return;
|
||||
let val = parseInt(input.value) || 0;
|
||||
val = Math.max(0, val + delta);
|
||||
input.value = val;
|
||||
@@ -112,20 +137,73 @@
|
||||
const price = parseFloat(card.dataset.price) || 0;
|
||||
const id = card.dataset.id;
|
||||
const qtyInput = document.getElementById('qty_' + id);
|
||||
const qty = parseInt(qtyInput.value) || 0;
|
||||
const qty = parseInt(qtyInput ? qtyInput.value : 0) || 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';
|
||||
const totalEl = document.getElementById('totalAmount');
|
||||
if (totalEl) {
|
||||
totalEl.innerText = total.toFixed(2).replace('.', ',') + ' €';
|
||||
}
|
||||
}
|
||||
|
||||
// --- LIVE COUNTDOWN TIMER ---
|
||||
{% if event and not is_closed %}
|
||||
const deadlineMs = new Date("{{ event.deadline.isoformat() }}").getTime();
|
||||
|
||||
function updateCountdown() {
|
||||
const nowMs = new Date().getTime();
|
||||
const diffMs = deadlineMs - nowMs;
|
||||
const timerEl = document.getElementById('countdownTimer');
|
||||
const containerEl = document.getElementById('countdownContainer');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
if (diffMs <= 0) {
|
||||
if (timerEl) timerEl.innerText = "Abgelaufen (geschlossen)";
|
||||
if (containerEl) {
|
||||
containerEl.classList.add('expired');
|
||||
}
|
||||
// Formular sperren
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = "🔒 Bestellschluss vorüber";
|
||||
}
|
||||
document.querySelectorAll('.qty-btn').forEach(btn => btn.disabled = true);
|
||||
document.querySelectorAll('.qty-input').forEach(inp => inp.disabled = true);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(diffMs / 1000);
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
let text = "";
|
||||
if (days > 0) {
|
||||
text = `${days}T ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
} else {
|
||||
text = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
}
|
||||
|
||||
if (timerEl) timerEl.innerText = `⏳ Noch ${text}`;
|
||||
|
||||
if (totalSeconds < 3600 && containerEl) {
|
||||
containerEl.classList.add('urgent');
|
||||
}
|
||||
}
|
||||
|
||||
function pad(num) {
|
||||
return num.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
setInterval(updateCountdown, 1000);
|
||||
updateCountdown();
|
||||
{% elif is_closed %}
|
||||
const timerEl = document.getElementById('countdownTimer');
|
||||
if (timerEl) timerEl.innerText = "Frist beendet";
|
||||
{% endif %}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', updateTotal);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container narrow">
|
||||
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<div class="login-icon">🎉</div>
|
||||
<h1>Team-Einladung</h1>
|
||||
<p class="login-sub">
|
||||
Portal: <strong>{{ tenant.name }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 1.5rem;">
|
||||
<a href="/login" class="btn-primary">Zur Anmeldung</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="privacy-note">
|
||||
👋 Du wurdest eingeladen, am Schmankerl-Brotzeitportal von <strong>{{ tenant.name }}</strong> teilzunehmen. Bitte lege deinen Namen und ein persönliches Passwort fest.
|
||||
</div>
|
||||
|
||||
<form action="/join" method="POST" class="auth-form" style="margin-top: 1.5rem;">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
|
||||
<div class="input-group">
|
||||
<label for="reg_first_name">Vorname</label>
|
||||
<input type="text" id="reg_first_name" name="first_name" required placeholder="z. B. Florian">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="reg_last_name">Nachname</label>
|
||||
<input type="text" id="reg_last_name" name="last_name" required placeholder="z. B. Meier">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="reg_email">E-Mail-Adresse</label>
|
||||
{% if invitation.email %}
|
||||
<input type="email" id="reg_email" name="email" value="{{ invitation.email }}" readonly class="readonly-input">
|
||||
<small class="form-hint">E-Mail durch Einladung vorgegeben.</small>
|
||||
{% else %}
|
||||
<input type="email" id="reg_email" name="email" required placeholder="name@firma.de">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="reg_password">Passwort festlegen</label>
|
||||
<input type="password" id="reg_password" name="password" required minlength="6" placeholder="Mindestens 6 Zeichen">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit" style="margin-top: 1rem;">
|
||||
🥨 Konto aktivieren & Team beitreten
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
+21
-56
@@ -1,16 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Anmelden – Schmankerl{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container narrow">
|
||||
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<div class="login-icon">🥨</div>
|
||||
<h1>Anmeldung erforderlich</h1>
|
||||
<h1>Anmeldung</h1>
|
||||
<p class="login-sub">
|
||||
Portal: <strong>{{ tenant.name if tenant else 'Schmankerl' }}</strong>
|
||||
Portal: <strong>{{ tenant.name }}</strong>
|
||||
</p>
|
||||
<div class="privacy-note">
|
||||
🔒 Aus Datenschutzgründen (DSGVO) sind Bestelllisten und Teilnehmerdaten nur für angemeldete Teammitglieder sichtbar.
|
||||
@@ -18,13 +16,18 @@
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="alert-success">{{ success }}</div>
|
||||
<div class="alert-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 1-Click Demo Login Box (For easy review & testing) -->
|
||||
{% if success %}
|
||||
<div class="alert-success">
|
||||
{{ success }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 1-Click Demo Login Box -->
|
||||
<div class="demo-login-card">
|
||||
<h3>🚀 1-Klick Demo-Zugang</h3>
|
||||
<p class="card-hint">Wähle eine Rolle zum sofortigen Ausprobieren:</p>
|
||||
@@ -46,14 +49,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="auth-tabs">
|
||||
<button class="auth-tab active" id="tabLoginBtn" onclick="switchAuthTab('login')">Anmelden</button>
|
||||
<button class="auth-tab" id="tabRegisterBtn" onclick="switchAuthTab('register')">Neu registrieren</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form action="/login" method="POST" id="formLogin" class="auth-form">
|
||||
<form action="/login" method="POST" id="formLogin" class="auth-form" style="margin-top: 1.5rem;">
|
||||
<div class="input-group">
|
||||
<label for="login_email">E-Mail-Adresse</label>
|
||||
<input type="email" id="login_email" name="email" required placeholder="name@firma.de">
|
||||
@@ -65,49 +62,17 @@
|
||||
<button type="submit" class="btn-submit">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<!-- Register Form (Hidden by default) -->
|
||||
<form action="/register" method="POST" id="formRegister" class="auth-form" style="display: none;">
|
||||
<div class="input-group">
|
||||
<label for="reg_first_name">Vorname</label>
|
||||
<input type="text" id="reg_first_name" name="first_name" required placeholder="z. B. Anna">
|
||||
<div class="invite-notice-card">
|
||||
<div class="invite-icon">✉️</div>
|
||||
<div>
|
||||
<strong>Neu im Team?</strong><br>
|
||||
<small class="text-muted">
|
||||
Registrierungen sind nur auf Einladung möglich. Wende dich an deinen Team-Organisator, um einen persönlichen Einladungslink zu erhalten.
|
||||
</small>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="reg_last_name">Nachname</label>
|
||||
<input type="text" id="reg_last_name" name="last_name" required placeholder="z. B. Huber">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="reg_email">E-Mail-Adresse</label>
|
||||
<input type="email" id="reg_email" name="email" required placeholder="anna@firma.de">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="reg_password">Passwort festlegen</label>
|
||||
<input type="password" id="reg_password" name="password" required minlength="6" placeholder="Mindestens 6 Zeichen">
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Konto erstellen & anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchAuthTab(tab) {
|
||||
const loginForm = document.getElementById('formLogin');
|
||||
const regForm = document.getElementById('formRegister');
|
||||
const loginBtn = document.getElementById('tabLoginBtn');
|
||||
const regBtn = document.getElementById('tabRegisterBtn');
|
||||
|
||||
if (tab === 'login') {
|
||||
loginForm.style.display = 'block';
|
||||
regForm.style.display = 'none';
|
||||
loginBtn.classList.add('active');
|
||||
regBtn.classList.remove('active');
|
||||
} else {
|
||||
loginForm.style.display = 'none';
|
||||
regForm.style.display = 'block';
|
||||
loginBtn.classList.remove('active');
|
||||
regBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user