feat: add email notifications, order references, stock badges, and sale flags

- Add customer email field and PayPal payment reminder emails with order reference (HEL-YEAR-ID format)
- Display stock availability with color-coded badges (available/low/unavailable)
- Add sale/clearance flag with animated red badge overlay
- Implement automatic fallback placeholder for missing/broken product images
- Add email column to order management view

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 13:56:16 +01:00
parent 99ecea1de9
commit 832aaf2b05
7 changed files with 417 additions and 55 deletions

View File

@@ -133,6 +133,50 @@
}
.card-price { color: var(--accent); font-weight: 800; font-size: 14px; }
.card-sub { color: var(--muted); font-size: 12px; }
.stock-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
margin-top: 6px;
}
.stock-badge.available {
background: rgba(123, 213, 141, 0.15);
color: var(--ok);
border: 1px solid rgba(123, 213, 141, 0.3);
}
.stock-badge.low {
background: rgba(243, 213, 42, 0.15);
color: var(--accent);
border: 1px solid rgba(243, 213, 42, 0.3);
}
.stock-badge.unavailable {
background: rgba(255, 107, 125, 0.15);
color: var(--bad);
border: 1px solid rgba(255, 107, 125, 0.3);
}
.sale-badge {
position: absolute;
top: 8px;
left: 8px;
background: linear-gradient(135deg, #ff0000, #cc0000);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
box-shadow: 0 4px 12px rgba(255, 0, 0, 0.4);
z-index: 10;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.card-tile { position: relative; }
.card-actions { padding: 0 12px 12px; display: flex; justify-content: flex-end; }
.detail-btn { width: 100%; }
.size-list { display: grid; gap: 8px; padding: 10px 2px 2px; }
@@ -422,6 +466,9 @@
<label>Handy
<input id="fHandy" name="handy" required />
</label>
<label>E-Mail
<input id="fEmail" name="email" type="email" required />
</label>
<label>Mannschaft
<input id="fMannschaft" name="mannschaft" placeholder="z. B. U12" required />
</label>
@@ -438,6 +485,10 @@
<div id="paypalInfo" style="margin-left: 28px; margin-bottom: 12px; font-size: 0.95em; color: #1e40af;">
Bitte sende den Betrag über PayPal an: <strong id="paypalAccount"></strong>
</div>
<div style="margin-left: 28px; margin-bottom: 12px; padding: 10px; background: #fef3c7; border: 2px solid #f59e0b; border-radius: 6px; font-size: 0.9em; color: #92400e;">
<strong>⚠️ WICHTIG:</strong> Bitte unbedingt die Option <strong>"Familie & Freunde"</strong> wählen!<br>
Bei normalen PayPal-Zahlungen fallen zusätzliche Gebühren an.
</div>
<label style="display: flex; align-items: center;">
<input type="radio" name="payment_method" value="bar" style="margin-right: 8px;" />
<span><strong>Bar bei Abholung</strong></span>
@@ -535,18 +586,41 @@ function render(items) {
const priceText = price > 0 ? `${price.toFixed(2)}` : "Preis auf Anfrage";
const img = (item.bild_url || "").trim();
// Verfügbarkeit prüfen
const totalStock = Number(t.gezaehlt) || 0;
let stockBadge = '';
let stockClass = '';
if (totalStock === 0) {
stockBadge = '<div class="stock-badge unavailable">⛔ Nicht verfügbar</div>';
stockClass = 'unavailable';
} else if (totalStock <= 5) {
stockBadge = `<div class="stock-badge low">⚠️ Nur noch ${totalStock} verfügbar</div>`;
stockClass = 'low';
} else {
stockBadge = `<div class="stock-badge available">✓ Verfügbar (${totalStock})</div>`;
stockClass = 'available';
}
// Sale-Badge anzeigen
const saleBadge = item.sale ? '<div class="sale-badge">SALE 🔥</div>' : '';
return `
<div class="card-tile">
${saleBadge}
<div class="card-media">
${img ? `<button class="thumb-btn" data-img="${img}" aria-label="Bild vergrößern" title="Bild vergrößern" style="width:100%;height:100%;background-image:url('${img}');background-size:cover;background-position:center;border:0;cursor:pointer;"></button>` : "Bild"}
${img ? `<button class="thumb-btn" data-img="${img}" aria-label="Bild vergrößern" title="Bild vergrößern" style="width:100%;height:100%;border:0;cursor:pointer;padding:0;position:relative;overflow:hidden;"><img src="${img}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';" style="width:100%;height:100%;object-fit:cover;display:block;"><div style="display:none;position:absolute;inset:0;align-items:center;justify-content:center;background:linear-gradient(135deg, rgba(42,138,138,.15), rgba(18,45,70,.2));font-size:40px;opacity:0.5;">📦</div></button>` : `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg, rgba(42,138,138,.15), rgba(18,45,70,.2));font-size:40px;opacity:0.5;">📦</div>`}
</div>
<div class="card-body">
<div class="card-title">${item.artikel}</div>
<div class="card-price">${priceText}</div>
<div class="card-sub">${item.rows.length} Größen</div>
${stockBadge}
</div>
<div class="card-actions">
<button class="order-btn detail-btn" data-artikel="${item.artikel}" data-preis="${priceText}" data-img="${img}">Bestellen / Details</button>
<button class="order-btn detail-btn" data-artikel="${item.artikel}" data-preis="${priceText}" data-img="${img}" ${totalStock === 0 ? 'disabled style="opacity: 0.5; cursor: not-allowed;"' : ''}>
${totalStock === 0 ? 'Nicht verfügbar' : 'Bestellen / Details'}
</button>
</div>
</div>
`;
@@ -607,6 +681,9 @@ document.addEventListener("click", (e) => {
}
const detailBtn = e.target.closest(".detail-btn");
if (detailBtn) {
// Verhindere Klick auf disabled Button
if (detailBtn.disabled) return;
const artikel = detailBtn.dataset.artikel;
const item = DATA_MAP.get(artikel);
if (!item) return;
@@ -616,12 +693,27 @@ document.addEventListener("click", (e) => {
document.getElementById("detailTitle").textContent = artikel;
document.getElementById("detailPrice").textContent = detailBtn.dataset.preis || "Preis auf Anfrage";
document.getElementById("detailSizes").textContent = `${item.rows.length} Größen`;
const list = item.rows.map(r => `
<div class="size-row">
<div class="size-meta"><strong>${fmt(r.groesse)}</strong> · Bestand: ${fmt(r.gezaehlt)}</div>
${(Number(r.gezaehlt) || 0) > 0 ? `<button class="order-btn" data-artikel="${item.artikel}" data-groesse="${fmt(r.groesse)}">Bestellen</button>` : ""}
</div>
`).join("");
const list = item.rows.map(r => {
const stock = Number(r.gezaehlt) || 0;
let stockInfo = '';
if (stock === 0) {
stockInfo = '<span class="stock-badge unavailable">Nicht verfügbar</span>';
} else if (stock <= 3) {
stockInfo = `<span class="stock-badge low">Nur noch ${stock} verfügbar</span>`;
} else {
stockInfo = `<span class="stock-badge available">Verfügbar (${stock})</span>`;
}
return `
<div class="size-row">
<div class="size-meta">
<strong>${fmt(r.groesse)}</strong> · ${stockInfo}
</div>
${stock > 0 ? `<button class="order-btn" data-artikel="${item.artikel}" data-groesse="${fmt(r.groesse)}">Bestellen</button>` : '<span style="color: var(--muted); font-size: 11px;"></span>'}
</div>
`;
}).join("");
document.getElementById("detailSizeList").innerHTML = list;
document.getElementById("detailModal").classList.add("open");
return;