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:
@@ -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
@@ -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
@@ -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 => {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user