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;
|
||||
}
|
||||
|
||||
/* Raise modal above cooking overlay when in cooking mode */
|
||||
.cooking-mode-active #modal-overlay {
|
||||
z-index: 600;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
@@ -2929,6 +2934,220 @@ body {
|
||||
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-list {
|
||||
display: flex;
|
||||
|
||||
+252
-45
@@ -75,6 +75,34 @@ const CATEGORY_LOCATION = {
|
||||
'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
|
||||
function mapToLocalCategory(ofCategory, productName) {
|
||||
if (!ofCategory) {
|
||||
@@ -4404,10 +4432,39 @@ function renderSmartShopping() {
|
||||
low: { color: '#22c55e', bg: 'rgba(34,197,94,0.08)', icon: '🟢', label: 'Previsione' },
|
||||
};
|
||||
|
||||
container.innerHTML = items.map((item, idx) => {
|
||||
// Group by section
|
||||
const smartSectionMap = new Map();
|
||||
items.forEach(item => {
|
||||
const sec = getItemSection(item.name);
|
||||
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 checked = !item.on_bring ? 'checked' : '';
|
||||
const globalIdx = smartShoppingItems.indexOf(item);
|
||||
|
||||
// Stock bar
|
||||
@@ -4448,7 +4505,7 @@ function renderSmartShopping() {
|
||||
return `
|
||||
<div class="smart-item" style="border-left: 3px solid ${u.color}; background: ${u.bg}">
|
||||
<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>
|
||||
<div class="smart-item-info">
|
||||
<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>`;
|
||||
}).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() {
|
||||
@@ -4581,8 +4633,11 @@ async function loadShoppingList() {
|
||||
renderShoppingItems();
|
||||
currentEl.style.display = 'block';
|
||||
|
||||
// Load smart shopping predictions (auto-add critical after loading)
|
||||
loadSmartShopping().then(() => autoAddCriticalItems());
|
||||
// Load smart shopping predictions, then re-render to show badges + auto-add critical
|
||||
loadSmartShopping().then(() => {
|
||||
autoAddCriticalItems();
|
||||
renderShoppingItems(); // re-render to apply urgency badges from smart data
|
||||
});
|
||||
|
||||
// Show tabs once data is ready
|
||||
updateShoppingTabCounts();
|
||||
@@ -4598,15 +4653,6 @@ async function renderShoppingItems() {
|
||||
const container = document.getElementById('shopping-items');
|
||||
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;
|
||||
// Update tab count too
|
||||
const tabCount = document.getElementById('tab-count-acquisto');
|
||||
@@ -4635,39 +4681,73 @@ async function renderShoppingItems() {
|
||||
} 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
|
||||
const smartData = smartShoppingItems.find(s => s.name.toLowerCase() === item.name.toLowerCase());
|
||||
const localTags = getShoppingTags(item.name);
|
||||
// Urgency/frequency badges
|
||||
let urgencyBadge = '';
|
||||
if (smartData) {
|
||||
// Build section groups, sorted by urgency weight within each section
|
||||
const TAG_LABELS = { urgente: '🔴 Urgente', prio: '⭐ Priorità', check: '✅ Verificare' };
|
||||
const urgencyMap = {
|
||||
critical: { icon: '🔴', label: 'Urgente', cls: 'badge-critical' },
|
||||
high: { icon: '🟠', label: 'Presto', cls: 'badge-high' },
|
||||
medium: { icon: '🟡', label: 'Presto', cls: 'badge-medium' },
|
||||
medium: { icon: '🟡', label: 'Medio', 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>`;
|
||||
|
||||
// Map each item to its section + urgency
|
||||
const enriched = shoppingItems.map((item, idx) => {
|
||||
const smartData = smartShoppingItems.find(sd => sd.name.toLowerCase() === item.name.toLowerCase());
|
||||
const urgency = smartData?.urgency || null;
|
||||
const sec = getItemSection(item.name);
|
||||
return { item, idx, smartData, urgency, sec };
|
||||
});
|
||||
|
||||
// Group by section key, preserving SHOPPING_SECTIONS order
|
||||
const sectionMap = new Map();
|
||||
for (const e of enriched) {
|
||||
const key = e.sec.key;
|
||||
if (!sectionMap.has(key)) sectionMap.set(key, { sec: e.sec, items: [] });
|
||||
sectionMap.get(key).items.push(e);
|
||||
}
|
||||
|
||||
// Sort items within each section: by urgency weight desc, then by use_count desc
|
||||
for (const [, group] of sectionMap) {
|
||||
group.items.sort((a, b) => {
|
||||
const wa = URGENCY_WEIGHT[a.urgency] || 0;
|
||||
const wb = URGENCY_WEIGHT[b.urgency] || 0;
|
||||
if (wb !== wa) return wb - wa;
|
||||
return (b.smartData?.use_count || 0) - (a.smartData?.use_count || 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Render sections in canonical order
|
||||
let html = '';
|
||||
for (const secDef of SHOPPING_SECTIONS) {
|
||||
const group = sectionMap.get(secDef.key);
|
||||
if (!group) continue;
|
||||
|
||||
html += `<div class="shopping-section-divider"><span class="sec-icon">${secDef.icon}</span>${secDef.label}</div>`;
|
||||
|
||||
for (const { item, idx, smartData, urgency } of group.items) {
|
||||
const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒';
|
||||
const priceKey = item.name.toLowerCase();
|
||||
const priceData = shoppingPrices[priceKey];
|
||||
const bgStyle = urgency && URGENCY_BG[urgency] ? ` style="background:${URGENCY_BG[urgency]}"` : '';
|
||||
const localTags = getShoppingTags(item.name);
|
||||
|
||||
// Urgency badge
|
||||
let urgencyBadge = '';
|
||||
if (urgency && urgencyMap[urgency]) {
|
||||
const u = urgencyMap[urgency];
|
||||
urgencyBadge = `<span class="sinv-badge ${u.cls}">${u.icon} ${u.label}</span>`;
|
||||
}
|
||||
|
||||
// Frequency badge
|
||||
let freqBadge = '';
|
||||
if (smartData && smartData.use_count >= 8) freqBadge = `<span class="sinv-badge badge-freq-high">📈 ${smartData.use_count}x</span>`;
|
||||
else if (smartData && smartData.use_count >= 4) freqBadge = `<span class="sinv-badge badge-freq-med">📊 ${smartData.use_count}x</span>`;
|
||||
else if (smartData && smartData.use_count >= 2) freqBadge = `<span class="sinv-badge badge-freq-low">📉 ${smartData.use_count}x</span>`;
|
||||
|
||||
// Local tags
|
||||
const TAG_LABELS = { urgente: '🔴 Urgente', prio: '⭐ Priorità', check: '✅ Verificare' };
|
||||
const localTagHtml = localTags.map(t =>
|
||||
`<span class="sinv-badge badge-local-tag" onclick="event.stopPropagation(); toggleShoppingTag(${idx}, '${t}')">${TAG_LABELS[t] || t} ✕</span>`
|
||||
).join('');
|
||||
|
||||
// Tag add button
|
||||
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>`
|
||||
@@ -4686,11 +4766,9 @@ async function renderShoppingItems() {
|
||||
? `<span class="spesa-promo-badge">${escapeHtml(p.promo.label)} -${Math.round(p.promo.discountPerc)}%</span>`
|
||||
: '';
|
||||
const est = estimateItemPrice(p, item.specification || priceData.spec || '');
|
||||
if (est) {
|
||||
priceTag = `<div class="shopping-item-price">~€${est.estimated.toFixed(2)}</div>`;
|
||||
} else {
|
||||
priceTag = `<div class="shopping-item-price">€${p.price.toFixed(2)}</div>`;
|
||||
}
|
||||
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>
|
||||
@@ -4698,7 +4776,7 @@ async function renderShoppingItems() {
|
||||
</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)} - ${escapeHtml(p.brand)}" onclick="event.stopPropagation()">🔗 Apri</a>
|
||||
<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>`;
|
||||
@@ -4712,8 +4790,8 @@ async function renderShoppingItems() {
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="shopping-item ${priceData && priceData.product && priceData.product.promo ? 'has-promo' : ''}" id="shop-item-${idx}" onclick="openScanForItem(${idx})" title="Tocca per scansionare">
|
||||
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">
|
||||
@@ -4736,8 +4814,10 @@ async function renderShoppingItems() {
|
||||
<div class="shopping-tag-menu-container" style="display:none">${tagMenu}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
updateSpesaTotal();
|
||||
}
|
||||
|
||||
@@ -5867,6 +5947,133 @@ function renderRecipe(r) {
|
||||
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() {
|
||||
const meal = getSelectedMealType();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
|
||||
Binary file not shown.
+21
@@ -928,6 +928,9 @@
|
||||
</div>
|
||||
<div id="recipe-result" style="display:none" class="recipe-result">
|
||||
<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()">
|
||||
🔄 Generane un'altra
|
||||
</button>
|
||||
@@ -958,6 +961,24 @@
|
||||
<div class="screensaver-clock" id="screensaver-clock"></div>
|
||||
<div class="screensaver-fact" id="screensaver-fact"></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">
|
||||
<button class="screensaver-shortcut-btn" id="screensaver-recipe-btn" title="Ricette">
|
||||
🍳
|
||||
|
||||
Reference in New Issue
Block a user