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
This commit is contained in:
@@ -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;
|
||||
|
||||
+248
-11
@@ -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 `<button type="button" class="loc-btn ${loc === defaultLoc ? 'active' : ''}" onclick="selectRecipeUseLoc(this, '${loc}')">${locInfo.icon} ${locInfo.label} (${qtyLabel})</button>`;
|
||||
}).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 = `
|
||||
<div class="use-unit-switch" style="display:flex;margin-bottom:8px">
|
||||
<button type="button" class="use-unit-btn active" id="ruse-unit-sub" onclick="switchRecipeUseUnit('sub')">${subLabel}</button>
|
||||
<button type="button" class="use-unit-btn" id="ruse-unit-conf" onclick="switchRecipeUseUnit('conf')">Confezioni</button>
|
||||
</div>
|
||||
<p id="ruse-hint" style="font-size:0.85rem;color:var(--text-muted);margin-bottom:8px">Quantità in ${subLabel} (totale: ${Math.round(totalSub)}${subLabel})</p>
|
||||
<div class="qty-control">
|
||||
<button type="button" class="qty-btn" onclick="adjustRecipeUseQty(-1)">−</button>
|
||||
<input type="number" id="ruse-quantity" value="${defaultQtyValue}" min="${step}" step="${step}" class="qty-input">
|
||||
<button type="button" class="qty-btn" onclick="adjustRecipeUseQty(1)">+</button>
|
||||
</div>`;
|
||||
} else {
|
||||
const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml' };
|
||||
const unitLabel = unitLabels[unit] || unit;
|
||||
qtySection = `
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:8px">Quantità da usare (${unitLabel}):</p>
|
||||
<div class="qty-control">
|
||||
<button type="button" class="qty-btn" onclick="adjustRecipeUseQty(-1)">−</button>
|
||||
<input type="number" id="ruse-quantity" value="${defaultQtyValue}" min="0.1" step="any" class="qty-input">
|
||||
<button type="button" class="qty-btn" onclick="adjustRecipeUseQty(1)">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="modal-header">
|
||||
<h3>📤 Usa ingrediente</h3>
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div style="padding:0 16px 16px">
|
||||
<p style="margin-bottom:8px;font-weight:600">${escapeHtml(items[0].name)}</p>
|
||||
<p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:12px">📦 ${availInfo}</p>
|
||||
<div class="form-group">
|
||||
<label>📍 Da dove?</label>
|
||||
<div class="location-selector">${locButtons}</div>
|
||||
<input type="hidden" id="ruse-location" value="${defaultLoc}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Quanto?</label>
|
||||
${qtySection}
|
||||
</div>
|
||||
<button type="button" class="btn btn-large btn-danger full-width" onclick="submitRecipeUse(false)" style="margin-top:8px">
|
||||
📤 Usa questa quantità
|
||||
</button>
|
||||
<button type="button" class="btn btn-large btn-secondary full-width" style="margin-top:8px" onclick="submitRecipeUse(true)">
|
||||
🗑️ Usa TUTTO / Finito
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
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]) =>
|
||||
`<button type="button" class="loc-btn" onclick="confirmRecipeMove(${productId}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
|
||||
).join('');
|
||||
|
||||
document.getElementById('modal-content').innerHTML = `
|
||||
<div class="modal-header">
|
||||
<h3>📦 Spostare il resto?</h3>
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div style="padding:0 16px 16px">
|
||||
<p style="margin-bottom:12px">Vuoi spostare ${openedId ? 'la confezione aperta' : 'il resto'} in un'altra posizione?</p>
|
||||
<div class="location-selector">${locButtons}</div>
|
||||
<button type="button" class="btn btn-secondary full-width" style="margin-top:12px" onclick="closeModal()">No, resta in ${LOCATIONS[fromLoc]?.label || fromLoc}</button>
|
||||
</div>
|
||||
`;
|
||||
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) {
|
||||
|
||||
Binary file not shown.
+2
-2
@@ -9,7 +9,7 @@
|
||||
<title>Dispensa Manager</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260316c">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260316h">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
</head>
|
||||
@@ -911,6 +911,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260316g"></script>
|
||||
<script src="assets/js/app.js?v=20260316h"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user