fix: shelf life formaggio, banner vacuum/modifica, preloader redesign, ricetta da ingrediente, porzioni, modal ricetta testo tradotto, use_btn semplificato
This commit is contained in:
+3
-1
@@ -379,8 +379,10 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
|
|||||||
if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 5;
|
if (preg_match('/\b(yogurt|yaourt|yoghurt)\b/', $n)) return 5;
|
||||||
if (preg_match('/mozzarella|burrata|stracciatella/', $n)) return 3;
|
if (preg_match('/mozzarella|burrata|stracciatella/', $n)) return 3;
|
||||||
if (preg_match('/philadelphia|spalmabile/', $n)) return 7;
|
if (preg_match('/philadelphia|spalmabile/', $n)) return 7;
|
||||||
|
// Specific hard cheeses that contain 'fresco' in their commercial name (e.g. Asiago fresco)
|
||||||
|
// must be matched BEFORE the generic 'formaggio fresco' catch-all
|
||||||
|
if (preg_match('/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza|groviera/', $n)) return 28;
|
||||||
if (preg_match('/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/', $n)) return 5;
|
if (preg_match('/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/', $n)) return 5;
|
||||||
if (preg_match('/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza/', $n)) return 28;
|
|
||||||
if (preg_match('/formaggio/', $n)) return 10;
|
if (preg_match('/formaggio/', $n)) return 10;
|
||||||
if (preg_match('/\bburro\b/', $n)) return 30;
|
if (preg_match('/\bburro\b/', $n)) return 30;
|
||||||
if (preg_match('/\bpanna\b/', $n)) return 4;
|
if (preg_match('/\bpanna\b/', $n)) return 4;
|
||||||
|
|||||||
+41
-17
@@ -3828,7 +3828,7 @@ You are an expert home chef. Generate ONE recipe for $mealLabel for $persons per
|
|||||||
REGOLE:
|
REGOLE:
|
||||||
{$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
|
{$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).
|
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.
|
3. Quantità MASSIME per $persons persona/e (NON superare mai): pasta/riso asciutto 90g/pers, carne 150g/pers, affettati/salumi/speck/prosciutto 70g/pers, pesce 180g/pers, legumi secchi 80g/pers (lessi 200g/pers), verdure contorno 150g/pers, verdure intere grosse (peperoni/melanzane/zucchine) 1 pz/pers, formaggio 70g/pers, latte 200ml/pers, farina per dolci 200g/pers, piadina/tortilla/wrap 1-2 pz/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. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
|
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).
|
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).
|
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
|
||||||
@@ -4196,6 +4196,7 @@ function recipeFromIngredient(PDO $db): void {
|
|||||||
}
|
}
|
||||||
$lang = recipeNormalizeLang($input['lang'] ?? 'it');
|
$lang = recipeNormalizeLang($input['lang'] ?? 'it');
|
||||||
$langName = recipeLangName($lang);
|
$langName = recipeLangName($lang);
|
||||||
|
$persons = max(1, intval($input['persons'] ?? 1));
|
||||||
|
|
||||||
// Fetch inventory (same as generateRecipe)
|
// Fetch inventory (same as generateRecipe)
|
||||||
$stmt = $db->query("
|
$stmt = $db->query("
|
||||||
@@ -4208,22 +4209,45 @@ function recipeFromIngredient(PDO $db): void {
|
|||||||
");
|
");
|
||||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Build compact pantry text (same logic as generateRecipe)
|
||||||
|
$ingredientLines = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$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]';
|
||||||
|
$dl = intval($item['days_left']);
|
||||||
|
if (!empty($item['expiry_date'])) {
|
||||||
|
if ($dl < 0) $line .= ' ⚠️SCADUTO';
|
||||||
|
elseif ($dl <= 3) $line .= " 🔴{$dl}gg";
|
||||||
|
elseif ($dl <= 7) $line .= " 🟠{$dl}gg";
|
||||||
|
}
|
||||||
|
if (!empty($item['opened_at'])) $line .= ' [APERTO]';
|
||||||
|
$ingredientLines[] = $line;
|
||||||
|
}
|
||||||
|
$ingredientsText = implode("\n", $ingredientLines);
|
||||||
|
|
||||||
$safeName = htmlspecialchars($ingredientName, ENT_QUOTES, 'UTF-8');
|
$safeName = htmlspecialchars($ingredientName, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
$prompt = <<<PROMPT
|
$prompt = <<<PROMPT
|
||||||
Generate a recipe in {$langName} that uses "{$safeName}" as a main ingredient.
|
You are an expert home chef. Generate ONE recipe in {$langName} that uses "{$safeName}" as the main ingredient, for {$persons} person(s).
|
||||||
Return ONLY a JSON object, no markdown.
|
Return ONLY a JSON object, no markdown fences.
|
||||||
|
|
||||||
Fields:
|
REGOLE:
|
||||||
- title: string (recipe name in {$langName})
|
1. Usa SOLO ingredienti dalla lista DISPENSA qui sotto + acqua/sale/pepe/olio (sempre disponibili).
|
||||||
- meal: null (do NOT categorize)
|
2. "{$safeName}" DEVE essere il primo ingrediente — è obbligatorio includerlo.
|
||||||
- persons: 2
|
3. Quantità MASSIME per {$persons} persona/e: pasta/riso 90g/pers, carne 150g/pers, affettati/salumi 70g/pers, pesce 180g/pers, legumi secchi 80g/pers, verdure 150g/pers, verdure intere grosse 1 pz/pers, formaggio 70g/pers, piadina/wrap 1-2 pz/pers.
|
||||||
- prep_time: string or null
|
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf). Per non-dispensa: 0.
|
||||||
- cook_time: string or null
|
5. "name": usa ESATTAMENTE il nome dalla lista dispensa (il sistema lo usa per scalare l'inventario).
|
||||||
- ingredients: array of {"name":"...","qty":"...","qty_number":0.0,"unit":"g|ml|pz|conf|kg|l","from_pantry":true}
|
6. "from_pantry": true se l'ingrediente è nella lista DISPENSA, false per acqua/sale/pepe/olio.
|
||||||
— "{$safeName}" MUST be the first ingredient; set from_pantry=true for ALL
|
7. Language: {$langName} for all text fields. Keep "meal" as English meal key (colazione/pranzo/cena/snack/dolce/libero).
|
||||||
- steps: array of strings (step text only, no numbers, in {$langName})
|
|
||||||
- nutrition_note: string or null
|
DISPENSA:
|
||||||
|
{$ingredientsText}
|
||||||
|
|
||||||
|
JSON schema:
|
||||||
|
{"title":"…","meal":"libero","persons":{$persons},"prep_time":"…","cook_time":"…","tags":["…"],"ingredients":[{"name":"…","qty":"80 g","qty_number":80,"from_pantry":true}],"steps":["…"],"nutrition_note":"…"}
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
@@ -4707,7 +4731,7 @@ You are an expert home chef. Generate ONE recipe for $mealLabel for $persons per
|
|||||||
REGOLE:
|
REGOLE:
|
||||||
{$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
|
{$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).
|
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.
|
3. Quantità MASSIME per $persons persona/e (NON superare mai): pasta/riso asciutto 90g/pers, carne 150g/pers, affettati/salumi/speck/prosciutto 70g/pers, pesce 180g/pers, legumi secchi 80g/pers (lessi 200g/pers), verdure contorno 150g/pers, verdure intere grosse (peperoni/melanzane/zucchine) 1 pz/pers, formaggio 70g/pers, latte 200ml/pers, farina per dolci 200g/pers, piadina/tortilla/wrap 1-2 pz/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. IMPORTANTE: per ingredienti con unità "pz" scrivi qty_number come numero di PEZZI (es. 2, non 200g).
|
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).
|
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).
|
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
|
||||||
@@ -7122,11 +7146,11 @@ function recipesList(PDO $db): void {
|
|||||||
function recipesSave(PDO $db): void {
|
function recipesSave(PDO $db): void {
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
$date = $input['date'] ?? date('Y-m-d');
|
$date = $input['date'] ?? date('Y-m-d');
|
||||||
$meal = $input['meal'] ?? '';
|
$meal = trim($input['meal'] ?? '') ?: 'libero';
|
||||||
$recipe = $input['recipe'] ?? null;
|
$recipe = $input['recipe'] ?? null;
|
||||||
|
|
||||||
if (!$meal || !$recipe) {
|
if (!$recipe) {
|
||||||
echo json_encode(['error' => 'Missing meal or recipe']);
|
echo json_encode(['error' => 'Missing recipe']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-80
@@ -72,12 +72,12 @@ body {
|
|||||||
#app-preloader {
|
#app-preloader {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: var(--bg-dark, #0f172a);
|
background: #0c1222;
|
||||||
z-index: 200000;
|
z-index: 200000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: opacity 0.35s ease;
|
transition: opacity 0.45s ease;
|
||||||
}
|
}
|
||||||
#app-preloader.fade-out {
|
#app-preloader.fade-out {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -87,123 +87,89 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
.app-preloader-spinner {
|
.app-preloader-spinner {
|
||||||
width: 48px;
|
width: 40px;
|
||||||
height: 48px;
|
height: 40px;
|
||||||
border: 4px solid rgba(255,255,255,0.15);
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
border-top-color: #4ade80;
|
border-top-color: #4ade80;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.9s linear infinite;
|
||||||
}
|
}
|
||||||
.app-preloader-label {
|
.app-preloader-label {
|
||||||
color: rgba(255,255,255,0.75);
|
color: rgba(255,255,255,0.75);
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
.app-preloader-logo {
|
.app-preloader-logo {
|
||||||
height: 160px;
|
height: 150px;
|
||||||
width: auto;
|
width: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(0 4px 16px rgba(74,222,128,0.2));
|
animation: logoPulse 3.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes logoPulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 4px 20px rgba(74,222,128,0.18)); }
|
||||||
|
50% { filter: drop-shadow(0 4px 36px rgba(74,222,128,0.48)); }
|
||||||
}
|
}
|
||||||
.app-preloader-version {
|
.app-preloader-version {
|
||||||
color: rgba(255,255,255,0.35);
|
color: rgba(255,255,255,0.22);
|
||||||
font-size: 0.72rem;
|
font-size: 0.68rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.6px;
|
||||||
margin-top: -8px;
|
margin-top: -16px;
|
||||||
}
|
}
|
||||||
/* ── Startup progress bar ───────────────────────────────────────────── */
|
/* ── Startup progress section ────────────────────────────────────────── */
|
||||||
.preloader-progress-wrap {
|
.preloader-progress-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 14px;
|
||||||
width: min(92vw, 600px);
|
width: min(92vw, 520px);
|
||||||
animation: zwFadeIn 0.2s ease;
|
animation: zwFadeIn 0.25s ease;
|
||||||
}
|
}
|
||||||
.preloader-bar-track {
|
.preloader-bar-track {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 3px;
|
||||||
background: rgba(255,255,255,0.12);
|
background: rgba(255,255,255,0.08);
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.preloader-bar {
|
.preloader-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
background: linear-gradient(90deg, #4ade80, #22c55e);
|
background: linear-gradient(90deg, #4ade80, #22c55e);
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
transition: width 0.18s ease, background 0.3s ease;
|
transition: width 0.22s cubic-bezier(0.4,0,0.2,1), background 0.3s ease;
|
||||||
|
box-shadow: 0 0 8px rgba(74,222,128,0.55);
|
||||||
}
|
}
|
||||||
.preloader-bar.bar-error { background: linear-gradient(90deg, #f87171, #ef4444); }
|
.preloader-bar.bar-error { background: linear-gradient(90deg,#f87171,#ef4444); box-shadow: 0 0 8px rgba(239,68,68,0.5); }
|
||||||
.preloader-bar.bar-warn { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
|
.preloader-bar.bar-warn { background: linear-gradient(90deg,#fbbf24,#f59e0b); box-shadow: 0 0 8px rgba(251,191,36,0.5); }
|
||||||
.preloader-check-label { display: none; } /* replaced by check-wheel */
|
.preloader-check-label { display: none; }
|
||||||
|
|
||||||
/* ── Startup check ticker (smooth fade queue) ───────────────────────── */
|
/* ── Status line: single element, opacity crossfade via JS ─────────── */
|
||||||
.preloader-progress-wrap {
|
|
||||||
width: min(96vw, 860px) !important;
|
|
||||||
}
|
|
||||||
.check-ticker {
|
.check-ticker {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 170px;
|
height: 1.5rem;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
margin-top: 6px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.ticker-item {
|
.preloader-status-text {
|
||||||
position: absolute;
|
font-size: clamp(0.78rem, 2vw, 0.9rem);
|
||||||
left: 0; right: 0;
|
font-weight: 500;
|
||||||
text-align: center;
|
letter-spacing: 0.03em;
|
||||||
padding: 0 12px;
|
color: rgba(255,255,255,0.45);
|
||||||
line-height: 1.45;
|
|
||||||
pointer-events: none;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
/* Current — fades in from below, bright and large */
|
.preloader-status-text.state-ok { color: #86efac; }
|
||||||
.ticker-current {
|
.preloader-status-text.state-warn { color: #fde68a; }
|
||||||
bottom: 8px;
|
.preloader-status-text.state-error { color: #fca5a5; }
|
||||||
font-size: clamp(1.1rem, 3.2vw, 1.35rem);
|
|
||||||
font-weight: 700;
|
|
||||||
opacity: 1;
|
|
||||||
color: #4ade80;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
animation: tickerFadeIn 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
.ticker-current.state-ok { color: #4ade80; }
|
|
||||||
.ticker-current.state-warn { color: #fbbf24; }
|
|
||||||
.ticker-current.state-error { color: #f87171; }
|
|
||||||
/* Previous items — progressively dimmer and smaller, scrolling up */
|
|
||||||
.ticker-prev-1 {
|
|
||||||
bottom: 54px;
|
|
||||||
font-size: clamp(0.92rem, 2.5vw, 1.1rem);
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: 0.48;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
.ticker-prev-2 {
|
|
||||||
bottom: 94px;
|
|
||||||
font-size: clamp(0.78rem, 2vw, 0.92rem);
|
|
||||||
font-weight: 500;
|
|
||||||
opacity: 0.22;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
.ticker-prev-3 {
|
|
||||||
bottom: 128px;
|
|
||||||
font-size: clamp(0.66rem, 1.7vw, 0.78rem);
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.09;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
@keyframes tickerFadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(22px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
.preloader-warnings {
|
.preloader-warnings {
|
||||||
max-width: min(92vw, 600px);
|
max-width: min(92vw, 600px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -4225,6 +4191,15 @@ body.server-offline .bottom-nav {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
.recipe-ing-name {
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px dashed rgba(74,222,128,0.5);
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.recipe-ing-name:hover {
|
||||||
|
color: #4ade80;
|
||||||
|
border-bottom-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-use-ingredient {
|
.btn-use-ingredient {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -7069,6 +7044,14 @@ body.cooking-mode-active .app-header {
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
.btn-banner-vacuum {
|
||||||
|
background: #ede9fe;
|
||||||
|
color: #6d28d9;
|
||||||
|
}
|
||||||
|
.btn-banner-edit2 {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== PAGE HEADER ACTION BUTTON (export etc.) ===== */
|
/* ===== PAGE HEADER ACTION BUTTON (export etc.) ===== */
|
||||||
.page-header-action-btn {
|
.page-header-action-btn {
|
||||||
|
|||||||
+82
-20
@@ -1740,8 +1740,10 @@ function estimateOpenedExpiryDays(product, location) {
|
|||||||
if (/\b(yogurt|yaourt|yoghurt)\b/.test(name)) return 5;
|
if (/\b(yogurt|yaourt|yoghurt)\b/.test(name)) return 5;
|
||||||
if (/mozzarella|burrata|stracciatella/.test(name)) return 3;
|
if (/mozzarella|burrata|stracciatella/.test(name)) return 3;
|
||||||
if (/philadelphia|spalmabile/.test(name)) return 7;
|
if (/philadelphia|spalmabile/.test(name)) return 7;
|
||||||
|
// Specific hard cheeses that contain 'fresco' in their commercial name (e.g. Asiago fresco)
|
||||||
|
// must be matched BEFORE the generic 'formaggio fresco' catch-all
|
||||||
|
if (/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza|groviera/.test(name)) return 28;
|
||||||
if (/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/.test(name)) return 5;
|
if (/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/.test(name)) return 5;
|
||||||
if (/parmigiano|grana|pecorino|provolone|asiago|fontina|emmental|gruyere|scamorza/.test(name)) return 28;
|
|
||||||
if (/formaggio/.test(name)) return 10;
|
if (/formaggio/.test(name)) return 10;
|
||||||
if (/\bburro\b/.test(name)) return 30;
|
if (/\bburro\b/.test(name)) return 30;
|
||||||
if (/\bpanna\b/.test(name)) return 4;
|
if (/\bpanna\b/.test(name)) return 4;
|
||||||
@@ -4356,7 +4358,12 @@ function renderBannerItem() {
|
|||||||
btns += `<button class="btn-banner btn-banner-use" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
|
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" 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>`;
|
// "Modifica" — opens full edit modal (includes date correction)
|
||||||
|
btns += `<button class="btn-banner btn-banner-edit2" onclick="editInventoryItem(${item.id})">${t('dashboard.banner_expired_action_modify')}</button>`;
|
||||||
|
if (isOpenedExpiry && !item.vacuum_sealed) {
|
||||||
|
// Offer to re-seal with vacuum — extends shelf life
|
||||||
|
btns += `<button class="btn-banner btn-banner-vacuum" onclick="bannerMarkVacuum()">${t('dashboard.banner_expired_action_vacuum')}</button>`;
|
||||||
|
}
|
||||||
if (!isOpenedExpiry && safety.level === 'danger') {
|
if (!isOpenedExpiry && safety.level === 'danger') {
|
||||||
btns += `<button class="btn-banner btn-banner-use btn-banner-use-danger" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
|
btns += `<button class="btn-banner btn-banner-use btn-banner-use-danger" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
|
||||||
}
|
}
|
||||||
@@ -4652,6 +4659,43 @@ function bannerThrowAway() {
|
|||||||
dismissBannerItem();
|
dismissBannerItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bannerMarkVacuum() {
|
||||||
|
const entry = _bannerQueue[_bannerIndex];
|
||||||
|
if (!entry || entry.type !== 'expired') return;
|
||||||
|
const item = entry.data;
|
||||||
|
if (item.vacuum_sealed) return; // already sealed
|
||||||
|
|
||||||
|
// Calculate new expiry: opened_at + opened_shelf_life_days_with_vacuum
|
||||||
|
let newExpiry = null;
|
||||||
|
if (item.opened_at) {
|
||||||
|
// estimateOpenedExpiryDays returns days without vacuum; add 50% for vacuum sealed
|
||||||
|
const baseDays = estimateOpenedExpiryDays(
|
||||||
|
{ name: item.name, category: item.category || '' },
|
||||||
|
item.location
|
||||||
|
);
|
||||||
|
const vacuumDays = Math.round(baseDays * 1.5);
|
||||||
|
const d = new Date(item.opened_at);
|
||||||
|
d.setDate(d.getDate() + vacuumDays);
|
||||||
|
newExpiry = d.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { id: item.id, vacuum_sealed: 1 };
|
||||||
|
if (newExpiry) body.expiry_date = newExpiry;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api('inventory_update', {}, 'POST', body);
|
||||||
|
if (res.success || res.ok) {
|
||||||
|
showToast(t('toast.vacuum_sealed', { name: item.name }), 'success');
|
||||||
|
dismissBannerItem();
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
showToast(res.error || t('error.generic'), 'error');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
showToast(t('error.connection'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bannerFinishAll() {
|
function bannerFinishAll() {
|
||||||
const entry = _bannerQueue[_bannerIndex];
|
const entry = _bannerQueue[_bannerIndex];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
@@ -8549,7 +8593,7 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu
|
|||||||
const vacuumRow = `
|
const vacuumRow = `
|
||||||
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer">
|
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer">
|
||||||
<input type="checkbox" id="move-vacuum-check" ${wasVacuum ? 'checked' : ''}>
|
<input type="checkbox" id="move-vacuum-check" ${wasVacuum ? 'checked' : ''}>
|
||||||
<span>${t('move.vacuum_seal_rest')}${wasVacuum ? ' ' + t('move.was_sealed') : ''}</span>
|
<span>${wasVacuum ? t('move.vacuum_restore') : t('move.vacuum_seal_rest')}</span>
|
||||||
</label>`;
|
</label>`;
|
||||||
document.getElementById('modal-content').innerHTML = `
|
document.getElementById('modal-content').innerHTML = `
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -12208,7 +12252,7 @@ function showRecipeMoveModal(productId, fromLoc, remaining, openedId, wasVacuum)
|
|||||||
<p style="margin-bottom:12px">${t('move.question_short').replace('{thing}', openedId ? t('move.thing_opened') : t('move.thing_rest'))}</p>
|
<p style="margin-bottom:12px">${t('move.question_short').replace('{thing}', openedId ? t('move.thing_opened') : t('move.thing_rest'))}</p>
|
||||||
<div class="location-selector">${locButtons}</div>
|
<div class="location-selector">${locButtons}</div>
|
||||||
${vacuumRow}
|
${vacuumRow}
|
||||||
<button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();closeModal()">No, resta in ${LOCATIONS[fromLoc]?.label || fromLoc}</button>
|
<button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();closeModal()">${t('move.stay_btn').replace('{location}', LOCATIONS[fromLoc]?.label || fromLoc)}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('modal-overlay').style.display = 'flex';
|
document.getElementById('modal-overlay').style.display = 'flex';
|
||||||
@@ -12316,7 +12360,7 @@ function renderRecipe(r) {
|
|||||||
const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
|
const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
|
||||||
const alreadyUsed = ing.used === true;
|
const alreadyUsed = ing.used === true;
|
||||||
html += `<li class="recipe-ingredient${alreadyUsed ? ' recipe-ing-used' : ''}" id="recipe-ing-${idx}">`;
|
html += `<li class="recipe-ingredient${alreadyUsed ? ' recipe-ing-used' : ''}" id="recipe-ing-${idx}">`;
|
||||||
html += `<span class="recipe-ing-text"><strong>${ing.name}</strong>${ing.brand ? ' <em>(' + ing.brand + ')</em>' : ''}: ${ing.qty} ✅`;
|
html += `<span class="recipe-ing-text"><strong class="recipe-ing-name" onclick="openIngredientDetail(${ing.product_id}, '${loc}')" title="${t('action.edit') || 'Modifica'}">${ing.name}</strong>${ing.brand ? ' <em>(' + ing.brand + ')</em>' : ''}: ${ing.qty} ✅`;
|
||||||
// Detail line: location + expiry
|
// Detail line: location + expiry
|
||||||
let details = [];
|
let details = [];
|
||||||
const ingredientLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`]));
|
const ingredientLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`]));
|
||||||
@@ -13694,6 +13738,21 @@ async function chatTransferToRecipes(btn, replyText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openIngredientDetail(productId, location) {
|
||||||
|
try {
|
||||||
|
const res = await api('inventory_list');
|
||||||
|
const items = res.inventory || res;
|
||||||
|
// Find by product_id + location; fallback to any row with that product_id
|
||||||
|
let item = items.find(i => i.product_id === productId && i.location === location);
|
||||||
|
if (!item) item = items.find(i => i.product_id === productId);
|
||||||
|
if (!item) { showToast(t('error.not_found'), 'error'); return; }
|
||||||
|
currentInventory = items;
|
||||||
|
editInventoryItem(item.id);
|
||||||
|
} catch(e) {
|
||||||
|
showToast(t('error.connection'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function generateRecipeForIngredient(ingredientName) {
|
async function generateRecipeForIngredient(ingredientName) {
|
||||||
if (!_requireGemini()) return;
|
if (!_requireGemini()) return;
|
||||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||||
@@ -14899,9 +14958,8 @@ async function _runStartupCheck() {
|
|||||||
if (spinnerEl) spinnerEl.style.display = 'none';
|
if (spinnerEl) spinnerEl.style.display = 'none';
|
||||||
wrapEl.style.display = '';
|
wrapEl.style.display = '';
|
||||||
|
|
||||||
// Helper: set progress bar + fade ticker queue
|
// Helper: set progress bar + crossfade status text
|
||||||
let _curPct = 0;
|
let _curPct = 0;
|
||||||
const _tickerHistory = [];
|
|
||||||
const setProgress = (pct, label, state) => {
|
const setProgress = (pct, label, state) => {
|
||||||
_curPct = pct;
|
_curPct = pct;
|
||||||
if (barEl) {
|
if (barEl) {
|
||||||
@@ -14911,13 +14969,18 @@ async function _runStartupCheck() {
|
|||||||
if (!label) return;
|
if (!label) return;
|
||||||
const ticker = document.getElementById('check-ticker');
|
const ticker = document.getElementById('check-ticker');
|
||||||
if (!ticker) return;
|
if (!ticker) return;
|
||||||
_tickerHistory.unshift({ label, state: state || 'ok' });
|
const sc = state === 'error' ? 'state-error' : state === 'warn' ? 'state-warn' : 'state-ok';
|
||||||
if (_tickerHistory.length > 4) _tickerHistory.pop();
|
// Strip emoji from label — colors convey the state
|
||||||
const posClass = ['ticker-current','ticker-prev-1','ticker-prev-2','ticker-prev-3'];
|
const cleanLabel = label.replace(/[\u{1F000}-\u{1FFFF}\u{2600}-\u{27BF}\u{2700}-\u{27BF}✅❌⚠️🔄]/gu, '').trim().replace(/^[-–—\s]+/, '');
|
||||||
ticker.innerHTML = _tickerHistory.map((item, i) => {
|
let el = ticker.querySelector('.preloader-status-text');
|
||||||
const sc = item.state === 'error' ? 'state-error' : item.state === 'warn' ? 'state-warn' : 'state-ok';
|
if (!el) {
|
||||||
return `<div class="ticker-item ${posClass[i]}${i === 0 ? ' ' + sc : ''}">${item.label}</div>`;
|
el = document.createElement('div');
|
||||||
}).join('');
|
el.className = 'preloader-status-text';
|
||||||
|
ticker.appendChild(el);
|
||||||
|
}
|
||||||
|
// Direct update — checks fire every 40ms, any fade would hide most labels
|
||||||
|
el.className = `preloader-status-text ${sc}`;
|
||||||
|
el.textContent = cleanLabel;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Phase 1: animate 0→15% while fetching (so it never looks stuck)
|
// Phase 1: animate 0→15% while fetching (so it never looks stuck)
|
||||||
@@ -14945,7 +15008,7 @@ async function _runStartupCheck() {
|
|||||||
tl('error_network_detail', 'Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell\'app non corretta\n\nControlla che il server sia avviato e riprova.'),
|
tl('error_network_detail', 'Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell\'app non corretta\n\nControlla che il server sia avviato e riprova.'),
|
||||||
errorEl, retryBtn
|
errorEl, retryBtn
|
||||||
);
|
);
|
||||||
setProgress(100, `❌ ${tl('error_network', 'Server non raggiungibile')}`, 'error');
|
setProgress(100, tl('error_network', 'Server non raggiungibile'), 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
clearInterval(slowAnim);
|
clearInterval(slowAnim);
|
||||||
@@ -15016,8 +15079,7 @@ async function _runStartupCheck() {
|
|||||||
if (!isOk && c.error) lbl += ` — ${c.error}`;
|
if (!isOk && c.error) lbl += ` — ${c.error}`;
|
||||||
if (!isOk && c.missing?.length) lbl += ` — mancanti: ${c.missing.join(', ')}`;
|
if (!isOk && c.missing?.length) lbl += ` — mancanti: ${c.missing.join(', ')}`;
|
||||||
|
|
||||||
const icon = isOk ? '✅' : isOpt ? '⚠️' : '❌';
|
setProgress(pct, lbl, isOk ? 'ok' : isOpt ? 'warn' : 'error');
|
||||||
setProgress(pct, `${icon} ${lbl}`);
|
|
||||||
|
|
||||||
if (!isOk && !isFresh) {
|
if (!isOk && !isFresh) {
|
||||||
(isOpt ? warnings : errors).push({ def, c });
|
(isOpt ? warnings : errors).push({ def, c });
|
||||||
@@ -15028,7 +15090,7 @@ async function _runStartupCheck() {
|
|||||||
|
|
||||||
// ── Errors → red bar + blocking popup ────────────────────────────────────
|
// ── Errors → red bar + blocking popup ────────────────────────────────────
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
setProgress(100, `❌ ${tl('critical_error_short', 'Errore critico')}`, 'error');
|
setProgress(100, tl('critical_error_short', 'Errore critico'), 'error');
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
const errLines = errors.map(e => {
|
const errLines = errors.map(e => {
|
||||||
const hint = e.c.hint || (e.c.error ? e.c.error : null);
|
const hint = e.c.hint || (e.c.error ? e.c.error : null);
|
||||||
@@ -15044,7 +15106,7 @@ async function _runStartupCheck() {
|
|||||||
|
|
||||||
// ── Warnings → amber bar + warning popup auto-close 5s ───────────────────
|
// ── Warnings → amber bar + warning popup auto-close 5s ───────────────────
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
setProgress(100, `⚠️ ${warnings.length} ${tl('warnings_found', 'avvisi')}`, 'warn');
|
setProgress(100, `${warnings.length} ${tl('warnings_found', 'avvisi')}`, 'warn');
|
||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
|
||||||
// Build warning popup (auto-close 5s)
|
// Build warning popup (auto-close 5s)
|
||||||
@@ -15056,7 +15118,7 @@ async function _runStartupCheck() {
|
|||||||
// Hide warning popup
|
// Hide warning popup
|
||||||
warningsEl.style.display = 'none';
|
warningsEl.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
setProgress(100, `✅ ${tl('all_ok', 'Sistema OK')}`);
|
setProgress(100, tl('all_ok', 'Sistema OK'), 'ok');
|
||||||
await new Promise(r => setTimeout(r, 600));
|
await new Promise(r => setTimeout(r, 600));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"banner_expired_action_finished": "Habe ich verbraucht!",
|
"banner_expired_action_finished": "Habe ich verbraucht!",
|
||||||
"banner_expired_action_throw": "Habe ich weggeworfen",
|
"banner_expired_action_throw": "Habe ich weggeworfen",
|
||||||
"banner_expired_action_edit": "Datum korrigieren",
|
"banner_expired_action_edit": "Datum korrigieren",
|
||||||
|
"banner_expired_action_modify": "Bearbeiten",
|
||||||
|
"banner_expired_action_vacuum": "Vakuumieren",
|
||||||
"banner_anomaly_action_edit": "Bestand korrigieren",
|
"banner_anomaly_action_edit": "Bestand korrigieren",
|
||||||
"banner_anomaly_action_dismiss": "Menge ist korrekt",
|
"banner_anomaly_action_dismiss": "Menge ist korrekt",
|
||||||
"banner_no_expiry_title": "Ablaufdatum fehlt: {name}",
|
"banner_no_expiry_title": "Ablaufdatum fehlt: {name}",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"title": "Was möchtest du tun?",
|
"title": "Was möchtest du tun?",
|
||||||
"add_btn": "📥 HINZUFÜGEN",
|
"add_btn": "📥 HINZUFÜGEN",
|
||||||
"add_sub": "in Vorrat/Kühlschrank",
|
"add_sub": "in Vorrat/Kühlschrank",
|
||||||
"use_btn": "📤 VERWENDEN / VERBRAUCHEN",
|
"use_btn": "VERWENDEN",
|
||||||
"use_sub": "aus Vorrat/Kühlschrank",
|
"use_sub": "aus Vorrat/Kühlschrank",
|
||||||
"have_title": "📦 Schon auf Lager!",
|
"have_title": "📦 Schon auf Lager!",
|
||||||
"add_more_sub": "weitere Menge",
|
"add_more_sub": "weitere Menge",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"prev": "◀ Zurück",
|
"prev": "◀ Zurück",
|
||||||
"next": "Weiter ▶",
|
"next": "Weiter ▶",
|
||||||
"ingredient_used": "✔️ Abgezogen",
|
"ingredient_used": "✔️ Abgezogen",
|
||||||
"ingredient_use_btn": "📦 Verwenden",
|
"ingredient_use_btn": "Usa",
|
||||||
"ingredient_deduct_title": "Von Vorrat abziehen",
|
"ingredient_deduct_title": "Von Vorrat abziehen",
|
||||||
"timer_expired_tts": "Timer {label} abgelaufen!",
|
"timer_expired_tts": "Timer {label} abgelaufen!",
|
||||||
"timer_warning_tts": "Achtung! {label}: noch 10 Sekunden!",
|
"timer_warning_tts": "Achtung! {label}: noch 10 Sekunden!",
|
||||||
@@ -831,6 +833,7 @@
|
|||||||
"thrown_away": "🗑️ {name} weggeworfen!",
|
"thrown_away": "🗑️ {name} weggeworfen!",
|
||||||
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
|
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
|
||||||
"finished_all": "📤 {name} aufgebraucht!",
|
"finished_all": "📤 {name} aufgebraucht!",
|
||||||
|
"vacuum_sealed": "{name} als vakuumversiegelt gespeichert",
|
||||||
"product_finished_confirmed": "✅ Entfernt — wieder hinzufügen, wenn du nachkaufst",
|
"product_finished_confirmed": "✅ Entfernt — wieder hinzufügen, wenn du nachkaufst",
|
||||||
"appliance_added": "Gerät hinzugefügt",
|
"appliance_added": "Gerät hinzugefügt",
|
||||||
"item_added": "{name} hinzugefügt"
|
"item_added": "{name} hinzugefügt"
|
||||||
@@ -1007,8 +1010,8 @@
|
|||||||
"thing_rest": "den Rest",
|
"thing_rest": "den Rest",
|
||||||
"stay_btn": "Nein, bleibt in {location}",
|
"stay_btn": "Nein, bleibt in {location}",
|
||||||
"moved_toast": "📦 Offene Packung bewegt nach {location}",
|
"moved_toast": "📦 Offene Packung bewegt nach {location}",
|
||||||
"vacuum_restore": "🫙 Vakuum wiederherstellen",
|
"vacuum_restore": "Vakuum wiederherstellen",
|
||||||
"vacuum_seal_rest": "🔒 Rest vakuumieren"
|
"vacuum_seal_rest": "Rest vakuumieren"
|
||||||
},
|
},
|
||||||
"nova": {
|
"nova": {
|
||||||
"1": "Unverarbeitet",
|
"1": "Unverarbeitet",
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"banner_expired_action_finished": "I finished it!",
|
"banner_expired_action_finished": "I finished it!",
|
||||||
"banner_expired_action_throw": "I threw it away",
|
"banner_expired_action_throw": "I threw it away",
|
||||||
"banner_expired_action_edit": "Fix date",
|
"banner_expired_action_edit": "Fix date",
|
||||||
|
"banner_expired_action_modify": "Edit",
|
||||||
|
"banner_expired_action_vacuum": "Put in vacuum seal",
|
||||||
"banner_anomaly_action_edit": "Fix inventory",
|
"banner_anomaly_action_edit": "Fix inventory",
|
||||||
"banner_anomaly_action_dismiss": "Quantity is correct",
|
"banner_anomaly_action_dismiss": "Quantity is correct",
|
||||||
"banner_no_expiry_title": "Missing expiry: {name}",
|
"banner_no_expiry_title": "Missing expiry: {name}",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"title": "What do you want to do?",
|
"title": "What do you want to do?",
|
||||||
"add_btn": "📥 ADD",
|
"add_btn": "📥 ADD",
|
||||||
"add_sub": "to pantry/fridge",
|
"add_sub": "to pantry/fridge",
|
||||||
"use_btn": "📤 USE / CONSUME",
|
"use_btn": "USE",
|
||||||
"use_sub": "from pantry/fridge",
|
"use_sub": "from pantry/fridge",
|
||||||
"have_title": "📦 Already in stock!",
|
"have_title": "📦 Already in stock!",
|
||||||
"add_more_sub": "add more",
|
"add_more_sub": "add more",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"prev": "◀ Previous",
|
"prev": "◀ Previous",
|
||||||
"next": "Next ▶",
|
"next": "Next ▶",
|
||||||
"ingredient_used": "✔️ Deducted",
|
"ingredient_used": "✔️ Deducted",
|
||||||
"ingredient_use_btn": "📦 Use",
|
"ingredient_use_btn": "Use",
|
||||||
"ingredient_deduct_title": "Deduct from pantry",
|
"ingredient_deduct_title": "Deduct from pantry",
|
||||||
"timer_expired_tts": "Timer {label} expired!",
|
"timer_expired_tts": "Timer {label} expired!",
|
||||||
"timer_warning_tts": "Heads up! {label}: 10 seconds left!",
|
"timer_warning_tts": "Heads up! {label}: 10 seconds left!",
|
||||||
@@ -831,6 +833,7 @@
|
|||||||
"thrown_away": "🗑️ {name} thrown away!",
|
"thrown_away": "🗑️ {name} thrown away!",
|
||||||
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
||||||
"finished_all": "📤 {name} finished!",
|
"finished_all": "📤 {name} finished!",
|
||||||
|
"vacuum_sealed": "{name} saved as vacuum sealed",
|
||||||
"product_finished_confirmed": "✅ Removed — add it again when you restock",
|
"product_finished_confirmed": "✅ Removed — add it again when you restock",
|
||||||
"appliance_added": "Appliance added",
|
"appliance_added": "Appliance added",
|
||||||
"item_added": "{name} added"
|
"item_added": "{name} added"
|
||||||
@@ -1007,8 +1010,8 @@
|
|||||||
"thing_rest": "rest",
|
"thing_rest": "rest",
|
||||||
"stay_btn": "No, stay in {location}",
|
"stay_btn": "No, stay in {location}",
|
||||||
"moved_toast": "📦 Opened package moved to {location}",
|
"moved_toast": "📦 Opened package moved to {location}",
|
||||||
"vacuum_restore": "🫙 Restore vacuum sealed",
|
"vacuum_restore": "Restore vacuum sealed",
|
||||||
"vacuum_seal_rest": "🔒 Vacuum seal the rest"
|
"vacuum_seal_rest": "Vacuum seal the rest"
|
||||||
},
|
},
|
||||||
"nova": {
|
"nova": {
|
||||||
"1": "Unprocessed",
|
"1": "Unprocessed",
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"banner_expired_action_finished": "L'ho finito!",
|
"banner_expired_action_finished": "L'ho finito!",
|
||||||
"banner_expired_action_throw": "L'ho buttato",
|
"banner_expired_action_throw": "L'ho buttato",
|
||||||
"banner_expired_action_edit": "Correggi data",
|
"banner_expired_action_edit": "Correggi data",
|
||||||
|
"banner_expired_action_modify": "Modifica",
|
||||||
|
"banner_expired_action_vacuum": "Metti sottovuoto",
|
||||||
"banner_anomaly_action_edit": "Correggi inventario",
|
"banner_anomaly_action_edit": "Correggi inventario",
|
||||||
"banner_anomaly_action_dismiss": "La quantità è giusta",
|
"banner_anomaly_action_dismiss": "La quantità è giusta",
|
||||||
"banner_no_expiry_title": "Scadenza mancante: {name}",
|
"banner_no_expiry_title": "Scadenza mancante: {name}",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"title": "Cosa vuoi fare?",
|
"title": "Cosa vuoi fare?",
|
||||||
"add_btn": "📥 AGGIUNGI",
|
"add_btn": "📥 AGGIUNGI",
|
||||||
"add_sub": "in dispensa/frigo",
|
"add_sub": "in dispensa/frigo",
|
||||||
"use_btn": "📤 USA / CONSUMA",
|
"use_btn": "USA",
|
||||||
"use_sub": "dalla dispensa/frigo",
|
"use_sub": "dalla dispensa/frigo",
|
||||||
"have_title": "📦 Ce l'hai già!",
|
"have_title": "📦 Ce l'hai già!",
|
||||||
"add_more_sub": "altra quantità",
|
"add_more_sub": "altra quantità",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"prev": "◀ Precedente",
|
"prev": "◀ Precedente",
|
||||||
"next": "Successivo ▶",
|
"next": "Successivo ▶",
|
||||||
"ingredient_used": "✔️ Scalato",
|
"ingredient_used": "✔️ Scalato",
|
||||||
"ingredient_use_btn": "📦 Usa",
|
"ingredient_use_btn": "Usa",
|
||||||
"ingredient_deduct_title": "Scala dalla dispensa",
|
"ingredient_deduct_title": "Scala dalla dispensa",
|
||||||
"timer_expired_tts": "Timer {label} scaduto!",
|
"timer_expired_tts": "Timer {label} scaduto!",
|
||||||
"timer_warning_tts": "Attenzione! {label}: mancano 10 secondi!",
|
"timer_warning_tts": "Attenzione! {label}: mancano 10 secondi!",
|
||||||
@@ -831,6 +833,7 @@
|
|||||||
"thrown_away": "🗑️ {name} buttato!",
|
"thrown_away": "🗑️ {name} buttato!",
|
||||||
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
||||||
"finished_all": "📤 {name} terminato!",
|
"finished_all": "📤 {name} terminato!",
|
||||||
|
"vacuum_sealed": "{name} salvato come sottovuoto",
|
||||||
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
|
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
|
||||||
"appliance_added": "Elettrodomestico aggiunto",
|
"appliance_added": "Elettrodomestico aggiunto",
|
||||||
"item_added": "{name} aggiunto"
|
"item_added": "{name} aggiunto"
|
||||||
@@ -1007,8 +1010,8 @@
|
|||||||
"thing_rest": "il resto",
|
"thing_rest": "il resto",
|
||||||
"stay_btn": "No, resta in {location}",
|
"stay_btn": "No, resta in {location}",
|
||||||
"moved_toast": "📦 Confezione aperta spostata in {location}",
|
"moved_toast": "📦 Confezione aperta spostata in {location}",
|
||||||
"vacuum_restore": "🫙 Torna sotto vuoto",
|
"vacuum_restore": "Torna sotto vuoto",
|
||||||
"vacuum_seal_rest": "🔒 Metti sotto vuoto il resto"
|
"vacuum_seal_rest": "Metti sotto vuoto il resto"
|
||||||
},
|
},
|
||||||
"nova": {
|
"nova": {
|
||||||
"1": "Non trasformato",
|
"1": "Non trasformato",
|
||||||
|
|||||||
Reference in New Issue
Block a user