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
+147 -4
View File
@@ -109,6 +109,14 @@ try {
bringSuggestItems($db);
break;
case 'save_settings':
saveSettings();
break;
case 'get_settings':
getServerSettings();
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown action: ' . $action]);
@@ -421,6 +429,7 @@ function useFromInventory(PDO $db): void {
$quantity = $input['quantity'] ?? 0;
$useAll = $input['use_all'] ?? false;
$location = $input['location'] ?? 'dispensa';
$notes = $input['notes'] ?? '';
if (!$productId) {
http_response_code(400);
@@ -428,6 +437,24 @@ function useFromInventory(PDO $db): void {
return;
}
// Handle "throw all from all locations"
if ($useAll && $location === '__all__') {
$stmt = $db->prepare("SELECT id, quantity, location FROM inventory WHERE product_id = ? AND quantity > 0");
$stmt->execute([$productId]);
$allItems = $stmt->fetchAll();
$totalRemoved = 0;
foreach ($allItems as $item) {
$totalRemoved += $item['quantity'];
$stmt = $db->prepare("DELETE FROM inventory WHERE id = ?");
$stmt->execute([$item['id']]);
$type = ($notes === 'Buttato') ? 'waste' : 'out';
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $type, $item['quantity'], $item['location'], $notes]);
}
echo json_encode(['success' => true, 'remaining' => 0, 'removed' => $totalRemoved]);
return;
}
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ? AND quantity > 0");
$stmt->execute([$productId, $location]);
$existing = $stmt->fetch();
@@ -453,8 +480,9 @@ function useFromInventory(PDO $db): void {
}
// Log transaction
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location) VALUES (?, 'out', ?, ?)");
$stmt->execute([$productId, $quantity, $location]);
$type = ($notes === 'Buttato') ? 'waste' : 'out';
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $type, $quantity, $location, $notes]);
// Auto-add to Bring! if product is completely finished (no inventory left anywhere)
$addedToBring = false;
@@ -514,6 +542,13 @@ function updateInventory(PDO $db): void {
$stmt = $db->prepare("UPDATE inventory SET " . implode(', ', $fields) . " WHERE id = ?");
$stmt->execute($params);
// Update unit on the product if provided
if (isset($input['unit']) && isset($input['product_id'])) {
$stmt = $db->prepare("UPDATE products SET unit = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->execute([$input['unit'], $input['product_id']]);
}
echo json_encode(['success' => true]);
}
@@ -572,7 +607,7 @@ function getStats(PDO $db): void {
// Expiring soonest (next 4 items to expire)
$expiring = $db->query("
SELECT i.*, p.name, p.brand, p.category
SELECT i.*, p.name, p.brand, p.category, p.unit
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
ORDER BY i.expiry_date ASC
@@ -581,7 +616,7 @@ function getStats(PDO $db): void {
// Expired
$expired = $db->query("
SELECT i.*, p.name, p.brand, p.category
SELECT i.*, p.name, p.brand, p.category, p.unit
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
ORDER BY i.expiry_date ASC
@@ -598,6 +633,76 @@ function getStats(PDO $db): void {
]);
}
// ===== SETTINGS =====
function getServerSettings(): void {
$envFile = __DIR__ . '/../.env';
$envVars = [];
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES);
foreach ($lines as $line) {
if (strpos($line, '#') === 0 || strpos($line, '=') === false) continue;
list($key, $val) = explode('=', $line, 2);
$envVars[trim($key)] = trim($val);
}
}
// Return masked versions for security
$geminiKey = $envVars['GEMINI_API_KEY'] ?? '';
$bringEmail = $envVars['BRING_EMAIL'] ?? '';
$bringPassword = $envVars['BRING_PASSWORD'] ?? '';
echo json_encode([
'gemini_key' => $geminiKey,
'gemini_key_set' => !empty($geminiKey),
'bring_email' => $bringEmail,
'bring_password_set' => !empty($bringPassword)
]);
}
function saveSettings(): void {
$input = json_decode(file_get_contents('php://input'), true);
$envFile = __DIR__ . '/../.env';
// Read existing .env content
$envContent = '';
$envVars = [];
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES);
foreach ($lines as $line) {
if (strpos($line, '#') === 0 || strpos($line, '=') === false) {
continue;
}
list($key, $val) = explode('=', $line, 2);
$envVars[trim($key)] = trim($val);
}
}
// Update values from input
if (isset($input['gemini_key'])) {
$envVars['GEMINI_API_KEY'] = $input['gemini_key'];
}
if (isset($input['bring_email'])) {
$envVars['BRING_EMAIL'] = $input['bring_email'];
}
if (isset($input['bring_password'])) {
$envVars['BRING_PASSWORD'] = $input['bring_password'];
}
// Write .env file
$lines = [];
foreach ($envVars as $key => $val) {
$lines[] = "{$key}={$val}";
}
$result = file_put_contents($envFile, implode("\n", $lines) . "\n");
if ($result !== false) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Could not write .env file']);
}
}
// ===== GEMINI AI FUNCTIONS =====
function geminiReadExpiry(): void {
@@ -728,6 +833,9 @@ function generateRecipe(PDO $db): void {
$input = json_decode(file_get_contents('php://input'), true);
$mealType = $input['meal'] ?? 'pranzo';
$persons = max(1, intval($input['persons'] ?? 1));
$options = $input['options'] ?? [];
$appliances = $input['appliances'] ?? [];
$dietaryRestrictions = $input['dietary_restrictions'] ?? '';
// Fetch all inventory items with expiry info
$stmt = $db->query("
@@ -774,8 +882,42 @@ function generateRecipe(PDO $db): void {
];
$mealLabel = $mealLabels[$mealType] ?? $mealType;
// Build extra rules from options
$extraRules = [];
$optionLabels = [
'veloce' => 'La ricetta deve essere VELOCE: massimo 15-20 minuti totali di preparazione e cottura.',
'pocafame' => 'L\'utente ha POCA FAME: proponi una porzione leggera, magari uno snack, un\'insalata o qualcosa di semplice e poco abbondante.',
'scadenze' => 'PRIORITÀ SCADENZE: usa ASSOLUTAMENTE per primi gli ingredienti più vicini alla scadenza o già scaduti (se ancora commestibili).',
'salutare' => 'Ricetta EXTRA SALUTARE: prediligi ingredienti integrali, tante verdure, pochi grassi, cotture leggere.',
'comfort' => 'Ricetta COMFORT FOOD: qualcosa di appagante, gustoso e che dia soddisfazione.',
'zerowaste' => 'ZERO SPRECHI: cerca di usare quanti più ingredienti in scadenza possibile, combina anche ingredienti insoliti pur di non sprecare nulla.'
];
foreach ($options as $opt) {
if (isset($optionLabels[$opt])) {
$extraRules[] = $optionLabels[$opt];
}
}
$extraRulesText = '';
if (!empty($extraRules)) {
$extraRulesText = "\n\nPREFERENZE DELL'UTENTE:\n" . implode("\n", $extraRules);
}
// Appliances
$appliancesText = '';
if (!empty($appliances)) {
$appliancesText = "\n\nELETTRODOMESTICI DISPONIBILI:\nL'utente dispone di: " . implode(', ', $appliances) . ".\nPuoi usare SOLO questi elettrodomestici (più fornelli e forno che si presumono sempre disponibili). Non suggerire ricette che richiedano elettrodomestici non elencati.";
}
// Dietary restrictions
$dietaryText = '';
if (!empty($dietaryRestrictions)) {
$dietaryText = "\n\nRESTRIZIONI ALIMENTARI:\n{$dietaryRestrictions}\nRispetta SEMPRE queste restrizioni.";
}
$prompt = <<<PROMPT
Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente.
{$extraRulesText}{$appliancesText}{$dietaryText}
REGOLE IMPORTANTI:
1. PRIORITÀ ASSOLUTA: usa prima gli ingredienti in scadenza o già scaduti (se ancora utilizzabili)
@@ -785,6 +927,7 @@ REGOLE IMPORTANTI:
5. Se non ci sono abbastanza ingredienti per una ricetta completa, suggerisci la migliore combinazione possibile
6. La ricetta deve essere adatta al pasto: $mealLabel
7. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2 kg" e servono 300g, qty_number = 0.3. Per ingredienti non dalla dispensa, qty_number = 0.
8. GESTIONE SMART QUANTITÀ: NON lasciare rimasugli poco usabili in dispensa. Se un ingrediente ha una quantità piccola (es. 50g di formaggio, 1 uovo, 100ml di latte), preferisci usarlo TUTTO piuttosto che lasciarne una quantità inutilizzabile. Se invece la quantità è abbondante, usa solo il necessario lasciando abbastanza per un altro pasto. Pensa sempre: "quello che resta sarà sufficiente per un altro utilizzo?"
INGREDIENTI DISPONIBILI IN DISPENSA:
$ingredientsText
+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';
BIN
View File
Binary file not shown.
+123 -2
View File
@@ -131,8 +131,9 @@
<button class="back-btn" onclick="showPage('scan')">← Indietro</button>
<h2>Cosa vuoi fare?</h2>
</div>
<div class="product-preview" id="action-product-preview"></div>
<div class="action-buttons">
<div class="product-preview product-preview-large" id="action-product-preview"></div>
<div class="inventory-status-bar" id="action-inventory-status" style="display:none"></div>
<div class="action-buttons" id="action-buttons-container">
<button class="btn btn-huge btn-success" onclick="showAddForm()">
<span class="btn-icon">📥</span>
<span class="btn-text">AGGIUNGI<br><small>in dispensa/frigo</small></span>
@@ -484,6 +485,111 @@
</button>
</section>
<!-- ===== SETTINGS PAGE ===== -->
<section class="page" id="page-settings">
<div class="page-header">
<button class="back-btn" onclick="showPage('dashboard')">← Indietro</button>
<h2>⚙️ Configurazione</h2>
</div>
<div class="settings-tabs">
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api">🔑 API</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-bring')" data-tab="tab-bring">🛒 Bring!</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-recipe')" data-tab="tab-recipe">🍳 Ricette</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances">🔌 Elettrodomestici</button>
</div>
<div class="settings-panels">
<!-- API Keys Tab -->
<div class="settings-panel active" id="tab-api">
<div class="settings-card">
<h4>🤖 Google Gemini AI</h4>
<p class="settings-hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
<div class="form-group">
<label>API Key Gemini</label>
<input type="password" id="setting-gemini-key" class="form-input" placeholder="AIza...">
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-gemini-key')">👁️ Mostra/Nascondi</button>
</div>
</div>
</div>
<!-- Bring! Tab -->
<div class="settings-panel" id="tab-bring">
<div class="settings-card">
<h4>🛒 Bring! Shopping List</h4>
<p class="settings-hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
<div class="form-group">
<label>📧 Email Bring!</label>
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
</div>
<div class="form-group">
<label>🔒 Password Bring!</label>
<input type="password" id="setting-bring-password" class="form-input" placeholder="Password">
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')">👁️ Mostra/Nascondi</button>
</div>
</div>
</div>
<!-- Recipe Tab -->
<div class="settings-panel" id="tab-recipe">
<div class="settings-card">
<h4>🍳 Preferenze Ricette</h4>
<p class="settings-hint">Configura le opzioni predefinite per la generazione delle ricette.</p>
<div class="form-group">
<label>👥 Persone predefinite</label>
<div class="qty-control">
<button type="button" class="qty-btn" onclick="adjustQty('setting-default-persons', -1)"></button>
<input type="number" id="setting-default-persons" value="1" min="1" max="20" class="qty-input">
<button type="button" class="qty-btn" onclick="adjustQty('setting-default-persons', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>🎯 Opzioni ricetta predefinite</label>
<div class="recipe-pref-checks">
<label class="checkbox-label"><input type="checkbox" id="setting-pref-veloce"> ⚡ Pasto Veloce</label>
<label class="checkbox-label"><input type="checkbox" id="setting-pref-pocafame"> 🥗 Poca Fame</label>
<label class="checkbox-label"><input type="checkbox" id="setting-pref-scadenze"> ⏰ Priorità Scadenze</label>
<label class="checkbox-label"><input type="checkbox" id="setting-pref-healthy"> 💚 Extra Salutare</label>
<label class="checkbox-label"><input type="checkbox" id="setting-pref-comfort"> 🍲 Comfort Food</label>
<label class="checkbox-label"><input type="checkbox" id="setting-pref-zerowaste"> ♻️ Zero Sprechi</label>
</div>
</div>
<div class="form-group">
<label>🚫 Intolleranze / Restrizioni</label>
<textarea id="setting-dietary" class="form-input" rows="2" placeholder="Es: senza glutine, senza lattosio, vegetariano..."></textarea>
</div>
</div>
</div>
<!-- Appliances Tab -->
<div class="settings-panel" id="tab-appliances">
<div class="settings-card">
<h4>🔌 Elettrodomestici Disponibili</h4>
<p class="settings-hint">Indica gli elettrodomestici che hai a disposizione. Saranno considerati nella generazione delle ricette.</p>
<div class="appliances-list" id="appliances-list"></div>
<div class="form-group mt-2">
<div class="barcode-input-row">
<input type="text" id="new-appliance-input" class="form-input" placeholder="Es: Macchina del pane, Bimby, Friggitrice ad aria..." onkeydown="if(event.key==='Enter'){event.preventDefault();addAppliance()}">
<button class="btn btn-accent" onclick="addAppliance()"></button>
</div>
</div>
<div class="common-appliances mt-2">
<p class="settings-hint">Aggiungi velocemente:</p>
<div class="appliance-quick-tags">
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Forno')">🔥 Forno</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Microonde')">📡 Microonde</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Friggitrice ad aria')">🍟 Friggitrice ad aria</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Macchina del pane')">🍞 Macchina pane</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Bimby/Moulinex Cookeo')">🤖 Bimby/Cookeo</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Planetaria')">🌀 Planetaria</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Vaporiera')">♨️ Vaporiera</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Pentola a pressione')">🫕 Pentola pressione</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Tostapane')">🍞 Tostapane</button>
<button class="btn btn-small btn-secondary" onclick="addApplianceQuick('Frullatore/Mixer')">🍹 Frullatore</button>
</div>
</div>
</div>
</div>
</div>
<button class="btn btn-large btn-success full-width mt-2" onclick="saveSettings()">💾 Salva Configurazione</button>
<div id="settings-status" class="settings-status" style="display:none"></div>
</section>
</main>
<!-- Bottom Navigation -->
@@ -508,6 +614,10 @@
<span class="nav-icon">📒</span>
<span class="nav-label">Log</span>
</button>
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
<span class="nav-icon">⚙️</span>
<span class="nav-label">Config</span>
</button>
</nav>
<!-- Recipe Dialog -->
@@ -524,6 +634,17 @@
<button type="button" class="qty-btn" onclick="adjustRecipePersons(1)">+</button>
</div>
</div>
<div class="form-group" style="text-align:left">
<label>🎯 Tipo di pasto</label>
<div class="recipe-options-grid">
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> ⚡ Pasto Veloce</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> 🥗 Poca Fame</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> ⏰ Priorità Scadenze</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-healthy"> 💚 Extra Salutare</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-comfort"> 🍲 Comfort Food</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-zerowaste"> ♻️ Zero Sprechi</label>
</div>
</div>
<button class="btn btn-large btn-success full-width" onclick="generateRecipe()">
✨ Genera Ricetta
</button>