From 1d8fb55f58d3cd54b2f37c74a8473dfe86aa65c7 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 16 Mar 2026 12:42:12 +0000 Subject: [PATCH] 20260316h: Recipe ingredient USE popup with qty/location controls - Clicking 'Usa' on a recipe ingredient now opens a modal popup instead of directly consuming. Shows location selector, quantity controls (+/- buttons), conf/sub-unit mode, and 'Usa TUTTO' option. - Quantity is pre-filled with the recipe's suggested amount - Auto-selects location with opened package - After use, shows move-after-use modal (stays on recipe page, not dashboard) - Separate recipe-context move functions (showRecipeMoveModal, confirmRecipeMove) - Modal overlay z-index 250 to appear above recipe dialog --- assets/css/style.css | 4 + assets/js/app.js | 259 +++++++++++++++++++++++++++++++++++++++++-- data/dispensa.db | Bin 180224 -> 184320 bytes index.html | 4 +- 4 files changed, 254 insertions(+), 13 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index 50b3685..129a1fd 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1672,6 +1672,10 @@ body { padding: 0; } +#modal-overlay { + z-index: 250; +} + .modal-content { background: var(--bg-card); border-radius: var(--radius) var(--radius) 0 0; diff --git a/assets/js/app.js b/assets/js/app.js index 63cf87b..159409b 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -4937,48 +4937,285 @@ function adjustRecipePersons(delta) { input.value = val; } +let _recipeUseContext = null; // { idx, productId, btn, qtyNumber } +let _recipeUseConfMode = null; + async function useRecipeIngredient(idx, productId, location, qtyNumber, btn) { if (btn.disabled) return; if (!qtyNumber || qtyNumber <= 0) qtyNumber = 1; + + _recipeUseContext = { idx, productId, btn, qtyNumber }; + _recipeUseConfMode = null; + + // Fetch inventory to build the modal + try { + const data = await api('inventory_list'); + const items = (data.inventory || []).filter(i => i.product_id == productId); + + if (items.length === 0) { + showToast('⚠️ Prodotto non trovato in inventario', 'error'); + return; + } + + const unit = items[0].unit || 'pz'; + const pkgSize = parseFloat(items[0].default_quantity) || 0; + const pkgUnit = items[0].package_unit || ''; + const isConf = unit === 'conf' && pkgSize > 0 && pkgUnit; + + // Find opened package location + const openedItem = items.find(i => { + const q = parseFloat(i.quantity); + const dq = parseFloat(i.default_quantity) || 0; + if (i.unit === 'conf' && dq > 0) return q !== Math.floor(q); + if (dq > 0) return Math.abs(q - Math.round(q / dq) * dq) > dq * 0.02; + return false; + }); + const defaultLoc = openedItem ? openedItem.location : (items.find(i => i.location === location) ? location : items[0].location); + + // Build location buttons + const productLocations = [...new Set(items.map(i => i.location))]; + const locButtons = productLocations.map(loc => { + const locInfo = LOCATIONS[loc] || { icon: '📦', label: loc }; + const locItems = items.filter(i => i.location === loc); + const locQty = locItems.reduce((s, i) => s + parseFloat(i.quantity), 0); + const qtyLabel = formatQuantity(locQty, unit, pkgSize, pkgUnit); + return ``; + }).join(''); + + // Build quantity controls + let qtySection = ''; + let defaultQtyValue = qtyNumber; + + if (isConf) { + const totalConf = items.reduce((s, i) => s + parseFloat(i.quantity), 0); + const totalSub = totalConf * pkgSize; + const unitLabels = { 'ml': 'ml', 'l': 'L', 'g': 'g', 'kg': 'kg', 'pz': 'pz' }; + const subLabel = unitLabels[pkgUnit] || pkgUnit; + _recipeUseConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel, _activeUnit: 'sub' }; + + // qtyNumber from recipe is in sub-units (g, ml) + const step = getSubUnitStep(pkgUnit); + defaultQtyValue = qtyNumber; + + qtySection = ` +
+ + +
+

Quantità in ${subLabel} (totale: ${Math.round(totalSub)}${subLabel})

