chore: auto-merge develop → main
Triggered by: f4ea9e7 fix: banner rotation (30s interval, skip empty phases) + normalize OFF categories
This commit is contained in:
+89
-5
@@ -3597,6 +3597,80 @@ function getStats(PDO $db): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== MONTHLY STATS =====
|
// ===== MONTHLY STATS =====
|
||||||
|
/**
|
||||||
|
* Normalize a raw category string (may contain OpenFoodFacts "en:slug" format)
|
||||||
|
* to one of the app's known Italian category slugs.
|
||||||
|
*/
|
||||||
|
function _normalizeCat(string $raw): string {
|
||||||
|
static $known = [
|
||||||
|
'frutta','verdura','carne','pesce','latticini',
|
||||||
|
'pasta','pane','cereali','bevande','condimenti',
|
||||||
|
'surgelati','conserve','snack','altro',
|
||||||
|
];
|
||||||
|
$raw = trim($raw);
|
||||||
|
if (in_array($raw, $known, true)) return $raw;
|
||||||
|
|
||||||
|
// Strip language prefix: "en:", "it:", "fr:", etc.
|
||||||
|
$slug = (string)preg_replace('/^[a-z]{2}:/', '', $raw);
|
||||||
|
if (in_array($slug, $known, true)) return $slug;
|
||||||
|
|
||||||
|
// Map common OpenFoodFacts slugs → app categories
|
||||||
|
static $map = [
|
||||||
|
// latticini
|
||||||
|
'dairies'=>'latticini','dairy'=>'latticini','milk'=>'latticini',
|
||||||
|
'fermented-milk-products'=>'latticini','cheeses'=>'latticini',
|
||||||
|
'yogurts'=>'latticini','plant-based-milks'=>'latticini',
|
||||||
|
'cream'=>'latticini','butter'=>'latticini','eggs'=>'latticini',
|
||||||
|
// frutta
|
||||||
|
'fruits'=>'frutta','fresh-fruits'=>'frutta','tropical-fruits'=>'frutta',
|
||||||
|
'dried-fruits'=>'frutta','berries'=>'frutta',
|
||||||
|
// verdura
|
||||||
|
'vegetables'=>'verdura','fresh-vegetables'=>'verdura',
|
||||||
|
'plant-based-foods'=>'verdura','legumes'=>'verdura',
|
||||||
|
'mushrooms'=>'verdura','herbs'=>'verdura',
|
||||||
|
// carne
|
||||||
|
'meats'=>'carne','beef'=>'carne','pork'=>'carne',
|
||||||
|
'poultry'=>'carne','chicken'=>'carne','processed-meat'=>'carne',
|
||||||
|
'sausages'=>'carne','charcuterie'=>'carne',
|
||||||
|
// pesce
|
||||||
|
'fish'=>'pesce','seafood'=>'pesce','fish-products'=>'pesce',
|
||||||
|
'canned-fish'=>'conserve',
|
||||||
|
// pasta
|
||||||
|
'pastas'=>'pasta','pasta'=>'pasta','pasta-based-dishes'=>'pasta',
|
||||||
|
'noodles'=>'pasta',
|
||||||
|
// pane
|
||||||
|
'breads'=>'pane','bread'=>'pane','baked-goods'=>'pane',
|
||||||
|
'pastries'=>'pane','cakes'=>'snack',
|
||||||
|
// cereali
|
||||||
|
'cereals'=>'cereali','breakfast-cereals'=>'cereali',
|
||||||
|
'rice'=>'cereali','grains'=>'cereali','flours'=>'cereali',
|
||||||
|
'seeds'=>'cereali',
|
||||||
|
// bevande
|
||||||
|
'beverages'=>'bevande','drinks'=>'bevande','waters'=>'bevande',
|
||||||
|
'juices'=>'bevande','fruit-juices'=>'bevande','sodas'=>'bevande',
|
||||||
|
'plant-based-foods-and-beverages'=>'bevande','coffee'=>'bevande',
|
||||||
|
'tea'=>'bevande','alcoholic-beverages'=>'bevande','wine'=>'bevande',
|
||||||
|
'beer'=>'bevande',
|
||||||
|
// condimenti
|
||||||
|
'sauces'=>'condimenti','condiments'=>'condimenti',
|
||||||
|
'spreads'=>'condimenti','oils'=>'condimenti',
|
||||||
|
'vinegars'=>'condimenti','dressings'=>'condimenti',
|
||||||
|
'sugar'=>'condimenti','salt'=>'condimenti','spices'=>'condimenti',
|
||||||
|
// surgelati
|
||||||
|
'frozen-foods'=>'surgelati','frozen-vegetables'=>'surgelati',
|
||||||
|
'frozen-fish'=>'surgelati','ice-cream'=>'surgelati',
|
||||||
|
// conserve
|
||||||
|
'preserved-foods'=>'conserve','canned-foods'=>'conserve',
|
||||||
|
'jams'=>'conserve','pickles'=>'conserve','tomato-sauces'=>'conserve',
|
||||||
|
// snack
|
||||||
|
'snacks'=>'snack','cookies'=>'snack','chips'=>'snack',
|
||||||
|
'chocolates'=>'snack','candies'=>'snack','sweets'=>'snack',
|
||||||
|
'crackers'=>'snack','biscuits'=>'snack','nuts'=>'snack',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $map[$slug] ?? $map[strtolower($slug)] ?? 'altro';
|
||||||
|
}
|
||||||
|
|
||||||
function getMonthlyStats(PDO $db): void {
|
function getMonthlyStats(PDO $db): void {
|
||||||
EverLog::debug('getMonthlyStats');
|
EverLog::debug('getMonthlyStats');
|
||||||
|
|
||||||
@@ -3637,11 +3711,21 @@ function getMonthlyStats(PDO $db): void {
|
|||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$totalCatEvents = array_sum(array_column($catRows, 'cnt')) ?: 1;
|
$totalCatEvents = array_sum(array_column($catRows, 'cnt')) ?: 1;
|
||||||
$topCats = array_map(fn($r) => [
|
|
||||||
'cat' => $r['cat'],
|
// Normalize OFF slugs (e.g. "en:dairies" → "latticini"), then re-aggregate
|
||||||
'count' => (int)$r['cnt'],
|
$normAgg = [];
|
||||||
'pct' => (int)round((int)$r['cnt'] / $totalCatEvents * 100),
|
foreach ($catRows as $r) {
|
||||||
], $catRows);
|
$norm = _normalizeCat((string)$r['cat']);
|
||||||
|
$normAgg[$norm] = ($normAgg[$norm] ?? 0) + (int)$r['cnt'];
|
||||||
|
}
|
||||||
|
arsort($normAgg);
|
||||||
|
$normAgg = array_slice($normAgg, 0, 4, true);
|
||||||
|
$totalNorm = array_sum($normAgg) ?: 1;
|
||||||
|
$topCats = array_map(fn($cat, $cnt) => [
|
||||||
|
'cat' => $cat,
|
||||||
|
'count' => $cnt,
|
||||||
|
'pct' => (int)round($cnt / $totalNorm * 100),
|
||||||
|
], array_keys($normAgg), array_values($normAgg));
|
||||||
|
|
||||||
// Top consumed products this month
|
// Top consumed products this month
|
||||||
$topProds = $db->query("
|
$topProds = $db->query("
|
||||||
|
|||||||
+26
-10
@@ -4395,7 +4395,9 @@ function _renderMonthlyStatsSection(data) {
|
|||||||
const catBars = top.map(c => {
|
const catBars = top.map(c => {
|
||||||
const color = _NUTR_COLORS[c.cat] || '#64748b';
|
const color = _NUTR_COLORS[c.cat] || '#64748b';
|
||||||
const barPct = Math.round(c.count / maxCnt * 100);
|
const barPct = Math.round(c.count / maxCnt * 100);
|
||||||
const label = t('categories.' + c.cat) || c.cat;
|
// t() returns the key itself when not found — guard against it
|
||||||
|
const catKey = 'categories.' + c.cat;
|
||||||
|
const label = t(catKey) !== catKey ? t(catKey) : c.cat.replace(/-/g, ' ');
|
||||||
return `<div class="ms-cat-row">
|
return `<div class="ms-cat-row">
|
||||||
<span class="ms-cat-name">${escapeHtml(label)}</span>
|
<span class="ms-cat-name">${escapeHtml(label)}</span>
|
||||||
<div class="ms-cat-bar-wrap">
|
<div class="ms-cat-bar-wrap">
|
||||||
@@ -4455,15 +4457,15 @@ const _INSIGHT_PHASES = ['waste', 'nutrition', 'monthly'];
|
|||||||
|
|
||||||
function _startInsightAlternation() {
|
function _startInsightAlternation() {
|
||||||
clearInterval(_insightFlipTimer);
|
clearInterval(_insightFlipTimer);
|
||||||
// Pick initial panel based on current hour, cycling through 3 phases
|
// Pick initial panel cycling through 3 phases based on current 30-second slot
|
||||||
const idx = Math.floor(Date.now() / 3_600_000) % _INSIGHT_PHASES.length;
|
const idx = Math.floor(Date.now() / 30_000) % _INSIGHT_PHASES.length;
|
||||||
_insightPhase = _INSIGHT_PHASES[idx];
|
_insightPhase = _INSIGHT_PHASES[idx];
|
||||||
_applyInsightPhase();
|
_applyInsightPhase();
|
||||||
// Advance to next phase every hour
|
// Advance every 30 seconds so the rotation is actually visible
|
||||||
_insightFlipTimer = setInterval(() => {
|
_insightFlipTimer = setInterval(() => {
|
||||||
_insightPhase = _INSIGHT_PHASES[(_INSIGHT_PHASES.indexOf(_insightPhase) + 1) % _INSIGHT_PHASES.length];
|
_insightPhase = _INSIGHT_PHASES[(_INSIGHT_PHASES.indexOf(_insightPhase) + 1) % _INSIGHT_PHASES.length];
|
||||||
_applyInsightPhase();
|
_applyInsightPhase();
|
||||||
}, 3_600_000);
|
}, 30_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _applyInsightPhase() {
|
function _applyInsightPhase() {
|
||||||
@@ -4471,9 +4473,25 @@ function _applyInsightPhase() {
|
|||||||
const nutrEl = document.getElementById('nutrition-section');
|
const nutrEl = document.getElementById('nutrition-section');
|
||||||
const monthlyEl = document.getElementById('monthly-stats-section');
|
const monthlyEl = document.getElementById('monthly-stats-section');
|
||||||
if (!wasteEl || !nutrEl) return;
|
if (!wasteEl || !nutrEl) return;
|
||||||
const showWaste = _insightPhase === 'waste' && wasteEl.innerHTML.trim() !== '';
|
|
||||||
const showNutr = _insightPhase === 'nutrition' && nutrEl.innerHTML.trim() !== '';
|
// Map of which panels actually have rendered content
|
||||||
const showMonthly = _insightPhase === 'monthly' && !!monthlyEl && monthlyEl.innerHTML.trim() !== '';
|
const hasContent = {
|
||||||
|
'waste': wasteEl.innerHTML.trim() !== '',
|
||||||
|
'nutrition': nutrEl.innerHTML.trim() !== '',
|
||||||
|
'monthly': !!monthlyEl && monthlyEl.innerHTML.trim() !== '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the intended phase has no content, advance to the next one that does
|
||||||
|
let phase = _insightPhase;
|
||||||
|
for (let i = 0; i < _INSIGHT_PHASES.length; i++) {
|
||||||
|
if (hasContent[phase]) break;
|
||||||
|
phase = _INSIGHT_PHASES[(_INSIGHT_PHASES.indexOf(phase) + 1) % _INSIGHT_PHASES.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
const showWaste = phase === 'waste';
|
||||||
|
const showNutr = phase === 'nutrition';
|
||||||
|
const showMonthly = phase === 'monthly';
|
||||||
|
|
||||||
// Fade-swap all three panels
|
// Fade-swap all three panels
|
||||||
const els = [wasteEl, nutrEl, ...(monthlyEl ? [monthlyEl] : [])];
|
const els = [wasteEl, nutrEl, ...(monthlyEl ? [monthlyEl] : [])];
|
||||||
els.forEach(el => { el.style.opacity = '0'; el.style.transition = 'opacity .6s'; });
|
els.forEach(el => { el.style.opacity = '0'; el.style.transition = 'opacity .6s'; });
|
||||||
@@ -4481,8 +4499,6 @@ function _applyInsightPhase() {
|
|||||||
wasteEl.style.display = showWaste ? 'block' : 'none';
|
wasteEl.style.display = showWaste ? 'block' : 'none';
|
||||||
nutrEl.style.display = showNutr ? 'block' : 'none';
|
nutrEl.style.display = showNutr ? 'block' : 'none';
|
||||||
if (monthlyEl) monthlyEl.style.display = showMonthly ? 'block' : 'none';
|
if (monthlyEl) monthlyEl.style.display = showMonthly ? 'block' : 'none';
|
||||||
// If nothing ready yet, keep waste visible as fallback
|
|
||||||
if (!showWaste && !showNutr && !showMonthly) wasteEl.style.display = 'block';
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
els.forEach(el => { el.style.opacity = '1'; });
|
els.forEach(el => { el.style.opacity = '1'; });
|
||||||
if (showNutr) {
|
if (showNutr) {
|
||||||
|
|||||||
Reference in New Issue
Block a user