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
+97 -19
View File
@@ -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> &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 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 %}