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); bringSuggestItems($db);
break; break;
case 'save_settings':
saveSettings();
break;
case 'get_settings':
getServerSettings();
break;
default: default:
http_response_code(404); http_response_code(404);
echo json_encode(['error' => 'Unknown action: ' . $action]); echo json_encode(['error' => 'Unknown action: ' . $action]);
@@ -421,6 +429,7 @@ function useFromInventory(PDO $db): void {
$quantity = $input['quantity'] ?? 0; $quantity = $input['quantity'] ?? 0;
$useAll = $input['use_all'] ?? false; $useAll = $input['use_all'] ?? false;
$location = $input['location'] ?? 'dispensa'; $location = $input['location'] ?? 'dispensa';
$notes = $input['notes'] ?? '';
if (!$productId) { if (!$productId) {
http_response_code(400); http_response_code(400);
@@ -428,6 +437,24 @@ function useFromInventory(PDO $db): void {
return; 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 = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ? AND quantity > 0");
$stmt->execute([$productId, $location]); $stmt->execute([$productId, $location]);
$existing = $stmt->fetch(); $existing = $stmt->fetch();
@@ -453,8 +480,9 @@ function useFromInventory(PDO $db): void {
} }
// Log transaction // Log transaction
$stmt = $db->prepare("INSERT INTO transactions (product_id, type, quantity, location) VALUES (?, 'out', ?, ?)"); $type = ($notes === 'Buttato') ? 'waste' : 'out';
$stmt->execute([$productId, $quantity, $location]); $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) // Auto-add to Bring! if product is completely finished (no inventory left anywhere)
$addedToBring = false; $addedToBring = false;
@@ -514,6 +542,13 @@ function updateInventory(PDO $db): void {
$stmt = $db->prepare("UPDATE inventory SET " . implode(', ', $fields) . " WHERE id = ?"); $stmt = $db->prepare("UPDATE inventory SET " . implode(', ', $fields) . " WHERE id = ?");
$stmt->execute($params); $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]); echo json_encode(['success' => true]);
} }
@@ -572,7 +607,7 @@ function getStats(PDO $db): void {
// Expiring soonest (next 4 items to expire) // Expiring soonest (next 4 items to expire)
$expiring = $db->query(" $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 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 WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
ORDER BY i.expiry_date ASC ORDER BY i.expiry_date ASC
@@ -581,7 +616,7 @@ function getStats(PDO $db): void {
// Expired // Expired
$expired = $db->query(" $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 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') WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
ORDER BY i.expiry_date ASC 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 ===== // ===== GEMINI AI FUNCTIONS =====
function geminiReadExpiry(): void { function geminiReadExpiry(): void {
@@ -728,6 +833,9 @@ function generateRecipe(PDO $db): void {
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$mealType = $input['meal'] ?? 'pranzo'; $mealType = $input['meal'] ?? 'pranzo';
$persons = max(1, intval($input['persons'] ?? 1)); $persons = max(1, intval($input['persons'] ?? 1));
$options = $input['options'] ?? [];
$appliances = $input['appliances'] ?? [];
$dietaryRestrictions = $input['dietary_restrictions'] ?? '';
// Fetch all inventory items with expiry info // Fetch all inventory items with expiry info
$stmt = $db->query(" $stmt = $db->query("
@@ -774,8 +882,42 @@ function generateRecipe(PDO $db): void {
]; ];
$mealLabel = $mealLabels[$mealType] ?? $mealType; $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 $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. 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: REGOLE IMPORTANTI:
1. PRIORITÀ ASSOLUTA: usa prima gli ingredienti in scadenza o già scaduti (se ancora utilizzabili) 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 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 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. 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: INGREDIENTI DISPONIBILI IN DISPENSA:
$ingredientsText $ingredientsText
+330
View File
@@ -2227,3 +2227,333 @@ body {
font-family: monospace; font-family: monospace;
flex-shrink: 0; 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 quaggaRunning = false;
let aiStream = null; 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 ===== // ===== API HELPER =====
async function api(action, params = {}, method = 'GET', body = null) { async function api(action, params = {}, method = 'GET', body = null) {
let url = `${API_BASE}?action=${action}`; let url = `${API_BASE}?action=${action}`;
@@ -367,6 +520,7 @@ function showPage(pageId, param = null) {
case 'shopping': loadShoppingList(); break; case 'shopping': loadShoppingList(); break;
case 'log': loadLog(); break; case 'log': loadLog(); break;
case 'ai': initAICamera(); break; case 'ai': initAICamera(); break;
case 'settings': loadSettingsUI(); break;
} }
// Stop scanner when leaving scan page // 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 <= 7) { badgeText = `${days} giorni`; badgeClass = 'expiring'; }
else if (days <= 30) { badgeText = `${days}g`; badgeClass = 'expiring-soon'; } 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'; } 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 ` return `
<div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})"> <div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item-info"> <div class="alert-item-info">
<span class="alert-item-name">${escapeHtml(item.name)}</span> <span class="alert-item-name">${escapeHtml(item.name)}</span>
${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''} ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
</div> </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>`; </div>`;
}).join(''); }).join('');
} else { } else {
@@ -443,11 +601,13 @@ async function loadDashboard() {
else daysText = `Da ${days}g`; else daysText = `Da ${days}g`;
const safety = getExpiredSafety(item, days); const safety = getExpiredSafety(item, days);
const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : ''; const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : '';
const qtyDisplayExp = formatQuantity(item.quantity, item.unit);
return ` return `
<div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})"> <div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item-info"> <div class="alert-item-info">
<span class="alert-item-name">${locIcon ? locIcon + ' ' : ''}${escapeHtml(item.name)}</span> <span class="alert-item-name">${locIcon ? locIcon + ' ' : ''}${escapeHtml(item.name)}</span>
${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''} ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
<span class="alert-item-qty">📦 ${qtyDisplayExp}</span>
</div> </div>
<div class="alert-item-badges"> <div class="alert-item-badges">
<span class="alert-item-badge expired">${daysText}</span> <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>` : ''} ${item.brand ? `<div class="inv-brand">${escapeHtml(item.brand)}</div>` : ''}
<div class="inv-meta"> <div class="inv-meta">
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span> <span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
<span class="inv-badge badge-qty">${qtyDisplay}</span>
${expiryBadge} ${expiryBadge}
</div> </div>
</div> </div>
<span class="inv-qty-prominent">${qtyDisplay}</span>
</div>`; </div>`;
} }
@@ -744,7 +904,7 @@ function editInventoryItem(id) {
<h3>Modifica ${escapeHtml(item.name)}</h3> <h3>Modifica ${escapeHtml(item.name)}</h3>
<button class="modal-close" onclick="closeModal()">✕</button> <button class="modal-close" onclick="closeModal()">✕</button>
</div> </div>
<form class="form" onsubmit="submitEditInventory(event, ${id})"> <form class="form" onsubmit="submitEditInventory(event, ${id}, ${item.product_id})">
<div class="form-group"> <div class="form-group">
<label>📦 Quantità</label> <label>📦 Quantità</label>
<div class="qty-control"> <div class="qty-control">
@@ -753,6 +913,12 @@ function editInventoryItem(id) {
<button type="button" class="qty-btn" onclick="adjustQty('edit-qty', 1)">+</button> <button type="button" class="qty-btn" onclick="adjustQty('edit-qty', 1)">+</button>
</div> </div>
</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"> <div class="form-group">
<label>📍 Posizione</label> <label>📍 Posizione</label>
<div class="location-selector"> <div class="location-selector">
@@ -773,13 +939,14 @@ function editInventoryItem(id) {
document.getElementById('modal-overlay').style.display = 'flex'; document.getElementById('modal-overlay').style.display = 'flex';
} }
async function submitEditInventory(e, id) { async function submitEditInventory(e, id, productId) {
e.preventDefault(); e.preventDefault();
const qty = parseFloat(document.getElementById('edit-qty').value); const qty = parseFloat(document.getElementById('edit-qty').value);
const loc = document.getElementById('edit-loc').value; const loc = document.getElementById('edit-loc').value;
const expiry = document.getElementById('edit-expiry').value || null; 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(); closeModal();
showToast('Aggiornato!', 'success'); showToast('Aggiornato!', 'success');
refreshCurrentPage(); refreshCurrentPage();
@@ -1393,9 +1560,6 @@ function showProductAction() {
// Ingredients (collapsible) // Ingredients (collapsible)
let ingredientsHtml = ''; let ingredientsHtml = '';
if (currentProduct.ingredients) { if (currentProduct.ingredients) {
const ingredShort = currentProduct.ingredients.length > 120
? currentProduct.ingredients.substring(0, 120) + '...'
: currentProduct.ingredients;
ingredientsHtml = ` ingredientsHtml = `
<details class="product-ingredients"> <details class="product-ingredients">
<summary>📋 Ingredienti</summary> <summary>📋 Ingredienti</summary>
@@ -1410,6 +1574,7 @@ function showProductAction() {
conservationHtml = `<div class="product-conservation">🧊 ${escapeHtml(currentProduct.conservation)}</div>`; conservationHtml = `<div class="product-conservation">🧊 ${escapeHtml(currentProduct.conservation)}</div>`;
} }
// LARGER product preview
document.getElementById('action-product-preview').innerHTML = ` document.getElementById('action-product-preview').innerHTML = `
${currentProduct.image_url ? ${currentProduct.image_url ?
`<img src="${escapeHtml(currentProduct.image_url)}" alt="">` : `<img src="${escapeHtml(currentProduct.image_url)}" alt="">` :
@@ -1418,6 +1583,7 @@ function showProductAction() {
<div class="product-preview-info"> <div class="product-preview-info">
<h3>${escapeHtml(currentProduct.name)}</h3> <h3>${escapeHtml(currentProduct.name)}</h3>
<p>${currentProduct.brand ? `<strong>${escapeHtml(currentProduct.brand)}</strong>` : ''}</p> <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>` : ''} ${currentProduct.barcode ? `<p style="font-size:0.75rem;color:var(--text-muted)">📊 ${currentProduct.barcode}</p>` : ''}
</div> </div>
`; `;
@@ -1467,7 +1633,6 @@ function showProductAction() {
</div> </div>
`; `;
editInfoEl.style.display = 'block'; editInfoEl.style.display = 'block';
// Focus name field if unknown
if (isUnknown) { if (isUnknown) {
setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100); setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100);
} }
@@ -1482,8 +1647,7 @@ function showProductAction() {
const container = document.getElementById('action-product-preview').parentElement; const container = document.getElementById('action-product-preview').parentElement;
extraInfoEl = document.createElement('div'); extraInfoEl = document.createElement('div');
extraInfoEl.id = 'action-product-details'; extraInfoEl.id = 'action-product-details';
// Insert after preview, before action buttons const actionBtns = document.getElementById('action-buttons-container');
const actionBtns = document.querySelector('#page-action .action-buttons');
actionBtns.parentElement.insertBefore(extraInfoEl, actionBtns); actionBtns.parentElement.insertBefore(extraInfoEl, actionBtns);
} }
@@ -1502,9 +1666,205 @@ function showProductAction() {
extraInfoEl.innerHTML = ''; 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'); 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() { async function saveEditedProductInfo() {
const name = (document.getElementById('edit-action-name')?.value || '').trim(); const name = (document.getElementById('edit-action-name')?.value || '').trim();
if (!name) { if (!name) {
@@ -2727,6 +3087,7 @@ const MEAL_LABELS = {
function openRecipeDialog() { function openRecipeDialog() {
const meal = getMealType(); const meal = getMealType();
const settings = getSettings();
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta'; document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
document.getElementById('recipe-overlay').style.display = 'flex'; document.getElementById('recipe-overlay').style.display = 'flex';
@@ -2742,13 +3103,33 @@ function openRecipeDialog() {
} }
} catch (e) { /* ignore parse errors */ } } catch (e) { /* ignore parse errors */ }
// No valid cache — show ask form // Pre-fill persons from settings
document.getElementById('recipe-persons').value = 1; 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-ask').style.display = '';
document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-loading').style.display = 'none';
document.getElementById('recipe-result').style.display = 'none'; document.getElementById('recipe-result').style.display = 'none';
} }
// Toggle recipe option chip
function toggleRecipeOption(btn) {
btn.classList.toggle('active');
}
function closeRecipeDialog() { function closeRecipeDialog() {
document.getElementById('recipe-overlay').style.display = 'none'; document.getElementById('recipe-overlay').style.display = 'none';
} }
@@ -2891,13 +3272,35 @@ function regenerateRecipe() {
async function generateRecipe() { async function generateRecipe() {
const meal = getMealType(); const meal = getMealType();
const persons = parseInt(document.getElementById('recipe-persons').value) || 1; 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-ask').style.display = 'none';
document.getElementById('recipe-loading').style.display = ''; document.getElementById('recipe-loading').style.display = '';
document.getElementById('recipe-result').style.display = 'none'; document.getElementById('recipe-result').style.display = 'none';
try { 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) { if (!result.success) {
document.getElementById('recipe-loading').style.display = 'none'; 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> <button class="back-btn" onclick="showPage('scan')">← Indietro</button>
<h2>Cosa vuoi fare?</h2> <h2>Cosa vuoi fare?</h2>
</div> </div>
<div class="product-preview" id="action-product-preview"></div> <div class="product-preview product-preview-large" id="action-product-preview"></div>
<div class="action-buttons"> <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()"> <button class="btn btn-huge btn-success" onclick="showAddForm()">
<span class="btn-icon">📥</span> <span class="btn-icon">📥</span>
<span class="btn-text">AGGIUNGI<br><small>in dispensa/frigo</small></span> <span class="btn-text">AGGIUNGI<br><small>in dispensa/frigo</small></span>
@@ -484,6 +485,111 @@
</button> </button>
</section> </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> </main>
<!-- Bottom Navigation --> <!-- Bottom Navigation -->
@@ -508,6 +614,10 @@
<span class="nav-icon">📒</span> <span class="nav-icon">📒</span>
<span class="nav-label">Log</span> <span class="nav-label">Log</span>
</button> </button>
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
<span class="nav-icon">⚙️</span>
<span class="nav-label">Config</span>
</button>
</nav> </nav>
<!-- Recipe Dialog --> <!-- Recipe Dialog -->
@@ -524,6 +634,17 @@
<button type="button" class="qty-btn" onclick="adjustRecipePersons(1)">+</button> <button type="button" class="qty-btn" onclick="adjustRecipePersons(1)">+</button>
</div> </div>
</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()"> <button class="btn btn-large btn-success full-width" onclick="generateRecipe()">
✨ Genera Ricetta ✨ Genera Ricetta
</button> </button>