diff --git a/api/index.php b/api/index.php
index 3a0ec63..e174a5e 100644
--- a/api/index.php
+++ b/api/index.php
@@ -3127,6 +3127,121 @@ function addToInventory(PDO $db): void {
invalidateSmartShoppingCache();
}
+/** Waste transaction notes use format Buttato|reason_key (legacy: plain "Buttato"). */
+function _isWasteNotes(string $notes): bool {
+ return $notes === 'Buttato' || str_starts_with($notes, 'Buttato|');
+}
+
+function _wasteReasonKey(string $notes): ?string {
+ if ($notes === 'Buttato') {
+ return 'unknown';
+ }
+ if (preg_match('/^Buttato\|([a-z_]+)/', $notes, $m)) {
+ return $m[1];
+ }
+ return null;
+}
+
+function _loadWasteLearning(PDO $db): array {
+ static $cache = null;
+ if ($cache !== null) {
+ return $cache;
+ }
+ $row = $db->query("SELECT value FROM app_settings WHERE key = 'waste_learning'")->fetchColumn();
+ $cache = ($row !== false && $row !== '') ? (json_decode((string)$row, true) ?: []) : [];
+ return $cache;
+}
+
+function _saveWasteLearning(PDO $db, array $data): void {
+ $stmt = $db->prepare("INSERT INTO app_settings (key, value, updated_at) VALUES ('waste_learning', ?, datetime('now'))
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at");
+ $stmt->execute([json_encode($data, JSON_UNESCAPED_UNICODE)]);
+ invalidateSmartShoppingCache();
+}
+
+function _guessPreferredStorageLocation(string $name, string $category): string {
+ $n = mb_strtolower($name . ' ' . $category);
+ if (preg_match('/surgelat|gelato|congelat|frozen|piselli surg|spinaci surg|basilico surg/', $n)) {
+ return 'freezer';
+ }
+ if (preg_match('/latte|yogurt|formaggio|burro|panna|uova|insalata|rucola|spinaci|pollo|carne|pesce|prosciutto|salame|mortadella|bresaola|affettato/', $n)) {
+ return 'frigo';
+ }
+ return 'dispensa';
+}
+
+function _applyWasteLearning(PDO $db, int $productId, string $reason, string $location, array $product): void {
+ if ($reason === '' || $reason === 'other') {
+ return;
+ }
+ $data = _loadWasteLearning($db);
+ $pid = (string)$productId;
+ if (!isset($data[$pid])) {
+ $data[$pid] = [];
+ }
+ $data[$pid]['last_reason'] = $reason;
+ $data[$pid]['last_at'] = time();
+ $data[$pid]['count_' . $reason] = (int)($data[$pid]['count_' . $reason] ?? 0) + 1;
+
+ switch ($reason) {
+ case 'expired':
+ case 'spoiled':
+ $data[$pid]['alert_days_sooner'] = min(5, (int)($data[$pid]['alert_days_sooner'] ?? 0) + 1);
+ break;
+ case 'wrong_location':
+ $preferred = _guessPreferredStorageLocation($product['name'] ?? '', $product['category'] ?? '');
+ if ($preferred !== $location) {
+ $data[$pid]['preferred_location'] = $preferred;
+ }
+ break;
+ case 'kept_too_long':
+ case 'forgotten':
+ $data[$pid]['buy_smaller'] = true;
+ $data[$pid]['max_suggested_pz'] = 2;
+ break;
+ case 'bought_too_much':
+ $data[$pid]['buy_less'] = true;
+ $data[$pid]['max_suggested_conf'] = 1;
+ $data[$pid]['max_suggested_pz'] = 2;
+ break;
+ case 'bad_quality':
+ $data[$pid]['buy_less'] = true;
+ break;
+ }
+ _saveWasteLearning($db, $data);
+}
+
+function _maybeApplyWasteLearning(PDO $db, int $productId, string $notes, string $location): void {
+ if (!_isWasteNotes($notes)) {
+ return;
+ }
+ $reason = _wasteReasonKey($notes) ?? 'unknown';
+ $stmt = $db->prepare("SELECT name, category FROM products WHERE id = ?");
+ $stmt->execute([$productId]);
+ $product = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (!$product) {
+ return;
+ }
+ _applyWasteLearning($db, $productId, $reason, $location, $product);
+}
+
+function _applyWasteHintsToSuggestion(int $productId, $suggestedQty, string $suggestedUnit, array $wasteLearning): array {
+ $hint = $wasteLearning[(string)$productId] ?? [];
+ if ($suggestedQty === null || empty($hint)) {
+ return [$suggestedQty, $suggestedUnit];
+ }
+ if (!empty($hint['buy_less']) || !empty($hint['buy_smaller'])) {
+ if ($suggestedUnit === 'conf') {
+ $cap = (float)($hint['max_suggested_conf'] ?? 1);
+ $suggestedQty = min((float)$suggestedQty, max(1.0, $cap));
+ } elseif ($suggestedUnit === 'pz') {
+ $cap = (float)($hint['max_suggested_pz'] ?? 2);
+ $suggestedQty = min((float)$suggestedQty, max(1.0, $cap));
+ }
+ }
+ return [$suggestedQty, $suggestedUnit];
+}
+
function useFromInventory(PDO $db): void {
EverLog::info('useFromInventory');
$input = json_decode(file_get_contents('php://input'), true);
@@ -3143,6 +3258,18 @@ function useFromInventory(PDO $db): void {
return;
}
+ try {
+ dbWithRetry(function () use ($db, $productId, $quantity, $useAll, $location, $notes): void {
+ useFromInventoryCore($db, $productId, $quantity, $useAll, $location, $notes);
+ });
+ } catch (\PDOException $e) {
+ EverLog::error('useFromInventory db error', ['msg' => $e->getMessage()]);
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => 'Database busy — please retry']);
+ }
+}
+
+function useFromInventoryCore(PDO $db, $productId, $quantity, $useAll, $location, $notes): void {
// ── Server-side deduplication ─────────────────────────────────────────
// Guard against accidental double-consume triggers (scale jitter, double tap,
// delayed/offline replay burst). We only apply this stricter gate to manual
@@ -3201,10 +3328,10 @@ function useFromInventory(PDO $db): void {
$stmt->execute([$productId]);
$allItems = $stmt->fetchAll();
$totalRemoved = 0;
- $explicitFinish = ($notes !== 'Buttato');
+ $explicitFinish = !_isWasteNotes($notes);
foreach ($allItems as $item) {
$totalRemoved += $item['quantity'];
- $type = ($notes === 'Buttato') ? 'waste' : 'out';
+ $type = _isWasteNotes($notes) ? 'waste' : 'out';
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $type, $item['quantity'], $item['location'], $notes]);
@@ -3218,6 +3345,7 @@ function useFromInventory(PDO $db): void {
$stmt->execute([$item['id']]);
}
}
+ _maybeApplyWasteLearning($db, (int)$productId, $notes, $location === '__all__' ? 'dispensa' : $location);
echo json_encode(['success' => true, 'remaining' => 0, 'removed' => $totalRemoved]);
return;
}
@@ -3276,9 +3404,10 @@ function useFromInventory(PDO $db): void {
}
// Log transaction
- $type = ($notes === 'Buttato') ? 'waste' : 'out';
+ $type = _isWasteNotes($notes) ? 'waste' : 'out';
$stmt3 = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, ?)");
$stmt3->execute([$productId, $type, $quantity, $location, $notes]);
+ _maybeApplyWasteLearning($db, (int)$productId, $notes, $location);
$remaining = $newFraction > 0.001 ? $newFraction : 0;
// Skip the normal flow — jump to Bring! check and response
@@ -3367,13 +3496,14 @@ function useFromInventory(PDO $db): void {
}
// Log transaction (actual amount removed, not requested)
- $type = ($notes === 'Buttato') ? 'waste' : 'out';
+ $type = _isWasteNotes($notes) ? 'waste' : 'out';
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $type, $actualDeducted, $location, $notes]);
+ _maybeApplyWasteLearning($db, (int)$productId, $notes, $location);
// User explicitly chose "use all/finished": remove this row now instead of
// leaving quantity=0 pending confirmation.
- if ($useAll && $notes !== 'Buttato' && $newQty <= 0) {
+ if ($useAll && !_isWasteNotes($notes) && $newQty <= 0) {
$stmt = $db->prepare("DELETE FROM inventory WHERE id = ?");
$stmt->execute([$existing['id']]);
}
@@ -10612,6 +10742,7 @@ function invalidateSmartShoppingCache(): void {
function smartShoppingCached(PDO $db): void {
EverLog::info('smartShoppingCached');
+ set_time_limit(120);
// Never let the browser or proxy cache this — urgency is time-sensitive
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
@@ -10690,6 +10821,7 @@ function _productOnBring(string $productName, array $bringItems, string $shoppin
function smartShopping(PDO $db): void {
EverLog::info('smartShopping');
+ set_time_limit(120);
$now = time();
$today = date('Y-m-d');
@@ -10801,6 +10933,7 @@ function smartShopping(PDO $db): void {
// 5. Analyze each product
$items = [];
+ $wasteLearning = _loadWasteLearning($db);
foreach ($products as $p) {
$pid = $p['id'];
$inv = $inventory[$pid] ?? null;
@@ -11363,6 +11496,13 @@ function smartShopping(PDO $db): void {
}
}
+ [$suggestedQty, $suggestedUnit] = _applyWasteHintsToSuggestion($pid, $suggestedQty, $suggestedUnit ?? $unit, $wasteLearning);
+ $wHint = $wasteLearning[(string)$pid] ?? [];
+ if (!empty($wHint['preferred_location'])) {
+ $locLabel = $wHint['preferred_location'];
+ $reasons[] = "Past waste: store in {$locLabel}";
+ }
+
$items[] = [
'product_id' => $pid,
'name' => $p['name'],
@@ -11644,10 +11784,30 @@ function shoppingGetList(PDO $db): void {
function shoppingAdd(PDO $db): void {
if (isShoppingBringMode()) {
- bringAddItems($db);
+ try {
+ dbWithRetry(function () use ($db): void {
+ bringAddItems($db);
+ });
+ } catch (\PDOException $e) {
+ EverLog::error('shoppingAdd/bring db error', ['msg' => $e->getMessage()]);
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => 'Database busy — please retry']);
+ }
return;
}
$input = json_decode(file_get_contents('php://input'), true) ?? [];
+ try {
+ dbWithRetry(function () use ($db, $input): void {
+ shoppingAddInternal($db, $input);
+ });
+ } catch (\PDOException $e) {
+ EverLog::error('shoppingAdd db error', ['msg' => $e->getMessage()]);
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => 'Database busy — please retry']);
+ }
+}
+
+function shoppingAddInternal(PDO $db, array $input): void {
$items = $input['items'] ?? [];
$added = 0; $updated = 0; $skipped = 0;
foreach ($items as $item) {
diff --git a/assets/js/app.js b/assets/js/app.js
index 3d1bb39..b8e2d55 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -6809,6 +6809,53 @@ async function quickUse(productId, location) {
}
}
+const WASTE_REASON_KEYS = ['expired', 'spoiled', 'wrong_location', 'kept_too_long', 'bought_too_much', 'forgotten', 'bad_quality', 'other'];
+
+function _wasteNotesForReason(reason) {
+ return 'Buttato|' + reason;
+}
+
+function _showWasteReasonModal(productLabel, onPick) {
+ const buttons = WASTE_REASON_KEYS.map(r =>
+ ``
+ ).join('');
+ document.getElementById('modal-content').innerHTML = `
+
+ ${escapeHtml(productLabel)}
+ ${t('waste.reason_subtitle')}
+ ${buttons}
+
+
+ `;
+ document.getElementById('modal-overlay').style.display = 'flex';
+ document.querySelectorAll('[data-waste-reason]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const reason = btn.getAttribute('data-waste-reason');
+ closeModal();
+ onPick(reason);
+ });
+ });
+}
+
+function _inventoryWaste(payload, productLabel) {
+ return new Promise((resolve, reject) => {
+ _showWasteReasonModal(productLabel || '', async (reason) => {
+ showLoading(true);
+ try {
+ const result = await api('inventory_use', {}, 'POST', { ...payload, notes: _wasteNotesForReason(reason) });
+ resolve(result);
+ } catch (e) {
+ reject(e);
+ } finally {
+ showLoading(false);
+ }
+ });
+ });
+}
+
async function deleteInventoryItem(id) {
const item = currentInventory.find(i => i.id === id);
const unit = item ? (item.unit || 'pz') : 'pz';
@@ -6853,19 +6900,15 @@ async function _discardOnePiece(inventoryId) {
const item = currentInventory.find(i => i.id === inventoryId);
if (!item) { closeModal(); return; }
closeModal();
- showLoading(true);
try {
- await api('inventory_use', {}, 'POST', {
+ await _inventoryWaste({
product_id: item.product_id,
quantity: 1,
location: item.location,
- notes: 'Buttato'
- });
- showLoading(false);
+ }, item.name);
showToast(t('toast.thrown_away_partial', { qty: 1, unit: item.unit || 'pz', name: item.name }), 'success');
refreshCurrentPage();
} catch(e) {
- showLoading(false);
showToast(t('error.connection'), 'error');
}
}
@@ -6874,19 +6917,15 @@ async function _discardAllFromModal(inventoryId) {
const item = currentInventory.find(i => i.id === inventoryId);
if (!item) { closeModal(); return; }
closeModal();
- showLoading(true);
try {
- await api('inventory_use', {}, 'POST', {
+ await _inventoryWaste({
product_id: item.product_id,
use_all: true,
location: item.location,
- notes: 'Buttato'
- });
- showLoading(false);
+ }, item.name);
showToast(t('toast.thrown_away', { name: item.name }), 'success');
refreshCurrentPage();
} catch(e) {
- showLoading(false);
showToast(t('error.connection'), 'error');
}
}
@@ -9180,15 +9219,12 @@ async function throwAll() {
t('use.throw_all_confirm_title') || '🗑️ Butta tutto',
(t('use.throw_all_confirm_msg') || 'Vuoi davvero buttare via tutto il prodotto?') + (name ? `\n"${name}"` : ''),
async () => {
- showLoading(true);
try {
- const result = await api('inventory_use', {}, 'POST', {
+ const result = await _inventoryWaste({
product_id: currentProduct.id,
use_all: true,
location: '__all__',
- notes: 'Buttato'
- });
- showLoading(false);
+ }, name);
if (result.success) {
showToast(t('toast.thrown_away', { name: currentProduct.name }), 'success');
showPage('dashboard');
@@ -9196,7 +9232,6 @@ async function throwAll() {
showToast(result.error || t('error.generic'), 'error');
}
} catch(e) {
- showLoading(false);
showToast(t('error.connection'), 'error');
}
},
@@ -9208,15 +9243,12 @@ async function throwPartial() {
const qty = parseFloat(document.getElementById('throw-quantity').value) || 1;
const loc = document.getElementById('throw-location').value;
closeModal();
- showLoading(true);
try {
- const result = await api('inventory_use', {}, 'POST', {
+ const result = await _inventoryWaste({
product_id: currentProduct.id,
quantity: qty,
location: loc,
- notes: 'Buttato'
- });
- showLoading(false);
+ }, currentProduct.name);
if (result.success) {
showToast(t('toast.thrown_away_partial', { qty, unit: currentProduct.unit || 'pz', name: currentProduct.name }), 'success');
showPage('dashboard');
@@ -9224,7 +9256,6 @@ async function throwPartial() {
showToast(result.error || t('error.generic'), 'error');
}
} catch(e) {
- showLoading(false);
showToast(t('error.connection'), 'error');
}
}
diff --git a/scripts/github-issue-triage.php b/scripts/github-issue-triage.php
new file mode 100644
index 0000000..83e95b0
--- /dev/null
+++ b/scripts/github-issue-triage.php
@@ -0,0 +1,81 @@
+#!/usr/bin/env php
+ true,
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_TIMEOUT => 20,
+ ]);
+ if ($method === 'PATCH') {
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+ } elseif ($method === 'POST') {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+ }
+ $raw = curl_exec($ch);
+ $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ return ['http_code' => $code, 'body' => json_decode($raw ?: '{}', true) ?: []];
+}
+
+function comment(string $token, int $num, string $body): void {
+ $r = ghApi($token, 'POST', 'https://api.github.com/repos/' . GH_REPO . "/issues/$num/comments", ['body' => $body]);
+ echo $r['http_code'] >= 200 && $r['http_code'] < 300 ? "OK comment #$num\n" : "FAIL comment #$num\n";
+}
+
+function closeIssue(string $token, int $num): void {
+ $r = ghApi($token, 'PATCH', 'https://api.github.com/repos/' . GH_REPO . "/issues/$num", ['state' => 'closed']);
+ echo $r['http_code'] >= 200 && $r['http_code'] < 300 ? "OK close #$num\n" : "FAIL close #$num\n";
+}
+
+function reopenIssue(string $token, int $num): void {
+ $r = ghApi($token, 'PATCH', 'https://api.github.com/repos/' . GH_REPO . "/issues/$num", ['state' => 'open']);
+ echo $r['http_code'] >= 200 && $r['http_code'] < 300 ? "OK reopen #$num\n" : "FAIL reopen #$num\n";
+}
+
+$reopen = [
+ 125 => "Reopened: **voice commands in cooking mode** are not implemented yet (only TTS readout exists). This was closed by mistake during bulk triage — the feature backlog should stay open until hands-free step navigation ships.",
+ 98 => "Reopened: **pin favourite products to the top of inventory** is not implemented yet (recipe favourites #124 are done, but product pinning is a separate request). Closed by mistake — keeping on the backlog.",
+];
+
+foreach ($reopen as $num => $msg) {
+ comment($token, $num, $msg);
+ reopenIssue($token, $num);
+}
+
+$bugs = [
+ 201 => 'Fixed in latest develop: `inventory_use` and `shopping_add` now retry on `SQLITE_BUSY` via `dbWithRetry()` (same pattern as #198).',
+ 202 => 'Fixed: Bring/internal `shopping_add` wrapped in `dbWithRetry()` to survive cron + PWA concurrent writes.',
+ 203 => 'Fixed: `smartShopping()` / `smartShoppingCached()` now call `set_time_limit(120)` so large pantries no longer hit the 30s PHP fatal.',
+ 204 => 'Fixed: same as #203 — smart shopping timeout caused HTTP 500; extended execution limit resolves the crash.',
+];
+
+foreach ($bugs as $num => $msg) {
+ comment($token, $num, $msg . "\n\n_Closed after triage — fix shipped in develop._");
+ closeIssue($token, $num);
+}
+
+echo "Done.\n";
diff --git a/translations/de.json b/translations/de.json
index 6ae9fb1..ac909b4 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -353,6 +353,18 @@
"throw_all_confirm_btn": "🗑️ Ja, entsorgen",
"locations_short": "Orte"
},
+ "waste": {
+ "reason_title": "Warum wirfst du es weg?",
+ "reason_subtitle": "Das hilft uns, ähnliche Verschwendung zu vermeiden.",
+ "reason_expired": "⏰ Abgelaufen",
+ "reason_spoiled": "🦠 Verdorben",
+ "reason_wrong_location": "📍 Falscher Lagerort",
+ "reason_kept_too_long": "⏳ Zu lange aufbewahrt",
+ "reason_bought_too_much": "🛒 Zu viel gekauft",
+ "reason_forgotten": "😴 Vergessen / nicht rechtzeitig genutzt",
+ "reason_bad_quality": "👎 Schlechte Qualität beim Kauf",
+ "reason_other": "❓ Sonstiges"
+ },
"product": {
"title_new": "Neues Produkt",
"title_edit": "Produkt bearbeiten",
diff --git a/translations/en.json b/translations/en.json
index e7898f3..3294cfc 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -353,6 +353,18 @@
"throw_all_confirm_btn": "🗑️ Yes, discard",
"locations_short": "places"
},
+ "waste": {
+ "reason_title": "Why are you discarding it?",
+ "reason_subtitle": "This helps us prevent similar waste next time.",
+ "reason_expired": "⏰ Expired",
+ "reason_spoiled": "🦠 Spoiled / gone bad",
+ "reason_wrong_location": "📍 Wrong storage (fridge/freezer/pantry)",
+ "reason_kept_too_long": "⏳ Kept too long",
+ "reason_bought_too_much": "🛒 Bought too much",
+ "reason_forgotten": "😴 Forgotten / not used in time",
+ "reason_bad_quality": "👎 Poor quality when bought",
+ "reason_other": "❓ Other"
+ },
"product": {
"title_new": "New Product",
"title_edit": "Edit Product",
diff --git a/translations/es.json b/translations/es.json
index 20e257d..629d375 100644
--- a/translations/es.json
+++ b/translations/es.json
@@ -353,6 +353,18 @@
"throw_all_confirm_btn": "🗑️ Sí, desechar",
"locations_short": "ubicaciones"
},
+ "waste": {
+ "reason_title": "¿Por qué lo tiras?",
+ "reason_subtitle": "Nos ayuda a evitar desperdicios similares.",
+ "reason_expired": "⏰ Caducado",
+ "reason_spoiled": "🦠 Estropeado",
+ "reason_wrong_location": "📍 Lugar de guardado incorrecto",
+ "reason_kept_too_long": "⏳ Guardado demasiado tiempo",
+ "reason_bought_too_much": "🛒 Comprado de más",
+ "reason_forgotten": "😴 Olvidado / no usado a tiempo",
+ "reason_bad_quality": "👎 Mala calidad al comprar",
+ "reason_other": "❓ Otro"
+ },
"product": {
"title_new": "Nuevo producto",
"title_edit": "Editar producto",
diff --git a/translations/fr.json b/translations/fr.json
index 7c2818f..35a324b 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -353,6 +353,18 @@
"throw_all_confirm_btn": "🗑️ Oui, jeter",
"locations_short": "emplacements"
},
+ "waste": {
+ "reason_title": "Pourquoi le jetez-vous ?",
+ "reason_subtitle": "Cela nous aide à éviter des gaspillages similaires.",
+ "reason_expired": "⏰ Périmé",
+ "reason_spoiled": "🦠 Abîmé / gâté",
+ "reason_wrong_location": "📍 Mauvais emplacement",
+ "reason_kept_too_long": "⏳ Conservé trop longtemps",
+ "reason_bought_too_much": "🛒 Acheté en trop grande quantité",
+ "reason_forgotten": "😴 Oublié / pas utilisé à temps",
+ "reason_bad_quality": "👎 Mauvaise qualité à l'achat",
+ "reason_other": "❓ Autre"
+ },
"product": {
"title_new": "Nouveau produit",
"title_edit": "Modifier le produit",
diff --git a/translations/it.json b/translations/it.json
index 4b2e086..78fae42 100644
--- a/translations/it.json
+++ b/translations/it.json
@@ -353,6 +353,18 @@
"throw_all_confirm_btn": "🗑️ Sì, butta",
"locations_short": "posti"
},
+ "waste": {
+ "reason_title": "Perché lo butti?",
+ "reason_subtitle": "Ci aiuta a evitare sprechi simili in futuro.",
+ "reason_expired": "⏰ Scaduto",
+ "reason_spoiled": "🦠 Andato a male / deperito",
+ "reason_wrong_location": "📍 Posto sbagliato (frigo/freezer/dispensa)",
+ "reason_kept_too_long": "⏳ Tenuto troppo a lungo",
+ "reason_bought_too_much": "🛒 Comprato troppo",
+ "reason_forgotten": "😴 Dimenticato / non usato in tempo",
+ "reason_bad_quality": "👎 Qualità scadente all'acquisto",
+ "reason_other": "❓ Altro"
+ },
"product": {
"title_new": "Nuovo Prodotto",
"title_edit": "Modifica Prodotto",