import { useState, useEffect } from 'react'; import { api, S } from '../lib.js'; const GOLD = '#f59e0b'; function getMyRole() { try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; } } function publisherColor(name) { if (!name) return '#555'; const n = name.toLowerCase(); if (n.includes('rewe')) return '#e2001a'; if (n.includes('edeka') || n.includes('e center')) return '#e8a800'; if (n.includes('netto')) return '#0057a8'; if (n.includes('trinkgut')) return '#e87722'; if (n.includes('penny')) return '#e2001a'; if (n.includes('kaufland')) return '#e2001a'; return '#888'; } // Logo-Bild je Händler — Reihenfolge wichtig: "Netto Getränke-Discount" muss // VOR dem generischen "netto"-Check geprüft werden. Kein Eintrag (z.B. // Kaufland) -> Aufrufer fällt auf den farbigen Text-Badge zurück. function retailerLogo(name) { if (!name) return null; const n = name.toLowerCase(); if (n.includes('netto') && n.includes('getränke')) return '/koepi/netto_getraenkemarkt.png'; if (n.includes('netto')) return '/koepi/netto.png'; if (n.includes('rewe dortmund')) return null; if (n.includes('rewe')) return '/koepi/rewe.png'; if (n.includes('edeka') || n.includes('e center')) return '/koepi/edeka.png'; if (n.includes('trinkgut')) return '/koepi/trinkgut.png'; if (n.includes('penny')) return '/koepi/penny.png'; if (n.includes('lidl')) return '/koepi/lidl.png'; if (n.includes('aldi')) return '/koepi/aldi.png'; if (n.includes('hornbach')) return '/koepi/hornbach.png'; if (n.includes('kaufland')) return '/koepi/kaufland.png'; return null; } function fmtDate(str) { if (!str) return null; try { return new Date(str).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' }); } catch { return str; } } // Prospekte mit Bier-Icon (König Pilsener im Angebot) immer nach oben, // ansonsten Reihenfolge vom Backend (chronologisch) beibehalten function sortBeerFirst(arr) { return [...arr].sort((a, b) => (b.hasKoenigPilsener?1:0) - (a.hasKoenigPilsener?1:0)); } // ISO-Kalenderwoche aus einem Datum berechnen function getISOWeek(dateStr) { const d = new Date(dateStr); d.setHours(0,0,0,0); d.setDate(d.getDate() + 4 - (d.getDay() || 7)); const yearStart = new Date(d.getFullYear(), 0, 1); return { week: Math.ceil((((d - yearStart) / 86400000) + 1) / 7), year: d.getFullYear() }; } // Kommende Prospekte nach Kalenderwoche gruppieren, Gruppen chronologisch sortiert function groupByWeek(arr) { const groups = new Map(); for (const b of arr) { if (!b.validFrom) { // kein Datum -> eigene Sammelgruppe ganz unten const key = 'unbekannt'; if (!groups.has(key)) groups.set(key, { label: 'Ohne Datum', sortKey: Infinity, items: [] }); groups.get(key).items.push(b); continue; } const { week, year } = getISOWeek(b.validFrom); const key = `${year}-${week}`; if (!groups.has(key)) groups.set(key, { label: `KW${week}`, sortKey: year*100+week, items: [] }); groups.get(key).items.push(b); } return [...groups.values()].sort((a,b) => a.sortKey - b.sortKey); } // ── Angebots-Karte (marktguru) ──────────────────────────────────────────────── function OfferCard({ offer }) { const color = publisherColor(offer.retailer); const logo = retailerLogo(offer.retailer); return (
{/* Bild oben */} {offer.imageLocal && (
König Pilsener
)} {/* Händler + Adresse */}
{logo ? ( {offer.retailer} ) : ( {offer.retailer} )} {offer.oldPrice && -29%}
{offer.address && (
📍 {offer.address}
)} {/* Name + Beschreibung */}
König Pilsener
{offer.description && (
{offer.description}
)} {/* Preis */}
{offer.price} {offer.oldPrice && ( {offer.oldPrice} )}
{/* Gültigkeit */}
{offer.dateRange && ( 📅 {offer.dateRange} )} {offer.leafletUrl && ( 🔗 Im Prospekt ansehen )}
); } // ── Prospekt-Karte (marktguru) ──────────────────────────────────────────────── function ProspektCard({ b }) { const color = publisherColor(b.publisher); const logo = retailerLogo(b.publisher); return (
{b.image && ( {b.title} { e.target.style.display='none'; }} /> )}
{logo ? ( {b.publisher} ) : (
{b.publisher}
)} {b.hasKoenigPilsener && ( 🍺 )}
📍 {b.street}
{b.title && (
{b.title}
)} {(b.validFrom || b.validTo || b.weekInfo) && (
{b.validFrom && `Ab ${fmtDate(b.validFrom)}`}{b.validFrom&&b.validTo&&' · '}{b.validTo&&`Bis ${fmtDate(b.validTo)}`} {b.weekInfo && ` ${b.weekInfo}`}
)}
); } // ── Hauptkomponente ─────────────────────────────────────────────────────────── export default function Koepi({ toast }) { const [tab, setTab] = useState('angebote'); const [offers, setOffers] = useState(null); const [prospekte, setProspekte] = useState(null); const [loading, setLoading] = useState(false); const isAdmin = getMyRole() === 'admin'; const loadOffers = async (force=false) => { if (offers && !force) return; setLoading(true); try { setOffers(await api('/tools/koepi/offers')); } catch(e) { toast?.(e.message||'Fehler','error'); } finally { setLoading(false); } }; const loadProspekte = async (force=false) => { if (prospekte && !force) return; setLoading(true); try { setProspekte(await api('/tools/koepi/prospekte')); } catch(e) { toast?.(e.message||'Fehler','error'); } finally { setLoading(false); } }; useEffect(() => { if (tab === 'angebote') loadOffers(); else loadProspekte(); }, [tab]); const clearCache = async () => { try { await api('/tools/koepi/clear-cache', { body:{} }); setOffers(null); setProspekte(null); toast('Cache geleert'); setTimeout(() => { if (tab==='angebote') loadOffers(true); else loadProspekte(true); }, 100); } catch(e) { toast?.(e.message,'error'); } }; const [cronRunning, setCronRunning] = useState(false); const testDailyCheck = async () => { setCronRunning(true); try { const r = await api('/tools/koepi/run-daily-check', { method:'POST', body:{} }); toast(`🔔 Test: ${r.offerCount} Angebot(e) gefunden, Pushover ${r.sent ? 'versendet' : 'nicht versendet'}${r.changed ? ' (Änderung erkannt)' : ' (unverändert)'}`); } catch(e) { toast?.(e.message||'Fehler','error'); } finally { setCronRunning(false); } }; const [resolvingStores, setResolvingStores] = useState(false); const resolveStores = async () => { setResolvingStores(true); try { const r = await api('/tools/koepi/local-stores/resolve', { method:'POST', body:{} }); toast(`🏪 Filial-Liste: ${r.resolved} von ${r.total} aufgelöst`); setOffers(null); setProspekte(null); setTimeout(() => { if (tab==='angebote') loadOffers(true); else loadProspekte(true); }, 100); } catch(e) { toast?.(e.message||'Fehler','error'); } finally { setResolvingStores(false); } }; return (
{/* Header */}

