feat: Paket 1 - Token-based invites, event & deadline management with live countdown, printable distribution sheet
This commit is contained in:
+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 %}
|
||||
|
||||
Reference in New Issue
Block a user