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 && (
)}
{/* Händler + Adresse */}
{logo ? (

) : (
{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 */}
);
}
// ── Prospekt-Karte (marktguru) ────────────────────────────────────────────────
function ProspektCard({ b }) {
const color = publisherColor(b.publisher);
const logo = retailerLogo(b.publisher);
return (
{b.image && (
}`})
{ e.target.style.display='none'; }}
/>
)}
{logo ? (

) : (
{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.
)}
)}
{/* 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.
)}
)}
);
}