Merge develop: Anti-Waste Report Card redesign
This commit is contained in:
+18
-9
@@ -1606,17 +1606,22 @@ function getStats(PDO $db): void {
|
|||||||
return $da <=> $db2;
|
return $da <=> $db2;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Waste vs consumption stats (last 30 days)
|
// Waste vs consumption trend (3 × 30-day buckets)
|
||||||
$wasteStats = $db->query("
|
$wasteStats3m = $db->query("
|
||||||
SELECT type, COUNT(*) as count
|
SELECT type,
|
||||||
|
SUM(CASE WHEN created_at >= datetime('now', '-30 days') THEN 1 ELSE 0 END) AS m0,
|
||||||
|
SUM(CASE WHEN created_at >= datetime('now', '-60 days') AND created_at < datetime('now', '-30 days') THEN 1 ELSE 0 END) AS m1,
|
||||||
|
SUM(CASE WHEN created_at >= datetime('now', '-90 days') AND created_at < datetime('now', '-60 days') THEN 1 ELSE 0 END) AS m2
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE type IN ('out', 'waste') AND created_at >= datetime('now', '-30 days')
|
WHERE type IN ('out', 'waste') AND created_at >= datetime('now', '-90 days')
|
||||||
GROUP BY type
|
GROUP BY type
|
||||||
")->fetchAll();
|
")->fetchAll();
|
||||||
$used30 = 0; $wasted30 = 0;
|
$used30 = 0; $wasted30 = 0;
|
||||||
foreach ($wasteStats as $ws) {
|
$usedP30 = 0; $wastedP30 = 0;
|
||||||
if ($ws['type'] === 'out') $used30 = (int)$ws['count'];
|
$usedP60 = 0; $wastedP60 = 0;
|
||||||
if ($ws['type'] === 'waste') $wasted30 = (int)$ws['count'];
|
foreach ($wasteStats3m as $ws) {
|
||||||
|
if ($ws['type'] === 'out') { $used30 = (int)$ws['m0']; $usedP30 = (int)$ws['m1']; $usedP60 = (int)$ws['m2']; }
|
||||||
|
if ($ws['type'] === 'waste') { $wasted30 = (int)$ws['m0']; $wastedP30 = (int)$ws['m1']; $wastedP60 = (int)$ws['m2']; }
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -1628,8 +1633,12 @@ function getStats(PDO $db): void {
|
|||||||
'expiring_soon' => $expiring,
|
'expiring_soon' => $expiring,
|
||||||
'expired' => $expired,
|
'expired' => $expired,
|
||||||
'opened' => $opened,
|
'opened' => $opened,
|
||||||
'used_30d' => $used30,
|
'used_30d' => $used30,
|
||||||
'wasted_30d' => $wasted30,
|
'wasted_30d' => $wasted30,
|
||||||
|
'used_prev_30d' => $usedP30,
|
||||||
|
'wasted_prev_30d' => $wastedP30,
|
||||||
|
'used_prev_60d' => $usedP60,
|
||||||
|
'wasted_prev_60d' => $wastedP60,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+179
-38
@@ -376,55 +376,196 @@ body {
|
|||||||
.btn-quick-recipe span:last-child { font-size: 1.1rem; opacity: 0.8; }
|
.btn-quick-recipe span:last-child { font-size: 1.1rem; opacity: 0.8; }
|
||||||
.btn-quick-recipe:active { transform: scale(0.98); }
|
.btn-quick-recipe:active { transform: scale(0.98); }
|
||||||
|
|
||||||
/* Waste chart section */
|
/* ── Anti-Waste Report Card ─────────────────────────────── */
|
||||||
.waste-chart-section {
|
#waste-chart-section {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.waste-chart-section h3 {
|
|
||||||
font-size: 0.95rem;
|
/* Header row: title + grade */
|
||||||
margin-bottom: 10px;
|
.aw-header {
|
||||||
}
|
|
||||||
.waste-chart-bar {
|
|
||||||
display: flex;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg-main);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.waste-bar-used {
|
|
||||||
background: var(--success);
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.waste-bar-wasted {
|
|
||||||
background: var(--danger);
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.waste-chart-legend {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
|
||||||
.waste-legend-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.waste-legend-dot {
|
.aw-title {
|
||||||
width: 10px;
|
font-size: 0.95rem;
|
||||||
height: 10px;
|
font-weight: 700;
|
||||||
border-radius: 50%;
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.aw-grade-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.aw-grade-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.aw-grade {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.aw-grade-ap { background: #16a34a; }
|
||||||
|
.aw-grade-a { background: #22c55e; }
|
||||||
|
.aw-grade-b { background: #f59e0b; }
|
||||||
|
.aw-grade-c { background: #fb923c; }
|
||||||
|
.aw-grade-d { background: #dc2626; }
|
||||||
|
|
||||||
|
/* Comparison bars */
|
||||||
|
.aw-compare { display: flex; flex-direction: column; gap: 7px; margin-bottom: 10px; }
|
||||||
|
.aw-compare-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.aw-compare-lbl {
|
||||||
|
min-width: 76px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.aw-you-lbl { color: var(--success); font-weight: 700; }
|
||||||
|
.aw-bar-track {
|
||||||
|
flex: 1;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-main);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.aw-bar-you {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--success);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.6s cubic-bezier(.4,0,.2,1);
|
||||||
|
min-width: 2px;
|
||||||
|
}
|
||||||
|
.aw-bar-avg {
|
||||||
|
height: 100%;
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.6s cubic-bezier(.4,0,.2,1);
|
||||||
|
min-width: 2px;
|
||||||
|
}
|
||||||
|
.aw-compare-pct { font-size: 0.78rem; font-weight: 700; color: var(--text-light); min-width: 30px; text-align: right; }
|
||||||
|
.aw-you-pct { color: var(--success); }
|
||||||
|
|
||||||
|
/* Status message */
|
||||||
|
.aw-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.aw-status-good { background: #f0fdf4; border-color: var(--success); color: #166534; }
|
||||||
|
.aw-status-bad { background: #fef2f2; border-color: var(--danger); color: #991b1b; }
|
||||||
|
.aw-status-ok { background: #fffbeb; border-color: var(--warning); color: #92400e; }
|
||||||
|
|
||||||
|
/* Savings badges */
|
||||||
|
.aw-savings-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.aw-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.aw-badge-money { background: #fef9c3; color: #854d0e; border-color: #fde047; }
|
||||||
|
.aw-badge-meals { background: #f0fdf4; color: #166534; border-color: #86efac; }
|
||||||
|
.aw-badge-co2 { background: #eff6ff; color: #1e40af; border-color: #93c5fd; }
|
||||||
|
|
||||||
|
/* Trend mini chart */
|
||||||
|
.aw-trend-section { margin-top: 2px; }
|
||||||
|
.aw-trend-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-light);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.aw-trend-bars {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
.aw-trend-col {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.aw-trend-col.aw-trend-empty { opacity: 0.35; }
|
||||||
|
.aw-trend-rate {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.aw-rate-good { color: var(--success); }
|
||||||
|
.aw-rate-ok { color: var(--warning); }
|
||||||
|
.aw-rate-bad { color: var(--danger); }
|
||||||
|
.aw-trend-bar-wrap {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-main);
|
||||||
|
min-height: 6px;
|
||||||
|
}
|
||||||
|
.aw-trend-bar-fill {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: height 0.5s ease;
|
||||||
|
}
|
||||||
|
.aw-tbar-good { background: var(--success); }
|
||||||
|
.aw-tbar-ok { background: var(--warning); }
|
||||||
|
.aw-tbar-bad { background: var(--danger); }
|
||||||
|
.aw-trend-label {
|
||||||
|
font-size: 0.64rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Source footnote */
|
||||||
|
.aw-source {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
.waste-legend-dot.used { background: var(--success); }
|
|
||||||
.waste-legend-dot.wasted { background: var(--danger); }
|
|
||||||
|
|
||||||
.alert-section h3 {
|
.alert-section h3 {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
|
|||||||
+138
-20
@@ -2071,6 +2071,138 @@ function showPage(pageId, param = null) {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== ANTI-WASTE SECTION =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benchmark data per language (data sources: REDUCE/Eurostat for IT/DE, USDA/NRDC for US).
|
||||||
|
* avgWasteRate: % of disposal events that end in waste (not consumed).
|
||||||
|
* avgKgMonth: kg wasted at household level per person per month.
|
||||||
|
* costPerKg: avg food cost per kg in the respective country (EUR/USD).
|
||||||
|
* KG_PER_EVENT: assumed avg weight per disposal transaction.
|
||||||
|
*/
|
||||||
|
const WASTE_BENCHMARKS = {
|
||||||
|
it: { avgWasteRate: 22, avgKgMonth: 5.4, costPerKg: 8.2, currency: '€', countryKey: 'antiwaste.country_it' },
|
||||||
|
de: { avgWasteRate: 20, avgKgMonth: 6.5, costPerKg: 7.7, currency: '€', countryKey: 'antiwaste.country_de' },
|
||||||
|
en: { avgWasteRate: 30, avgKgMonth: 9.2, costPerKg: 8.5, currency: '$', countryKey: 'antiwaste.country_en' },
|
||||||
|
};
|
||||||
|
const _AW_KG_PER_EVENT = 0.5; // estimated avg kg per out/waste transaction
|
||||||
|
|
||||||
|
function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60, wastedP60) {
|
||||||
|
const section = document.getElementById('waste-chart-section');
|
||||||
|
const total30 = used30 + wasted30;
|
||||||
|
if (total30 === 0) { section.style.display = 'none'; return; }
|
||||||
|
section.style.display = 'block';
|
||||||
|
|
||||||
|
const bm = WASTE_BENCHMARKS[_currentLang] || WASTE_BENCHMARKS['it'];
|
||||||
|
const country = t(bm.countryKey);
|
||||||
|
|
||||||
|
// Waste rates (0–100 %)
|
||||||
|
const myRate = Math.round((wasted30 / total30) * 100);
|
||||||
|
const avgRate = bm.avgWasteRate;
|
||||||
|
|
||||||
|
// Grade
|
||||||
|
let grade, gradeClass;
|
||||||
|
if (myRate <= 3) { grade = 'A+'; gradeClass = 'ap'; }
|
||||||
|
else if (myRate <= 8) { grade = 'A'; gradeClass = 'a'; }
|
||||||
|
else if (myRate <= 15) { grade = 'B'; gradeClass = 'b'; }
|
||||||
|
else if (myRate <= 25) { grade = 'C'; gradeClass = 'c'; }
|
||||||
|
else { grade = 'D'; gradeClass = 'd'; }
|
||||||
|
|
||||||
|
// Estimated savings vs average person
|
||||||
|
const avgWastedEvents = total30 * (avgRate / 100);
|
||||||
|
const savedEvents = Math.max(0, avgWastedEvents - wasted30);
|
||||||
|
const savedKg = +(savedEvents * _AW_KG_PER_EVENT).toFixed(1);
|
||||||
|
const savedMoney = Math.round(savedKg * bm.costPerKg);
|
||||||
|
const savedMeals = Math.round(savedKg * 2.5); // ~400 g per meal
|
||||||
|
const savedCO2 = +(savedKg * 2.5).toFixed(1); // ~2.5 kg CO2 / kg food wasted
|
||||||
|
|
||||||
|
// Status message
|
||||||
|
let statusMsg, statusCls;
|
||||||
|
if (myRate < avgRate) {
|
||||||
|
statusMsg = t('antiwaste.better').replace('{country}', country).replace('{diff}', avgRate - myRate);
|
||||||
|
statusCls = 'aw-status-good';
|
||||||
|
} else if (myRate > avgRate) {
|
||||||
|
statusMsg = t('antiwaste.worse').replace('{country}', country);
|
||||||
|
statusCls = 'aw-status-bad';
|
||||||
|
} else {
|
||||||
|
statusMsg = t('antiwaste.on_par').replace('{country}', country);
|
||||||
|
statusCls = 'aw-status-ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison bars (scaled to max of the two rates, min 5 for visual)
|
||||||
|
const maxRate = Math.max(myRate, avgRate, 5);
|
||||||
|
const myBarPct = Math.round((myRate / maxRate) * 100);
|
||||||
|
const avgBarPct = Math.round((avgRate / maxRate) * 100);
|
||||||
|
|
||||||
|
// Trend (3 monthly buckets: m2=oldest … m0=current)
|
||||||
|
const totals = [usedP60 + wastedP60, usedP30 + wastedP30, total30];
|
||||||
|
const rates60 = totals.map((tot, i) => {
|
||||||
|
const w = [wastedP60, wastedP30, wasted30][i];
|
||||||
|
return tot > 0 ? Math.round((w / tot) * 100) : null;
|
||||||
|
});
|
||||||
|
const trendLabels = [t('antiwaste.months_ago_2'), t('antiwaste.months_ago_1'), t('antiwaste.this_month')];
|
||||||
|
const maxTrend = Math.max(...rates60.filter(r => r !== null), 5);
|
||||||
|
const hasTrendData = rates60[0] !== null || rates60[1] !== null;
|
||||||
|
|
||||||
|
const trendBars = rates60.map((rate, i) => {
|
||||||
|
if (rate === null) {
|
||||||
|
return `<div class="aw-trend-col aw-trend-empty">
|
||||||
|
<span class="aw-trend-rate">–</span>
|
||||||
|
<div class="aw-trend-bar-wrap"><div class="aw-trend-bar-fill" style="height:2px"></div></div>
|
||||||
|
<span class="aw-trend-label">${trendLabels[i]}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const hPct = Math.max(4, Math.round((rate / maxTrend) * 100));
|
||||||
|
const cls = rate <= 8 ? 'good' : rate <= 20 ? 'ok' : 'bad';
|
||||||
|
return `<div class="aw-trend-col">
|
||||||
|
<span class="aw-trend-rate aw-rate-${cls}">${rate}%</span>
|
||||||
|
<div class="aw-trend-bar-wrap"><div class="aw-trend-bar-fill aw-tbar-${cls}" style="height:${hPct}%"></div></div>
|
||||||
|
<span class="aw-trend-label">${trendLabels[i]}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Savings badges
|
||||||
|
const badges = [];
|
||||||
|
if (savedMoney > 0) badges.push(`<div class="aw-badge aw-badge-money">💰 ${t('antiwaste.saved_money').replace('{amount}', bm.currency + savedMoney)}</div>`);
|
||||||
|
if (savedMeals > 0) badges.push(`<div class="aw-badge aw-badge-meals">🥗 ${t('antiwaste.saved_meals').replace('{n}', savedMeals)}</div>`);
|
||||||
|
if (savedCO2 > 0) badges.push(`<div class="aw-badge aw-badge-co2">🌍 ${t('antiwaste.saved_co2').replace('{n}', savedCO2)}</div>`);
|
||||||
|
|
||||||
|
section.innerHTML = `
|
||||||
|
<div class="aw-header">
|
||||||
|
<h3 class="aw-title">${t('antiwaste.title')}</h3>
|
||||||
|
<div class="aw-grade-wrap">
|
||||||
|
<span class="aw-grade-label">${t('antiwaste.grade_label')}</span>
|
||||||
|
<span class="aw-grade aw-grade-${gradeClass}">${grade}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aw-compare">
|
||||||
|
<div class="aw-compare-row">
|
||||||
|
<span class="aw-compare-lbl aw-you-lbl">${t('antiwaste.you')}</span>
|
||||||
|
<div class="aw-bar-track"><div class="aw-bar-you" style="width:${myBarPct}%"></div></div>
|
||||||
|
<span class="aw-compare-pct aw-you-pct">${myRate}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="aw-compare-row">
|
||||||
|
<span class="aw-compare-lbl">${t('antiwaste.avg_label')} ${country}</span>
|
||||||
|
<div class="aw-bar-track"><div class="aw-bar-avg" style="width:${avgBarPct}%"></div></div>
|
||||||
|
<span class="aw-compare-pct">${avgRate}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aw-status ${statusCls}">${statusMsg}</div>
|
||||||
|
|
||||||
|
${badges.length > 0 ? `<div class="aw-savings-row">${badges.join('')}</div>` : ''}
|
||||||
|
|
||||||
|
${hasTrendData ? `
|
||||||
|
<div class="aw-trend-section">
|
||||||
|
<span class="aw-trend-title">${t('antiwaste.trend_title')}</span>
|
||||||
|
<div class="aw-trend-bars">${trendBars}</div>
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
|
<div class="aw-source">${t('antiwaste.source')}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== DASHBOARD =====
|
// ===== DASHBOARD =====
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
try {
|
try {
|
||||||
@@ -2169,26 +2301,12 @@ async function loadDashboard() {
|
|||||||
// Banner alerts (suspicious quantities + consumption predictions)
|
// Banner alerts (suspicious quantities + consumption predictions)
|
||||||
loadBannerAlerts();
|
loadBannerAlerts();
|
||||||
|
|
||||||
// Waste vs consumption chart
|
// Anti-waste section
|
||||||
const wasteSection = document.getElementById('waste-chart-section');
|
_renderAntiWasteSection(
|
||||||
const used30 = statsData.used_30d || 0;
|
statsData.used_30d || 0, statsData.wasted_30d || 0,
|
||||||
const wasted30 = statsData.wasted_30d || 0;
|
statsData.used_prev_30d || 0, statsData.wasted_prev_30d || 0,
|
||||||
const total30 = used30 + wasted30;
|
statsData.used_prev_60d || 0, statsData.wasted_prev_60d || 0
|
||||||
if (total30 > 0) {
|
);
|
||||||
wasteSection.style.display = 'block';
|
|
||||||
const usedPct = Math.round((used30 / total30) * 100);
|
|
||||||
const wastedPct = 100 - usedPct;
|
|
||||||
document.getElementById('waste-chart-bar').innerHTML = `
|
|
||||||
<div class="waste-bar-used" style="width:${usedPct}%"></div>
|
|
||||||
<div class="waste-bar-wasted" style="width:${wastedPct}%"></div>
|
|
||||||
`;
|
|
||||||
document.getElementById('waste-chart-legend').innerHTML = `
|
|
||||||
<span class="waste-legend-item"><span class="waste-legend-dot used"></span> ${t('dashboard.consumed').replace('{n}', used30).replace('{pct}', usedPct)}</span>
|
|
||||||
<span class="waste-legend-item"><span class="waste-legend-dot wasted"></span> ${t('dashboard.wasted').replace('{n}', wasted30).replace('{pct}', wastedPct)}</span>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
wasteSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opened (partially used products with known package capacity)
|
// Opened (partially used products with known package capacity)
|
||||||
const openedSection = document.getElementById('alert-opened');
|
const openedSection = document.getElementById('alert-opened');
|
||||||
|
|||||||
+2
-6
@@ -96,12 +96,8 @@
|
|||||||
<div id="expiring-list"></div>
|
<div id="expiring-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waste vs consumption mini chart -->
|
<!-- Anti-Waste Report Card (content fully rendered by JS) -->
|
||||||
<div class="waste-chart-section" id="waste-chart-section" style="display:none">
|
<div id="waste-chart-section" style="display:none"></div>
|
||||||
<h3 data-i18n="dashboard.stats_period">📊 Ultimi 30 giorni</h3>
|
|
||||||
<div class="waste-chart-bar" id="waste-chart-bar"></div>
|
|
||||||
<div class="waste-chart-legend" id="waste-chart-legend"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Opened (partially used) products -->
|
<!-- Opened (partially used) products -->
|
||||||
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
|
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
|
||||||
|
|||||||
@@ -675,6 +675,26 @@
|
|||||||
"appliance_added": "Gerät hinzugefügt",
|
"appliance_added": "Gerät hinzugefügt",
|
||||||
"item_added": "{name} hinzugefügt"
|
"item_added": "{name} hinzugefügt"
|
||||||
},
|
},
|
||||||
|
"antiwaste": {
|
||||||
|
"title": "🌱 Anti-Waste Report",
|
||||||
|
"grade_label": "Note",
|
||||||
|
"you": "Du",
|
||||||
|
"avg_label": "Ø",
|
||||||
|
"better": "🎉 Du verschwendest {diff}% weniger als der {country}!",
|
||||||
|
"worse": "⚠️ Du verschwendest mehr als der {country}. Verbesserungspotenzial!",
|
||||||
|
"on_par": "→ Du liegst beim {country}. Du kannst noch besser werden!",
|
||||||
|
"saved_money": "~{amount}/Monat gespart",
|
||||||
|
"saved_meals": "~{n} Mahlzeiten gerettet",
|
||||||
|
"saved_co2": "{n} kg CO₂ eingespart",
|
||||||
|
"trend_title": "Trend (letzte 3 Monate)",
|
||||||
|
"months_ago_2": "-60 Tage",
|
||||||
|
"months_ago_1": "-30 Tage",
|
||||||
|
"this_month": "Jetzt",
|
||||||
|
"country_it": "ital. Durchschnitt",
|
||||||
|
"country_de": "dt. Durchschnitt",
|
||||||
|
"country_en": "US-Durchschnitt",
|
||||||
|
"source": "Quellen: REDUCE, Eurostat, USDA 2021"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"generic": "Fehler",
|
"generic": "Fehler",
|
||||||
"network": "Netzwerkfehler",
|
"network": "Netzwerkfehler",
|
||||||
|
|||||||
@@ -675,6 +675,26 @@
|
|||||||
"appliance_added": "Appliance added",
|
"appliance_added": "Appliance added",
|
||||||
"item_added": "{name} added"
|
"item_added": "{name} added"
|
||||||
},
|
},
|
||||||
|
"antiwaste": {
|
||||||
|
"title": "🌱 Anti-Waste Report",
|
||||||
|
"grade_label": "Grade",
|
||||||
|
"you": "You",
|
||||||
|
"avg_label": "Avg",
|
||||||
|
"better": "🎉 You waste {diff}% less than the {country}!",
|
||||||
|
"worse": "⚠️ You waste more than the {country}. Let's improve!",
|
||||||
|
"on_par": "→ You're at the {country}. You can do better!",
|
||||||
|
"saved_money": "~{amount}/month saved",
|
||||||
|
"saved_meals": "~{n} meals saved",
|
||||||
|
"saved_co2": "{n} kg CO₂ avoided",
|
||||||
|
"trend_title": "Trend (last 3 months)",
|
||||||
|
"months_ago_2": "-60 days",
|
||||||
|
"months_ago_1": "-30 days",
|
||||||
|
"this_month": "Now",
|
||||||
|
"country_it": "Italian avg",
|
||||||
|
"country_de": "German avg",
|
||||||
|
"country_en": "US average",
|
||||||
|
"source": "Sources: REDUCE, Eurostat, USDA 2021"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"generic": "Error",
|
"generic": "Error",
|
||||||
"network": "Network error",
|
"network": "Network error",
|
||||||
|
|||||||
@@ -675,6 +675,26 @@
|
|||||||
"appliance_added": "Elettrodomestico aggiunto",
|
"appliance_added": "Elettrodomestico aggiunto",
|
||||||
"item_added": "{name} aggiunto"
|
"item_added": "{name} aggiunto"
|
||||||
},
|
},
|
||||||
|
"antiwaste": {
|
||||||
|
"title": "🌱 Anti-Waste Report",
|
||||||
|
"grade_label": "Voto",
|
||||||
|
"you": "Tu",
|
||||||
|
"avg_label": "Media",
|
||||||
|
"better": "🎉 Sprechi il {diff}% in meno della {country}!",
|
||||||
|
"worse": "⚠️ Sprechi più della media {country}. Puoi migliorare!",
|
||||||
|
"on_par": "→ Sei nella media {country}. Prova a fare ancora meglio!",
|
||||||
|
"saved_money": "~{amount}/mese risparmiati",
|
||||||
|
"saved_meals": "~{n} pasti salvati",
|
||||||
|
"saved_co2": "{n} kg CO₂ evitata",
|
||||||
|
"trend_title": "Andamento (ultimi 3 mesi)",
|
||||||
|
"months_ago_2": "-60 gg",
|
||||||
|
"months_ago_1": "-30 gg",
|
||||||
|
"this_month": "Ora",
|
||||||
|
"country_it": "media italiana",
|
||||||
|
"country_de": "media tedesca",
|
||||||
|
"country_en": "media USA",
|
||||||
|
"source": "Fonti: REDUCE, Eurostat, USDA 2021"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"generic": "Errore",
|
"generic": "Errore",
|
||||||
"network": "Errore di rete",
|
"network": "Errore di rete",
|
||||||
|
|||||||
Reference in New Issue
Block a user