From b7ed9899fafab52f8b1f531d48bd323577ec4e95 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Tue, 7 Apr 2026 12:10:14 +0000 Subject: [PATCH] feat+fix: Bring removal, multi-expiry batches, FIFO in cooking steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BRING! REMOVAL FIX (latte/aglio not removed after shopping): - PHP addToInventory: replace exact strcasecmp with token-based fuzzy matching (same logic as _productOnBring) so custom Bring item names and translated catalog keys both match correctly - JS submitAdd: add client-side fallback — if PHP removal missed the item, use _findSimilarItem against the loaded shoppingItems and call bring_remove MULTI-EXPIRY BATCHES (when buying conf with different expiry dates): - Add form (unit=conf): shows '+ Lotto con scadenza diversa' button - Each extra batch has its own qty + expiry date input with +/- controls - On submitAdd, extra batches are submitted as additional inventory_add calls (separate DB rows, separate expiry dates) - Multi-batch section hidden in 'Ce l'avevo già' mode and for non-conf units - Re-shown/hidden when switching unit via onAddUnitChange RECIPE COOKING STEPS - FIFO ingredient display: - renderCookingStep: each ingredient row now shows brand chip, location chip, and expiry date chip (color-coded: red ≤3d, yellow ≤7d) - PHP already selected earliest-expiry inventory entry (ORDER BY days_left ASC with > not >= ensures first/earliest match wins) - CSS: .cooking-ing-meta, .cooking-ing-chip, .exp-soon, .exp-close, .multi-batch-row, .multi-batch-qty, .multi-batch-date, .btn-icon-sm --- api/index.php | 38 +++- assets/css/style.css | 67 +++++++ assets/js/app.js | 121 +++++++++++- data/cron.log | 328 +++++++++++++++++++++++++++++++++ data/dispensa.db | Bin 352256 -> 364544 bytes data/smart_shopping_cache.json | 2 +- 6 files changed, 551 insertions(+), 5 deletions(-) diff --git a/api/index.php b/api/index.php index be1561f..e0a1024 100644 --- a/api/index.php +++ b/api/index.php @@ -667,13 +667,45 @@ function addToInventory(PDO $db): void { $bringKey = italianToBring($prodName); $listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); if ($listData && isset($listData['purchase'])) { + // Token-based matching — same logic as _productOnBring() in smart_shopping + $stop = ['di','del','della','dei','degli','dalle','delle','da','in','con','per', + 'a','e','il','lo','la','i','gli','le','un','uno','una','al','alle','agli','allo']; + $tokenize = function(string $s) use ($stop): array { + $clean = mb_strtolower(preg_replace('/[^\p{L}\s]/u', ' ', $s)); + return array_values(array_filter( + preg_split('/\s+/', trim($clean)), + fn($t) => mb_strlen($t) > 2 && !in_array($t, $stop) + )); + }; + $prodTokens = $tokenize($prodName); + $keyTokens = $tokenize($bringKey); + $prodFirst = $prodTokens[0] ?? ''; + $keyFirst = $keyTokens[0] ?? ''; foreach ($listData['purchase'] as $item) { - if (strcasecmp($item['name'] ?? '', $bringKey) === 0) { - $body = http_build_query(['uuid' => $listUUID, 'remove' => $bringKey]); - bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body); + $rawName = $item['name'] ?? ''; + // 1. Exact match on translated catalog key or original Italian name + if (strcasecmp($rawName, $bringKey) === 0 || strcasecmp($rawName, $prodName) === 0) { + bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", + http_build_query(['uuid' => $listUUID, 'remove' => $rawName])); $removedFromBring = true; break; } + // 2. Token-based fuzzy: first significant word must match + if ($prodFirst || $keyFirst) { + $rawTokens = $tokenize($rawName); + $rawFirst = $rawTokens[0] ?? ''; + if ($rawFirst && ( + $rawFirst === $prodFirst || + $rawFirst === $keyFirst || + in_array($prodFirst, $rawTokens) || + in_array($keyFirst, $rawTokens) + )) { + bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", + http_build_query(['uuid' => $listUUID, 'remove' => $rawName])); + $removedFromBring = true; + break; + } + } } } } diff --git a/assets/css/style.css b/assets/css/style.css index b1e32e3..e769989 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -2671,6 +2671,47 @@ body { font-weight: 600; } +/* ===== MULTI-BATCH EXPIRY (conf products with different expiry dates) ===== */ +.multi-batch-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 8px 10px; + background: rgba(0,0,0,0.04); + border: 1px dashed var(--border); + border-radius: var(--radius-sm); +} + +.multi-batch-qty { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.multi-batch-unit { + font-size: 0.8rem; + color: var(--text-muted); +} + +.multi-batch-date { + flex: 1; + min-width: 0; + font-size: 0.85rem !important; +} + +.btn-icon-sm { + background: none; + border: 1px solid var(--border); + border-radius: 6px; + padding: 4px 8px; + font-size: 0.75rem; + cursor: pointer; + color: var(--text-muted); + flex-shrink: 0; +} + /* ===== PRODUCT DETAILS CARD (Action Page) ===== */ .product-details-card { background: var(--bg-card); @@ -3489,6 +3530,32 @@ body { min-width: 0; } +.cooking-ing-meta { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-top: 5px; +} + +.cooking-ing-chip { + font-size: 0.72rem; + background: rgba(255,255,255,0.12); + border-radius: 5px; + padding: 2px 7px; + color: rgba(255,255,255,0.75); + white-space: nowrap; +} + +.cooking-ing-chip.exp-soon { + background: rgba(239,68,68,0.3); + color: #fca5a5; +} + +.cooking-ing-chip.exp-close { + background: rgba(234,179,8,0.3); + color: #fde047; +} + .cooking-use-btn { flex-shrink: 0; background: #16a34a; diff --git a/assets/js/app.js b/assets/js/app.js index 22b81b3..e0ce1c9 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3269,6 +3269,8 @@ function showAddForm() { // Reset historical expiry for this product; will be fetched async window._historyExpiryDays = null; window._historyExpiryCount = 0; + // Reset extra batches from previous add + window._addExtraBatches = []; // Store base expiry for vacuum recalculation window._addBaseExpiryDays = estimatedDays; @@ -3293,6 +3295,12 @@ function showAddForm() {

