Multipack→conf pre-fill (2x200g→2 conf da 200g) + modifica inventario da action page

This commit is contained in:
dadaloop82
2026-03-15 17:58:11 +00:00
parent b90fe409d6
commit 3f0f27e938
4 changed files with 127 additions and 7 deletions
+8
View File
@@ -2808,6 +2808,14 @@ body {
font-weight: 700; font-weight: 700;
} }
.inventory-status-bar .inv-status-item-clickable {
cursor: pointer;
transition: background 0.15s;
}
.inventory-status-bar .inv-status-item-clickable:active {
background: rgba(59, 130, 246, 0.15);
}
/* ===== ACTION BUTTONS GRID ===== */ /* ===== ACTION BUTTONS GRID ===== */
.action-buttons-3col { .action-buttons-3col {
display: grid; display: grid;
+117 -5
View File
@@ -232,15 +232,14 @@ const CATEGORY_LABELS = {
function detectUnitAndQuantity(quantityInfo) { function detectUnitAndQuantity(quantityInfo) {
if (!quantityInfo) return { unit: 'pz', quantity: 1, weightInfo: '' }; if (!quantityInfo) return { unit: 'pz', quantity: 1, weightInfo: '' };
const q = quantityInfo.toLowerCase().trim(); const q = quantityInfo.toLowerCase().trim();
// Match multi-pack patterns like "6 x 1l", "4 x 125g" → total weight // Match multi-pack patterns like "6 x 1l", "4 x 125g" → confezioni
const multiMatch = q.match(/(\d+)\s*x\s*([\d.,]+)\s*(ml|l|g|kg|cl)/i); const multiMatch = q.match(/(\d+)\s*x\s*([\d.,]+)\s*(ml|l|g|kg|cl)/i);
if (multiMatch) { if (multiMatch) {
const count = parseInt(multiMatch[1]); const count = parseInt(multiMatch[1]);
let perUnitVal = parseFloat(multiMatch[2].replace(',', '.')); let perUnitVal = parseFloat(multiMatch[2].replace(',', '.'));
let perUnitUnit = multiMatch[3].toLowerCase(); let perUnitUnit = multiMatch[3].toLowerCase();
if (perUnitUnit === 'cl') { perUnitUnit = 'ml'; perUnitVal *= 10; } if (perUnitUnit === 'cl') { perUnitUnit = 'ml'; perUnitVal *= 10; }
const totalVal = count * perUnitVal; return { unit: 'conf', quantity: perUnitVal, packageUnit: perUnitUnit, confCount: count, weightInfo: quantityInfo };
return { unit: perUnitUnit, quantity: totalVal, weightInfo: quantityInfo };
} }
// Match single package patterns like "500 g", "1 l", "750 ml", "1.5 kg" // Match single package patterns like "500 g", "1 l", "750 ml", "1.5 kg"
const match = q.match(/([\d.,]+)\s*(kg|g|l|ml|cl)/i); const match = q.match(/([\d.,]+)\s*(kg|g|l|ml|cl)/i);
@@ -349,6 +348,7 @@ function guessLocation(product) {
// ===== STATE ===== // ===== STATE =====
let currentProduct = null; let currentProduct = null;
let currentInventory = []; let currentInventory = [];
let _actionInventoryItems = [];
let currentLocation = ''; let currentLocation = '';
let scannerStream = null; let scannerStream = null;
let quaggaRunning = false; let quaggaRunning = false;
@@ -1786,6 +1786,8 @@ async function onBarcodeDetected(barcode) {
currentProduct.unit = detected.unit; currentProduct.unit = detected.unit;
currentProduct.default_quantity = detected.quantity; currentProduct.default_quantity = detected.quantity;
currentProduct.weight_info = weightStr; currentProduct.weight_info = weightStr;
if (detected.packageUnit) currentProduct.package_unit = detected.packageUnit;
if (detected.confCount) currentProduct._confCount = detected.confCount;
// Update product in DB for future scans // Update product in DB for future scans
api('product_save', {}, 'POST', { api('product_save', {}, 'POST', {
id: currentProduct.id, id: currentProduct.id,
@@ -1796,6 +1798,7 @@ async function onBarcodeDetected(barcode) {
image_url: currentProduct.image_url || '', image_url: currentProduct.image_url || '',
unit: detected.unit, unit: detected.unit,
default_quantity: detected.quantity, default_quantity: detected.quantity,
package_unit: detected.packageUnit || '',
notes: currentProduct.notes, notes: currentProduct.notes,
}); });
} }
@@ -1806,6 +1809,11 @@ async function onBarcodeDetected(barcode) {
const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/); const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim(); if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim();
} }
// Detect confCount from weight_info for multipack pre-fill
if (currentProduct.weight_info && currentProduct.unit === 'conf' && !currentProduct._confCount) {
const detected = detectUnitAndQuantity(currentProduct.weight_info);
if (detected.confCount) currentProduct._confCount = detected.confCount;
}
showLoading(false); showLoading(false);
stopScanner(); stopScanner();
showProductAction(); showProductAction();
@@ -1837,6 +1845,7 @@ async function onBarcodeDetected(barcode) {
image_url: p.image_url || '', image_url: p.image_url || '',
unit: detected.unit, unit: detected.unit,
default_quantity: detected.quantity, default_quantity: detected.quantity,
package_unit: detected.packageUnit || '',
notes: notesParts.join(' · '), notes: notesParts.join(' · '),
}); });
@@ -1850,6 +1859,8 @@ async function onBarcodeDetected(barcode) {
image_url: p.image_url || '', image_url: p.image_url || '',
unit: detected.unit, unit: detected.unit,
default_quantity: detected.quantity, default_quantity: detected.quantity,
package_unit: detected.packageUnit || '',
_confCount: detected.confCount || 0,
weight_info: p.quantity_info || '', weight_info: p.quantity_info || '',
nutriscore: p.nutriscore || '', nutriscore: p.nutriscore || '',
ingredients: p.ingredients || '', ingredients: p.ingredients || '',
@@ -2379,6 +2390,7 @@ function showProductAction() {
// === CHECK INVENTORY FOR THIS PRODUCT === // === CHECK INVENTORY FOR THIS PRODUCT ===
checkInventoryForProduct(currentProduct.id).then(inventoryItems => { checkInventoryForProduct(currentProduct.id).then(inventoryItems => {
_actionInventoryItems = inventoryItems;
const statusBar = document.getElementById('action-inventory-status'); const statusBar = document.getElementById('action-inventory-status');
const btnsContainer = document.getElementById('action-buttons-container'); const btnsContainer = document.getElementById('action-buttons-container');
@@ -2402,7 +2414,7 @@ function showProductAction() {
else if (d <= 7) expiryStr = ` · 🟡 Scade tra ${d}g`; else if (d <= 7) expiryStr = ` · 🟡 Scade tra ${d}g`;
else expiryStr = ` · 📅 ${formatDate(inv.expiry_date)}`; else expiryStr = ` · 📅 ${formatDate(inv.expiry_date)}`;
} }
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''}</span></div>`; return `<div class="inv-status-item inv-status-item-clickable" onclick="editActionInventoryItem(${inv.id})"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''} ✏️</span></div>`;
}).join(''); }).join('');
const totalStr = formatQuantity(totalQty, unit, defQty, pkgUnit); const totalStr = formatQuantity(totalQty, unit, defQty, pkgUnit);
@@ -2417,6 +2429,7 @@ function showProductAction() {
</div> </div>
</div> </div>
<div class="inv-status-items">${invHtml}</div> <div class="inv-status-items">${invHtml}</div>
<p style="font-size:0.75rem;color:var(--text-muted);text-align:center;margin:4px 0 0">Tocca una riga per modificare</p>
`; `;
btnsContainer.className = 'action-buttons-4col'; btnsContainer.className = 'action-buttons-4col';
@@ -2505,6 +2518,105 @@ function editProductFromAction() {
showPage('product-form'); showPage('product-form');
} }
// === EDIT INVENTORY ITEM FROM ACTION PAGE ===
function editActionInventoryItem(inventoryId) {
const item = _actionInventoryItems.find(i => i.id === inventoryId);
if (!item) return;
const isConf = (item.unit || 'pz') === 'conf';
const confSizeVal = (isConf && item.default_quantity > 0) ? item.default_quantity : '';
const confUnitVal = (isConf && item.package_unit) ? item.package_unit : 'g';
document.getElementById('modal-content').innerHTML = `
<div class="modal-header">
<h3>Modifica ${escapeHtml(item.name || currentProduct.name)}</h3>
<button class="modal-close" onclick="closeModal()">✕</button>
</div>
<form class="form" onsubmit="submitActionEditInventory(event, ${inventoryId}, ${item.product_id})">
<div class="form-group">
<label>📦 Quantità</label>
<div class="qty-control">
<button type="button" class="qty-btn" onclick="adjustQty('action-edit-qty', -1)"></button>
<input type="number" id="action-edit-qty" value="${item.quantity}" min="0" step="any" class="qty-input">
<button type="button" class="qty-btn" onclick="adjustQty('action-edit-qty', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>📏 Unità di misura</label>
<select id="action-edit-unit" class="form-input" onchange="onActionEditUnitChange()">
${['pz','g','kg','ml','l','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'kg' ? 'kg (chilogrammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'l' ? 'L (litri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
</select>
</div>
<div class="form-group" id="action-edit-conf-group" style="display:${isConf ? 'block' : 'none'}">
<label>📦 Ogni confezione contiene:</label>
<div class="conf-size-inputs">
<input type="number" id="action-edit-conf-size" class="form-input conf-size-input" min="1" step="any" value="${confSizeVal}" placeholder="es. 300">
<select id="action-edit-conf-unit" class="form-input conf-size-unit">
${['g','kg','ml','l'].map(u => `<option value="${u}" ${confUnitVal === u ? 'selected' : ''}>${u === 'l' ? 'L' : u}</option>`).join('')}
</select>
</div>
</div>
<div class="form-group">
<label>📍 Posizione</label>
<div class="location-selector">
${Object.entries(LOCATIONS).map(([k, v]) => `
<button type="button" class="loc-btn ${item.location === k ? 'active' : ''}"
onclick="this.parentElement.querySelectorAll('.loc-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.getElementById('action-edit-loc').value='${k}'">${v.icon} ${v.label}</button>
`).join('')}
</div>
<input type="hidden" id="action-edit-loc" value="${item.location}">
</div>
<div class="form-group">
<label>📅 Scadenza</label>
<input type="date" id="action-edit-expiry" value="${item.expiry_date || ''}" class="form-input">
</div>
<div class="modal-actions" style="margin-top:12px">
<button type="submit" class="btn btn-large btn-primary flex-1">💾 Salva</button>
<button type="button" class="btn btn-secondary" onclick="deleteActionInventoryItem(${inventoryId})" style="padding:12px">🗑️</button>
</div>
</form>
`;
document.getElementById('modal-overlay').style.display = 'flex';
}
function onActionEditUnitChange() {
const unit = document.getElementById('action-edit-unit').value;
const confGroup = document.getElementById('action-edit-conf-group');
if (confGroup) confGroup.style.display = unit === 'conf' ? 'block' : 'none';
}
async function submitActionEditInventory(e, id, productId) {
e.preventDefault();
const qty = parseFloat(document.getElementById('action-edit-qty').value);
const loc = document.getElementById('action-edit-loc').value;
const expiry = document.getElementById('action-edit-expiry').value || null;
const unit = document.getElementById('action-edit-unit').value;
const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId };
if (unit === 'conf') {
payload.package_unit = document.getElementById('action-edit-conf-unit')?.value || '';
payload.package_size = parseFloat(document.getElementById('action-edit-conf-size')?.value) || 0;
} else {
payload.package_unit = '';
payload.package_size = 0;
}
await api('inventory_update', {}, 'POST', payload);
closeModal();
showToast('Aggiornato!', 'success');
showProductAction(); // Refresh the action page
}
async function deleteActionInventoryItem(id) {
if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) {
await api('inventory_delete', {}, 'POST', { id });
closeModal();
showToast('Prodotto rimosso', 'success');
showProductAction(); // Refresh the action page
}
}
// === THROW AWAY FORM === // === THROW AWAY FORM ===
function showThrowForm() { function showThrowForm() {
// Open a modal to ask how much to throw away // Open a modal to ask how much to throw away
@@ -2700,7 +2812,7 @@ function showAddForm() {
const unitSelect = document.getElementById('add-unit'); const unitSelect = document.getElementById('add-unit');
unitSelect.value = unit; unitSelect.value = unit;
document.getElementById('add-quantity').value = unit === 'conf' ? (currentProduct.last_qty || 1) : (currentProduct.default_quantity || 1); document.getElementById('add-quantity').value = unit === 'conf' ? (currentProduct._confCount || currentProduct.last_qty || 1) : (currentProduct.default_quantity || 1);
document.getElementById('add-quantity').dataset.manuallySet = 'false'; document.getElementById('add-quantity').dataset.manuallySet = 'false';
// Show/hide conf size row and pre-fill // Show/hide conf size row and pre-fill
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -9,7 +9,7 @@
<title>Dispensa Manager</title> <title>Dispensa Manager</title>
<link rel="manifest" href="manifest.json"> <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="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=20260315c"> <link rel="stylesheet" href="assets/css/style.css?v=20260315d">
<!-- QuaggaJS for barcode scanning --> <!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
</head> </head>
@@ -884,6 +884,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260315c"></script> <script src="assets/js/app.js?v=20260315d"></script>
</body> </body>
</html> </html>