🫙 Sotto vuoto: flag per estendere scadenza prodotti conservati sotto vuoto

This commit is contained in:
dadaloop82
2026-03-15 18:09:38 +00:00
parent 3f0f27e938
commit 4239e0b204
6 changed files with 163 additions and 13 deletions
+7
View File
@@ -140,4 +140,11 @@ function migrateDB(PDO $db): void {
); );
"); ");
} }
// Add vacuum_sealed column to inventory if missing
$invCols = $db->query("PRAGMA table_info(inventory)")->fetchAll();
$invColNames = array_column($invCols, 'name');
if (!in_array('vacuum_sealed', $invColNames)) {
$db->exec("ALTER TABLE inventory ADD COLUMN vacuum_sealed INTEGER DEFAULT 0");
}
} }
+15 -8
View File
@@ -467,7 +467,8 @@ function searchProducts(PDO $db): void {
function listInventory(PDO $db): void { function listInventory(PDO $db): void {
$location = $_GET['location'] ?? ''; $location = $_GET['location'] ?? '';
$query = " $query = "
SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode, p.default_quantity, p.package_unit SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode, p.default_quantity, p.package_unit,
COALESCE(i.vacuum_sealed, 0) as vacuum_sealed
FROM inventory i FROM inventory i
JOIN products p ON i.product_id = p.id JOIN products p ON i.product_id = p.id
"; ";
@@ -510,6 +511,8 @@ function addToInventory(PDO $db): void {
$stmt->execute([$packageUnit, $packageSize ?: 0, $productId]); $stmt->execute([$packageUnit, $packageSize ?: 0, $productId]);
} }
$vacuumSealed = (int)($input['vacuum_sealed'] ?? 0);
// Check if product already exists in this location // Check if product already exists in this location
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?"); $stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?");
$stmt->execute([$productId, $location]); $stmt->execute([$productId, $location]);
@@ -518,13 +521,13 @@ function addToInventory(PDO $db): void {
if ($existing) { if ($existing) {
// Update quantity // Update quantity
$newQty = $existing['quantity'] + $quantity; $newQty = $existing['quantity'] + $quantity;
$stmt = $db->prepare("UPDATE inventory SET quantity = ?, expiry_date = COALESCE(?, expiry_date), updated_at = CURRENT_TIMESTAMP WHERE id = ?"); $stmt = $db->prepare("UPDATE inventory SET quantity = ?, expiry_date = COALESCE(?, expiry_date), vacuum_sealed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->execute([$newQty, $expiry, $existing['id']]); $stmt->execute([$newQty, $expiry, $vacuumSealed, $existing['id']]);
} else { } else {
$newQty = $quantity; $newQty = $quantity;
// Insert new inventory entry // Insert new inventory entry
$stmt = $db->prepare("INSERT INTO inventory (product_id, location, quantity, expiry_date) VALUES (?, ?, ?, ?)"); $stmt = $db->prepare("INSERT INTO inventory (product_id, location, quantity, expiry_date, vacuum_sealed) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $location, $quantity, $expiry]); $stmt->execute([$productId, $location, $quantity, $expiry, $vacuumSealed]);
} }
// Get total across all locations // Get total across all locations
@@ -711,6 +714,7 @@ function updateInventory(PDO $db): void {
if (isset($input['quantity'])) { $fields[] = "quantity = ?"; $params[] = $input['quantity']; } if (isset($input['quantity'])) { $fields[] = "quantity = ?"; $params[] = $input['quantity']; }
if (isset($input['location'])) { $fields[] = "location = ?"; $params[] = $input['location']; } if (isset($input['location'])) { $fields[] = "location = ?"; $params[] = $input['location']; }
if (isset($input['expiry_date'])) { $fields[] = "expiry_date = ?"; $params[] = $input['expiry_date']; } if (isset($input['expiry_date'])) { $fields[] = "expiry_date = ?"; $params[] = $input['expiry_date']; }
if (isset($input['vacuum_sealed'])) { $fields[] = "vacuum_sealed = ?"; $params[] = (int)$input['vacuum_sealed']; }
$fields[] = "updated_at = CURRENT_TIMESTAMP"; $fields[] = "updated_at = CURRENT_TIMESTAMP";
$params[] = $id; $params[] = $id;
@@ -787,7 +791,8 @@ function getStats(PDO $db): void {
// Expiring soonest (next 4 items to expire) // Expiring soonest (next 4 items to expire)
$expiring = $db->query(" $expiring = $db->query("
SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit,
COALESCE(i.vacuum_sealed, 0) as vacuum_sealed
FROM inventory i JOIN products p ON i.product_id = p.id FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0 WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
ORDER BY i.expiry_date ASC ORDER BY i.expiry_date ASC
@@ -796,7 +801,8 @@ function getStats(PDO $db): void {
// Expired // Expired
$expired = $db->query(" $expired = $db->query("
SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit,
COALESCE(i.vacuum_sealed, 0) as vacuum_sealed
FROM inventory i JOIN products p ON i.product_id = p.id FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now') WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
ORDER BY i.expiry_date ASC ORDER BY i.expiry_date ASC
@@ -804,7 +810,8 @@ function getStats(PDO $db): void {
// Opened (partially used conf items with known package capacity) // Opened (partially used conf items with known package capacity)
$opened = $db->query(" $opened = $db->query("
SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit, p.image_url SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit, p.image_url,
COALESCE(i.vacuum_sealed, 0) as vacuum_sealed
FROM inventory i JOIN products p ON i.product_id = p.id FROM inventory i JOIN products p ON i.product_id = p.id
WHERE p.unit = 'conf' AND p.default_quantity > 0 AND p.package_unit IS NOT NULL WHERE p.unit = 'conf' AND p.default_quantity > 0 AND p.package_unit IS NOT NULL
AND i.quantity > 0 AND CAST(i.quantity AS REAL) != CAST(CAST(i.quantity AS INTEGER) AS REAL) AND i.quantity > 0 AND CAST(i.quantity AS REAL) != CAST(CAST(i.quantity AS INTEGER) AS REAL)
+54
View File
@@ -1998,6 +1998,60 @@ body {
margin-top: 4px; margin-top: 4px;
} }
/* ===== TOGGLE SWITCH ===== */
.toggle-row {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 28px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #ccc;
border-radius: 28px;
transition: background 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 22px;
height: 22px;
left: 3px;
bottom: 3px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle-switch input:checked + .toggle-slider {
background: var(--primary, #3b82f6);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(20px);
}
.vacuum-badge {
font-size: 0.7rem;
background: rgba(59, 130, 246, 0.12);
color: #2563eb;
padding: 1px 6px;
border-radius: 8px;
font-weight: 600;
white-space: nowrap;
}
/* ===== REMAINING QUANTITY OPTIONS ===== */ /* ===== REMAINING QUANTITY OPTIONS ===== */
.remaining-options { .remaining-options {
display: grid; display: grid;
+75 -3
View File
@@ -1172,6 +1172,8 @@ function renderInventoryItem(item) {
expiryBadge = `<span class="inv-badge ${isExpired ? 'badge-expired' : isExpiring ? 'badge-expiry' : ''}">${expiryText}</span>`; expiryBadge = `<span class="inv-badge ${isExpired ? 'badge-expired' : isExpiring ? 'badge-expiry' : ''}">${expiryText}</span>`;
} }
const vacuumBadge = item.vacuum_sealed ? '<span class="vacuum-badge">🫙 Sotto vuoto</span>' : '';
return ` return `
<div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})"> <div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})">
<div class="inv-image"> <div class="inv-image">
@@ -1183,6 +1185,7 @@ function renderInventoryItem(item) {
<div class="inv-meta"> <div class="inv-meta">
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span> <span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
${expiryBadge} ${expiryBadge}
${vacuumBadge}
</div> </div>
</div> </div>
<div class="inv-qty-col"> <div class="inv-qty-col">
@@ -1261,6 +1264,11 @@ function showItemDetail(inventoryId, productId) {
<span class="modal-detail-label">📅 Scadenza</span> <span class="modal-detail-label">📅 Scadenza</span>
<span class="modal-detail-value">${formatDate(item.expiry_date)}</span> <span class="modal-detail-value">${formatDate(item.expiry_date)}</span>
</div>` : ''} </div>` : ''}
${item.vacuum_sealed ? `
<div class="modal-detail-row">
<span class="modal-detail-label">🫙 Conservazione</span>
<span class="modal-detail-value">Sotto vuoto</span>
</div>` : ''}
${item.barcode ? ` ${item.barcode ? `
<div class="modal-detail-row"> <div class="modal-detail-row">
<span class="modal-detail-label">🔖 Barcode</span> <span class="modal-detail-label">🔖 Barcode</span>
@@ -1383,6 +1391,15 @@ function editInventoryItem(id) {
<label>📅 Scadenza</label> <label>📅 Scadenza</label>
<input type="date" id="edit-expiry" value="${item.expiry_date || ''}" class="form-input"> <input type="date" id="edit-expiry" value="${item.expiry_date || ''}" class="form-input">
</div> </div>
<div class="form-group">
<label class="toggle-row">
<span>🫙 Sotto vuoto</span>
<span class="toggle-switch">
<input type="checkbox" id="edit-vacuum" ${item.vacuum_sealed ? 'checked' : ''}>
<span class="toggle-slider"></span>
</span>
</label>
</div>
<button type="submit" class="btn btn-large btn-primary full-width">💾 Salva</button> <button type="submit" class="btn btn-large btn-primary full-width">💾 Salva</button>
</form> </form>
`; `;
@@ -1402,7 +1419,8 @@ async function submitEditInventory(e, id, productId) {
const expiry = document.getElementById('edit-expiry').value || null; const expiry = document.getElementById('edit-expiry').value || null;
const unit = document.getElementById('edit-unit').value; const unit = document.getElementById('edit-unit').value;
const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId }; const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId,
vacuum_sealed: document.getElementById('edit-vacuum')?.checked ? 1 : 0 };
// Add package info if conf // Add package info if conf
if (unit === 'conf') { if (unit === 'conf') {
@@ -2414,7 +2432,8 @@ 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 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>`; const vacuumIcon = inv.vacuum_sealed ? ' 🫙' : '';
return `<div class="inv-status-item inv-status-item-clickable" onclick="editActionInventoryItem(${inv.id})"><span>${locInfo.icon} ${locInfo.label}${vacuumIcon}${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);
@@ -2570,6 +2589,15 @@ function editActionInventoryItem(inventoryId) {
<label>📅 Scadenza</label> <label>📅 Scadenza</label>
<input type="date" id="action-edit-expiry" value="${item.expiry_date || ''}" class="form-input"> <input type="date" id="action-edit-expiry" value="${item.expiry_date || ''}" class="form-input">
</div> </div>
<div class="form-group">
<label class="toggle-row">
<span>🫙 Sotto vuoto</span>
<span class="toggle-switch">
<input type="checkbox" id="action-edit-vacuum" ${item.vacuum_sealed ? 'checked' : ''}>
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div class="modal-actions" style="margin-top:12px"> <div class="modal-actions" style="margin-top:12px">
<button type="submit" class="btn btn-large btn-primary flex-1">💾 Salva</button> <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> <button type="button" class="btn btn-secondary" onclick="deleteActionInventoryItem(${inventoryId})" style="padding:12px">🗑️</button>
@@ -2592,7 +2620,8 @@ async function submitActionEditInventory(e, id, productId) {
const expiry = document.getElementById('action-edit-expiry').value || null; const expiry = document.getElementById('action-edit-expiry').value || null;
const unit = document.getElementById('action-edit-unit').value; const unit = document.getElementById('action-edit-unit').value;
const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId }; const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId,
vacuum_sealed: document.getElementById('action-edit-vacuum')?.checked ? 1 : 0 };
if (unit === 'conf') { if (unit === 'conf') {
payload.package_unit = document.getElementById('action-edit-conf-unit')?.value || ''; payload.package_unit = document.getElementById('action-edit-conf-unit')?.value || '';
@@ -2862,6 +2891,15 @@ function showAddForm() {
const estimatedDate = addDays(estimatedDays); const estimatedDate = addDays(estimatedDays);
const estimateLabel = formatEstimatedExpiry(estimatedDays); const estimateLabel = formatEstimatedExpiry(estimatedDays);
// Reset vacuum sealed toggle
const vacuumCb = document.getElementById('add-vacuum-sealed');
if (vacuumCb) {
vacuumCb.checked = false;
document.getElementById('add-vacuum-hint').style.display = 'none';
}
// Store base expiry for vacuum recalculation
window._addBaseExpiryDays = estimatedDays;
expirySection.innerHTML = ` expirySection.innerHTML = `
<label>🛒 Questo prodotto è...</label> <label>🛒 Questo prodotto è...</label>
<div class="purchase-type-selector"> <div class="purchase-type-selector">
@@ -2888,6 +2926,39 @@ function showAddForm() {
showPage('add'); showPage('add');
} }
function toggleVacuumSealed() {
const cb = document.getElementById('add-vacuum-sealed');
if (cb) cb.checked = !cb.checked;
onVacuumSealedChange();
}
function onVacuumSealedChange() {
const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
const hint = document.getElementById('add-vacuum-hint');
if (hint) hint.style.display = isVacuum ? 'block' : 'none';
// Recalculate expiry based on vacuum sealed
const baseDays = window._addBaseExpiryDays || 180;
const days = isVacuum ? getVacuumExpiryDays(baseDays) : baseDays;
const newDate = addDays(days);
const newLabel = formatEstimatedExpiry(days);
const expiryInput = document.getElementById('add-expiry');
const estimateEl = document.querySelector('.expiry-estimate-label');
const dateEl = document.querySelector('.expiry-estimate-date');
if (expiryInput) expiryInput.value = newDate;
if (estimateEl) estimateEl.innerHTML = `Scadenza stimata: <strong>${newLabel}${isVacuum ? ' (sotto vuoto)' : ''}</strong>`;
if (dateEl) dateEl.textContent = formatDate(newDate);
}
function getVacuumExpiryDays(baseDays) {
// Vacuum sealing roughly triples fresh food shelf life, less effect on long-lasting items
if (baseDays <= 7) return Math.round(baseDays * 3); // fresh: 3x (e.g., 3→9, 7→21)
if (baseDays <= 30) return Math.round(baseDays * 2.5); // short: 2.5x (e.g., 14→35)
if (baseDays <= 90) return Math.round(baseDays * 2); // medium: 2x
return Math.round(baseDays * 1.5); // long-lasting: 1.5x
}
function onAddUnitChange() { function onAddUnitChange() {
updateAddQtyStep(); updateAddQtyStep();
const unit = document.getElementById('add-unit').value; const unit = document.getElementById('add-unit').value;
@@ -3061,6 +3132,7 @@ async function submitAdd(e) {
unit: selectedUnit !== productUnit ? selectedUnit : null, unit: selectedUnit !== productUnit ? selectedUnit : null,
package_unit: selectedUnit === 'conf' ? (document.getElementById('add-conf-unit')?.value || null) : null, package_unit: selectedUnit === 'conf' ? (document.getElementById('add-conf-unit')?.value || null) : null,
package_size: selectedUnit === 'conf' ? (parseFloat(document.getElementById('add-conf-size')?.value) || null) : null, package_size: selectedUnit === 'conf' ? (parseFloat(document.getElementById('add-conf-size')?.value) || null) : null,
vacuum_sealed: document.getElementById('add-vacuum-sealed')?.checked ? 1 : 0,
}); });
showLoading(false); showLoading(false);
BIN
View File
Binary file not shown.
+12 -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=20260315d"> <link rel="stylesheet" href="assets/css/style.css?v=20260315e">
<!-- 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>
@@ -218,6 +218,16 @@
</div> </div>
<div id="add-weight-info" class="form-hint" style="display:none"></div> <div id="add-weight-info" class="form-hint" style="display:none"></div>
</div> </div>
<div class="form-group" id="add-vacuum-group">
<label class="toggle-row" onclick="toggleVacuumSealed()">
<span>🫙 Sotto vuoto</span>
<span class="toggle-switch" id="add-vacuum-toggle">
<input type="checkbox" id="add-vacuum-sealed" onchange="onVacuumSealedChange()">
<span class="toggle-slider"></span>
</span>
</label>
<p class="form-hint" id="add-vacuum-hint" style="display:none">La scadenza verrà estesa automaticamente</p>
</div>
<div class="form-group" id="add-expiry-section"> <div class="form-group" id="add-expiry-section">
<!-- Populated dynamically by showAddForm() --> <!-- Populated dynamically by showAddForm() -->
</div> </div>
@@ -884,6 +894,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260315d"></script> <script src="assets/js/app.js?v=20260315e"></script>
</body> </body>
</html> </html>