📝 Puoi modificare la data o scansionarla con la fotocamera

+
+
+ +
`; showPage('add'); @@ -3406,6 +3414,10 @@ function onAddUnitChange() { // Scroll into view so the user sees the new field if (isConf) setTimeout(() => confRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 100); } + + // Show/hide multi-batch section (only for conf unit) + const mbSection = document.getElementById('multi-batch-section'); + if (mbSection) mbSection.style.display = unit === 'conf' ? 'block' : 'none'; // If switching units, suggest a sensible quantity // BUT only if the user hasn't manually changed the quantity in this form @@ -3457,6 +3469,11 @@ function selectPurchaseType(btn, type) { btn.parentElement.querySelectorAll('.purchase-type-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); + // Reset extra batches when switching purchase type + window._addExtraBatches = []; + const mbContainer = document.getElementById('multi-batch-container'); + if (mbContainer) mbContainer.innerHTML = ''; + const detailDiv = document.getElementById('expiry-detail'); // Save current quantity before switching, so we can preserve it @@ -3489,6 +3506,9 @@ function selectPurchaseType(btn, type) { `; // Restore quantity - switching purchase type should NOT change it document.getElementById('add-quantity').value = currentQty; + // Show multi-batch section only in "new" mode (and only for conf unit) + const mbSection = document.getElementById('multi-batch-section'); + if (mbSection) mbSection.style.display = (document.getElementById('add-unit')?.value === 'conf') ? 'block' : 'none'; } else { detailDiv.innerHTML = `
@@ -3511,6 +3531,9 @@ function selectPurchaseType(btn, type) {
`; // DON'T auto-set remaining percentage - keep the quantity the user already entered + // Hide multi-batch section in "existing" mode + const mbSection = document.getElementById('multi-batch-section'); + if (mbSection) mbSection.style.display = 'none'; } } @@ -3528,6 +3551,50 @@ function setRemainingPct(pct) { document.getElementById('add-quantity').value = adjustedQty; } +// ===== MULTI-EXPIRY BATCHES (for conf products with different expiry dates) ===== +window._addExtraBatches = []; + +function addExpiryBatch() { + const loc = document.getElementById('add-location')?.value || ''; + const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc); + const estimatedDate = addDays(baseDays); + window._addExtraBatches.push({ qty: 1, expiry: estimatedDate }); + _rebuildMultiBatchUI(); +} + +function removeExpiryBatch(i) { + window._addExtraBatches.splice(i, 1); + _rebuildMultiBatchUI(); +} + +function adjustBatchQty(i, delta) { + window._addExtraBatches[i].qty = Math.max(1, (window._addExtraBatches[i].qty || 1) + delta); + _rebuildMultiBatchUI(); +} + +function _rebuildMultiBatchUI() { + const container = document.getElementById('multi-batch-container'); + if (!container) return; + if (window._addExtraBatches.length === 0) { + container.innerHTML = ''; + return; + } + container.innerHTML = window._addExtraBatches.map((b, i) => ` +
+
+ + + + conf +
+ + +
+ `).join(''); +} + function selectLocation(btn, loc) { btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); @@ -3583,8 +3650,46 @@ async function submitAdd(e) { showToast(`✅ ${currentProduct.name} aggiunto!${qtyInfo}`, 'success'); if (result.removed_from_bring) { setTimeout(() => showToast('🛒 Rimosso dalla lista della spesa', 'info'), 1500); + } else if (shoppingItems.length > 0 && shoppingListUUID) { + // PHP matching may have missed the item (custom name / no catalog match) — + // try a client-side fuzzy remove using the already-loaded shoppingItems + const match = _findSimilarItem(currentProduct.name, shoppingItems); + if (match) { + api('bring_remove', {}, 'POST', { + name: match.name, + rawName: match.rawName || '', + listUUID: shoppingListUUID + }).then(r => { + if (r && r.success) { + shoppingItems = shoppingItems.filter(i => i !== match); + setTimeout(() => showToast('🛒 Rimosso dalla lista della spesa', 'info'), 1500); + } + }).catch(() => {}); + } } if (!spesaModeAfterAdd()) showPage('dashboard'); + + // Submit extra batches (different expiry dates) in the background, silently + if ((window._addExtraBatches || []).length > 0) { + const loc = document.getElementById('add-location')?.value || result.location || 'dispensa'; + const selectedUnit = document.getElementById('add-unit').value; + const productUnit = currentProduct.unit || 'pz'; + const confUnit = document.getElementById('add-conf-unit')?.value || null; + const confSize = parseFloat(document.getElementById('add-conf-size')?.value) || null; + for (const batch of window._addExtraBatches) { + if (!batch.qty || batch.qty <= 0) continue; + api('inventory_add', {}, 'POST', { + product_id: currentProduct.id, + quantity: batch.qty, + location: loc, + expiry_date: batch.expiry || null, + unit: selectedUnit !== productUnit ? selectedUnit : null, + package_unit: selectedUnit === 'conf' ? confUnit : null, + package_size: selectedUnit === 'conf' ? confSize : null, + }).catch(() => {}); + } + window._addExtraBatches = []; + } } else { showToast(result.error || 'Errore', 'error'); } @@ -6911,11 +7016,25 @@ function renderCookingStep() { const ingsEl = document.getElementById('cooking-step-ings'); if (ings.length > 0) { + const LOC_LABELS = { dispensa: '🏠 Dispensa', frigo: '❄️ Frigo', freezer: '🧊 Freezer' }; ingsEl.innerHTML = ings.map(ing => { const loc = (ing.location || 'dispensa').replace(/'/g, "\\'"); const qtyNum = ing.qty_number || 0; + // Build info chips: brand, location, expiry + const chips = []; + if (ing.brand) chips.push(`${escapeHtml(ing.brand)}`); + const locLabel = LOC_LABELS[ing.location] || (ing.location ? `📍 ${ing.location}` : '🏠 Dispensa'); + chips.push(`${locLabel}`); + if (ing.expiry_date) { + const daysLeft = Math.round((new Date(ing.expiry_date) - new Date()) / 86400000); + const expClass = daysLeft <= 3 ? 'exp-soon' : daysLeft <= 7 ? 'exp-close' : ''; + chips.push(`📅 scade ${formatDate(ing.expiry_date)}`); + } return `
- 📦 ${escapeHtml(ing.name)}: ${escapeHtml(ing.qty)} +
+ 📦 ${escapeHtml(ing.name)}: ${escapeHtml(ing.qty)} +
${chips.join('')}
+
`; }).join(''); diff --git a/data/cron.log b/data/cron.log index 89cd6e4..b724db9 100644 --- a/data/cron.log +++ b/data/cron.log @@ -1474,3 +1474,331 @@ [2026-04-06 08:40:02] OK — 13 items cached [2026-04-06 08:45:02] OK — 13 items cached [2026-04-06 08:50:01] OK — 13 items cached +[2026-04-06 08:55:02] OK — 12 items cached +[2026-04-06 09:00:02] OK — 13 items cached +[2026-04-06 09:05:01] OK — 13 items cached +[2026-04-06 09:10:02] OK — 13 items cached +[2026-04-06 09:15:02] OK — 12 items cached +[2026-04-06 09:20:01] OK — 12 items cached +[2026-04-06 09:25:02] OK — 12 items cached +[2026-04-06 09:30:02] OK — 12 items cached +[2026-04-06 09:35:01] OK — 12 items cached +[2026-04-06 09:40:02] OK — 12 items cached +[2026-04-06 09:45:02] OK — 12 items cached +[2026-04-06 09:50:01] OK — 12 items cached +[2026-04-06 09:55:02] OK — 12 items cached +[2026-04-06 10:00:02] OK — 12 items cached +[2026-04-06 10:05:01] OK — 12 items cached +[2026-04-06 10:10:02] OK — 12 items cached +[2026-04-06 10:15:02] OK — 12 items cached +[2026-04-06 10:20:02] OK — 12 items cached +[2026-04-06 10:25:01] OK — 12 items cached +[2026-04-06 10:30:02] OK — 11 items cached +[2026-04-06 10:35:02] OK — 11 items cached +[2026-04-06 10:40:01] OK — 11 items cached +[2026-04-06 10:45:02] OK — 29 items cached +[2026-04-06 10:50:02] OK — 29 items cached +[2026-04-06 10:55:01] OK — 29 items cached +[2026-04-06 11:00:02] OK — 29 items cached +[2026-04-06 11:05:02] OK — 29 items cached +[2026-04-06 11:10:01] OK — 29 items cached +[2026-04-06 11:15:03] OK — 29 items cached +[2026-04-06 11:20:01] OK — 29 items cached +[2026-04-06 11:25:02] OK — 29 items cached +[2026-04-06 11:30:02] OK — 29 items cached +[2026-04-06 11:35:01] OK — 29 items cached +[2026-04-06 11:40:02] OK — 29 items cached +[2026-04-06 11:45:02] OK — 29 items cached +[2026-04-06 11:50:01] OK — 29 items cached +[2026-04-06 11:55:02] OK — 29 items cached +[2026-04-06 12:00:02] OK — 29 items cached +[2026-04-06 12:05:02] OK — 29 items cached +[2026-04-06 12:10:01] OK — 29 items cached +[2026-04-06 12:15:02] OK — 29 items cached +[2026-04-06 12:20:02] OK — 29 items cached +[2026-04-06 12:25:02] OK — 29 items cached +[2026-04-06 12:30:01] OK — 29 items cached +[2026-04-06 12:35:02] OK — 29 items cached +[2026-04-06 12:40:01] OK — 29 items cached +[2026-04-06 12:45:02] OK — 29 items cached +[2026-04-06 12:50:02] OK — 29 items cached +[2026-04-06 12:55:01] OK — 29 items cached +[2026-04-06 13:00:02] OK — 29 items cached +[2026-04-06 13:05:02] OK — 29 items cached +[2026-04-06 13:10:01] OK — 29 items cached +[2026-04-06 13:15:03] OK — 29 items cached +[2026-04-06 13:20:01] OK — 29 items cached +[2026-04-06 13:25:02] OK — 29 items cached +[2026-04-06 13:30:01] OK — 29 items cached +[2026-04-06 13:35:01] OK — 29 items cached +[2026-04-06 13:40:02] OK — 29 items cached +[2026-04-06 13:45:01] OK — 29 items cached +[2026-04-06 13:50:02] OK — 29 items cached +[2026-04-06 13:55:02] OK — 29 items cached +[2026-04-06 14:00:01] OK — 29 items cached +[2026-04-06 14:05:02] OK — 29 items cached +[2026-04-06 14:10:02] OK — 29 items cached +[2026-04-06 14:15:02] OK — 29 items cached +[2026-04-06 14:20:02] OK — 29 items cached +[2026-04-06 14:25:01] OK — 29 items cached +[2026-04-06 14:30:02] OK — 29 items cached +[2026-04-06 14:35:02] OK — 29 items cached +[2026-04-06 14:40:01] OK — 29 items cached +[2026-04-06 14:45:01] OK — 29 items cached +[2026-04-06 14:50:02] OK — 29 items cached +[2026-04-06 14:55:02] OK — 29 items cached +[2026-04-06 15:00:07] OK — 29 items cached +[2026-04-06 15:05:01] OK — 29 items cached +[2026-04-06 15:10:02] OK — 29 items cached +[2026-04-06 15:15:02] OK — 29 items cached +[2026-04-06 15:20:02] OK — 29 items cached +[2026-04-06 15:25:02] OK — 29 items cached +[2026-04-06 15:30:02] OK — 29 items cached +[2026-04-06 15:35:01] OK — 29 items cached +[2026-04-06 15:40:02] OK — 29 items cached +[2026-04-06 15:45:02] OK — 29 items cached +[2026-04-06 15:50:01] OK — 29 items cached +[2026-04-06 15:55:02] OK — 29 items cached +[2026-04-06 16:00:02] OK — 29 items cached +[2026-04-06 16:05:02] OK — 29 items cached +[2026-04-06 16:10:01] OK — 29 items cached +[2026-04-06 16:15:02] OK — 29 items cached +[2026-04-06 16:20:02] OK — 29 items cached +[2026-04-06 16:25:02] OK — 29 items cached +[2026-04-06 16:30:01] OK — 29 items cached +[2026-04-06 16:35:01] OK — 29 items cached +[2026-04-06 16:40:02] OK — 29 items cached +[2026-04-06 16:45:02] OK — 29 items cached +[2026-04-06 16:50:01] OK — 29 items cached +[2026-04-06 16:55:02] OK — 29 items cached +[2026-04-06 17:00:02] OK — 29 items cached +[2026-04-06 17:05:02] OK — 29 items cached +[2026-04-06 17:10:01] OK — 29 items cached +[2026-04-06 17:15:02] OK — 29 items cached +[2026-04-06 17:20:02] OK — 29 items cached +[2026-04-06 17:25:02] OK — 29 items cached +[2026-04-06 17:30:01] OK — 29 items cached +[2026-04-06 17:35:02] OK — 29 items cached +[2026-04-06 17:40:02] OK — 29 items cached +[2026-04-06 17:45:01] OK — 29 items cached +[2026-04-06 17:50:02] OK — 29 items cached +[2026-04-06 17:55:02] OK — 29 items cached +[2026-04-06 18:00:01] OK — 29 items cached +[2026-04-06 18:05:02] OK — 29 items cached +[2026-04-06 18:10:02] OK — 29 items cached +[2026-04-06 18:15:02] OK — 29 items cached +[2026-04-06 18:20:01] OK — 29 items cached +[2026-04-06 18:25:02] OK — 29 items cached +[2026-04-06 18:30:02] OK — 29 items cached +[2026-04-06 18:35:01] OK — 29 items cached +[2026-04-06 18:40:02] OK — 29 items cached +[2026-04-06 18:45:02] OK — 29 items cached +[2026-04-06 18:50:01] OK — 29 items cached +[2026-04-06 18:55:02] OK — 29 items cached +[2026-04-06 19:00:02] OK — 29 items cached +[2026-04-06 19:05:02] OK — 29 items cached +[2026-04-06 19:10:01] OK — 29 items cached +[2026-04-06 19:15:02] OK — 29 items cached +[2026-04-06 19:20:02] OK — 29 items cached +[2026-04-06 19:25:02] OK — 29 items cached +[2026-04-06 19:30:02] OK — 29 items cached +[2026-04-06 19:35:01] OK — 29 items cached +[2026-04-06 19:40:02] OK — 29 items cached +[2026-04-06 19:45:02] OK — 29 items cached +[2026-04-06 19:50:01] OK — 29 items cached +[2026-04-06 19:55:02] OK — 29 items cached +[2026-04-06 20:00:02] OK — 29 items cached +[2026-04-06 20:05:01] OK — 29 items cached +[2026-04-06 20:10:02] OK — 29 items cached +[2026-04-06 20:15:02] OK — 29 items cached +[2026-04-06 20:20:02] OK — 29 items cached +[2026-04-06 20:25:02] OK — 29 items cached +[2026-04-06 20:30:01] OK — 29 items cached +[2026-04-06 20:35:02] OK — 29 items cached +[2026-04-06 20:40:02] OK — 29 items cached +[2026-04-06 20:45:01] OK — 29 items cached +[2026-04-06 20:50:02] OK — 29 items cached +[2026-04-06 20:55:01] OK — 29 items cached +[2026-04-06 21:00:02] OK — 29 items cached +[2026-04-06 21:05:02] OK — 29 items cached +[2026-04-06 21:10:01] OK — 29 items cached +[2026-04-06 21:15:02] OK — 29 items cached +[2026-04-06 21:20:02] OK — 29 items cached +[2026-04-06 21:25:02] OK — 29 items cached +[2026-04-06 21:30:01] OK — 29 items cached +[2026-04-06 21:35:02] OK — 29 items cached +[2026-04-06 21:40:02] OK — 29 items cached +[2026-04-06 21:45:01] OK — 29 items cached +[2026-04-06 21:50:02] OK — 29 items cached +[2026-04-06 21:55:02] OK — 29 items cached +[2026-04-06 22:00:02] OK — 29 items cached +[2026-04-06 22:05:01] OK — 29 items cached +[2026-04-06 22:10:02] OK — 29 items cached +[2026-04-06 22:15:02] OK — 29 items cached +[2026-04-06 22:20:02] OK — 29 items cached +[2026-04-06 22:25:01] OK — 29 items cached +[2026-04-06 22:30:02] OK — 29 items cached +[2026-04-06 22:35:01] OK — 29 items cached +[2026-04-06 22:40:02] OK — 29 items cached +[2026-04-06 22:45:02] OK — 29 items cached +[2026-04-06 22:50:02] OK — 29 items cached +[2026-04-06 22:55:01] OK — 29 items cached +[2026-04-06 23:00:02] OK — 29 items cached +[2026-04-06 23:05:01] OK — 29 items cached +[2026-04-06 23:10:02] OK — 29 items cached +[2026-04-06 23:15:02] OK — 29 items cached +[2026-04-06 23:20:02] OK — 29 items cached +[2026-04-06 23:25:02] OK — 29 items cached +[2026-04-06 23:30:01] OK — 29 items cached +[2026-04-06 23:35:02] OK — 29 items cached +[2026-04-06 23:40:02] OK — 29 items cached +[2026-04-06 23:45:01] OK — 29 items cached +[2026-04-06 23:50:02] OK — 29 items cached +[2026-04-06 23:55:02] OK — 29 items cached +[2026-04-07 00:00:01] OK — 30 items cached +[2026-04-07 00:05:02] OK — 30 items cached +[2026-04-07 00:10:01] OK — 30 items cached +[2026-04-07 00:15:03] OK — 30 items cached +[2026-04-07 00:20:01] OK — 30 items cached +[2026-04-07 00:25:02] OK — 30 items cached +[2026-04-07 00:30:02] OK — 30 items cached +[2026-04-07 00:35:02] OK — 30 items cached +[2026-04-07 00:40:01] OK — 30 items cached +[2026-04-07 00:45:02] OK — 30 items cached +[2026-04-07 00:50:01] OK — 30 items cached +[2026-04-07 00:55:01] OK — 30 items cached +[2026-04-07 01:00:02] OK — 30 items cached +[2026-04-07 01:05:01] OK — 30 items cached +[2026-04-07 01:10:02] OK — 30 items cached +[2026-04-07 01:15:02] OK — 30 items cached +[2026-04-07 01:20:02] OK — 30 items cached +[2026-04-07 01:25:01] OK — 30 items cached +[2026-04-07 01:30:02] OK — 30 items cached +[2026-04-07 01:35:02] OK — 30 items cached +[2026-04-07 01:40:01] OK — 30 items cached +[2026-04-07 01:45:02] OK — 30 items cached +[2026-04-07 01:50:02] OK — 30 items cached +[2026-04-07 01:55:02] OK — 30 items cached +[2026-04-07 02:00:01] OK — 30 items cached +[2026-04-07 02:05:02] OK — 30 items cached +[2026-04-07 02:10:02] OK — 30 items cached +[2026-04-07 02:15:02] OK — 30 items cached +[2026-04-07 02:20:01] OK — 30 items cached +[2026-04-07 02:25:02] OK — 30 items cached +[2026-04-07 02:30:02] OK — 30 items cached +[2026-04-07 02:35:01] OK — 30 items cached +[2026-04-07 02:40:02] OK — 30 items cached +[2026-04-07 02:45:01] OK — 30 items cached +[2026-04-07 02:50:02] OK — 30 items cached +[2026-04-07 02:55:02] OK — 30 items cached +[2026-04-07 03:00:02] OK — 30 items cached +[2026-04-07 03:05:02] OK — 30 items cached +[2026-04-07 03:10:01] OK — 30 items cached +[2026-04-07 03:15:02] OK — 30 items cached +[2026-04-07 03:20:02] OK — 30 items cached +[2026-04-07 03:25:02] OK — 30 items cached +[2026-04-07 03:30:01] OK — 30 items cached +[2026-04-07 03:35:02] OK — 30 items cached +[2026-04-07 03:40:02] OK — 30 items cached +[2026-04-07 03:45:01] OK — 30 items cached +[2026-04-07 03:50:02] OK — 30 items cached +[2026-04-07 03:55:02] OK — 30 items cached +[2026-04-07 04:00:01] OK — 30 items cached +[2026-04-07 04:05:01] OK — 30 items cached +[2026-04-07 04:10:02] OK — 30 items cached +[2026-04-07 04:15:02] OK — 30 items cached +[2026-04-07 04:20:02] OK — 30 items cached +[2026-04-07 04:25:01] OK — 30 items cached +[2026-04-07 04:30:02] OK — 30 items cached +[2026-04-07 04:35:02] OK — 30 items cached +[2026-04-07 04:40:01] OK — 30 items cached +[2026-04-07 04:45:02] OK — 30 items cached +[2026-04-07 04:50:02] OK — 30 items cached +[2026-04-07 04:55:01] OK — 31 items cached +[2026-04-07 05:00:02] OK — 31 items cached +[2026-04-07 05:05:02] OK — 31 items cached +[2026-04-07 05:10:02] OK — 31 items cached +[2026-04-07 05:15:02] OK — 31 items cached +[2026-04-07 05:20:02] OK — 31 items cached +[2026-04-07 05:25:01] OK — 31 items cached +[2026-04-07 05:30:02] OK — 31 items cached +[2026-04-07 05:35:02] OK — 31 items cached +[2026-04-07 05:40:01] OK — 31 items cached +[2026-04-07 05:45:02] OK — 31 items cached +[2026-04-07 05:50:01] OK — 31 items cached +[2026-04-07 05:55:02] OK — 31 items cached +[2026-04-07 06:00:02] OK — 31 items cached +[2026-04-07 06:05:02] OK — 31 items cached +[2026-04-07 06:10:01] OK — 31 items cached +[2026-04-07 06:15:02] OK — 31 items cached +[2026-04-07 06:20:02] OK — 31 items cached +[2026-04-07 06:25:02] OK — 31 items cached +[2026-04-07 06:30:01] OK — 31 items cached +[2026-04-07 06:35:02] OK — 31 items cached +[2026-04-07 06:40:02] OK — 31 items cached +[2026-04-07 06:45:01] OK — 31 items cached +[2026-04-07 06:50:02] OK — 31 items cached +[2026-04-07 06:55:02] OK — 31 items cached +[2026-04-07 07:00:02] OK — 31 items cached +[2026-04-07 07:05:01] OK — 31 items cached +[2026-04-07 07:10:02] OK — 31 items cached +[2026-04-07 07:15:02] OK — 31 items cached +[2026-04-07 07:20:02] OK — 31 items cached +[2026-04-07 07:25:01] OK — 31 items cached +[2026-04-07 07:30:02] OK — 31 items cached +[2026-04-07 07:35:01] OK — 31 items cached +[2026-04-07 07:40:02] OK — 31 items cached +[2026-04-07 07:45:02] OK — 31 items cached +[2026-04-07 07:50:01] OK — 31 items cached +[2026-04-07 07:55:02] OK — 31 items cached +[2026-04-07 08:00:02] OK — 31 items cached +[2026-04-07 08:05:01] OK — 31 items cached +[2026-04-07 08:10:02] OK — 31 items cached +[2026-04-07 08:15:02] OK — 31 items cached +[2026-04-07 08:20:02] OK — 31 items cached +[2026-04-07 08:25:02] OK — 31 items cached +[2026-04-07 08:30:01] OK — 31 items cached +[2026-04-07 08:35:02] OK — 31 items cached +[2026-04-07 08:40:02] OK — 31 items cached +[2026-04-07 08:45:01] OK — 31 items cached +[2026-04-07 08:50:02] OK — 31 items cached +[2026-04-07 08:55:01] OK — 31 items cached +[2026-04-07 09:00:02] OK — 31 items cached +[2026-04-07 09:05:02] OK — 31 items cached +[2026-04-07 09:10:01] OK — 31 items cached +[2026-04-07 09:15:03] OK — 31 items cached +[2026-04-07 09:20:01] OK — 31 items cached +[2026-04-07 09:25:02] OK — 31 items cached +[2026-04-07 09:30:02] OK — 31 items cached +[2026-04-07 09:35:01] OK — 31 items cached +[2026-04-07 09:40:02] OK — 31 items cached +[2026-04-07 09:45:01] OK — 31 items cached +[2026-04-07 09:50:02] OK — 31 items cached +[2026-04-07 09:55:02] OK — 31 items cached +[2026-04-07 10:00:01] OK — 31 items cached +[2026-04-07 10:05:02] OK — 31 items cached +[2026-04-07 10:10:02] OK — 31 items cached +[2026-04-07 10:15:02] OK — 31 items cached +[2026-04-07 10:20:01] OK — 31 items cached +[2026-04-07 10:25:02] OK — 31 items cached +[2026-04-07 10:30:02] OK — 31 items cached +[2026-04-07 10:35:01] OK — 31 items cached +[2026-04-07 10:40:02] OK — 31 items cached +[2026-04-07 10:45:02] OK — 31 items cached +[2026-04-07 10:50:01] OK — 31 items cached +[2026-04-07 10:55:02] OK — 31 items cached +[2026-04-07 11:00:02] OK — 31 items cached +[2026-04-07 11:05:01] OK — 31 items cached +[2026-04-07 11:10:02] OK — 31 items cached +[2026-04-07 11:15:02] OK — 31 items cached +[2026-04-07 11:20:02] OK — 31 items cached +[2026-04-07 11:25:01] OK — 31 items cached +[2026-04-07 11:30:02] OK — 31 items cached +[2026-04-07 11:35:02] OK — 31 items cached +[2026-04-07 11:40:01] OK — 31 items cached +[2026-04-07 11:45:02] OK — 31 items cached +[2026-04-07 11:50:01] OK — 31 items cached +[2026-04-07 11:55:02] OK — 31 items cached +[2026-04-07 12:00:02] OK — 31 items cached +[2026-04-07 12:05:02] OK — 31 items cached +[2026-04-07 12:10:01] OK — 30 items cached diff --git a/data/dispensa.db b/data/dispensa.db index 753240d2f8b963d4f8bb845dfadb0b09e519bcf4..d8a1b6c4e551ebb033b767a21cdd781d9e59c310 100644 GIT binary patch delta 6343 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 Kghopyp#KL?PrYsc delta 1907 zcmcIki*r@Q8Q*XB?Ai0!b59ijaCvMx$PuY2ua3N zs|Cc?Mr}go081;6R2U*C2ua`=iK8hqDz&IoYl~pz(JG8ktN{!OlI|rX@(*;rGjq=Q zeZSrBH^2S%Th%st)mx+1Cem_*(0#4mFqH3GyxmSV?YAX>7TAOK75hK-dHYNIFZOBs zGy8A$pY69hV@um@=WxnQvQOFQFmLK@2C{#nCYfRL6ScrDq!-jI)#DU+8=Tj@lbkQS z>CWi!Q|{1+@`mbK70MOQkFC$thxHRIz+%la+FY~TYPY&QCH8xrHRGSgPF_q;=f*qC z#5?!JQj&@r>Xt04UsBmny?j|c9JaqfMpPU^Tp@|LE>c-t8>vU#9!yej^|D7J%Nmx~ zJ%;voejZNs{$CYV{mdkdJmRq0i*Y<6qwAJ9)9H^{WIB;2_QU4R#3!dK(`~~9vF8LCGfw7G6G#vTe1WVqUog!d^!hV$f|)s)*=Tb|+iMYj zG?ks^n~~-VjF9|Ze`ZdG6kdf2H}4XR1BEuBVwos#U%3o@ zAbvFn#fl0jD6#3{$RHFdjG-~D`J+VLcThOvq1EFIgl}i#gF!8bR{x}~`lrkD;)l9J zFWiHMUCc`J|IfUPG+(CImz5KY`YG}sr}3kQDYqmtgVHw0ZodjKAl&buMB%?hcS$T)+&Ks(qUAa~r&tEckdRMImITc+5oysw6w~|RsOTAj z0>uuYnUat#&VCC;qHjPh+$xnSk`V8go>|2Q5RP7$P2K@WqCLZn5>T)cD<1r zZrK9tfEYLn21d`h>MT6SS8BV^ddNk*Mr+l6u2pEWwKVNEHpou1!)z}T?3XOUe#C-o zECcnd`hj{t-K;*XKCIrO&QQH7qyMIVm7TDSK1)~7G8(26sHI#|jw^pub}DO?Mx|US zRHiBMWPqF^?~+%^2J$4SB6CTAj3Eerxe~vR_v1}?4X(lSaR{ei6)wQXa1dUECa8t` zA(zkRA)dlj^Md)Yc~B0BCbQPO-^?{rO>XoVpBlZg`C5!ejYUR*G1Z9Euj(iDcl15_ zdi@E#QlF#y^#pB5`$9XSb;$w$zP;bxWUsMn?D=-c7C$V-!5Hn3wkw3rB}6@sUR2Tg z6pnXaF2naSgRi+chuwcH#D|EOya4m4?Ng=g6NimGdV)+RV{X0T0pE>o%g)INLEM4G z_$6#)d3~Xr%z%8J=~#;0j-{mZ5w%~spN~XODPAbrJ|uI*v{L-MxY$UO+<8Yy1rwDg z$lPePfUK4tt!BBUkI}uV$UjQrowv5?&ybVEB_|c|krbm>uoxQd^vC?daC^tnSv0|5 zvB>i$OYo3cZ4~HV=}nr@GFhzTTvpxKiL@^P`dpev-vci{rtYHU{3CvZzr%i~ZsWcD zpd9tP`3}~uRx20Xuk-0E0OOt2Unjc