🫙 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 {
$location = $_GET['location'] ?? '';
$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
JOIN products p ON i.product_id = p.id
";
@@ -510,6 +511,8 @@ function addToInventory(PDO $db): void {
$stmt->execute([$packageUnit, $packageSize ?: 0, $productId]);
}
$vacuumSealed = (int)($input['vacuum_sealed'] ?? 0);
// Check if product already exists in this location
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?");
$stmt->execute([$productId, $location]);
@@ -518,13 +521,13 @@ function addToInventory(PDO $db): void {
if ($existing) {
// Update quantity
$newQty = $existing['quantity'] + $quantity;
$stmt = $db->prepare("UPDATE inventory SET quantity = ?, expiry_date = COALESCE(?, expiry_date), updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->execute([$newQty, $expiry, $existing['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, $vacuumSealed, $existing['id']]);
} else {
$newQty = $quantity;
// Insert new inventory entry
$stmt = $db->prepare("INSERT INTO inventory (product_id, location, quantity, expiry_date) VALUES (?, ?, ?, ?)");
$stmt->execute([$productId, $location, $quantity, $expiry]);
$stmt = $db->prepare("INSERT INTO inventory (product_id, location, quantity, expiry_date, vacuum_sealed) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $location, $quantity, $expiry, $vacuumSealed]);
}
// 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['location'])) { $fields[] = "location = ?"; $params[] = $input['location']; }
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";
$params[] = $id;
@@ -787,7 +791,8 @@ function getStats(PDO $db): void {
// Expiring soonest (next 4 items to expire)
$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
WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
ORDER BY i.expiry_date ASC
@@ -796,7 +801,8 @@ function getStats(PDO $db): void {
// Expired
$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
WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
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 = $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
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)
+54
View File
@@ -1998,6 +1998,60 @@ body {
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-options {
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>`;
}
const vacuumBadge = item.vacuum_sealed ? '<span class="vacuum-badge">🫙 Sotto vuoto</span>' : '';
return `
<div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})">
<div class="inv-image">
@@ -1183,6 +1185,7 @@ function renderInventoryItem(item) {
<div class="inv-meta">
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
${expiryBadge}
${vacuumBadge}
</div>
</div>
<div class="inv-qty-col">
@@ -1261,6 +1264,11 @@ function showItemDetail(inventoryId, productId) {
<span class="modal-detail-label">📅 Scadenza</span>
<span class="modal-detail-value">${formatDate(item.expiry_date)}</span>
</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 ? `
<div class="modal-detail-row">
<span class="modal-detail-label">🔖 Barcode</span>
@@ -1383,6 +1391,15 @@ function editInventoryItem(id) {
<label>📅 Scadenza</label>
<input type="date" id="edit-expiry" value="${item.expiry_date || ''}" class="form-input">
</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>
</form>
`;
@@ -1402,7 +1419,8 @@ async function submitEditInventory(e, id, productId) {
const expiry = document.getElementById('edit-expiry').value || null;
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
if (unit === 'conf') {
@@ -2414,7 +2432,8 @@ function showProductAction() {
else if (d <= 7) expiryStr = ` · 🟡 Scade tra ${d}g`;
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('');
const totalStr = formatQuantity(totalQty, unit, defQty, pkgUnit);
@@ -2570,6 +2589,15 @@ function editActionInventoryItem(inventoryId) {
<label>📅 Scadenza</label>
<input type="date" id="action-edit-expiry" value="${item.expiry_date || ''}" class="form-input">
</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">
<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>
@@ -2592,7 +2620,8 @@ async function submitActionEditInventory(e, id, productId) {
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 };
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') {
payload.package_unit = document.getElementById('action-edit-conf-unit')?.value || '';
@@ -2862,6 +2891,15 @@ function showAddForm() {
const estimatedDate = addDays(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 = `
<label>🛒 Questo prodotto è...</label>
<div class="purchase-type-selector">
@@ -2888,6 +2926,39 @@ function showAddForm() {
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() {
updateAddQtyStep();
const unit = document.getElementById('add-unit').value;
@@ -3061,6 +3132,7 @@ async function submitAdd(e) {
unit: selectedUnit !== productUnit ? selectedUnit : 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,
vacuum_sealed: document.getElementById('add-vacuum-sealed')?.checked ? 1 : 0,
});
showLoading(false);
BIN
View File
Binary file not shown.
+12 -2
View File
@@ -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=20260315d">
<link rel="stylesheet" href="assets/css/style.css?v=20260315e">
<!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
</head>
@@ -218,6 +218,16 @@
</div>
<div id="add-weight-info" class="form-hint" style="display:none"></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">
<!-- Populated dynamically by showAddForm() -->
</div>
@@ -884,6 +894,6 @@
</div>
</div>
<script src="assets/js/app.js?v=20260315d"></script>
<script src="assets/js/app.js?v=20260315e"></script>
</body>
</html>