Aggiunta pagina impostazioni, preview prodotto migliorata, gestione inventario smart e ricette avanzate

- Icona ingranaggio nella navbar, salvataggio su localStorage e .env
- Preview prodotto più grande dopo scansione barcode
- Controllo inventario dopo scan: mostra quantità disponibile in grande
- 3 pulsanti contestuali (AGGIUNGI/USA/BUTTA) se prodotto già presente
- Funzionalità BUTTA con modale per quantità parziale o totale
- Quantità prominenti nella lista inventario
- Quantità visibili negli alert scadenza/scaduti in dashboard
- Unità di misura modificabile nella modale di modifica inventario
- Opzioni ricetta: Pasto Veloce, Poca Fame, Priorità Scadenze, ecc.
- Gestione smart quantità ricette (evita rimasugli inutilizzabili)
- Elettrodomestici configurabili per suggerimenti ricette
- Restrizioni alimentari nel prompt ricette
- Endpoint API: save_settings, get_settings
This commit is contained in:
dadaloop82
2026-03-11 13:08:02 +00:00
parent 05cc2b9138
commit 469aadb8fc
5 changed files with 1017 additions and 20 deletions
+330
View File
@@ -2227,3 +2227,333 @@ body {
font-family: monospace;
flex-shrink: 0;
}
/* ===== SETTINGS PAGE ===== */
.settings-tabs {
display: flex;
gap: 4px;
overflow-x: auto;
padding-bottom: 12px;
margin-bottom: 8px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.settings-tabs::-webkit-scrollbar { display: none; }
.settings-tab {
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 25px;
padding: 8px 14px;
font-size: 0.82rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
}
.settings-tab.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.settings-panel {
display: none;
}
.settings-panel.active {
display: block;
}
.settings-card {
background: var(--bg-card);
border-radius: var(--radius);
padding: 16px;
box-shadow: var(--shadow);
margin-bottom: 12px;
}
.settings-card h4 {
font-size: 1.05rem;
margin-bottom: 4px;
color: var(--primary);
}
.settings-hint {
font-size: 0.82rem;
color: var(--text-muted);
margin-bottom: 12px;
line-height: 1.4;
}
.settings-status {
margin-top: 12px;
padding: 12px;
border-radius: var(--radius-sm);
text-align: center;
font-weight: 600;
font-size: 0.9rem;
}
.settings-status.success {
background: #d1fae5;
color: #047857;
}
.settings-status.error {
background: #fee2e2;
color: #dc2626;
}
.btn-small {
padding: 6px 12px;
font-size: 0.82rem;
border-radius: var(--radius-sm);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg);
border-radius: var(--radius-sm);
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
}
.checkbox-label:active {
background: var(--border);
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary);
}
.recipe-pref-checks {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Appliances */
.appliances-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.appliance-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--bg);
border-radius: var(--radius-sm);
font-size: 0.95rem;
font-weight: 500;
}
.appliance-item .appliance-remove {
background: none;
border: none;
color: var(--danger);
font-size: 1.1rem;
cursor: pointer;
padding: 2px 6px;
border-radius: 50%;
transition: background 0.2s;
}
.appliance-item .appliance-remove:active {
background: #fee2e2;
}
.appliance-quick-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.common-appliances .btn-small {
font-size: 0.78rem;
padding: 5px 10px;
}
/* ===== RECIPE OPTIONS GRID ===== */
.recipe-options-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 4px;
}
.recipe-option-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
background: var(--bg);
border-radius: var(--radius-sm);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
border: 2px solid transparent;
}
.recipe-option-chip:has(input:checked) {
background: rgba(45, 80, 22, 0.08);
border-color: var(--primary);
}
.recipe-option-chip input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--primary);
}
/* ===== LARGER PRODUCT PREVIEW (Action page) ===== */
.product-preview-large {
flex-direction: column;
text-align: center;
padding: 20px;
}
.product-preview-large img {
width: 120px;
height: 120px;
border-radius: var(--radius);
object-fit: cover;
margin-bottom: 8px;
}
.product-preview-large .product-preview-emoji {
font-size: 4rem;
width: auto;
margin-bottom: 8px;
}
.product-preview-large .product-preview-info {
text-align: center;
}
.product-preview-large .product-preview-info h3 {
font-size: 1.4rem;
margin-bottom: 4px;
}
.product-preview-large .product-preview-info p {
font-size: 0.95rem;
}
/* ===== INVENTORY STATUS BAR ===== */
.inventory-status-bar {
background: linear-gradient(135deg, #dbeafe 0%, #c7d2fe 100%);
border: 2px solid #93b4f8;
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.inventory-status-bar .inv-status-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.inventory-status-bar .inv-status-title {
font-size: 1rem;
font-weight: 700;
color: #1e40af;
}
.inventory-status-bar .inv-status-total {
font-size: 2.2rem;
font-weight: 900;
color: #1e3a8a;
background: rgba(255,255,255,0.8);
padding: 8px 22px;
border-radius: 24px;
letter-spacing: 0.5px;
line-height: 1.1;
}
.inventory-status-bar .inv-status-items {
display: flex;
flex-direction: column;
gap: 4px;
}
.inventory-status-bar .inv-status-item {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #1d4ed8;
padding: 5px 10px;
background: rgba(255,255,255,0.55);
border-radius: var(--radius-sm);
}
.inventory-status-bar .inv-status-item .inv-status-qty {
font-weight: 700;
}
/* ===== THROW AWAY BUTTONS ===== */
.action-buttons-3col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.action-buttons-3col .btn-huge {
min-height: 100px;
padding: 18px 8px;
font-size: 1rem;
}
.action-buttons-3col .btn-icon {
font-size: 2rem;
}
.btn-throw {
background: #f97316;
color: white;
}
.btn-throw:active {
background: #ea580c;
}
/* ===== LARGER QUANTITY IN INVENTORY ===== */
.inv-qty-prominent {
font-size: 1.2rem;
font-weight: 800;
color: var(--primary);
white-space: nowrap;
background: #d1fae5;
padding: 4px 12px;
border-radius: 20px;
flex-shrink: 0;
}
/* ===== ALERT QUANTITY BADGES ===== */
.alert-item-qty {
font-size: 0.78rem;
font-weight: 600;
color: var(--text-light);
padding: 2px 8px;
background: rgba(255,255,255,0.6);
border-radius: 12px;
white-space: nowrap;
}
+417 -14
View File
@@ -306,6 +306,159 @@ let scannerStream = null;
let quaggaRunning = false;
let aiStream = null;
// ===== SETTINGS / CONFIG =====
function getSettings() {
try {
const s = JSON.parse(localStorage.getItem('dispensa_settings') || '{}');
// Build recipe_prefs array from individual booleans
s.recipe_prefs = [];
if (s.pref_veloce) s.recipe_prefs.push('veloce');
if (s.pref_pocafame) s.recipe_prefs.push('pocafame');
if (s.pref_scadenze) s.recipe_prefs.push('scadenze');
if (s.pref_healthy) s.recipe_prefs.push('salutare');
if (s.pref_comfort) s.recipe_prefs.push('comfort');
if (s.pref_zerowaste) s.recipe_prefs.push('zerowaste');
s.dietary_restrictions = s.dietary || '';
return s;
} catch(e) { return {}; }
}
function saveSettingsToStorage(settings) {
localStorage.setItem('dispensa_settings', JSON.stringify(settings));
}
async function loadSettingsUI() {
const s = getSettings();
document.getElementById('setting-gemini-key').value = s.gemini_key || '';
document.getElementById('setting-bring-email').value = s.bring_email || '';
document.getElementById('setting-bring-password').value = s.bring_password || '';
document.getElementById('setting-default-persons').value = s.default_persons || 1;
document.getElementById('setting-pref-veloce').checked = !!s.pref_veloce;
document.getElementById('setting-pref-pocafame').checked = !!s.pref_pocafame;
document.getElementById('setting-pref-scadenze').checked = !!s.pref_scadenze;
document.getElementById('setting-pref-healthy').checked = !!s.pref_healthy;
document.getElementById('setting-pref-comfort').checked = !!s.pref_comfort;
document.getElementById('setting-pref-zerowaste').checked = !!s.pref_zerowaste;
document.getElementById('setting-dietary').value = s.dietary || '';
renderAppliances(s.appliances || []);
// Load server-side settings if not already set locally
try {
const serverSettings = await api('get_settings');
if (!s.gemini_key && serverSettings.gemini_key) {
document.getElementById('setting-gemini-key').value = serverSettings.gemini_key;
}
if (!s.bring_email && serverSettings.bring_email) {
document.getElementById('setting-bring-email').value = serverSettings.bring_email;
}
} catch(e) { /* ignore */ }
}
function renderAppliances(appliances) {
const container = document.getElementById('appliances-list');
if (!appliances || appliances.length === 0) {
container.innerHTML = '<p style="color:var(--text-muted);font-size:0.85rem;padding:8px 0">Nessun elettrodomestico aggiunto</p>';
return;
}
container.innerHTML = appliances.map((a, i) => `
<div class="appliance-item">
<span>🔌 ${escapeHtml(a)}</span>
<button class="appliance-remove" onclick="removeAppliance(${i})" title="Rimuovi">✕</button>
</div>
`).join('');
}
function addAppliance() {
const input = document.getElementById('new-appliance-input');
const name = (input.value || '').trim();
if (!name) return;
const s = getSettings();
if (!s.appliances) s.appliances = [];
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
showToast('Elettrodomestico già presente', 'error');
return;
}
s.appliances.push(name);
saveSettingsToStorage(s);
renderAppliances(s.appliances);
input.value = '';
showToast('Elettrodomestico aggiunto', 'success');
}
function addApplianceQuick(name) {
const s = getSettings();
if (!s.appliances) s.appliances = [];
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
showToast('Già presente', 'error');
return;
}
s.appliances.push(name);
saveSettingsToStorage(s);
renderAppliances(s.appliances);
showToast(`${name} aggiunto`, 'success');
}
function removeAppliance(idx) {
const s = getSettings();
if (!s.appliances) return;
s.appliances.splice(idx, 1);
saveSettingsToStorage(s);
renderAppliances(s.appliances);
}
async function saveSettings() {
const s = getSettings();
s.gemini_key = document.getElementById('setting-gemini-key').value.trim();
s.bring_email = document.getElementById('setting-bring-email').value.trim();
s.bring_password = document.getElementById('setting-bring-password').value.trim();
s.default_persons = parseInt(document.getElementById('setting-default-persons').value) || 1;
s.pref_veloce = document.getElementById('setting-pref-veloce').checked;
s.pref_pocafame = document.getElementById('setting-pref-pocafame').checked;
s.pref_scadenze = document.getElementById('setting-pref-scadenze').checked;
s.pref_healthy = document.getElementById('setting-pref-healthy').checked;
s.pref_comfort = document.getElementById('setting-pref-comfort').checked;
s.pref_zerowaste = document.getElementById('setting-pref-zerowaste').checked;
s.dietary = document.getElementById('setting-dietary').value.trim();
saveSettingsToStorage(s);
// Also save to server .env
try {
const result = await api('save_settings', {}, 'POST', {
gemini_key: s.gemini_key,
bring_email: s.bring_email,
bring_password: s.bring_password
});
const statusEl = document.getElementById('settings-status');
if (result.success) {
statusEl.className = 'settings-status success';
statusEl.textContent = '✅ Configurazione salvata!';
} else {
statusEl.className = 'settings-status error';
statusEl.textContent = '⚠️ Salvato localmente, errore server: ' + (result.error || '');
}
statusEl.style.display = 'block';
setTimeout(() => statusEl.style.display = 'none', 4000);
} catch(e) {
const statusEl = document.getElementById('settings-status');
statusEl.className = 'settings-status success';
statusEl.textContent = '✅ Configurazione salvata localmente';
statusEl.style.display = 'block';
setTimeout(() => statusEl.style.display = 'none', 4000);
}
}
function switchSettingsTab(btn, tabId) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(tabId).classList.add('active');
}
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
input.type = input.type === 'password' ? 'text' : 'password';
}
// ===== API HELPER =====
async function api(action, params = {}, method = 'GET', body = null) {
let url = `${API_BASE}?action=${action}`;
@@ -367,6 +520,7 @@ function showPage(pageId, param = null) {
case 'shopping': loadShoppingList(); break;
case 'log': loadLog(); break;
case 'ai': initAICamera(); break;
case 'settings': loadSettingsUI(); break;
}
// Stop scanner when leaving scan page
@@ -417,13 +571,17 @@ async function loadDashboard() {
else if (days <= 7) { badgeText = `${days} giorni`; badgeClass = 'expiring'; }
else if (days <= 30) { badgeText = `${days}g`; badgeClass = 'expiring-soon'; }
else { const m = Math.round(days/30); badgeText = m <= 1 ? `${days}g` : `~${m} mesi`; badgeClass = 'expiring-later'; }
const qtyDisplay = formatQuantity(item.quantity, item.unit);
return `
<div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item-info">
<span class="alert-item-name">${escapeHtml(item.name)}</span>
${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
</div>
<span class="alert-item-badge ${badgeClass}">${badgeText}</span>
<div class="alert-item-badges">
<span class="alert-item-qty">📦 ${qtyDisplay}</span>
<span class="alert-item-badge ${badgeClass}">${badgeText}</span>
</div>
</div>`;
}).join('');
} else {
@@ -443,11 +601,13 @@ async function loadDashboard() {
else daysText = `Da ${days}g`;
const safety = getExpiredSafety(item, days);
const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : '';
const qtyDisplayExp = formatQuantity(item.quantity, item.unit);
return `
<div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item-info">
<span class="alert-item-name">${locIcon ? locIcon + ' ' : ''}${escapeHtml(item.name)}</span>
${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
<span class="alert-item-qty">📦 ${qtyDisplayExp}</span>
</div>
<div class="alert-item-badges">
<span class="alert-item-badge expired">${daysText}</span>
@@ -589,10 +749,10 @@ function renderInventoryItem(item) {
${item.brand ? `<div class="inv-brand">${escapeHtml(item.brand)}</div>` : ''}
<div class="inv-meta">
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
<span class="inv-badge badge-qty">${qtyDisplay}</span>
${expiryBadge}
</div>
</div>
<span class="inv-qty-prominent">${qtyDisplay}</span>
</div>`;
}
@@ -744,7 +904,7 @@ function editInventoryItem(id) {
<h3>Modifica ${escapeHtml(item.name)}</h3>
<button class="modal-close" onclick="closeModal()">✕</button>
</div>
<form class="form" onsubmit="submitEditInventory(event, ${id})">
<form class="form" onsubmit="submitEditInventory(event, ${id}, ${item.product_id})">
<div class="form-group">
<label>📦 Quantità</label>
<div class="qty-control">
@@ -753,6 +913,12 @@ function editInventoryItem(id) {
<button type="button" class="qty-btn" onclick="adjustQty('edit-qty', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>📏 Unità di misura</label>
<select id="edit-unit" class="form-input">
${['pz','g','kg','ml','l','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'kg' ? 'kg (chilogrammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'l' ? 'L (litri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label>📍 Posizione</label>
<div class="location-selector">
@@ -773,13 +939,14 @@ function editInventoryItem(id) {
document.getElementById('modal-overlay').style.display = 'flex';
}
async function submitEditInventory(e, id) {
async function submitEditInventory(e, id, productId) {
e.preventDefault();
const qty = parseFloat(document.getElementById('edit-qty').value);
const loc = document.getElementById('edit-loc').value;
const expiry = document.getElementById('edit-expiry').value || null;
const unit = document.getElementById('edit-unit').value;
await api('inventory_update', {}, 'POST', { id, quantity: qty, location: loc, expiry_date: expiry });
await api('inventory_update', {}, 'POST', { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId });
closeModal();
showToast('Aggiornato!', 'success');
refreshCurrentPage();
@@ -1393,9 +1560,6 @@ function showProductAction() {
// Ingredients (collapsible)
let ingredientsHtml = '';
if (currentProduct.ingredients) {
const ingredShort = currentProduct.ingredients.length > 120
? currentProduct.ingredients.substring(0, 120) + '...'
: currentProduct.ingredients;
ingredientsHtml = `
<details class="product-ingredients">
<summary>📋 Ingredienti</summary>
@@ -1410,6 +1574,7 @@ function showProductAction() {
conservationHtml = `<div class="product-conservation">🧊 ${escapeHtml(currentProduct.conservation)}</div>`;
}
// LARGER product preview
document.getElementById('action-product-preview').innerHTML = `
${currentProduct.image_url ?
`<img src="${escapeHtml(currentProduct.image_url)}" alt="">` :
@@ -1418,6 +1583,7 @@ function showProductAction() {
<div class="product-preview-info">
<h3>${escapeHtml(currentProduct.name)}</h3>
<p>${currentProduct.brand ? `<strong>${escapeHtml(currentProduct.brand)}</strong>` : ''}</p>
${currentProduct.weight_info ? `<p style="font-size:0.85rem;color:var(--text-light)">⚖️ ${escapeHtml(currentProduct.weight_info)}</p>` : ''}
${currentProduct.barcode ? `<p style="font-size:0.75rem;color:var(--text-muted)">📊 ${currentProduct.barcode}</p>` : ''}
</div>
`;
@@ -1467,7 +1633,6 @@ function showProductAction() {
</div>
`;
editInfoEl.style.display = 'block';
// Focus name field if unknown
if (isUnknown) {
setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100);
}
@@ -1482,8 +1647,7 @@ function showProductAction() {
const container = document.getElementById('action-product-preview').parentElement;
extraInfoEl = document.createElement('div');
extraInfoEl.id = 'action-product-details';
// Insert after preview, before action buttons
const actionBtns = document.querySelector('#page-action .action-buttons');
const actionBtns = document.getElementById('action-buttons-container');
actionBtns.parentElement.insertBefore(extraInfoEl, actionBtns);
}
@@ -1502,9 +1666,205 @@ function showProductAction() {
extraInfoEl.innerHTML = '';
}
// === CHECK INVENTORY FOR THIS PRODUCT ===
checkInventoryForProduct(currentProduct.id).then(inventoryItems => {
const statusBar = document.getElementById('action-inventory-status');
const btnsContainer = document.getElementById('action-buttons-container');
if (inventoryItems.length > 0) {
// Product IS in inventory - show status and 3 buttons
statusBar.style.display = 'block';
let totalQty = 0;
const unit = inventoryItems[0].unit || 'pz';
const invHtml = inventoryItems.map(inv => {
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
const qtyStr = formatQuantity(inv.quantity, inv.unit);
totalQty += parseFloat(inv.quantity);
let expiryStr = '';
if (inv.expiry_date) {
const d = daysUntilExpiry(inv.expiry_date);
if (d < 0) expiryStr = ` · ⚠️ Scaduto da ${Math.abs(d)}g`;
else if (d <= 3) expiryStr = ` · 🔴 Scade tra ${d}g`;
else if (d <= 7) expiryStr = ` · 🟡 Scade tra ${d}g`;
else expiryStr = ` · 📅 ${formatDate(inv.expiry_date)}`;
}
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}</span></div>`;
}).join('');
const totalStr = formatQuantity(totalQty, unit);
statusBar.innerHTML = `
<div class="inv-status-header">
<span class="inv-status-title">📦 Ce l'hai già!</span>
<span class="inv-status-total">${totalStr}</span>
</div>
<div class="inv-status-items">${invHtml}</div>
`;
btnsContainer.className = 'action-buttons-3col';
btnsContainer.innerHTML = `
<button class="btn btn-huge btn-success" onclick="showAddForm()">
<span class="btn-icon">📥</span>
<span class="btn-text">AGGIUNGI<br><small>altra quantità</small></span>
</button>
<button class="btn btn-huge btn-danger" onclick="showUseForm()">
<span class="btn-icon">📤</span>
<span class="btn-text">USA<br><small>quanto ne hai usato</small></span>
</button>
<button class="btn btn-huge btn-throw" onclick="showThrowForm()">
<span class="btn-icon">🗑️</span>
<span class="btn-text">BUTTA<br><small>butta il prodotto</small></span>
</button>
`;
} else {
// Product NOT in inventory - show only AGGIUNGI
statusBar.style.display = 'none';
btnsContainer.className = 'action-buttons';
btnsContainer.innerHTML = `
<button class="btn btn-huge btn-success" onclick="showAddForm()" style="flex:1">
<span class="btn-icon">📥</span>
<span class="btn-text">AGGIUNGI<br><small>in dispensa/frigo</small></span>
</button>
`;
}
});
showPage('action');
}
// Check if product exists in inventory
async function checkInventoryForProduct(productId) {
try {
const data = await api('inventory_list');
return (data.inventory || []).filter(i => i.product_id == productId);
} catch(e) {
return [];
}
}
// === THROW AWAY FORM ===
function showThrowForm() {
// Open a modal to ask how much to throw away
api('inventory_list').then(data => {
const items = (data.inventory || []).filter(i => i.product_id == currentProduct.id);
if (items.length === 0) {
showToast('Prodotto non nell\'inventario', 'error');
return;
}
const totalQty = items.reduce((sum, i) => sum + parseFloat(i.quantity), 0);
const unit = items[0].unit || 'pz';
const qtyDisplay = formatQuantity(totalQty, unit);
let locOptionsHtml = items.map(inv => {
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}</span><span class="inv-status-qty">${formatQuantity(inv.quantity, inv.unit)}</span></div>`;
}).join('');
document.getElementById('modal-content').innerHTML = `
<div class="modal-header">
<h3>🗑️ Butta Prodotto</h3>
<button class="modal-close" onclick="closeModal()">✕</button>
</div>
<div class="product-preview-small" style="margin-bottom:12px">
${currentProduct.image_url ?
`<img src="${escapeHtml(currentProduct.image_url)}" alt="" style="width:50px;height:50px;border-radius:10px;object-fit:cover">` :
`<span style="font-size:2rem">${CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || '📦'}</span>`
}
<div class="product-preview-info">
<h3>${escapeHtml(currentProduct.name)}</h3>
<p>Disponibile: <strong>${qtyDisplay}</strong></p>
</div>
</div>
<div class="inventory-status-bar" style="margin-bottom:16px">
<div class="inv-status-items">${locOptionsHtml}</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<button class="btn btn-large btn-danger full-width" onclick="throwAll()">
🗑️ Butta TUTTO (${qtyDisplay})
</button>
<div style="text-align:center;color:var(--text-muted);font-size:0.85rem">oppure specifica la quantità:</div>
<div class="form-group">
<label>📍 Da dove?</label>
<div class="location-selector" id="throw-location-selector">
${items.map((inv, idx) => {
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
return `<button type="button" class="loc-btn ${idx === 0 ? 'active' : ''}" onclick="selectThrowLocation(this, '${inv.location}')">${locInfo.icon} ${locInfo.label} (${formatQuantity(inv.quantity, inv.unit)})</button>`;
}).join('')}
</div>
<input type="hidden" id="throw-location" value="${items[0].location}">
</div>
<div class="form-group">
<label>Quanto butti?</label>
<div class="qty-control">
<button type="button" class="qty-btn" onclick="adjustQty('throw-quantity', -1)"></button>
<input type="number" id="throw-quantity" value="1" min="0.1" step="any" class="qty-input">
<button type="button" class="qty-btn" onclick="adjustQty('throw-quantity', 1)">+</button>
</div>
</div>
<button class="btn btn-large btn-warning full-width" onclick="throwPartial()">
🗑 Butta questa quantità
</button>
</div>
`;
document.getElementById('modal-overlay').style.display = 'flex';
});
}
function selectThrowLocation(btn, loc) {
btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('throw-location').value = loc;
}
async function throwAll() {
closeModal();
showLoading(true);
try {
const result = await api('inventory_use', {}, 'POST', {
product_id: currentProduct.id,
use_all: true,
location: '__all__',
notes: 'Buttato'
});
showLoading(false);
if (result.success) {
showToast(`🗑 ${currentProduct.name} buttato!`, 'success');
showPage('dashboard');
} else {
showToast(result.error || 'Errore', 'error');
}
} catch(e) {
showLoading(false);
showToast('Errore di connessione', 'error');
}
}
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', {
product_id: currentProduct.id,
quantity: qty,
location: loc,
notes: 'Buttato'
});
showLoading(false);
if (result.success) {
showToast(`🗑 Buttato ${qty} ${currentProduct.unit || 'pz'} di ${currentProduct.name}`, 'success');
showPage('dashboard');
} else {
showToast(result.error || 'Errore', 'error');
}
} catch(e) {
showLoading(false);
showToast('Errore di connessione', 'error');
}
}
async function saveEditedProductInfo() {
const name = (document.getElementById('edit-action-name')?.value || '').trim();
if (!name) {
@@ -2727,6 +3087,7 @@ const MEAL_LABELS = {
function openRecipeDialog() {
const meal = getMealType();
const settings = getSettings();
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
document.getElementById('recipe-overlay').style.display = 'flex';
@@ -2742,13 +3103,33 @@ function openRecipeDialog() {
}
} catch (e) { /* ignore parse errors */ }
// No valid cache — show ask form
document.getElementById('recipe-persons').value = 1;
// Pre-fill persons from settings
document.getElementById('recipe-persons').value = settings.default_persons || 1;
// Pre-select option chips from settings
const prefMap = {
'veloce': 'recipe-opt-veloce',
'pocafame': 'recipe-opt-pocafame',
'scadenze': 'recipe-opt-scadenze',
'salutare': 'recipe-opt-healthy',
'comfort': 'recipe-opt-comfort',
'zerowaste': 'recipe-opt-zerowaste'
};
Object.entries(prefMap).forEach(([key, id]) => {
const cb = document.getElementById(id);
if (cb) cb.checked = settings.recipe_prefs && settings.recipe_prefs.includes(key);
});
document.getElementById('recipe-ask').style.display = '';
document.getElementById('recipe-loading').style.display = 'none';
document.getElementById('recipe-result').style.display = 'none';
}
// Toggle recipe option chip
function toggleRecipeOption(btn) {
btn.classList.toggle('active');
}
function closeRecipeDialog() {
document.getElementById('recipe-overlay').style.display = 'none';
}
@@ -2891,13 +3272,35 @@ function regenerateRecipe() {
async function generateRecipe() {
const meal = getMealType();
const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
const settings = getSettings();
// Gather active options from checkboxes
const options = [];
const optMap = {
'recipe-opt-veloce': 'veloce',
'recipe-opt-pocafame': 'pocafame',
'recipe-opt-scadenze': 'scadenze',
'recipe-opt-healthy': 'salutare',
'recipe-opt-comfort': 'comfort',
'recipe-opt-zerowaste': 'zerowaste'
};
Object.entries(optMap).forEach(([id, key]) => {
const cb = document.getElementById(id);
if (cb && cb.checked) options.push(key);
});
document.getElementById('recipe-ask').style.display = 'none';
document.getElementById('recipe-loading').style.display = '';
document.getElementById('recipe-result').style.display = 'none';
try {
const result = await api('generate_recipe', {}, 'POST', { meal, persons });
const result = await api('generate_recipe', {}, 'POST', {
meal,
persons,
options,
appliances: settings.appliances || [],
dietary_restrictions: settings.dietary_restrictions || ''
});
if (!result.success) {
document.getElementById('recipe-loading').style.display = 'none';