feat: KoePi Mobile-Buttons scrollbar, Prospekte chronologisch getrennt, exakte Bildauswahl-Tabelle, Sondermagazine ausgeblendet
This commit is contained in:
@@ -384,27 +384,39 @@ async function fetchOffersViaPuppeteer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Prüft, ob "COUNT x VOL" (z.B. "20 x 0,5") in der Beschreibung vorkommt —
|
||||
// robust gegen Leerzeichen um das "x", Komma/Punkt als Dezimaltrennzeichen,
|
||||
// und verhindert Fehltreffer wie "0,5" innerhalb von "0,55"
|
||||
function hasQty(desc, count, vol) {
|
||||
const volPattern = vol.replace(',', '[,.]');
|
||||
const re = new RegExp(`${count}\\s*x\\s*${volPattern}(?!\\d)`, 'i');
|
||||
return re.test(desc);
|
||||
}
|
||||
|
||||
// Bild pro Angebot exakt nach Mengenangabe wählen (siehe Vorgabe):
|
||||
// 20x0,5 -> kasten_050 | 20x0,33 -> kasten_steini | 24x0,33 -> kasten_033
|
||||
// 11x0,5 -> kasten_11er | 6x0,33 -> traeger | 24x0,5 -> palette_050
|
||||
// 20x0,5 UND 24x0,33 gleichzeitig -> kasten_033_055 | sonst -> koepi_platzhalter
|
||||
function pickOfferImage(description) {
|
||||
const d = (description || '').toLowerCase();
|
||||
const q2005 = hasQty(d, 20, '0,5');
|
||||
const q2433 = hasQty(d, 24, '0,33');
|
||||
if (q2005 && q2433) return '/koepi/kasten_033_055.png';
|
||||
if (hasQty(d, 20, '0,33')) return '/koepi/kasten_steini.png';
|
||||
if (q2433) return '/koepi/kasten_033.png';
|
||||
if (hasQty(d, 11, '0,5')) return '/koepi/kasten_11er.png';
|
||||
if (hasQty(d, 6, '0,33')) return '/koepi/traeger.png';
|
||||
if (hasQty(d, 24, '0,5')) return '/koepi/palette_050.png';
|
||||
if (q2005) return '/koepi/kasten_050.png';
|
||||
return '/koepi/koepi_platzhalter.png';
|
||||
}
|
||||
|
||||
// Gemeinsame Weiterverarbeitung, egal ob die Rohdaten über die API oder per
|
||||
// Puppeteer/HTML-Scraping ermittelt wurden
|
||||
function processOffers(offers) {
|
||||
// Lokale Bilder auswählen basierend auf Beschreibung
|
||||
for (const offer of offers) {
|
||||
const desc = (offer.description || '').toLowerCase();
|
||||
if (desc.includes('steini')) {
|
||||
offer.imageLocal = '/koepi/kasten_steini.png';
|
||||
} else if (desc.includes('11 x 0,5') || desc.includes('11x0,5')) {
|
||||
offer.imageLocal = '/koepi/kasten_lang.png';
|
||||
} else if (desc.includes('6 x') || desc.includes('6x') || (desc.includes('flasche') && !desc.includes('kasten'))) {
|
||||
offer.imageLocal = '/koepi/traeger.png';
|
||||
} else if (desc.includes('24 x 0,33') || desc.includes('24x0,33')) {
|
||||
offer.imageLocal = '/koepi/kasten_033.png';
|
||||
} else if (desc.includes('0,5')) {
|
||||
offer.imageLocal = '/koepi/kasten_050.png';
|
||||
} else if (desc.includes('0,33')) {
|
||||
offer.imageLocal = '/koepi/kasten_033.png';
|
||||
} else {
|
||||
offer.imageLocal = '/koepi/kasten_050.png';
|
||||
}
|
||||
offer.imageLocal = pickOfferImage(offer.description);
|
||||
offer.image = null; // CDN nicht mehr nötig
|
||||
}
|
||||
|
||||
@@ -547,12 +559,30 @@ async function scrapeProspekte() {
|
||||
for (const b of all) b.hasKoenigPilsener = false;
|
||||
}
|
||||
|
||||
// "Sondermagazin"-Prospekte ausblenden (keine normalen Wochenangebote)
|
||||
const withoutSondermagazin = all.filter(b => !(b.title || '').toLowerCase().includes('sondermagazin'));
|
||||
const targeted = withoutSondermagazin.filter(b => b.isTarget);
|
||||
|
||||
// Chronologisch in "aktuell laufend" und "kommend" trennen
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
const isCurrent = b => {
|
||||
const from = (b.validFrom || '').slice(0, 10);
|
||||
const to = (b.validTo || '').slice(0, 10);
|
||||
if (!from) return true; // ohne Datum lieber anzeigen als verstecken
|
||||
if (from > todayIso) return false; // startet erst noch
|
||||
if (to && to < todayIso) return false; // schon abgelaufen
|
||||
return true;
|
||||
};
|
||||
const byValidFrom = (a, b) => (a.validFrom || '').localeCompare(b.validFrom || '');
|
||||
const current = targeted.filter(isCurrent).sort(byValidFrom);
|
||||
const future = targeted.filter(b => !isCurrent(b)).sort(byValidFrom);
|
||||
|
||||
const result = {
|
||||
targeted: all.filter(b => b.isTarget),
|
||||
others: all.filter(b => !b.isTarget),
|
||||
current,
|
||||
future,
|
||||
scrapedAt: new Date().toISOString(),
|
||||
};
|
||||
console.log(`🍺 KöPi Prospekte: ${result.targeted.length} eigene Filiale(n), ${result.others.length} andere Filialen derselben Ketten, ${result.targeted.filter(b=>b.hasKoenigPilsener).length} davon mit König Pilsener im Angebot`);
|
||||
console.log(`🍺 KöPi Prospekte: ${current.length} aktuell laufend, ${future.length} kommend, ${current.filter(b=>b.hasKoenigPilsener).length} davon aktuell mit König Pilsener im Angebot`);
|
||||
cacheSet(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
BIN
frontend/public/koepi/kasten_033_05.png
Normal file
BIN
frontend/public/koepi/kasten_033_05.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -140,7 +140,6 @@ export default function Koepi({ toast }) {
|
||||
const [offers, setOffers] = useState(null);
|
||||
const [prospekte, setProspekte] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const isAdmin = getMyRole() === 'admin';
|
||||
|
||||
const loadOffers = async (force=false) => {
|
||||
@@ -199,20 +198,21 @@ export default function Koepi({ toast }) {
|
||||
<div style={{ maxWidth:700, margin:'0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:20 }}>
|
||||
<h2 style={{ margin:0, fontSize:15, fontFamily:'monospace', color:'rgba(255,255,255,0.55)', letterSpacing:2, fontWeight:400 }}>
|
||||
<h2 style={{ margin:0, fontSize:15, fontFamily:'monospace', color:'rgba(255,255,255,0.55)', letterSpacing:2, fontWeight:400, whiteSpace:'nowrap', flexShrink:0 }}>
|
||||
🍺 KÖPI — König Pilsener
|
||||
</h2>
|
||||
<div style={{ flex:1 }}/>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<button onClick={testDailyCheck} disabled={cronRunning} style={{ ...S.btn('#f59e0b',true), fontSize:10, opacity:cronRunning?0.5:1 }}>
|
||||
<div style={{ display:'flex', gap:8, overflowX:'auto', scrollbarWidth:'none',
|
||||
WebkitOverflowScrolling:'touch', paddingBottom:2, minWidth:0 }}>
|
||||
<button onClick={testDailyCheck} disabled={cronRunning} style={{ ...S.btn('#f59e0b',true), fontSize:10, opacity:cronRunning?0.5:1, flexShrink:0, whiteSpace:'nowrap' }}>
|
||||
{cronRunning ? '⏳ läuft…' : '🔔 Cron testen'}
|
||||
</button>
|
||||
<button onClick={resolveStores} disabled={resolvingStores} style={{ ...S.btn('#4ecdc4',true), fontSize:10, opacity:resolvingStores?0.5:1 }}>
|
||||
<button onClick={resolveStores} disabled={resolvingStores} style={{ ...S.btn('#4ecdc4',true), fontSize:10, opacity:resolvingStores?0.5:1, flexShrink:0, whiteSpace:'nowrap' }}>
|
||||
{resolvingStores ? '⏳ läuft…' : '🏪 Filial-Liste neu auflösen'}
|
||||
</button>
|
||||
<button onClick={clearCache} style={{ ...S.btn('#666666',true), fontSize:10 }}>🗑 Cache leeren</button>
|
||||
</>
|
||||
<button onClick={clearCache} style={{ ...S.btn('#666666',true), fontSize:10, flexShrink:0, whiteSpace:'nowrap' }}>🗑 Cache leeren</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -263,27 +263,26 @@ export default function Koepi({ toast }) {
|
||||
{/* Prospekte */}
|
||||
{!loading && tab==='prospekte' && prospekte && (
|
||||
<div>
|
||||
{prospekte.targeted?.length > 0 && (
|
||||
{prospekte.current?.length > 0 && (
|
||||
<>
|
||||
<div style={{ ...S.head, marginBottom:12 }}>DEINE MÄRKTE ({prospekte.targeted.length})</div>
|
||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12, marginBottom:16 }}>
|
||||
{prospekte.targeted.map(b=><ProspektCard key={b.id} b={b}/>)}
|
||||
<div style={{ ...S.head, marginBottom:12 }}>DEINE MÄRKTE — AKTUELL ({prospekte.current.length})</div>
|
||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12, marginBottom:24 }}>
|
||||
{prospekte.current.map(b=><ProspektCard key={b.id} b={b}/>)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{prospekte.others?.length > 0 && (
|
||||
{prospekte.future?.length > 0 && (
|
||||
<>
|
||||
<button onClick={()=>setShowAll(v=>!v)} style={{ ...S.btn('#444444',true), marginBottom:10, fontSize:11 }}>
|
||||
{showAll ? '▾ Weniger' : `▸ Weitere Prospekte (${prospekte.others.length})`}
|
||||
</button>
|
||||
{showAll && (
|
||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12 }}>
|
||||
{prospekte.others.map(b=><ProspektCard key={b.id} b={b}/>)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ ...S.head, marginBottom:12, paddingTop:12, borderTop:'1px solid rgba(255,255,255,0.08)',
|
||||
color:'rgba(255,255,255,0.35)' }}>
|
||||
KOMMENDE PROSPEKTE ({prospekte.future.length})
|
||||
</div>
|
||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12, opacity:0.75 }}>
|
||||
{prospekte.future.map(b=><ProspektCard key={b.id} b={b}/>)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!prospekte.targeted?.length && !prospekte.others?.length && (
|
||||
{!prospekte.current?.length && !prospekte.future?.length && (
|
||||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'40px 0' }}>
|
||||
Keine Prospekte gefunden.
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user