+
+ + + +
`; + } else { + const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml' }; + const unitLabel = unitLabels[unit] || unit; + qtySection = ` +

Quantità da usare (${unitLabel}):

+
+ + + +
`; + } + + // Available info + const availInfo = items.map(i => { + const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location }; + return `${loc.icon} ${formatQuantity(i.quantity, i.unit, i.default_quantity, i.package_unit)}`; + }).join(' · '); + + document.getElementById('modal-content').innerHTML = ` + +
+

${escapeHtml(items[0].name)}

+

📦 ${availInfo}

+
+ +
${locButtons}
+ +
+
+ + ${qtySection} +
+ + +
+ `; + document.getElementById('modal-overlay').style.display = 'flex'; + + } catch (err) { + console.error('useRecipeIngredient error:', err); + showToast('Errore nel caricamento', 'error'); + } +} + +function selectRecipeUseLoc(btn, loc) { + btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + document.getElementById('ruse-location').value = loc; +} + +function switchRecipeUseUnit(mode) { + if (!_recipeUseConfMode) return; + const subBtn = document.getElementById('ruse-unit-sub'); + const confBtn = document.getElementById('ruse-unit-conf'); + const qtyInput = document.getElementById('ruse-quantity'); + const hint = document.getElementById('ruse-hint'); + + if (mode === 'sub') { + subBtn.classList.add('active'); + confBtn.classList.remove('active'); + _recipeUseConfMode._activeUnit = 'sub'; + const step = getSubUnitStep(_recipeUseConfMode.packageUnit); + qtyInput.value = _recipeUseContext.qtyNumber || step; + qtyInput.step = step; + qtyInput.min = step; + hint.textContent = `Quantità in ${_recipeUseConfMode.subLabel} (totale: ${Math.round(_recipeUseConfMode.totalSub)}${_recipeUseConfMode.subLabel})`; + } else { + confBtn.classList.add('active'); + subBtn.classList.remove('active'); + _recipeUseConfMode._activeUnit = 'conf'; + qtyInput.value = 1; + qtyInput.step = 0.5; + qtyInput.min = 0.5; + hint.textContent = `Confezioni da ${_recipeUseConfMode.packageSize}${_recipeUseConfMode.subLabel} (hai ${_recipeUseConfMode.totalConf.toFixed(1)} conf)`; + } +} + +function adjustRecipeUseQty(direction) { + const input = document.getElementById('ruse-quantity'); + let val = parseFloat(input.value) || 0; + let step; + if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') { + step = getSubUnitStep(_recipeUseConfMode.packageUnit); + } else if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'conf') { + step = 0.5; + } else { + step = 0.5; + } + val = Math.max(step, val + direction * step); + input.value = Math.round(val * 1000) / 1000; +} + +async function submitRecipeUse(useAll) { + if (!_recipeUseContext) return; + const { idx, productId, btn } = _recipeUseContext; + const location = document.getElementById('ruse-location').value; + + let qty; + if (useAll) { + qty = 0; // API handles use_all + } else { + qty = parseFloat(document.getElementById('ruse-quantity').value) || 1; + if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') { + qty = qty / _recipeUseConfMode.packageSize; + } + } + + closeModal(); btn.disabled = true; btn.textContent = '⏳...'; - + try { const result = await api('inventory_use', {}, 'POST', { product_id: productId, - quantity: qtyNumber, - use_all: false, + quantity: qty, + use_all: useAll, location: location }); - + if (result.success) { const li = document.getElementById(`recipe-ing-${idx}`); - if (li) { - li.classList.add('recipe-ing-used'); - } + if (li) li.classList.add('recipe-ing-used'); btn.textContent = '✔️ Scalato'; btn.classList.add('btn-used'); - - // Persist used state in cached recipe + if (_cachedRecipe && _cachedRecipe.recipe && _cachedRecipe.recipe.ingredients && _cachedRecipe.recipe.ingredients[idx]) { _cachedRecipe.recipe.ingredients[idx].used = true; } - + showToast('📦 Ingrediente scalato dalla dispensa!', 'success'); if (result.added_to_bring) { setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500); } + + // Offer to move opened package (stays on recipe page, modal over recipe overlay) + if (result.remaining > 0) { + setTimeout(() => showRecipeMoveModal(productId, location, result.remaining, result.opened_id), 400); + } } else { btn.disabled = false; btn.textContent = '📦 Usa'; showToast(result.error || 'Errore nello scalare', 'error'); } } catch (err) { - console.error('Use ingredient error:', err); + console.error('Recipe use error:', err); btn.disabled = false; btn.textContent = '📦 Usa'; showToast('Errore di connessione', 'error'); } + _recipeUseContext = null; +} + +function showRecipeMoveModal(productId, fromLoc, remaining, openedId) { + const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc); + const locButtons = otherLocs.map(([k, v]) => + `` + ).join(''); + + document.getElementById('modal-content').innerHTML = ` + +
+

