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:
+147
-4
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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';
|
||||||
|
|||||||
Binary file not shown.
+123
-2
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user