Dashboard: move waste-chart above expiring; fix opened-items conf split, expiry cache, AI validation, MAX_SHOWN 20; remove DupliClick from README

This commit is contained in:
dadaloop82
2026-04-29 17:02:10 +00:00
parent 3c9fe7dfea
commit e71ef3aba3
5 changed files with 86 additions and 6 deletions
+1 -1
View File
@@ -46,7 +46,7 @@
- **Smart predictions** — Know what you'll need before you run out - **Smart predictions** — Know what you'll need before you run out
- **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed - **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed
- **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-migration** — Items already on the Bring! list are silently renamed to their generic name in the background (throttled, runs on list load) - **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-migration** — Items already on the Bring! list are silently renamed to their generic name in the background (throttled, runs on list load)
- **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app- **DupliClick integration** — Online grocery ordering (Gruppo Poli) - **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app
### 🍳 Cooking Mode ### 🍳 Cooking Mode
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface - **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
+36 -1
View File
@@ -1691,6 +1691,38 @@ function getStats(PDO $db): void {
} }
$item['is_edible'] = $item['days_to_expiry'] === null || $item['days_to_expiry'] >= 0; $item['is_edible'] = $item['days_to_expiry'] === null || $item['days_to_expiry'] >= 0;
$item['has_opened_at'] = !empty($item['opened_at']); $item['has_opened_at'] = !empty($item['opened_at']);
// For conf items with opened_at that contain both whole and fractional confs:
// split into a "sealed" entry (whole confs, package expiry) and an "opened" entry (fraction, shelf-life expiry).
// This prevents a row like "1.59 conf" from showing a single misleading entry that mixes
// a still-sealed package with an opened portion.
if ($item['unit'] === 'conf' && $item['has_opened_at'] && $originalExpiry !== null) {
$qty = (float)$item['quantity'];
$whole = (int)floor($qty + 0.001);
$frac = round($qty - (float)$whole, 4);
if ($whole >= 1 && $frac >= 0.001) {
// Sealed whole confs: show with original package expiry (only if near expiry ≤ 7 d)
$sealedDays = (int)round(($originalExpiry - $today) / 86400);
if ($sealedDays <= 7 && $sealedDays >= -30) {
$si = $item;
$si['quantity'] = (float)$whole;
$si['opened_at'] = null;
$si['opened_expiry'] = date('Y-m-d', $originalExpiry);
$si['days_to_expiry'] = $sealedDays;
$si['is_edible'] = $sealedDays >= 0;
$si['has_opened_at'] = false;
$opened[] = $si;
}
// Opened fractional part: use the already-computed opened shelf-life expiry
if ($item['days_to_expiry'] === null || $item['days_to_expiry'] <= 365) {
$fi = $item;
$fi['quantity'] = $frac;
$opened[] = $fi;
}
continue;
}
}
// Hide non-perishable items (salt, sugar, spirits, oil, etc.) — they won't expire usefully // Hide non-perishable items (salt, sugar, spirits, oil, etc.) — they won't expire usefully
if ($item['days_to_expiry'] !== null && $item['days_to_expiry'] > 365) continue; if ($item['days_to_expiry'] !== null && $item['days_to_expiry'] > 365) continue;
// Hide legacy fractional items (no opened_at) with far-off expiry — not useful for home widget // Hide legacy fractional items (no opened_at) with far-off expiry — not useful for home widget
@@ -2155,7 +2187,10 @@ function getOpenedShelfLifeDays(string $name, string $category, string $location
if ($result['http_code'] === 200) { if ($result['http_code'] === 200) {
$text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? ''); $text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '');
$parsed = (int)preg_replace('/\D/', '', $text); $parsed = (int)preg_replace('/\D/', '', $text);
if ($parsed > 0 && $parsed <= 3650) { // Reject AI values if they are suspiciously low compared to the rule-based estimate
// (protects against Gemini hallucinations like "1 day for butter").
$ruleMin = estimateOpenedExpiryDaysPHP($name, $category, $location);
if ($parsed > 0 && $parsed <= 3650 && $parsed >= max(1, (int)floor($ruleMin * 0.5))) {
$days = $parsed; $days = $parsed;
} }
} }
+1 -1
View File
@@ -2529,7 +2529,7 @@ async function loadDashboard() {
if (statsData.opened && statsData.opened.length > 0) { if (statsData.opened && statsData.opened.length > 0) {
// Sorted server-side by days_to_expiry ASC // Sorted server-side by days_to_expiry ASC
openedSection.style.display = 'block'; openedSection.style.display = 'block';
const MAX_SHOWN = 10; const MAX_SHOWN = 20;
const visible = statsData.opened.slice(0, MAX_SHOWN); const visible = statsData.opened.slice(0, MAX_SHOWN);
const extra = statsData.opened.length - visible.length; const extra = statsData.opened.length - visible.length;
openedList.innerHTML = visible.map(item => { openedList.innerHTML = visible.map(item => {
+44
View File
@@ -0,0 +1,44 @@
{
"226887def70e33ef73290ebfe75ed4d0": {
"days": 7,
"source": "ai",
"name": "Polpa di pomodoro finissima",
"location": "frigo",
"ts": 1777444819
},
"0ed51c9496aa9edfe38caf41772f54ed": {
"days": 7,
"source": "rule",
"name": "Latte di Montagna",
"location": "frigo",
"ts": 1777444820
},
"2d63d0216a75d46b465150e925d2e7ad": {
"days": 30,
"source": "rule",
"name": "Burro",
"location": "frigo",
"ts": 1777444821
},
"f6504a014f17457e3dbe0ba917ad681f": {
"days": 7,
"source": "rule",
"name": "Latte di Montagna",
"location": "dispensa",
"ts": 1777444888
},
"7b15356b493402e17fa417a389e89716": {
"days": 60,
"source": "rule",
"name": "Yaourt Vanille",
"location": "dispensa",
"ts": 1777472391
},
"9afdf35c4a256867ef47c32495349eb6": {
"days": 5,
"source": "rule",
"name": "Yaourt Vanille",
"location": "frigo",
"ts": 1777480477
}
}
+4 -3
View File
@@ -90,15 +90,16 @@
<h3 data-i18n="dashboard.expired_title">🚫 Scaduti</h3> <h3 data-i18n="dashboard.expired_title">🚫 Scaduti</h3>
<div id="expired-list"></div> <div id="expired-list"></div>
</div> </div>
<!-- Anti-Waste Report Card (content fully rendered by JS) -->
<div id="waste-chart-section" style="display:none"></div>
<!-- Alert for soonest expiring items --> <!-- Alert for soonest expiring items -->
<div class="alert-section" id="alert-expiring" style="display:none"> <div class="alert-section" id="alert-expiring" style="display:none">
<h3 data-i18n="dashboard.expiring_title">⏰ Prossime Scadenze</h3> <h3 data-i18n="dashboard.expiring_title">⏰ Prossime Scadenze</h3>
<div id="expiring-list"></div> <div id="expiring-list"></div>
</div> </div>
<!-- Anti-Waste Report Card (content fully rendered by JS) -->
<div id="waste-chart-section" style="display:none"></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">
<h3 data-i18n="dashboard.opened_title">📦 Prodotti Aperti</h3> <h3 data-i18n="dashboard.opened_title">📦 Prodotti Aperti</h3>