feat: sezioni reparto lista spesa, gradient urgenza, modalità cucina con TTS
- renderShoppingItems: raggruppamento per reparto (sezioni), ordinamento per urgenza+frequenza, sfondo con gradiente colore urgenza - renderSmartShopping: stesso raggruppamento per reparto in tab previsione - Modalità Cucina: overlay fullscreen nero, step per step con navigazione, TTS italiano via Web Speech API, pulsante 'Usa' ingredienti per step - CSS: modal z-index 600 in cooking-mode-active per sovrapposizione corretta
This commit is contained in:
@@ -2120,6 +2120,11 @@ body {
|
|||||||
z-index: 250;
|
z-index: 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Raise modal above cooking overlay when in cooking mode */
|
||||||
|
.cooking-mode-active #modal-overlay {
|
||||||
|
z-index: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-radius: var(--radius) var(--radius) 0 0;
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
@@ -2929,6 +2934,220 @@ body {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== SHOPPING SECTION (REPARTO) HEADERS ===== */
|
||||||
|
.shopping-section-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 2px 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopping-section-divider:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopping-section-divider span.sec-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== COOKING MODE ===== */
|
||||||
|
.cooking-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: #0a0a0a;
|
||||||
|
z-index: 500;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.10);
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
margin: 0 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-close-btn,
|
||||||
|
.cooking-tts-btn {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-close-btn:active,
|
||||||
|
.cooking-tts-btn:active {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 20px;
|
||||||
|
gap: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-step-num {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-step-text {
|
||||||
|
font-size: clamp(1.4rem, 5vw, 2.2rem);
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-step-ings {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-ing-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(255,255,255,0.07);
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-ing-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-use-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #16a34a;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-use-btn:active {
|
||||||
|
background: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-use-btn.btn-used {
|
||||||
|
background: #374151;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-nav-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1.5px solid rgba(255,255,255,0.2);
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-nav-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-next-btn {
|
||||||
|
background: rgba(22, 163, 74, 0.35);
|
||||||
|
border-color: rgba(22, 163, 74, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-next-btn.is-finish {
|
||||||
|
background: rgba(22, 163, 74, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooking-nav-btn:not(:disabled):active {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cooking button in recipe dialog */
|
||||||
|
.btn-cooking {
|
||||||
|
background: linear-gradient(135deg, #1e3a5f, #2d5016);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cooking:hover, .btn-cooking:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== LOG PAGE ===== */
|
/* ===== LOG PAGE ===== */
|
||||||
.log-list {
|
.log-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+325
-118
@@ -75,6 +75,34 @@ const CATEGORY_LOCATION = {
|
|||||||
'cereali': 'dispensa', 'igiene': 'altro', 'pulizia': 'altro', 'altro': 'dispensa'
|
'cereali': 'dispensa', 'igiene': 'altro', 'pulizia': 'altro', 'altro': 'dispensa'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Shopping section (reparto) map — groups categories into grocery departments
|
||||||
|
const SHOPPING_SECTIONS = [
|
||||||
|
{ key: 'frutta_verdura', icon: '🥬', label: 'Frutta & Verdura', cats: new Set(['frutta','verdura']) },
|
||||||
|
{ key: 'carne_pesce', icon: '🥩', label: 'Carne & Pesce', cats: new Set(['carne','pesce']) },
|
||||||
|
{ key: 'latticini', icon: '🥛', label: 'Latticini & Fresco', cats: new Set(['latticini']) },
|
||||||
|
{ key: 'pane_dolci', icon: '🍞', label: 'Pane & Dolci', cats: new Set(['pane','snack','cereali']) },
|
||||||
|
{ key: 'pasta', icon: '🍝', label: 'Pasta & Cereali', cats: new Set(['pasta']) },
|
||||||
|
{ key: 'conserve', icon: '🥫', label: 'Conserve & Salse', cats: new Set(['conserve','condimenti']) },
|
||||||
|
{ key: 'surgelati', icon: '❄️', label: 'Surgelati', cats: new Set(['surgelati']) },
|
||||||
|
{ key: 'bevande', icon: '🥤', label: 'Bevande', cats: new Set(['bevande']) },
|
||||||
|
{ key: 'pulizia_igiene', icon: '🧴', label: 'Pulizia & Igiene', cats: new Set(['igiene','pulizia']) },
|
||||||
|
{ key: 'altro', icon: '📦', label: 'Altro', cats: new Set(['altro']) },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getItemSection(name) {
|
||||||
|
const cat = guessCategoryFromName(name) || 'altro';
|
||||||
|
for (const s of SHOPPING_SECTIONS) { if (s.cats.has(cat)) return s; }
|
||||||
|
return SHOPPING_SECTIONS[SHOPPING_SECTIONS.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const URGENCY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||||
|
const URGENCY_BG = {
|
||||||
|
critical: 'rgba(194,65,12,0.14)',
|
||||||
|
high: 'rgba(234,88,12,0.09)',
|
||||||
|
medium: 'rgba(245,158,11,0.07)',
|
||||||
|
low: 'rgba(34,197,94,0.05)',
|
||||||
|
};
|
||||||
|
|
||||||
// Map Open Food Facts categories to local categories
|
// Map Open Food Facts categories to local categories
|
||||||
function mapToLocalCategory(ofCategory, productName) {
|
function mapToLocalCategory(ofCategory, productName) {
|
||||||
if (!ofCategory) {
|
if (!ofCategory) {
|
||||||
@@ -4404,11 +4432,40 @@ function renderSmartShopping() {
|
|||||||
low: { color: '#22c55e', bg: 'rgba(34,197,94,0.08)', icon: '🟢', label: 'Previsione' },
|
low: { color: '#22c55e', bg: 'rgba(34,197,94,0.08)', icon: '🟢', label: 'Previsione' },
|
||||||
};
|
};
|
||||||
|
|
||||||
container.innerHTML = items.map((item, idx) => {
|
// Group by section
|
||||||
const u = urgencyConfig[item.urgency] || urgencyConfig.low;
|
const smartSectionMap = new Map();
|
||||||
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
|
items.forEach(item => {
|
||||||
const checked = !item.on_bring ? 'checked' : '';
|
const sec = getItemSection(item.name);
|
||||||
const globalIdx = smartShoppingItems.indexOf(item);
|
if (!smartSectionMap.has(sec.key)) smartSectionMap.set(sec.key, { sec, items: [] });
|
||||||
|
smartSectionMap.get(sec.key).items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
let smartHtml = '';
|
||||||
|
for (const secDef of SHOPPING_SECTIONS) {
|
||||||
|
const group = smartSectionMap.get(secDef.key);
|
||||||
|
if (!group) continue;
|
||||||
|
smartHtml += `<div class="shopping-section-divider"><span class="sec-icon">${secDef.icon}</span>${secDef.label}</div>`;
|
||||||
|
for (const item of group.items) {
|
||||||
|
smartHtml += renderSmartItem(item, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.innerHTML = smartHtml;
|
||||||
|
|
||||||
|
// Show/hide add button based on checkable items
|
||||||
|
const hasCheckable = items.some(i => !i.on_bring);
|
||||||
|
actionsEl.style.display = hasCheckable ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSmartItem(item) {
|
||||||
|
const urgencyConfig = {
|
||||||
|
critical: { color: '#ef4444', bg: 'rgba(239,68,68,0.08)', icon: '🔴', label: 'Urgente' },
|
||||||
|
high: { color: '#f97316', bg: 'rgba(249,115,22,0.08)', icon: '🟠', label: 'Presto' },
|
||||||
|
medium: { color: '#eab308', bg: 'rgba(234,179,8,0.08)', icon: '🟡', label: 'Pianifica' },
|
||||||
|
low: { color: '#22c55e', bg: 'rgba(34,197,94,0.08)', icon: '🟢', label: 'Previsione' },
|
||||||
|
};
|
||||||
|
const u = urgencyConfig[item.urgency] || urgencyConfig.low;
|
||||||
|
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
|
||||||
|
const globalIdx = smartShoppingItems.indexOf(item);
|
||||||
|
|
||||||
// Stock bar
|
// Stock bar
|
||||||
const pct = Math.min(100, Math.max(0, item.pct_left));
|
const pct = Math.min(100, Math.max(0, item.pct_left));
|
||||||
@@ -4448,7 +4505,7 @@ function renderSmartShopping() {
|
|||||||
return `
|
return `
|
||||||
<div class="smart-item" style="border-left: 3px solid ${u.color}; background: ${u.bg}">
|
<div class="smart-item" style="border-left: 3px solid ${u.color}; background: ${u.bg}">
|
||||||
<div class="smart-item-top">
|
<div class="smart-item-top">
|
||||||
${!item.on_bring ? `<input type="checkbox" class="smart-check" data-idx="${globalIdx}" ${checked}>` : ''}
|
${!item.on_bring ? `<input type="checkbox" class="smart-check" data-idx="${globalIdx}">` : ''}
|
||||||
<span class="smart-item-icon">${catIcon}</span>
|
<span class="smart-item-icon">${catIcon}</span>
|
||||||
<div class="smart-item-info">
|
<div class="smart-item-info">
|
||||||
<div class="smart-item-name">${escapeHtml(item.name)}${item.brand ? ` <small class="smart-brand">${escapeHtml(item.brand)}</small>` : ''}</div>
|
<div class="smart-item-name">${escapeHtml(item.name)}${item.brand ? ` <small class="smart-brand">${escapeHtml(item.brand)}</small>` : ''}</div>
|
||||||
@@ -4466,11 +4523,6 @@ function renderSmartShopping() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Show/hide add button based on checkable items
|
|
||||||
const hasCheckable = items.some(i => !i.on_bring);
|
|
||||||
actionsEl.style.display = hasCheckable ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addSmartToBring() {
|
async function addSmartToBring() {
|
||||||
@@ -4581,8 +4633,11 @@ async function loadShoppingList() {
|
|||||||
renderShoppingItems();
|
renderShoppingItems();
|
||||||
currentEl.style.display = 'block';
|
currentEl.style.display = 'block';
|
||||||
|
|
||||||
// Load smart shopping predictions (auto-add critical after loading)
|
// Load smart shopping predictions, then re-render to show badges + auto-add critical
|
||||||
loadSmartShopping().then(() => autoAddCriticalItems());
|
loadSmartShopping().then(() => {
|
||||||
|
autoAddCriticalItems();
|
||||||
|
renderShoppingItems(); // re-render to apply urgency badges from smart data
|
||||||
|
});
|
||||||
|
|
||||||
// Show tabs once data is ready
|
// Show tabs once data is ready
|
||||||
updateShoppingTabCounts();
|
updateShoppingTabCounts();
|
||||||
@@ -4598,15 +4653,6 @@ async function renderShoppingItems() {
|
|||||||
const container = document.getElementById('shopping-items');
|
const container = document.getElementById('shopping-items');
|
||||||
const countEl = document.getElementById('shopping-count');
|
const countEl = document.getElementById('shopping-count');
|
||||||
|
|
||||||
// Sort shoppingItems in-place by use_count (cross-reference smartShoppingItems), most frequent first
|
|
||||||
shoppingItems.sort((a, b) => {
|
|
||||||
const smartA = smartShoppingItems.find(s => s.name.toLowerCase() === a.name.toLowerCase());
|
|
||||||
const smartB = smartShoppingItems.find(s => s.name.toLowerCase() === b.name.toLowerCase());
|
|
||||||
const freqA = smartA ? smartA.use_count : 0;
|
|
||||||
const freqB = smartB ? smartB.use_count : 0;
|
|
||||||
return freqB - freqA;
|
|
||||||
});
|
|
||||||
|
|
||||||
countEl.textContent = shoppingItems.length;
|
countEl.textContent = shoppingItems.length;
|
||||||
// Update tab count too
|
// Update tab count too
|
||||||
const tabCount = document.getElementById('tab-count-acquisto');
|
const tabCount = document.getElementById('tab-count-acquisto');
|
||||||
@@ -4634,110 +4680,144 @@ async function renderShoppingItems() {
|
|||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = shoppingItems.map((item, idx) => {
|
|
||||||
const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒';
|
|
||||||
const priceKey = item.name.toLowerCase();
|
|
||||||
const priceData = shoppingPrices[priceKey];
|
|
||||||
|
|
||||||
// Cross-reference with smart shopping for urgency + frequency
|
// Build section groups, sorted by urgency weight within each section
|
||||||
const smartData = smartShoppingItems.find(s => s.name.toLowerCase() === item.name.toLowerCase());
|
const TAG_LABELS = { urgente: '🔴 Urgente', prio: '⭐ Priorità', check: '✅ Verificare' };
|
||||||
const localTags = getShoppingTags(item.name);
|
const urgencyMap = {
|
||||||
// Urgency/frequency badges
|
critical: { icon: '🔴', label: 'Urgente', cls: 'badge-critical' },
|
||||||
let urgencyBadge = '';
|
high: { icon: '🟠', label: 'Presto', cls: 'badge-high' },
|
||||||
if (smartData) {
|
medium: { icon: '🟡', label: 'Medio', cls: 'badge-medium' },
|
||||||
const urgencyMap = {
|
low: { icon: '🟢', label: 'Ok', cls: 'badge-low' },
|
||||||
critical: { icon: '🔴', label: 'Urgente', cls: 'badge-critical' },
|
};
|
||||||
high: { icon: '🟠', label: 'Presto', cls: 'badge-high' },
|
|
||||||
medium: { icon: '🟡', label: 'Presto', cls: 'badge-medium' },
|
|
||||||
low: { icon: '🟢', label: 'Ok', cls: 'badge-low' },
|
|
||||||
};
|
|
||||||
const u = urgencyMap[smartData.urgency];
|
|
||||||
if (u) urgencyBadge = `<span class="sinv-badge ${u.cls}">${u.icon} ${u.label}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let freqBadge = '';
|
// Map each item to its section + urgency
|
||||||
if (smartData && smartData.use_count >= 8) freqBadge = `<span class="sinv-badge badge-freq-high">📈 ${smartData.use_count}x</span>`;
|
const enriched = shoppingItems.map((item, idx) => {
|
||||||
else if (smartData && smartData.use_count >= 4) freqBadge = `<span class="sinv-badge badge-freq-med">📊 ${smartData.use_count}x</span>`;
|
const smartData = smartShoppingItems.find(sd => sd.name.toLowerCase() === item.name.toLowerCase());
|
||||||
else if (smartData && smartData.use_count >= 2) freqBadge = `<span class="sinv-badge badge-freq-low">📉 ${smartData.use_count}x</span>`;
|
const urgency = smartData?.urgency || null;
|
||||||
|
const sec = getItemSection(item.name);
|
||||||
|
return { item, idx, smartData, urgency, sec };
|
||||||
|
});
|
||||||
|
|
||||||
// Local tags
|
// Group by section key, preserving SHOPPING_SECTIONS order
|
||||||
const TAG_LABELS = { urgente: '🔴 Urgente', prio: '⭐ Priorità', check: '✅ Verificare' };
|
const sectionMap = new Map();
|
||||||
const localTagHtml = localTags.map(t =>
|
for (const e of enriched) {
|
||||||
`<span class="sinv-badge badge-local-tag" onclick="event.stopPropagation(); toggleShoppingTag(${idx}, '${t}')">${TAG_LABELS[t] || t} ✕</span>`
|
const key = e.sec.key;
|
||||||
).join('');
|
if (!sectionMap.has(key)) sectionMap.set(key, { sec: e.sec, items: [] });
|
||||||
|
sectionMap.get(key).items.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Tag add button
|
// Sort items within each section: by urgency weight desc, then by use_count desc
|
||||||
const tagMenu = `<div class="shopping-tag-menu" onclick="event.stopPropagation()">
|
for (const [, group] of sectionMap) {
|
||||||
${Object.entries(TAG_LABELS).map(([k, v]) =>
|
group.items.sort((a, b) => {
|
||||||
`<button class="sinv-badge badge-tag-add ${localTags.includes(k) ? 'active' : ''}" onclick="toggleShoppingTag(${idx}, '${k}')">${v}</button>`
|
const wa = URGENCY_WEIGHT[a.urgency] || 0;
|
||||||
).join('')}
|
const wb = URGENCY_WEIGHT[b.urgency] || 0;
|
||||||
</div>`;
|
if (wb !== wa) return wb - wa;
|
||||||
|
return (b.smartData?.use_count || 0) - (a.smartData?.use_count || 0);
|
||||||
let detailHtml = '';
|
});
|
||||||
let priceTag = '';
|
}
|
||||||
let spesaBar = '';
|
|
||||||
if (hasSpesa) {
|
// Render sections in canonical order
|
||||||
if (priceData && priceData.loading) {
|
let html = '';
|
||||||
detailHtml = `<div class="spesa-loading">🔍 Cerco...</div>`;
|
for (const secDef of SHOPPING_SECTIONS) {
|
||||||
} else if (priceData && priceData.product) {
|
const group = sectionMap.get(secDef.key);
|
||||||
const p = priceData.product;
|
if (!group) continue;
|
||||||
const promoHtml = p.promo
|
|
||||||
? `<span class="spesa-promo-badge">${escapeHtml(p.promo.label)} -${Math.round(p.promo.discountPerc)}%</span>`
|
html += `<div class="shopping-section-divider"><span class="sec-icon">${secDef.icon}</span>${secDef.label}</div>`;
|
||||||
: '';
|
|
||||||
const est = estimateItemPrice(p, item.specification || priceData.spec || '');
|
for (const { item, idx, smartData, urgency } of group.items) {
|
||||||
if (est) {
|
const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒';
|
||||||
priceTag = `<div class="shopping-item-price">~€${est.estimated.toFixed(2)}</div>`;
|
const priceKey = item.name.toLowerCase();
|
||||||
} else {
|
const priceData = shoppingPrices[priceKey];
|
||||||
priceTag = `<div class="shopping-item-price">€${p.price.toFixed(2)}</div>`;
|
const bgStyle = urgency && URGENCY_BG[urgency] ? ` style="background:${URGENCY_BG[urgency]}"` : '';
|
||||||
}
|
const localTags = getShoppingTags(item.name);
|
||||||
detailHtml = `<div class="spesa-detail-left">
|
|
||||||
<span class="spesa-found-name">${escapeHtml(p.name)}</span>
|
// Urgency badge
|
||||||
<span class="spesa-pkg">${escapeHtml(p.packageDescr)}${est ? ' · ' + escapeHtml(String(p.priceUm || '')) + '/kg' : ''}</span>
|
let urgencyBadge = '';
|
||||||
${promoHtml}
|
if (urgency && urgencyMap[urgency]) {
|
||||||
</div>`;
|
const u = urgencyMap[urgency];
|
||||||
spesaBar = `<div class="spesa-bar">
|
urgencyBadge = `<span class="sinv-badge ${u.cls}">${u.icon} ${u.label}</span>`;
|
||||||
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx}, true)" title="Ricerca">🔄 Ricerca</button>
|
|
||||||
<a href="${escapeHtml(p.url)}" target="_blank" class="spesa-bar-btn" title="${escapeHtml(p.name)} - ${escapeHtml(p.brand)}" onclick="event.stopPropagation()">🔗 Apri</a>
|
|
||||||
</div>`;
|
|
||||||
} else if (priceData && priceData.searched && !priceData.product) {
|
|
||||||
detailHtml = `<div class="spesa-detail-left"><span class="spesa-not-found">Non trovato</span></div>`;
|
|
||||||
spesaBar = `<div class="spesa-bar">
|
|
||||||
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx}, true)" title="Riprova">🔄 Riprova</button>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
spesaBar = `<div class="spesa-bar">
|
|
||||||
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx})" title="Cerca prezzo">🔍 Cerca prezzo</button>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Frequency badge
|
||||||
return `
|
let freqBadge = '';
|
||||||
<div class="shopping-item ${priceData && priceData.product && priceData.product.promo ? 'has-promo' : ''}" id="shop-item-${idx}" onclick="openScanForItem(${idx})" title="Tocca per scansionare">
|
if (smartData && smartData.use_count >= 8) freqBadge = `<span class="sinv-badge badge-freq-high">📈 ${smartData.use_count}x</span>`;
|
||||||
<span class="shopping-item-icon">${catIcon}</span>
|
else if (smartData && smartData.use_count >= 4) freqBadge = `<span class="sinv-badge badge-freq-med">📊 ${smartData.use_count}x</span>`;
|
||||||
<div class="shopping-item-body">
|
else if (smartData && smartData.use_count >= 2) freqBadge = `<span class="sinv-badge badge-freq-low">📉 ${smartData.use_count}x</span>`;
|
||||||
<div class="shopping-item-top">
|
|
||||||
<div class="shopping-item-info">
|
const localTagHtml = localTags.map(t =>
|
||||||
<div class="shopping-item-name-row">
|
`<span class="sinv-badge badge-local-tag" onclick="event.stopPropagation(); toggleShoppingTag(${idx}, '${t}')">${TAG_LABELS[t] || t} ✕</span>`
|
||||||
<span class="shopping-item-name">${escapeHtml(item.name)}</span>
|
).join('');
|
||||||
<span class="shopping-item-scan-hint">📷</span>
|
|
||||||
|
const tagMenu = `<div class="shopping-tag-menu" onclick="event.stopPropagation()">
|
||||||
|
${Object.entries(TAG_LABELS).map(([k, v]) =>
|
||||||
|
`<button class="sinv-badge badge-tag-add ${localTags.includes(k) ? 'active' : ''}" onclick="toggleShoppingTag(${idx}, '${k}')">${v}</button>`
|
||||||
|
).join('')}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
let detailHtml = '';
|
||||||
|
let priceTag = '';
|
||||||
|
let spesaBar = '';
|
||||||
|
if (hasSpesa) {
|
||||||
|
if (priceData && priceData.loading) {
|
||||||
|
detailHtml = `<div class="spesa-loading">🔍 Cerco...</div>`;
|
||||||
|
} else if (priceData && priceData.product) {
|
||||||
|
const p = priceData.product;
|
||||||
|
const promoHtml = p.promo
|
||||||
|
? `<span class="spesa-promo-badge">${escapeHtml(p.promo.label)} -${Math.round(p.promo.discountPerc)}%</span>`
|
||||||
|
: '';
|
||||||
|
const est = estimateItemPrice(p, item.specification || priceData.spec || '');
|
||||||
|
priceTag = est
|
||||||
|
? `<div class="shopping-item-price">~€${est.estimated.toFixed(2)}</div>`
|
||||||
|
: `<div class="shopping-item-price">€${p.price.toFixed(2)}</div>`;
|
||||||
|
detailHtml = `<div class="spesa-detail-left">
|
||||||
|
<span class="spesa-found-name">${escapeHtml(p.name)}</span>
|
||||||
|
<span class="spesa-pkg">${escapeHtml(p.packageDescr)}${est ? ' · ' + escapeHtml(String(p.priceUm || '')) + '/kg' : ''}</span>
|
||||||
|
${promoHtml}
|
||||||
|
</div>`;
|
||||||
|
spesaBar = `<div class="spesa-bar">
|
||||||
|
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx}, true)" title="Ricerca">🔄 Ricerca</button>
|
||||||
|
<a href="${escapeHtml(p.url)}" target="_blank" class="spesa-bar-btn" title="${escapeHtml(p.name)}" onclick="event.stopPropagation()">🔗 Apri</a>
|
||||||
|
</div>`;
|
||||||
|
} else if (priceData && priceData.searched && !priceData.product) {
|
||||||
|
detailHtml = `<div class="spesa-detail-left"><span class="spesa-not-found">Non trovato</span></div>`;
|
||||||
|
spesaBar = `<div class="spesa-bar">
|
||||||
|
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx}, true)" title="Riprova">🔄 Riprova</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
spesaBar = `<div class="spesa-bar">
|
||||||
|
<button class="spesa-bar-btn" onclick="event.stopPropagation(); searchItemPrice(${idx})" title="Cerca prezzo">🔍 Cerca prezzo</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="shopping-item ${priceData?.product?.promo ? 'has-promo' : ''}" id="shop-item-${idx}" onclick="openScanForItem(${idx})" title="Tocca per scansionare"${bgStyle}>
|
||||||
|
<span class="shopping-item-icon">${catIcon}</span>
|
||||||
|
<div class="shopping-item-body">
|
||||||
|
<div class="shopping-item-top">
|
||||||
|
<div class="shopping-item-info">
|
||||||
|
<div class="shopping-item-name-row">
|
||||||
|
<span class="shopping-item-name">${escapeHtml(item.name)}</span>
|
||||||
|
<span class="shopping-item-scan-hint">📷</span>
|
||||||
|
</div>
|
||||||
|
${item.specification ? `<div class="shopping-item-spec">${escapeHtml(item.specification)}</div>` : ''}
|
||||||
|
${(urgencyBadge || freqBadge || localTagHtml) ? `<div class="shopping-item-badges">${urgencyBadge}${freqBadge}${localTagHtml}</div>` : ''}
|
||||||
|
${detailHtml}
|
||||||
|
</div>
|
||||||
|
<div class="shopping-item-right" onclick="event.stopPropagation()">
|
||||||
|
${priceTag}
|
||||||
|
<button class="shopping-item-tag-btn" onclick="toggleShoppingTagMenu(this)" title="Tag">🏷️</button>
|
||||||
|
<button class="shopping-item-remove" onclick="removeBringItem(${idx})" title="Rimuovi">✕</button>
|
||||||
</div>
|
</div>
|
||||||
${item.specification ? `<div class="shopping-item-spec">${escapeHtml(item.specification)}</div>` : ''}
|
|
||||||
${(urgencyBadge || freqBadge || localTagHtml) ? `<div class="shopping-item-badges">${urgencyBadge}${freqBadge}${localTagHtml}</div>` : ''}
|
|
||||||
${detailHtml}
|
|
||||||
</div>
|
|
||||||
<div class="shopping-item-right" onclick="event.stopPropagation()">
|
|
||||||
${priceTag}
|
|
||||||
<button class="shopping-item-tag-btn" onclick="toggleShoppingTagMenu(this)" title="Tag">🏷️</button>
|
|
||||||
<button class="shopping-item-remove" onclick="removeBringItem(${idx})" title="Rimuovi">✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
${spesaBar}
|
||||||
|
<div class="shopping-tag-menu-container" style="display:none">${tagMenu}</div>
|
||||||
</div>
|
</div>
|
||||||
${spesaBar}
|
</div>`;
|
||||||
<div class="shopping-tag-menu-container" style="display:none">${tagMenu}</div>
|
}
|
||||||
</div>
|
}
|
||||||
</div>`;
|
|
||||||
}).join('');
|
container.innerHTML = html;
|
||||||
|
|
||||||
updateSpesaTotal();
|
updateSpesaTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5867,6 +5947,133 @@ function renderRecipe(r) {
|
|||||||
document.getElementById('recipe-content').innerHTML = html;
|
document.getElementById('recipe-content').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== COOKING MODE =====
|
||||||
|
let _cookingRecipe = null;
|
||||||
|
let _cookingStep = 0;
|
||||||
|
let _cookingTTS = true;
|
||||||
|
|
||||||
|
function startCookingMode() {
|
||||||
|
const recipe = _cachedRecipe && _cachedRecipe.recipe ? _cachedRecipe.recipe : null;
|
||||||
|
if (!recipe || !(recipe.steps || []).length) {
|
||||||
|
showToast('Nessun procedimento disponibile', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cookingRecipe = JSON.parse(JSON.stringify(recipe)); // deep copy so we can track .used
|
||||||
|
_cookingStep = 0;
|
||||||
|
_cookingTTS = 'speechSynthesis' in window;
|
||||||
|
document.getElementById('cooking-title').textContent = _cookingRecipe.title || '';
|
||||||
|
document.getElementById('cooking-tts-btn').textContent = _cookingTTS ? '🔊' : '🔇';
|
||||||
|
document.getElementById('cooking-overlay').style.display = 'flex';
|
||||||
|
document.body.classList.add('cooking-mode-active');
|
||||||
|
try { screen.orientation?.lock('portrait'); } catch (_) { /* ignore */ }
|
||||||
|
// Preload voices for TTS
|
||||||
|
if (_cookingTTS) window.speechSynthesis.getVoices();
|
||||||
|
renderCookingStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCookingMode() {
|
||||||
|
document.getElementById('cooking-overlay').style.display = 'none';
|
||||||
|
document.body.classList.remove('cooking-mode-active');
|
||||||
|
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
|
||||||
|
try { screen.orientation?.unlock(); } catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCookingStep() {
|
||||||
|
if (!_cookingRecipe) return;
|
||||||
|
const steps = _cookingRecipe.steps || [];
|
||||||
|
const step = steps[_cookingStep] || '';
|
||||||
|
const cleanStep = step.replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
||||||
|
const total = steps.length;
|
||||||
|
|
||||||
|
document.getElementById('cooking-step-num').textContent = `${_cookingStep + 1} / ${total}`;
|
||||||
|
document.getElementById('cooking-step-text').textContent = cleanStep;
|
||||||
|
|
||||||
|
// Find pantry ingredients that appear in this step's text and haven't been used yet
|
||||||
|
const stepLower = cleanStep.toLowerCase();
|
||||||
|
const ings = (_cookingRecipe.ingredients || [])
|
||||||
|
.map((ing, idx) => ({ ...ing, _idx: idx }))
|
||||||
|
.filter(ing => ing.from_pantry && ing.product_id && ing.used !== true)
|
||||||
|
.filter(ing => {
|
||||||
|
const name = (ing.name || '').toLowerCase();
|
||||||
|
return name.split(' ').some(word => word.length > 2 && stepLower.includes(word));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ingsEl = document.getElementById('cooking-step-ings');
|
||||||
|
if (ings.length > 0) {
|
||||||
|
ingsEl.innerHTML = ings.map(ing => {
|
||||||
|
const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
|
||||||
|
const qtyNum = ing.qty_number || 0;
|
||||||
|
return `<div class="cooking-ing-row">
|
||||||
|
<span class="cooking-ing-name">📦 <strong>${escapeHtml(ing.name)}</strong>: ${escapeHtml(ing.qty)}</span>
|
||||||
|
<button class="cooking-use-btn" onclick="cookingUseIngredient(${ing._idx}, ${ing.product_id}, '${loc}', ${qtyNum}, this)">📤 Usa</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
ingsEl.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
ingsEl.innerHTML = '';
|
||||||
|
ingsEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation button states
|
||||||
|
const prevBtn = document.getElementById('cooking-prev');
|
||||||
|
const nextBtn = document.getElementById('cooking-next');
|
||||||
|
prevBtn.disabled = _cookingStep === 0;
|
||||||
|
nextBtn.textContent = _cookingStep === total - 1 ? '✅ Fine' : 'Successivo ▶';
|
||||||
|
|
||||||
|
// Speak step
|
||||||
|
if (_cookingTTS) speakCookingStep(cleanStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
function speakCookingStep(text) {
|
||||||
|
if (!('speechSynthesis' in window)) return;
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
const utt = new SpeechSynthesisUtterance(text);
|
||||||
|
utt.lang = 'it-IT';
|
||||||
|
utt.rate = 0.9;
|
||||||
|
utt.pitch = 1.0;
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
const itVoice = voices.find(v => v.lang.startsWith('it'));
|
||||||
|
if (itVoice) utt.voice = itVoice;
|
||||||
|
window.speechSynthesis.speak(utt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCookingTTS() {
|
||||||
|
_cookingTTS = !_cookingTTS;
|
||||||
|
const btn = document.getElementById('cooking-tts-btn');
|
||||||
|
btn.textContent = _cookingTTS ? '🔊' : '🔇';
|
||||||
|
if (_cookingTTS) {
|
||||||
|
const steps = _cookingRecipe?.steps || [];
|
||||||
|
const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
||||||
|
speakCookingStep(text);
|
||||||
|
} else {
|
||||||
|
window.speechSynthesis?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateCookingStep(delta) {
|
||||||
|
if (!_cookingRecipe) return;
|
||||||
|
const total = (_cookingRecipe.steps || []).length;
|
||||||
|
const next = _cookingStep + delta;
|
||||||
|
if (next < 0) return;
|
||||||
|
if (next >= total) {
|
||||||
|
closeCookingMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cookingStep = next;
|
||||||
|
renderCookingStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookingUseIngredient(idx, productId, location, qtyNumber, btn) {
|
||||||
|
// Reuse the same modal used in the recipe dialog
|
||||||
|
useRecipeIngredient(idx, productId, location, qtyNumber, btn);
|
||||||
|
// Mark ingredient as used so it's hidden from further steps
|
||||||
|
if (_cookingRecipe && _cookingRecipe.ingredients && _cookingRecipe.ingredients[idx]) {
|
||||||
|
_cookingRecipe.ingredients[idx].used = true;
|
||||||
|
}
|
||||||
|
setTimeout(() => renderCookingStep(), 400);
|
||||||
|
}
|
||||||
|
// ===== END COOKING MODE =====
|
||||||
|
|
||||||
function updateRecipeMealTitle() {
|
function updateRecipeMealTitle() {
|
||||||
const meal = getSelectedMealType();
|
const meal = getSelectedMealType();
|
||||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||||
|
|||||||
Binary file not shown.
+21
@@ -928,6 +928,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="recipe-result" style="display:none" class="recipe-result">
|
<div id="recipe-result" style="display:none" class="recipe-result">
|
||||||
<div id="recipe-content"></div>
|
<div id="recipe-content"></div>
|
||||||
|
<button class="btn btn-large btn-cooking full-width mt-2" onclick="startCookingMode()">
|
||||||
|
👨🍳 Modalità Cucina
|
||||||
|
</button>
|
||||||
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()">
|
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()">
|
||||||
🔄 Generane un'altra
|
🔄 Generane un'altra
|
||||||
</button>
|
</button>
|
||||||
@@ -958,6 +961,24 @@
|
|||||||
<div class="screensaver-clock" id="screensaver-clock"></div>
|
<div class="screensaver-clock" id="screensaver-clock"></div>
|
||||||
<div class="screensaver-fact" id="screensaver-fact"></div>
|
<div class="screensaver-fact" id="screensaver-fact"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== COOKING MODE OVERLAY ===== -->
|
||||||
|
<div id="cooking-overlay" class="cooking-overlay" style="display:none" aria-live="polite">
|
||||||
|
<div class="cooking-header">
|
||||||
|
<button class="cooking-close-btn" onclick="closeCookingMode()" title="Chiudi">✕</button>
|
||||||
|
<span class="cooking-title" id="cooking-title"></span>
|
||||||
|
<button class="cooking-tts-btn" id="cooking-tts-btn" onclick="toggleCookingTTS()" title="Leggi ad alta voce">🔊</button>
|
||||||
|
</div>
|
||||||
|
<div class="cooking-body">
|
||||||
|
<div class="cooking-step-num" id="cooking-step-num">1 / 1</div>
|
||||||
|
<div class="cooking-step-text" id="cooking-step-text"></div>
|
||||||
|
<div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cooking-nav">
|
||||||
|
<button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button>
|
||||||
|
<button class="cooking-nav-btn cooking-next-btn" id="cooking-next" onclick="navigateCookingStep(1)">Successivo ▶</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="screensaver-shortcuts">
|
<div class="screensaver-shortcuts">
|
||||||
<button class="screensaver-shortcut-btn" id="screensaver-recipe-btn" title="Ricette">
|
<button class="screensaver-shortcut-btn" id="screensaver-recipe-btn" title="Ricette">
|
||||||
🍳
|
🍳
|
||||||
|
|||||||
Reference in New Issue
Block a user