merge: develop → main (v1.7.13 — cooking mode kiosk fix, potato shelf life, move-after-use preference)
This commit is contained in:
@@ -104,7 +104,7 @@
|
||||
- **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming
|
||||
- **Real-time status** — Scale connection indicator always visible in the header
|
||||
- **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models
|
||||
- **Built into kiosk (v1.6.0+)** — BLE gateway runs as an integrated foreground service inside the [EverShelf Kiosk](evershelf-kiosk/) app; no separate APK needed. The standalone gateway app in [`evershelf-scale-gateway/`](evershelf-scale-gateway/) is deprecated but kept for non-kiosk use cases.
|
||||
- **Built into kiosk (v1.6.0+)** — BLE gateway runs as an integrated foreground service inside the [EverShelf Kiosk](evershelf-kiosk/) app; no separate APK needed.
|
||||
|
||||
### 📺 Android Kiosk Mode (Add-on)
|
||||
- **Dedicated tablet app** — Full-screen WebView wrapper for wall-mounted kitchen tablets
|
||||
|
||||
@@ -363,6 +363,10 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
|
||||
if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 2;
|
||||
if (preg_match('/\blatte\b/', $n)) return 1;
|
||||
if (preg_match('/\bformaggio\b/', $n)) return 2;
|
||||
// Root vegetables / tubers in pantry: sfusi in un sacchetto, durano 3-5 settimane
|
||||
if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 30;
|
||||
if (preg_match('/\b(cipolla|cipolle|aglio|scalogno|porro)\b/', $n)) return 30;
|
||||
if (preg_match('/\b(carota|carote)\b/', $n)) return 14;
|
||||
return 60; // generic pantry fallback
|
||||
}
|
||||
|
||||
|
||||
+75
-4
@@ -1553,7 +1553,7 @@ function estimateExpiryDays(product, location) {
|
||||
else if (/carota|carote|zucchina|zucchine|peperoni|melanzane/.test(name)) days = 7;
|
||||
else if (/broccoli|cavolfiore|cavolo|spinaci|bietola/.test(name)) days = 5;
|
||||
else if (/cipolla|cipolle/.test(name)) days = 10;
|
||||
else if (/patata|patate/.test(name)) days = 14;
|
||||
else if (/patata|patate/.test(name)) days = 30; // whole tubers in a bag, pantry: 3-5 weeks
|
||||
else if (/biscott|cracker|grissini|fette\s+biscott/.test(name)) days = 180;
|
||||
else if (/nutella|marmellata|miele/.test(name)) days = 365;
|
||||
else if (/passata|pelati|pomodor/.test(name)) days = 730;
|
||||
@@ -1573,7 +1573,7 @@ function estimateExpiryDays(product, location) {
|
||||
else if (/arancia|arance|agrumi|mandarini|limone|limoni/.test(name)) days = Math.max(days, 21);
|
||||
else if (/carota|carote/.test(name)) days = Math.max(days, 21);
|
||||
else if (/cipolla/.test(name)) days = Math.max(days, 14);
|
||||
else if (/patata|patate/.test(name)) days = Math.max(days, 21);
|
||||
else if (/patata|patate/.test(name)) days = Math.max(days, 30);
|
||||
else if (/pera|pere/.test(name)) days = Math.max(days, 21);
|
||||
else if (/kiwi/.test(name)) days = Math.max(days, 28);
|
||||
else if (/uva/.test(name)) days = Math.max(days, 14);
|
||||
@@ -7626,6 +7626,9 @@ async function loadUseInventoryInfo() {
|
||||
// Build location buttons only for locations where the product exists
|
||||
const productLocations = [...new Set(items.map(i => i.location))];
|
||||
const locSelector = document.getElementById('use-location-selector');
|
||||
// Hide the location row when the product is in only one location (nothing to choose)
|
||||
const locGroup = document.getElementById('use-location-group');
|
||||
if (locGroup) locGroup.style.display = productLocations.length > 1 ? '' : 'none';
|
||||
|
||||
// Prefer the remembered location (if confirmed), else use the opened-package heuristic
|
||||
const prefLoc = _getPreferredUseLocation(currentProduct.id);
|
||||
@@ -7824,6 +7827,42 @@ function selectUseLocation(btn, loc) {
|
||||
const _PREF_LOC_KEY = '_prefUseLoc';
|
||||
const _PREF_LOC_NEEDED = 2; // choices needed to confirm a preference
|
||||
|
||||
// ── PREFERRED MOVE-AFTER-USE LOCATION ────────────────────────────────────
|
||||
// Tracks where the user puts the remainder after using a product.
|
||||
// After _PREF_MOVE_NEEDED consistent choices, the modal is skipped entirely.
|
||||
const _PREF_MOVE_KEY = '_prefMoveLoc';
|
||||
const _PREF_MOVE_NEEDED = 2;
|
||||
let _pendingMoveCtx = null; // { productId, fromLoc, openedId } — set before showing modal
|
||||
|
||||
function _getMoveLocHistory(productId, fromLoc) {
|
||||
try {
|
||||
const all = JSON.parse(localStorage.getItem(_PREF_MOVE_KEY) || '{}');
|
||||
return all[`${productId}|${fromLoc}`] || [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function _recordMoveLocChoice(productId, fromLoc, toLoc) {
|
||||
try {
|
||||
const all = JSON.parse(localStorage.getItem(_PREF_MOVE_KEY) || '{}');
|
||||
const key = `${productId}|${fromLoc}`;
|
||||
const hist = all[key] || [];
|
||||
hist.push(toLoc);
|
||||
if (hist.length > 8) hist.splice(0, hist.length - 8);
|
||||
all[key] = hist;
|
||||
localStorage.setItem(_PREF_MOVE_KEY, JSON.stringify(all));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
function _getPreferredMoveLoc(productId, fromLoc) {
|
||||
const hist = _getMoveLocHistory(productId, fromLoc);
|
||||
if (hist.length < _PREF_MOVE_NEEDED) return null;
|
||||
const recent = hist.slice(-5);
|
||||
const counts = {};
|
||||
for (const loc of recent) counts[loc] = (counts[loc] || 0) + 1;
|
||||
const [topLoc, topCount] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
|
||||
return topCount >= _PREF_MOVE_NEEDED ? topLoc : null;
|
||||
}
|
||||
|
||||
function _getPrefLocHistory(productId) {
|
||||
try {
|
||||
const all = JSON.parse(localStorage.getItem(_PREF_LOC_KEY) || '{}');
|
||||
@@ -8176,6 +8215,22 @@ function startMoveModalCountdown(btnId, onExpire) {
|
||||
}
|
||||
|
||||
function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacuumSealed, unit) {
|
||||
// Store context so _saveVacuumAndStay can record the choice
|
||||
_pendingMoveCtx = { productId: product.id, fromLoc, openedId };
|
||||
|
||||
// If a preference is established, skip the modal entirely and auto-apply
|
||||
const prefMoveLoc = _getPreferredMoveLoc(product.id, fromLoc);
|
||||
if (prefMoveLoc) {
|
||||
if (prefMoveLoc === fromLoc) {
|
||||
// Preference: stay in place — silent, no modal
|
||||
_saveVacuumAndStay(openedId || 0);
|
||||
} else {
|
||||
// Preference: move to another location — apply silently
|
||||
confirmMoveAfterUse(product.id, fromLoc, prefMoveLoc, openedId || 0, !!(openedVacuumSealed ?? product.vacuum_sealed));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
|
||||
const locButtons = otherLocs.map(([k, v]) =>
|
||||
`<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
|
||||
@@ -8209,6 +8264,11 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu
|
||||
|
||||
/** Save vacuum state when user chooses to keep the item at the current location. */
|
||||
async function _saveVacuumAndStay(openedId) {
|
||||
// Record the "stay" preference before closing
|
||||
if (_pendingMoveCtx) {
|
||||
_recordMoveLocChoice(_pendingMoveCtx.productId, _pendingMoveCtx.fromLoc, _pendingMoveCtx.fromLoc);
|
||||
_pendingMoveCtx = null;
|
||||
}
|
||||
closeModal();
|
||||
if (openedId) {
|
||||
const isVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
|
||||
@@ -8220,9 +8280,14 @@ async function _saveVacuumAndStay(openedId) {
|
||||
showPage('dashboard');
|
||||
}
|
||||
|
||||
async function confirmMoveAfterUse(productId, fromLoc, toLoc, openedId) {
|
||||
async function confirmMoveAfterUse(productId, fromLoc, toLoc, openedId, forcedVacuum) {
|
||||
clearMoveModalTimer();
|
||||
const newVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
|
||||
const newVacuum = forcedVacuum !== undefined ? (forcedVacuum ? 1 : 0) : (document.getElementById('move-vacuum-check')?.checked ? 1 : 0);
|
||||
// Record preference
|
||||
if (_pendingMoveCtx && _pendingMoveCtx.productId === productId) {
|
||||
_recordMoveLocChoice(productId, fromLoc, toLoc);
|
||||
_pendingMoveCtx = null;
|
||||
}
|
||||
closeModal();
|
||||
showLoading(true);
|
||||
try {
|
||||
@@ -12026,6 +12091,9 @@ function startCookingMode() {
|
||||
document.getElementById('cooking-tts-btn').textContent = '🔊';
|
||||
document.getElementById('cooking-overlay').style.display = 'flex';
|
||||
document.body.classList.add('cooking-mode-active');
|
||||
// Hide kiosk overlay — it lives outside <body> with z-index:2147483647 and would overlap cooking UI
|
||||
const _kioskOvl = document.getElementById('_kiosk_overlay');
|
||||
if (_kioskOvl) _kioskOvl.style.display = 'none';
|
||||
_bindCookingWheelControls();
|
||||
const wheelEl = document.getElementById('cooking-wheel');
|
||||
if (wheelEl) setTimeout(() => wheelEl.focus(), 20);
|
||||
@@ -12039,6 +12107,9 @@ function startCookingMode() {
|
||||
function closeCookingMode() {
|
||||
document.getElementById('cooking-overlay').style.display = 'none';
|
||||
document.body.classList.remove('cooking-mode-active');
|
||||
// Restore kiosk overlay
|
||||
const _kioskOvl = document.getElementById('_kiosk_overlay');
|
||||
if (_kioskOvl) _kioskOvl.style.display = 'flex';
|
||||
// NOTE: intentionally keep _cookingRecipe, _cookingStep, _cookingVisited
|
||||
// so the user can resume from the same step when they reopen
|
||||
try { screen.orientation?.unlock().catch(() => {}); } catch (_) { /* ignore */ }
|
||||
|
||||
+1
-1
@@ -407,7 +407,7 @@
|
||||
<div class="use-inventory-info" id="use-inventory-info"></div>
|
||||
<div id="use-expiry-hint" style="display:none"></div>
|
||||
<form class="form" onsubmit="submitUse(event)">
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="use-location-group">
|
||||
<label>📍 Da dove?</label>
|
||||
<div class="location-selector" id="use-location-selector">
|
||||
<button type="button" class="loc-btn active" onclick="selectUseLocation(this, 'dispensa')">🗄️ Dispensa</button>
|
||||
|
||||
Reference in New Issue
Block a user