From 2d70e7a688d1b4ae823a6ef13a4cdbb31ba01d68 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Wed, 13 May 2026 11:40:05 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20release=20v1.7.12=20=E2=80=94=20banner?= =?UTF-8?q?=20aperti,=20fix=20ricette=20pz,=20fix=20usa-tutto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +++ README.md | 8 ++- api/database.php | 31 ++++----- api/index.php | 29 +++++++-- assets/js/app.js | 64 +++++++++++++------ .../src/main/res/layout/activity_kiosk.xml | 8 +-- index.html | 6 +- manifest.json | 2 +- translations/de.json | 3 + translations/en.json | 3 + translations/it.json | 3 + 11 files changed, 116 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55078b6..7cdb56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 41f711e..addb521 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,15 @@ [![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/) [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile) [![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE-orange.svg)](translations/) -[![Version](https://img.shields.io/badge/version-1.7.11-brightgreen.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-1.7.12-brightgreen.svg)](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. diff --git a/api/database.php b/api/database.php index 66e1068..a49bf3f 100644 --- a/api/database.php +++ b/api/database.php @@ -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; diff --git a/api/index.php b/api/index.php index 2ce3e45..a3bbfa2 100644 --- a/api/index.php +++ b/api/index.php @@ -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. diff --git a/assets/js/app.js b/assets/js/app.js index 166280e..ef4bae0 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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} `; let btns = ''; + btns += ``; if (safety.level !== 'danger') { btns += ``; } - btns += ``; + btns += ``; btns += ``; if (safety.level === 'danger') { btns += ``; @@ -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}', `${dateStr}`).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}', `${dateStr}`).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}` : '', }); diff --git a/evershelf-kiosk/app/src/main/res/layout/activity_kiosk.xml b/evershelf-kiosk/app/src/main/res/layout/activity_kiosk.xml index 8a3f05b..389a55a 100644 --- a/evershelf-kiosk/app/src/main/res/layout/activity_kiosk.xml +++ b/evershelf-kiosk/app/src/main/res/layout/activity_kiosk.xml @@ -43,14 +43,14 @@ android:layout_height="match_parent" android:visibility="gone" /> - + EverShelf - + @@ -67,7 +67,7 @@

- EverShelfv1.7.9 + EverShelfv1.7.12

@@ -1522,6 +1522,6 @@
- + diff --git a/manifest.json b/manifest.json index 5a46642..e225195 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/translations/de.json b/translations/de.json index aa7a48f..e606cb6 100644 --- a/translations/de.json +++ b/translations/de.json @@ -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" diff --git a/translations/en.json b/translations/en.json index 70e36f5..cbd8f98 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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" diff --git a/translations/it.json b/translations/it.json index ffc35a4..264303f 100644 --- a/translations/it.json +++ b/translations/it.json @@ -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"