merge: v1.7.12 from develop
This commit is contained in:
@@ -5,6 +5,16 @@ All notable changes to EverShelf will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.7.12] - 2026-05-13
|
||||
|
||||
### Fixed
|
||||
- **Banner "Usa prima" con data calcolata confusa** — `_renderUseExpiryHint` mostrava una data di scadenza *calcolata* (shelf life dopo apertura) anziché la data reale. Ora, se il prodotto ha `opened_at`, il banner mostra "Quella [nel frigo], aperta da X giorni — usala prima!" usando la nuova chiave `use.expiry_warning_opened`.
|
||||
- **"Usa TUTTO / Finito" nelle ricette cancellava la riga** — `submitRecipeUse(true)` inviava `use_all: true` all'API che eseguiva un `DELETE` diretto sulla riga di inventario senza conferma. La funzione ora calcola la quantità esatta dagli item disponibili (`_recipeUseContext.items`) e invia un normale `inventory_use` con quantità esplicita.
|
||||
- **Ricette: `qty_number` in grammi per prodotti `pz`** — Il prompt AI e la post-elaborazione PHP ora istruiscono Gemini a esprimere `qty_number` come pezzi interi per ingredienti con unità `pz` (Pan bauletto, fette biscottate, ecc.). La lista ingredienti nel prompt include `[usa PEZZI interi]` per ogni prodotto `pz`. Il fallback PHP per `pz` senza `default_quantity` non divide più per 100 (trattando grammi come pezzi), ma usa il `qty_number` restituito dall'AI se sembra un conteggio plausibile, altrimenti 1.
|
||||
|
||||
### Added
|
||||
- **Traduzione `use.expiry_warning_opened`** — Nuova chiave in `it.json`, `en.json`, `de.json` con placeholder `{loc}` (posizione) e `{when}` (giorni dall'apertura).
|
||||
|
||||
## [1.7.11] - 2026-05-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -25,11 +25,15 @@
|
||||
[](https://www.sqlite.org/)
|
||||
[](Dockerfile)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## 🌍 Recent Updates (v1.7.11)
|
||||
## 🌍 Recent Updates (v1.7.12)
|
||||
|
||||
- **Banner aperto con indicazione posizione** — Nella sezione "Usa prima" il testo ora mostra "Quella nel frigo, aperta da X giorni" invece di una data di scadenza calcolata che poteva risultare confusa.
|
||||
- **Ricette: quantità in pezzi per prodotti pz** — Il prompt AI e la post-elaborazione PHP ora istruiscono Gemini a esprimere `qty_number` come pezzi interi (non grammi) per i prodotti con unità `pz` (es. Pan bauletto, fette biscottate). Il fallback PHP non divide più per 100 quando `default_quantity = 0`.
|
||||
- **Fix: "Usa TUTTO" nelle ricette non elimina più la riga** — Il pulsante "Usa TUTTO / Finito" nella modal di utilizzo ricette inviava `use_all: true` che causava un `DELETE` immediato senza conferma. Ora calcola la quantità esatta dagli item disponibili e fa un normale `inventory_use`.
|
||||
|
||||
- **Scan page redesign** — La pagina di scansione è stata completamente ridisegnata: **2× zoom fisso** (hardware o CSS), **torcia** con feedback visivo, **flip fotocamera** (front/back), **3 tab input** (Barcode / Nome / AI), **prodotti recenti** (ultimi 6 in localStorage), **live code overlay** durante la scansione parziale, **confirm overlay** al successo, **angoli guida** nel viewport.
|
||||
- **AI Number OCR** — Dopo 4 secondi senza scansione compare il bottone "Leggi numeri con AI": Gemini analizza il frame video e restituisce le cifre del barcode anche quando lo scanner ottico non riesce a leggerlo.
|
||||
|
||||
+16
-15
@@ -379,25 +379,26 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
|
||||
if (preg_match('/\b(pollo|tacchino|maiale|manzo|vitello|agnello)\b/', $n)) return 2;
|
||||
if (preg_match('/salmone|tonno\s+fresco|pesce(?!\s+in)/', $n)) return 2;
|
||||
if (preg_match('/\b(passata|pelati|polpa|sugo|salsa\s+di\s+pomodoro)\b/', $n)) return 5;
|
||||
if (preg_match('/insalata|rucola|spinaci|lattuga|crescione|germogli/', $n)) return 2;
|
||||
if (preg_match('/insalata|rucola|spinaci|lattuga|crescione|germogli/', $n)) return 4;
|
||||
if (preg_match('/\b(succo|spremuta)\b/', $n)) return 3;
|
||||
if (preg_match('/\b(birra|beer)\b/', $n)) return 3;
|
||||
if (preg_match('/\bvino\b/', $n)) return 5;
|
||||
if (preg_match('/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/', $n)) return 4;
|
||||
// Fruit opened/cut in fridge — much shorter than sealed
|
||||
if (preg_match('/\bavocado\b/', $n)) return 2;
|
||||
if (preg_match('/\b(banana|banane|fragola|lampone|pesca|albicocca|ciliegia|mango|papaya)\b/', $n)) return 2;
|
||||
if (preg_match('/\b(mela|pera|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/', $n)) return 3;
|
||||
if (preg_match('/\b(arancia|mandarino|pompelmo|clementina|limone)\b/', $n)) return 3; // cut citrus
|
||||
// Vegetables opened/cut in fridge
|
||||
if (preg_match('/\b(zucchina|zucchine|melanzana|pomodor)\b/', $n)) return 3;
|
||||
if (preg_match('/\b(peperone|peperoni)\b/', $n)) return 3;
|
||||
if (preg_match('/\b(broccolo|broccoli|cavolfiore|cavolo)\b/', $n)) return 3;
|
||||
if (preg_match('/\bsedano\b|\bfinocchio\b/', $n)) return 3;
|
||||
if (preg_match('/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/', $n)) return 4;
|
||||
if (preg_match('/\b(carota|carote)\b/', $n)) return 5;
|
||||
if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 3; // cooked/cut potato
|
||||
if (preg_match('/\baglio\b/', $n)) return 10;
|
||||
// Fruit in fridge (opened pack, not necessarily cut)
|
||||
if (preg_match('/\bavocado\b/', $n)) return 3;
|
||||
if (preg_match('/\b(fragola|fragole|lampone|lamponi|mirtillo|mirtilli|mora|more)\b/', $n)) return 4;
|
||||
if (preg_match('/\b(banana|banane|pesca|pesche|albicocca|albicocche|ciliegia|ciliegie|mango|papaya)\b/', $n)) return 4;
|
||||
if (preg_match('/\b(mela|mele|pera|pere|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/', $n)) return 5;
|
||||
if (preg_match('/\b(arancia|arance|mandarino|mandarini|pompelmo|clementina|limone|limoni)\b/', $n)) return 7;
|
||||
// Vegetables in fridge (opened pack)
|
||||
if (preg_match('/\b(zucchina|zucchine|melanzana|melanzane|pomodor)\b/', $n)) return 5;
|
||||
if (preg_match('/\b(peperone|peperoni)\b/', $n)) return 5;
|
||||
if (preg_match('/\b(broccolo|broccoli|cavolfiore|cavolo)\b/', $n)) return 4;
|
||||
if (preg_match('/\bsedano\b|\bfinocchio\b/', $n)) return 5;
|
||||
if (preg_match('/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/', $n)) return 6;
|
||||
if (preg_match('/\b(carota|carote)\b/', $n)) return 7;
|
||||
if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 4;
|
||||
if (preg_match('/\baglio\b/', $n)) return 14;
|
||||
|
||||
// ── G: Fridge condiments — medium shelf-life ─────────────────────────
|
||||
if (preg_match('/maionese|mayo|mayon/', $n)) return 90;
|
||||
|
||||
+24
-5
@@ -2497,7 +2497,7 @@ function prewarmShelfLifeCache(PDO $db, int $limit = 5): array {
|
||||
*/
|
||||
function getOpenedShelfLifeDays(string $name, string $category, string $location, bool $vacuumSealed = false, bool $allowAI = true): int {
|
||||
$cacheFile = __DIR__ . '/../data/opened_shelf_cache.json';
|
||||
$cacheKey = md5(mb_strtolower($name) . '|' . mb_strtolower($location));
|
||||
$cacheKey = md5(mb_strtolower($name) . '|' . mb_strtolower($location) . '|v2');
|
||||
|
||||
// Static in-memory cache: the file is read only ONCE per PHP request,
|
||||
// even when this function is called for many items in a loop (e.g. getStats).
|
||||
@@ -3136,6 +3136,9 @@ function generateRecipe(PDO $db): void {
|
||||
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) {
|
||||
$line .= " ({$item['default_quantity']}{$item['package_unit']}/conf)";
|
||||
}
|
||||
if ($item['unit'] === 'pz') {
|
||||
$line .= ' [usa PEZZI interi — qty_number in pz, non grammi]';
|
||||
}
|
||||
// Add expiry info only for priority groups 1-4
|
||||
if ($group <= 4 && $item['expiry_date']) {
|
||||
if ($daysLeft < 0) {
|
||||
@@ -3414,7 +3417,7 @@ REGOLE:
|
||||
{$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
|
||||
2. Usa SOLO ingredienti dalla lista + acqua/sale/pepe/olio (sempre disponibili).
|
||||
3. Quantità MASSIME per $persons persona/e (NON superare mai): pasta/riso asciutto 90g/pers, carne 180g/pers, pesce 200g/pers, legumi secchi 80g/pers (lessi 200g/pers), verdure contorno 200g/pers, formaggio 80g/pers, latte 200ml/pers, farina per dolci 200g/pers. Se un ingrediente rimasto è inferiore a questi limiti, usalo tutto.
|
||||
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0.
|
||||
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
|
||||
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
|
||||
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
|
||||
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged.
|
||||
@@ -3619,7 +3622,7 @@ PROMPT;
|
||||
$qtyNum = $recipeVal / 1000;
|
||||
} elseif ($recipeUnit === 'ml' && $invUnit === 'ml') {
|
||||
$qtyNum = $recipeVal;
|
||||
// g/ml → pz (approximate to nearest piece)
|
||||
// g/ml → pz/conf (approximate to nearest piece)
|
||||
} elseif ($invUnit === 'pz' || $invUnit === 'conf') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
if ($defQty > 0) {
|
||||
@@ -3627,9 +3630,23 @@ PROMPT;
|
||||
$qtyNum = $recipeVal / $defQty;
|
||||
$qtyNum = max(0.25, round($qtyNum * 4) / 4); // round to nearest quarter
|
||||
} else {
|
||||
$qtyNum = max(1, round($recipeVal / 100)); // fallback heuristic
|
||||
// No default_quantity: AI was told to use pieces but sent grams.
|
||||
// If the original qty_number looks like a piece count (≤ invQty and ≤ 100)
|
||||
// keep it; otherwise fall back to 1.
|
||||
$origQtyNum = (float)($ing['qty_number'] ?? 0);
|
||||
if ($origQtyNum >= 1 && $origQtyNum <= $invQty && $origQtyNum <= 100) {
|
||||
$qtyNum = $origQtyNum; // already a plausible piece count
|
||||
} else {
|
||||
$qtyNum = 1; // safe minimum: 1 piece
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($invUnit === 'pz' && !$recipeUnit) {
|
||||
// AI returned qty_number without a parseable unit string.
|
||||
// If qty_number looks like grams (>> available pz count), clamp to 1.
|
||||
if ($qtyNum > $invQty || $qtyNum > 100) {
|
||||
$qtyNum = max(1, round($qtyNum / 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: qty_number should not exceed available
|
||||
@@ -4061,6 +4078,8 @@ function generateRecipeStream(PDO $db): void {
|
||||
$line = "- {$item['name']}: {$item['quantity']} {$item['unit']}";
|
||||
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0)
|
||||
$line .= " ({$item['default_quantity']}{$item['package_unit']}/conf)";
|
||||
if ($item['unit'] === 'pz')
|
||||
$line .= ' [usa PEZZI interi — qty_number in pz, non grammi]';
|
||||
// Annotazioni urgenza: solo gruppi 1-3 (riduce token per gruppi 4-6)
|
||||
if ($group <= 3 && $item['expiry_date']) {
|
||||
if ($daysLeft < 0) $line .= ' ⚠️SCADUTO';
|
||||
@@ -4274,7 +4293,7 @@ REGOLE:
|
||||
{$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
|
||||
2. Usa SOLO ingredienti dalla lista + acqua/sale/pepe/olio (sempre disponibili).
|
||||
3. Quantità MASSIME per $persons persona/e (NON superare mai): pasta/riso asciutto 90g/pers, carne 180g/pers, pesce 200g/pers, legumi secchi 80g/pers (lessi 200g/pers), verdure contorno 200g/pers, formaggio 80g/pers, latte 200ml/pers, farina per dolci 200g/pers. Se un ingrediente rimasto è inferiore a questi limiti, usalo tutto.
|
||||
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0.
|
||||
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
|
||||
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
|
||||
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
|
||||
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged.
|
||||
|
||||
+43
-21
@@ -1683,25 +1683,26 @@ function estimateOpenedExpiryDays(product, location) {
|
||||
if (/\b(pollo|tacchino|maiale|manzo|vitello|agnello)\b/.test(name)) return 2;
|
||||
if (/salmone|tonno\s+fresco|pesce(?!\s+in)/.test(name)) return 2;
|
||||
if (/\b(passata|pelati|polpa|sugo|salsa\s+di\s+pomodoro)\b/.test(name)) return 5;
|
||||
if (/insalata|rucola|spinaci|lattuga|crescione|germogli/.test(name)) return 2;
|
||||
if (/insalata|rucola|spinaci|lattuga|crescione|germogli/.test(name)) return 4;
|
||||
if (/\b(succo|spremuta)\b/.test(name)) return 3;
|
||||
if (/\b(birra|beer)\b/.test(name)) return 3;
|
||||
if (/\bvino\b/.test(name)) return 5;
|
||||
if (/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/.test(name)) return 4;
|
||||
// Fruit opened/cut in fridge
|
||||
if (/\bavocado\b/.test(name)) return 2;
|
||||
if (/\b(banana|banane|fragola|lampone|pesca|albicocca|ciliegia|mango|papaya)\b/.test(name)) return 2;
|
||||
if (/\b(mela|pera|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/.test(name)) return 3;
|
||||
if (/\b(arancia|mandarino|pompelmo|clementina|limone)\b/.test(name)) return 3;
|
||||
// Vegetables opened/cut in fridge
|
||||
if (/\b(zucchina|zucchine|melanzana|pomodor)\b/.test(name)) return 3;
|
||||
if (/\b(peperone|peperoni)\b/.test(name)) return 3;
|
||||
if (/\b(broccolo|broccoli|cavolfiore|cavolo)\b/.test(name)) return 3;
|
||||
if (/\bsedano\b|\bfinocchio\b/.test(name)) return 3;
|
||||
if (/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/.test(name)) return 4;
|
||||
if (/\b(carota|carote)\b/.test(name)) return 5;
|
||||
if (/\b(patata|patate|tubero)\b/.test(name)) return 3;
|
||||
if (/\baglio\b/.test(name)) return 10;
|
||||
// Fruit in fridge (opened pack, not necessarily cut)
|
||||
if (/\bavocado\b/.test(name)) return 3;
|
||||
if (/\b(fragola|fragole|lampone|lamponi|mirtillo|mirtilli|mora|more)\b/.test(name)) return 4;
|
||||
if (/\b(banana|banane|pesca|pesche|albicocca|albicocche|ciliegia|ciliegie|mango|papaya)\b/.test(name)) return 4;
|
||||
if (/\b(mela|mele|pera|pere|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/.test(name)) return 5;
|
||||
if (/\b(arancia|arance|mandarino|mandarini|pompelmo|clementina|limone|limoni)\b/.test(name)) return 7;
|
||||
// Vegetables in fridge (opened pack)
|
||||
if (/\b(zucchina|zucchine|melanzana|melanzane|pomodor)\b/.test(name)) return 5;
|
||||
if (/\b(peperone|peperoni)\b/.test(name)) return 5;
|
||||
if (/\b(broccolo|broccoli|cavolfiore|cavolo)\b/.test(name)) return 4;
|
||||
if (/\bsedano\b|\bfinocchio\b/.test(name)) return 5;
|
||||
if (/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/.test(name)) return 6;
|
||||
if (/\b(carota|carote)\b/.test(name)) return 7;
|
||||
if (/\b(patata|patate|tubero)\b/.test(name)) return 4;
|
||||
if (/\baglio\b/.test(name)) return 14;
|
||||
|
||||
// ── G: Fridge condiments ─────────────────────────────────────────────
|
||||
if (/maionese|mayo|mayon/.test(name)) return 90;
|
||||
@@ -3949,10 +3950,11 @@ function renderBannerItem() {
|
||||
}
|
||||
detailEl.innerHTML = `${baseDetail} <span class="banner-safety-tip banner-safety-${safety.level}">${safety.icon} ${safety.tip}</span>`;
|
||||
let btns = '';
|
||||
btns += `<button class="btn-banner btn-banner-finish" onclick="bannerFinishAll()">${t('dashboard.banner_expired_action_finished')}</button>`;
|
||||
if (safety.level !== 'danger') {
|
||||
btns += `<button class="btn-banner btn-banner-use" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
|
||||
}
|
||||
btns += `<button class="btn-banner btn-banner-throw${safety.level === 'danger' ? ' btn-banner-throw-primary' : ''}" onclick="bannerThrowAway()">${t('dashboard.banner_expired_action_throw')}</button>`;
|
||||
btns += `<button class="btn-banner btn-banner-throw" onclick="bannerThrowAway()">${t('dashboard.banner_expired_action_throw')}</button>`;
|
||||
btns += `<button class="btn-banner btn-banner-edit" onclick="editBannerExpiry()">${t('dashboard.banner_expired_action_edit')}</button>`;
|
||||
if (safety.level === 'danger') {
|
||||
btns += `<button class="btn-banner btn-banner-use btn-banner-use-danger" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
|
||||
@@ -4258,7 +4260,7 @@ function bannerFinishAll() {
|
||||
location: '__all__',
|
||||
}).then(res => {
|
||||
if (res.success) {
|
||||
showToast(`📤 ${item.name} terminato!`, 'success');
|
||||
showToast(t('toast.finished_all').replace('{name}', item.name), 'success');
|
||||
showLowStockBringPrompt(res, () => loadDashboard());
|
||||
} else {
|
||||
showToast(res.error || 'Errore', 'error');
|
||||
@@ -7479,7 +7481,17 @@ function _renderUseExpiryHint(items) {
|
||||
? ` (${locInfo.icon} ${locInfo.label})`
|
||||
: '';
|
||||
|
||||
hintEl.innerHTML = t('use.expiry_warning').replace('{loc}', locLabel).replace('{date}', `<strong>${dateStr}</strong>`).replace('{when}', whenStr);
|
||||
if (soonest.opened_at) {
|
||||
// The soonest "expiry" is a calculated date from when the item was opened — show days-open instead
|
||||
const todayBase = new Date(); todayBase.setHours(0, 0, 0, 0);
|
||||
const openedDays = Math.round((todayBase - new Date(soonest.opened_at)) / 86400000);
|
||||
const whenOpenedStr = openedDays <= 0
|
||||
? t('expiry.opened_today_long')
|
||||
: t('expiry.opened_ago_long').replace('{n}', openedDays);
|
||||
hintEl.innerHTML = t('use.expiry_warning_opened').replace('{loc}', locLabel).replace('{when}', whenOpenedStr);
|
||||
} else {
|
||||
hintEl.innerHTML = t('use.expiry_warning').replace('{loc}', locLabel).replace('{date}', `<strong>${dateStr}</strong>`).replace('{when}', whenStr);
|
||||
}
|
||||
hintEl.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -11408,7 +11420,7 @@ function adjustRecipePersons(delta) {
|
||||
input.value = val;
|
||||
}
|
||||
|
||||
let _recipeUseContext = null; // { idx, productId, btn, qtyNumber }
|
||||
let _recipeUseContext = null; // { idx, productId, btn, qtyNumber, items }
|
||||
let _recipeUseConfMode = null;
|
||||
let _recipeUseNormalUnit = 'pz';
|
||||
|
||||
@@ -11432,6 +11444,7 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn, rec
|
||||
try {
|
||||
const data = await api('inventory_list');
|
||||
const items = (data.inventory || []).filter(i => i.product_id == productId);
|
||||
_recipeUseContext.items = items; // cache for "use all" quantity lookup
|
||||
|
||||
if (items.length === 0) {
|
||||
showToast(t('error.not_in_inventory'), 'error');
|
||||
@@ -11622,7 +11635,17 @@ async function submitRecipeUse(useAll) {
|
||||
|
||||
let qty;
|
||||
if (useAll) {
|
||||
qty = 0; // API handles use_all
|
||||
// Use the exact available qty at the selected location — do NOT send use_all=true
|
||||
// to the API, because that would permanently DELETE the inventory row without a
|
||||
// confirmation step. Instead send the precise quantity so the row is set to qty=0
|
||||
// and the normal "finished items" banner can handle the reconciliation.
|
||||
const cachedItems = _recipeUseContext.items || [];
|
||||
const locItems = cachedItems.filter(i => i.location === location && parseFloat(i.quantity) > 0);
|
||||
qty = locItems.reduce((s, i) => s + parseFloat(i.quantity || 0), 0) || 0;
|
||||
if (qty <= 0) {
|
||||
// Nothing at this location — fallback to current input value
|
||||
qty = parseFloat(document.getElementById('ruse-quantity').value) || 1;
|
||||
}
|
||||
} else {
|
||||
qty = parseFloat(document.getElementById('ruse-quantity').value) || 1;
|
||||
if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') {
|
||||
@@ -11639,7 +11662,6 @@ async function submitRecipeUse(useAll) {
|
||||
const result = await api('inventory_use', {}, 'POST', {
|
||||
product_id: productId,
|
||||
quantity: qty,
|
||||
use_all: useAll,
|
||||
location: location,
|
||||
notes: recipeTitle ? `Ricetta: ${recipeTitle}` : '',
|
||||
});
|
||||
|
||||
@@ -43,14 +43,14 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Settings gear (shown after setup, over WebView) -->
|
||||
<!-- Settings gear (shown after setup, over WebView) — top-right corner to avoid overlapping modals -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnSettings"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:layout_marginBottom="70dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@android:drawable/ic_menu_manage"
|
||||
android:alpha="0.12"
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260512a">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260513a">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||
@@ -67,7 +67,7 @@
|
||||
<!-- Title — left-aligned; grows to fill space -->
|
||||
<div class="header-title-wrap">
|
||||
<h1 class="header-title" onclick="showPage('dashboard')">
|
||||
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.9</span>
|
||||
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.12</span>
|
||||
</h1>
|
||||
<!-- Update badge — shown alongside title, never replaces it -->
|
||||
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
|
||||
@@ -1522,6 +1522,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260512a"></script>
|
||||
<script src="assets/js/app.js?v=20260513a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "EverShelf",
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.7.9",
|
||||
"version": "1.7.12",
|
||||
"start_url": "/evershelf/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"banner_expired_today": "Heute abgelaufen",
|
||||
"banner_expired_days": "Seit {days} Tagen abgelaufen",
|
||||
"banner_expired_action_use": "Trotzdem verwenden",
|
||||
"banner_expired_action_finished": "Habe ich verbraucht!",
|
||||
"banner_expired_action_throw": "Habe ich weggeworfen",
|
||||
"banner_expired_action_edit": "Datum korrigieren",
|
||||
"banner_anomaly_action_edit": "Bestand korrigieren",
|
||||
@@ -256,6 +257,7 @@
|
||||
"opened_badge": "GEOEFFNET",
|
||||
"not_in_inventory": "⚠️ Produkt nicht im Bestand.",
|
||||
"expiry_warning": "⚠️ Verwende zuerst die{loc}, die am {date} abläuft — {when}!",
|
||||
"expiry_warning_opened": "⚠️ Die{loc} ist seit {when} geöffnet — zuerst verwenden!",
|
||||
"throw_title": "🗑️ Produkt entsorgen",
|
||||
"throw_all": "🗑️ ALLES entsorgen ({qty})",
|
||||
"throw_qty_label": "Wie viel wegwerfen?",
|
||||
@@ -748,6 +750,7 @@
|
||||
"finished_to_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt",
|
||||
"thrown_away": "🗑️ {name} weggeworfen!",
|
||||
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
|
||||
"finished_all": "📤 {name} aufgebraucht!",
|
||||
"product_finished_confirmed": "✅ Entfernt — wieder hinzufügen, wenn du nachkaufst",
|
||||
"appliance_added": "Gerät hinzugefügt",
|
||||
"item_added": "{name} hinzugefügt"
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"banner_expired_today": "Expired today",
|
||||
"banner_expired_days": "Expired {days} days ago",
|
||||
"banner_expired_action_use": "Use anyway",
|
||||
"banner_expired_action_finished": "I finished it!",
|
||||
"banner_expired_action_throw": "I threw it away",
|
||||
"banner_expired_action_edit": "Fix date",
|
||||
"banner_anomaly_action_edit": "Fix inventory",
|
||||
@@ -256,6 +257,7 @@
|
||||
"opened_badge": "OPENED",
|
||||
"not_in_inventory": "⚠️ Product not in inventory.",
|
||||
"expiry_warning": "⚠️ Use first the one{loc} that expires on {date} — {when}!",
|
||||
"expiry_warning_opened": "⚠️ The one{loc} has been open for {when} — use it first!",
|
||||
"throw_title": "🗑️ Discard Product",
|
||||
"throw_all": "🗑️ Discard ALL ({qty})",
|
||||
"throw_qty_label": "How much to discard?",
|
||||
@@ -748,6 +750,7 @@
|
||||
"finished_to_bring": "🛒 Product finished → added to Bring!",
|
||||
"thrown_away": "🗑️ {name} thrown away!",
|
||||
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
||||
"finished_all": "📤 {name} finished!",
|
||||
"product_finished_confirmed": "✅ Removed — add it again when you restock",
|
||||
"appliance_added": "Appliance added",
|
||||
"item_added": "{name} added"
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"banner_expired_today": "Scaduto oggi",
|
||||
"banner_expired_days": "Scaduto da {days} giorni",
|
||||
"banner_expired_action_use": "Usa comunque",
|
||||
"banner_expired_action_finished": "L'ho finito!",
|
||||
"banner_expired_action_throw": "L'ho buttato",
|
||||
"banner_expired_action_edit": "Correggi data",
|
||||
"banner_anomaly_action_edit": "Correggi inventario",
|
||||
@@ -256,6 +257,7 @@
|
||||
"opened_badge": "APERTO",
|
||||
"not_in_inventory": "⚠️ Prodotto non presente nell'inventario.",
|
||||
"expiry_warning": "⚠️ Usa prima quella{loc} che scade il {date} — {when}!",
|
||||
"expiry_warning_opened": "⚠️ Quella{loc}, aperta da {when} — usala prima!",
|
||||
"throw_title": "🗑️ Butta Prodotto",
|
||||
"throw_all": "🗑️ Butta TUTTO ({qty})",
|
||||
"throw_qty_label": "Quanto butti?",
|
||||
@@ -748,6 +750,7 @@
|
||||
"finished_to_bring": "🛒 Prodotto finito → aggiunto a Bring!",
|
||||
"thrown_away": "🗑️ {name} buttato!",
|
||||
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
||||
"finished_all": "📤 {name} terminato!",
|
||||
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
|
||||
"appliance_added": "Elettrodomestico aggiunto",
|
||||
"item_added": "{name} aggiunto"
|
||||
|
||||
Reference in New Issue
Block a user