Merge develop: smarter proactive shopping list urgency

This commit is contained in:
dadaloop82
2026-04-19 06:06:36 +00:00
5 changed files with 60 additions and 18 deletions
+22 -2
View File
@@ -3349,12 +3349,32 @@ function smartShopping(PDO $db): void {
$score += 40;
}
// Frequently used but stock getting low (predictive) — stricter thresholds
// Frequently used but stock getting low (predictive) — scale urgency by imminence
if ($urgency === 'none' && $dailyRate > 0 && $daysLeft <= 14 && $isFrequent && $isRecent) {
$daysLeftDisplay = (int)round($daysLeft);
$reasons[] = 'Finisce tra ~' . $daysLeftDisplay . 'gg';
if ($daysLeftDisplay <= 3) {
// Running out within 3 days for a frequent product → high urgency
$urgency = 'high';
$score += 70;
} elseif ($daysLeftDisplay <= 7) {
// Running out within a week → medium
$urgency = 'medium';
$score += 45;
} else {
$urgency = 'low';
$reasons[] = 'Previsto esaurimento tra ~' . round($daysLeft) . 'gg';
$score += 25;
}
}
// Also upgrade existing low urgency when imminent depletion is detected
if ($urgency === 'low' && $dailyRate > 0 && (int)round($daysLeft) <= 3 && $isFrequent) {
$urgency = 'high';
$daysLeftLbl = 'Finisce tra ~' . (int)round($daysLeft) . 'gg';
if (!in_array($daysLeftLbl, $reasons)) {
$reasons[] = $daysLeftLbl;
}
$score += 45;
}
// Opened item with fast consumption — only if actually used regularly
if ($isOpened && $urgency === 'none' && $dailyRate > 0 && $daysLeft <= 7 && $isRegular) {
+7
View File
@@ -3905,6 +3905,13 @@ body {
margin-left: 4px;
}
.log-recipe-note {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 2px;
font-style: italic;
}
.btn-log-undo {
flex-shrink: 0;
background: none;
+18 -7
View File
@@ -6725,11 +6725,17 @@ async function autoAddCriticalItems() {
const lastRun = parseInt(localStorage.getItem('_autoAddedCriticalTs') || '0');
if (Date.now() - lastRun < 10 * 60 * 1000) return;
localStorage.setItem('_autoAddedCriticalTs', String(Date.now()));
// Auto-add: critical urgency (always) + high urgency that are completely out of stock (qty=0)
const toAdd = smartShoppingItems.filter(i =>
!i.on_bring && !_isBringPurchased(i.name, i.urgency) &&
(i.urgency === 'critical' || (i.urgency === 'high' && i.current_qty === 0))
);
// Auto-add rules:
// - critical: always
// - high: when qty=0 OR pct_left<20 (almost gone) OR days_left<=3 (imminent)
// - any urgency with days_left<=2 and uses_per_month>=5 (running out tomorrow for heavy user)
const toAdd = smartShoppingItems.filter(i => {
if (i.on_bring || _isBringPurchased(i.name, i.urgency)) return false;
if (i.urgency === 'critical') return true;
if (i.urgency === 'high' && (i.current_qty === 0 || i.pct_left < 20 || i.days_left <= 3)) return true;
if (i.days_left <= 2 && (i.uses_per_month || 0) >= 5) return true;
return false;
});
if (toAdd.length === 0) return;
const itemsToAdd = toAdd.map(i => ({ name: i.name, specification: _urgencyToSpec(i.urgency, i.brand) }));
try {
@@ -8112,7 +8118,9 @@ async function loadLog(more = false) {
const locLabels = { 'frigo': '🧊 Frigo', 'freezer': '❄️ Freezer', 'dispensa': '🗄️ Dispensa' };
const locStr = t.type === 'bring' ? '' : (locLabels[loc] || ('📍 ' + loc));
const isAnnotation = (t.notes || '').includes('[Annullato]');
const notes = t.notes && !isAnnotation ? ` · ${t.notes}` : '';
const isRecipeNote = !isAnnotation && (t.notes || '').startsWith('Ricetta:');
const notes = t.notes && !isAnnotation && !isRecipeNote ? ` · ${t.notes}` : '';
const recipeNote = isRecipeNote ? `<div class="log-recipe-note">🍳 ${escapeHtml(t.notes)}</div>` : '';
const undone = t.undone == 1 || isAnnotation;
// Can undo if within 24h, not already undone, not a bring entry, not a counter-transaction
@@ -8124,6 +8132,7 @@ async function loadLog(more = false) {
html += `<div class="log-info">`;
html += `<div class="log-product"><strong>${escapeHtml(t.name)}</strong>${brand}${undone ? ' <span class="log-undone-badge">Annullato</span>' : ''}</div>`;
html += `<div class="log-detail">${typeLabel} ${t.type !== 'bring' ? (t.quantity + ' ' + (t.unit || '')) + ' · ' : ''}${locStr}${notes} · ${timeStr}</div>`;
html += recipeNote;
html += `</div>`;
if (canUndo) {
html += `<button class="btn-log-undo" onclick="undoTransactionEntry(${t.id}, '${escapeHtml(t.type)}', '${escapeHtml(t.name || '')}')" title="Annulla questa operazione">↩</button>`;
@@ -8769,11 +8778,13 @@ async function submitRecipeUse(useAll) {
btn.textContent = '⏳...';
try {
const recipeTitle = _cachedRecipe?.recipe?.title || '';
const result = await api('inventory_use', {}, 'POST', {
product_id: productId,
quantity: qty,
use_all: useAll,
location: location
location: location,
notes: recipeTitle ? `Ricetta: ${recipeTitle}` : '',
});
if (result.success) {
+7 -3
View File
@@ -16,15 +16,19 @@ android {
}
signingConfigs {
// Use the standard Android debug keystore so every machine produces
// APKs with the same debug signature — required for over-the-air updates.
// Use the standard Android debug keystore when building locally so the
// debug APK signature stays consistent across machines (needed for OTA updates).
// In CI the keystore doesn't exist — fall back to Gradle's auto-generated key.
getByName("debug") {
storeFile = file("${System.getProperty("user.home")}/.android/debug.keystore")
val ks = file("${System.getProperty("user.home")}/.android/debug.keystore")
if (ks.exists()) {
storeFile = ks
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
}
buildTypes {
debug {
+1 -1
View File
@@ -1144,7 +1144,7 @@
<span class="nav-label" data-i18n="nav.shopping">Spesa</span>
</button>
<button class="nav-btn" onclick="showPage('log')" data-page="log">
<span class="nav-icon"></span>
<span class="nav-icon">📋</span>
<span class="nav-label" data-i18n="nav.log">Storico</span>
</button>
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">