🍺 KÖPI — König Pilsener

{isAdmin && (
)}
Raum Duisburg 47259 · EDEKA · REWE · Netto · Kaufland · Penny · Trinkgut · Lidl · ALDI Süd · HORNBACH · Cache 1h
{/* Tabs */}
{[['angebote','🍺 Angebote'],['prospekte','📋 Prospekte']].map(([t,l]) => ( ))}
{/* Laden */} {loading && (
⏳ Lade… (kann bis zu 20 Sekunden dauern)
)} {/* Angebote */} {!loading && tab==='angebote' && offers && (
{offers.offers?.length > 0 ? ( <>
{offers.offers.length} AKTUELLE ANGEBOTE · Quelle: marktguru.de
{offers.offers.map((o,i) => )}
) : (
Aktuell keine König Pilsener Angebote bei deinen Märkten gefunden.
)}
→ Alle Angebote auf marktguru.de
)} {/* Prospekte */} {!loading && tab==='prospekte' && prospekte && (
{prospekte.current?.length > 0 && ( <>
DEINE MÄRKTE — AKTUELL ({prospekte.current.length})
{sortBeerFirst(prospekte.current).map(b=>)}
)} {prospekte.future?.length > 0 && ( <>
KOMMENDE PROSPEKTE ({prospekte.future.length})
{groupByWeek(prospekte.future).map(group => (
{group.label}
{sortBeerFirst(group.items).map(b=>)}
))} )} {!prospekte.current?.length && !prospekte.future?.length && (
Keine Prospekte gefunden.
)}
)}
); }