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:
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user