20260316c: Dashboard quick recipe button + waste chart

- Added 'Ricetta veloce' button between stat cards and expired section
  (navigates to chat and auto-asks Gemini for a recipe with expiring products)
- Added waste vs consumption mini chart between expiring and opened sections
  (horizontal bar showing used/wasted ratio from last 30 days)
- API: getStats() now returns used_30d and wasted_30d counts
- Cache busters updated to 20260316c
This commit is contained in:
dadaloop82
2026-03-16 07:49:10 +00:00
parent eb5ee60dd7
commit c6a3ae6e63
4 changed files with 146 additions and 2 deletions
+15
View File
@@ -835,6 +835,19 @@ function getStats(PDO $db): void {
ORDER BY i.updated_at DESC
")->fetchAll();
// Waste vs consumption stats (last 30 days)
$wasteStats = $db->query("
SELECT type, COUNT(*) as count
FROM transactions
WHERE type IN ('out', 'waste') AND created_at >= datetime('now', '-30 days')
GROUP BY type
")->fetchAll();
$used30 = 0; $wasted30 = 0;
foreach ($wasteStats as $ws) {
if ($ws['type'] === 'out') $used30 = (int)$ws['count'];
if ($ws['type'] === 'waste') $wasted30 = (int)$ws['count'];
}
echo json_encode([
'total_products' => (int)$totalProducts,
'total_items' => (float)$totalItems,
@@ -844,6 +857,8 @@ function getStats(PDO $db): void {
'expiring_soon' => $expiring,
'expired' => $expired,
'opened' => $opened,
'used_30d' => $used30,
'wasted_30d' => $wasted30,
]);
}
+75
View File
@@ -275,6 +275,81 @@ body {
margin-bottom: 12px;
}
/* Quick recipe bar */
.quick-recipe-bar {
margin-bottom: 12px;
}
.btn-quick-recipe {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 14px 16px;
background: linear-gradient(135deg, #f97316, #ea580c);
color: #fff;
border: none;
border-radius: var(--radius);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
text-align: left;
box-shadow: 0 2px 8px rgba(249,115,22,0.3);
}
.btn-quick-recipe span:first-child { font-size: 1.3rem; }
.btn-quick-recipe .quick-recipe-text { flex: 1; }
.btn-quick-recipe span:last-child { font-size: 1.1rem; opacity: 0.8; }
.btn-quick-recipe:active { transform: scale(0.98); }
/* Waste chart section */
.waste-chart-section {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 12px;
}
.waste-chart-section h3 {
font-size: 0.95rem;
margin-bottom: 10px;
}
.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;
align-items: center;
gap: 4px;
}
.waste-legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.waste-legend-dot.used { background: var(--success); }
.waste-legend-dot.wasted { background: var(--danger); }
.alert-section h3 {
font-size: 1.05rem;
margin-bottom: 10px;
+38
View File
@@ -768,6 +768,14 @@ async function loadDashboard() {
// Load shopping list count from Bring!
loadShoppingCount();
// Quick recipe button - show when there are expiring products
const recipeBar = document.getElementById('quick-recipe-bar');
if (statsData.expiring_soon && statsData.expiring_soon.length > 0) {
recipeBar.style.display = 'block';
} else {
recipeBar.style.display = 'none';
}
// Expiring items
const expiringSection = document.getElementById('alert-expiring');
const expiringList = document.getElementById('expiring-list');
@@ -832,6 +840,27 @@ async function loadDashboard() {
// Review suspicious quantities
loadReviewItems();
// Waste vs consumption chart
const wasteSection = document.getElementById('waste-chart-section');
const used30 = statsData.used_30d || 0;
const wasted30 = statsData.wasted_30d || 0;
const total30 = used30 + wasted30;
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> Consumati: ${used30} (${usedPct}%)</span>
<span class="waste-legend-item"><span class="waste-legend-dot wasted"></span> Buttati: ${wasted30} (${wastedPct}%)</span>
`;
} else {
wasteSection.style.display = 'none';
}
// Opened (partially used products with known package capacity)
const openedSection = document.getElementById('alert-opened');
const openedList = document.getElementById('opened-list');
@@ -889,6 +918,15 @@ async function loadDashboard() {
}
}
function quickRecipeSuggestion() {
// Navigate to chat and auto-send a prompt about expiring products
showPage('chat');
setTimeout(() => {
document.getElementById('chat-input').value = 'Suggeriscimi una ricetta veloce usando i prodotti che scadono prima!';
sendChatMessage();
}, 500);
}
// === SUSPICIOUS QUANTITY REVIEW ===
const QTY_THRESHOLDS = {
'pz': { min: 0.3, max: 50 },
+18 -2
View File
@@ -9,7 +9,7 @@
<title>Dispensa Manager</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
<link rel="stylesheet" href="assets/css/style.css?v=20260315f">
<link rel="stylesheet" href="assets/css/style.css?v=20260316c">
<!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
</head>
@@ -58,6 +58,15 @@
</div>
</div>
<!-- Quick recipe suggestion -->
<div class="quick-recipe-bar" id="quick-recipe-bar" style="display:none">
<button class="btn-quick-recipe" onclick="quickRecipeSuggestion()">
<span>🍳</span>
<span class="quick-recipe-text">Ricetta veloce con prodotti in scadenza</span>
<span></span>
</button>
</div>
<!-- Alert for expired items (on top) -->
<div class="alert-section alert-danger" id="alert-expired" style="display:none">
<h3>🚫 Scaduti</h3>
@@ -69,6 +78,13 @@
<div id="expiring-list"></div>
</div>
<!-- Waste vs consumption mini chart -->
<div class="waste-chart-section" id="waste-chart-section" style="display:none">
<h3>📊 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 -->
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
<h3>📦 Prodotti Aperti</h3>
@@ -895,6 +911,6 @@
</div>
</div>
<script src="assets/js/app.js?v=20260316b"></script>
<script src="assets/js/app.js?v=20260316c"></script>
</body>
</html>