feat: v1.3.0 — banner notifications, quick-access, swipe navigation, bug fixes

Added:
- Expired/expiring product banner alerts with use, throw, edit, dismiss actions
- Priority-sorted notifications (expired > expiring > suspicious qty > predictions)
- Touch swipe navigation for banner with dot indicators and arrow buttons
- Quick-access buttons on inventory (4 recent + 8 popular products)
- Auto-refresh banner every 5 min on dashboard
- Edit expiry dates directly from expired/expiring notifications

Fixed:
- Ignore negative BLE scale readings
- Banner re-appearing after edit (confirmation now persisted)
- False consumption predictions when inventory was manually edited
- Kiosk overlay no longer blocks web app header
This commit is contained in:
dadaloop82
2026-04-18 05:37:03 +00:00
parent 52cfbba663
commit 9e2a24def4
10 changed files with 593 additions and 12 deletions
+65
View File
@@ -193,6 +193,10 @@ try {
getConsumptionPredictions($db);
break;
case 'recent_popular_products':
recentPopularProducts($db);
break;
// ===== AI =====
case 'gemini_expiry':
geminiReadExpiry();
@@ -1275,6 +1279,43 @@ function getStats(PDO $db): void {
]);
}
// ===== RECENT & POPULAR PRODUCTS =====
function recentPopularProducts(PDO $db): void {
// Last 4 distinct products used (type='out'), most recent first
$recentStmt = $db->query("
SELECT DISTINCT t.product_id, p.name, p.brand, p.category, p.image_url, p.unit,
MAX(t.created_at) as last_used
FROM transactions t
JOIN products p ON p.id = t.product_id
WHERE t.type = 'out'
GROUP BY t.product_id
ORDER BY last_used DESC
LIMIT 4
");
$recent = $recentStmt->fetchAll(PDO::FETCH_ASSOC);
$recentIds = array_map(fn($r) => (int)$r['product_id'], $recent);
// Top 12 most frequently used products (to allow filtering out recent ones client-side)
$popularStmt = $db->query("
SELECT t.product_id, p.name, p.brand, p.category, p.image_url, p.unit,
COUNT(*) as usage_count
FROM transactions t
JOIN products p ON p.id = t.product_id
WHERE t.type = 'out'
AND t.created_at >= datetime('now', '-90 days')
GROUP BY t.product_id
ORDER BY usage_count DESC
LIMIT 12
");
$popular = $popularStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'recent' => $recent,
'popular' => $popular,
'recent_ids' => $recentIds,
]);
}
// ===== CONSUMPTION PREDICTIONS =====
/**
@@ -1337,6 +1378,30 @@ function getConsumptionPredictions(PDO $db): void {
$restockDate = strtotime($restock['created_at']);
$restockQty = floatval($restock['quantity']);
// If inventory was manually edited (updated_at > last restock), use the
// manual update as baseline instead — otherwise the prediction is comparing
// against a stale restock quantity that no longer reflects reality.
$lastManualUpdate = strtotime($item['updated_at']);
if ($lastManualUpdate > $restockDate) {
// Inventory was manually corrected after last restock → use current qty
// as a fresh baseline from that point; only consider OUT transactions
// that happened AFTER the manual update.
$txnsSinceUpdate = $db->prepare("
SELECT SUM(quantity) as total
FROM transactions
WHERE product_id = ? AND location = ? AND type = 'out'
AND created_at > ?
");
$txnsSinceUpdate->execute([$pid, $loc, $item['updated_at']]);
$usedSinceUpdate = floatval($txnsSinceUpdate->fetchColumn() ?: 0);
$daysSinceBaseline = max(1, (time() - $lastManualUpdate) / 86400);
// The effective "restock" qty is what inventory had at manual edit time
// which is current qty + what was consumed since then
$restockQty = floatval($item['quantity']) + $usedSinceUpdate;
$restockDate = $lastManualUpdate;
}
$daysSinceRestock = max(1, (time() - $restockDate) / 86400);
// Predicted remaining qty = restock qty - (daily rate * days since restock)