Vuoi spostare ${openedId ? 'la confezione aperta' : 'il resto'} in un'altra posizione?

+
${locButtons}
+ +
+ `; + document.getElementById('modal-overlay').style.display = 'flex'; +} + +async function confirmRecipeMove(productId, fromLoc, toLoc, openedId) { + closeModal(); + try { + if (openedId) { + let days = estimateExpiryDays({ name: '', category: '' }, toLoc); + await api('inventory_update', {}, 'POST', { + id: openedId, + location: toLoc, + expiry_date: addDays(days), + product_id: productId, + }); + } else { + const data = await api('inventory_list'); + const item = (data.inventory || []).find(i => i.product_id == productId && i.location === fromLoc && parseFloat(i.quantity) > 0); + if (item) { + let days = estimateExpiryDays({ name: item.name || '', category: item.category || '' }, toLoc); + if (item.vacuum_sealed) days = getVacuumExpiryDays(days); + await api('inventory_update', {}, 'POST', { + id: item.id, + location: toLoc, + expiry_date: addDays(days), + product_id: productId, + }); + } + } + showToast(`📦 Spostato in ${LOCATIONS[toLoc]?.label || toLoc}`, 'success'); + } catch (e) { + console.error('Recipe move error:', e); + } } function renderRecipe(r) { diff --git a/data/dispensa.db b/data/dispensa.db index 784107153525e6d2854ef7085473bf79c4f01036..fd857527adc72c0b113014576c5003ae24d5425f 100644 GIT binary patch delta 2343 zcmah~TWl0n7~VOZUa*%_rKOZoJ>3)+bQky5-F6#VD#pZU0Mi;`jNs|)+3g9lbB3AO z8Wu^5PpFBC6Fi6nB{5K-3Rnd-B5L~LgNc_!ec^>feb5I@jG7oP|CyF;)dya7cK&nz z`}ch_J>59{YUA@=wLPIwD0YYpYdsepGlsR+x%uI{?OQ6S*40}=6?_u?-f+D6Ywaa} z_4)Bzo?KglLy^0(w;jl~YX@Sf6!u5oK2&Q6tI+8;z0>8Nf3wlwc@4{FP1Nc~FGdM@6Y zLwlQa{C)_(k3Yx1;BV)8T4!;4TTM)J*d%9OJuXDOu9dv>C0sMqT$RkOES}!-M>@^iTCH?*<8Bs z(#~FV>Y3WQ*-+gq#6MAkns&hRZ?xux2V3exJ)N&Tl3H@J9copCpE-4_ZZLz(of384 zL>Ms~YV2l?Y<>&fDc{U$?eghUs8!BOw7NSQHbki)9FG))$cLlh3UdT;ZHE~XTn^Qs zwHlWnzJ=QEUyMRrgXYT~kCMHjSO&i$Tor^{!2z=>RAq$pWWBl>7%Ty&HrNW>pdQ`e zkIZ!WT|acTz@4Hn3@%LS@eOjKUmMZV^2EF74jGSVD{Ip6xV$if+U52^bbB|vks_2h z4t?gsx$u1^*;@7-USgIfZal4O)}@ z^9&l0@pn+Y+1vnw%0-4UQSR3H;@*M;&IGY$FuBow}|d$s8FqsF|mvY!Rbu zztz@Yj*&2Ac9&_F5 zwJ4#*A}?D-PWZw~mQ5SBRDhVAxYSgo5}P?>Pnnv=1S2JGl|2!dV2Mz&V{+Ml9Cc4k zG6xbYhVwYHc^;rp%~KtL;lN(0+1y>=!u5$i{G$&h3c> zjZ5d^>1L2Ct?uDa-NV4*ZFN(AF9sHOoUB>K;y{G3_w06sOts2L$RZ=nz02bz`CUI+t;AqWTY*( zsD1kcHx-m^067s3FLH~TGTwl?rgr?j~@jl{*R%}E2^GyT1TtuL7Rcq=^L}U zWkmJP$QV@%5z@+OLp6!Mg#0UqZkJaFwRSBoKRS&DP$KT{nO&o&V#%Z&Jc`!KflDYW zj~zvWH_(I3Sv2H-b|x%eJc|y?o=sZ&$e0M2PE1xTGWa+t$OEQ>aRgYSfHMVMwLV5H zW)eW7M{Q+Sw3nx6QFzCwO-(gY!cFq8*|}v9k^h7X0(fFcT0j8q;jS$l2(bLNEBC*S zdZyBfP#`#5B`_n47y;e@Bj}_u<{2m$s24C~0{H9&f@bAGnLx~JR;eht*{G6$-j0_! z=mBvTc(^EfOT_?vwt8WDC1unWSe2+NL QXc<3&p76O%C3DI2KcaF7P5=M^ delta 1579 zcmZ`(U2IfE6uvXtmbO5r{54iYP8(<`Y~gOVE$yN%B}OI0SfqvrwK}~!dpq&&otyhZ z(i&3A6Nwrj8y&#J_6co*g=Uq43I=RUd@zxi=!+zV#Q4Gk5QB-nc<%N$Cce#_Ie*`G zzB4B#SD!es`t;^XFbpGk2C@WZ-n6o$t~8fD;H^JSD~{5Km3{02`L+7vwZ9n0i;rC% zN}zA)+{|3X`jd?bX0-0vyLEi;Ix^m#POmM#d-+Vl8?7&{xw^Ud^vvPf6r|D_GnGko zmtMaz-AleHt-0@4k!N+&-=wdGRvTmsX-cPy-#;>I(6>92?#-Ab`WGFkO|aVryUqS& zzn9kS|CZ_7V;lv56h3=Qk6a>kvmag}u2)4W291hAG!InF z4sN5Lyi=K&G7?j0m`EHtJ5aaEXllI7(;a3$N91j&pXGMtv!hwD+0s#UX1%W55w?MiO zSUe}(QJzFQ1_Yv@9OHi8{OigQRU2I=t% zY$5v*-Js7W=;QiAFP={CBir@81ih}muB2(wq3>7HhdNnH_v@~U^pJipNis*15QWl_ zqobG$`;xxJp~ti#C&617S%6IAv;=gRX2FSH93F;ZH}v7$BG;A>x!_?6X{q=%;EvEQ zw3C*xklT*r|2G2^Kr|%6P_CQ?2S)}W_l4sK2o%0LayNqQ=Ru$#oq>ZcmWy?iLl+L* z4J96Z6qiUso=fmmpd3s9j=puBwvFX@?6>GIpinNEi%mYFSjyd*fx}Umh1h)o`)iSD ziXQO%cwk95CVWJPh~g#?GxDHT@F15EAq@-49z$ub)LlUY7WM~2!WEF0t~Fe)(F-na zLxAjAQaQFl^;y0sw}X{LK960OO$CIvq9n%zB5uZtENfVDtQL=8QNf;g$_Ji9bq8fi zgl!AI5R(pL;hx8B9H!7JOB?lP6SQ_L8S@?r zi9`voeN=$l4Y)(gf5wV6Mq`YiOdNNRM2uMsPvX$sFqVXZh34Zw2en(KE>zaMa`Qe8 fXIS7e#KBvAH6#`ORw%&Z%Rd{K-I>lF{J#GMRDispensa Manager - + @@ -911,6 +911,6 @@ - +