🫙 Sotto vuoto: flag per estendere scadenza prodotti conservati sotto vuoto
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Binary file not shown.
+12
-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=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>
|
||||
|
||||
Reference in New Issue
Block a user