diff --git a/backend/app/main.py b/backend/app/main.py index 52156e5..d2064c3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Depends, HTTPException, APIRouter, status, Response from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session +from sqlalchemy import func from typing import List, Optional, Dict, Any from datetime import datetime, timedelta, date import math @@ -263,6 +264,34 @@ class DrugWithVariantsResponse(BaseModel): class Config: from_attributes = True + +class VariantSummaryResponse(BaseModel): + """Lightweight variant summary returned by GET /drugs list — no packs or batches.""" + id: int + drug_id: int + strength: str + quantity: float + unit: str + low_stock_threshold: float + has_inventory_history: bool = False + expired_quantity: float = 0.0 + + class Config: + from_attributes = True + + +class DrugSummaryResponse(BaseModel): + """Lightweight drug summary for the main list — variants without packs or batches.""" + id: int + name: str + description: Optional[str] = None + is_controlled: bool = False + locations: List[str] = [] + variants: List[VariantSummaryResponse] = [] + + class Config: + from_attributes = True + class DispensingAllocationCreate(BaseModel): batch_id: int quantity: float @@ -399,6 +428,40 @@ def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, return variant_dict +def serialize_variant_with_packs(db: Session, variant: DrugVariant) -> Dict[str, Any]: + """Return variant data with packs but without batch details (level-2 detail).""" + has_batch_history = ( + db.query(Batch.id) + .filter(Batch.drug_variant_id == variant.id) + .first() + is not None + ) + has_dispense_history = ( + db.query(Dispensing.id) + .filter(Dispensing.drug_variant_id == variant.id) + .first() + is not None + ) + packs = ( + db.query(VariantPack) + .filter(VariantPack.drug_variant_id == variant.id) + .order_by(VariantPack.is_active.desc(), VariantPack.id.asc()) + .all() + ) + return { + "id": variant.id, + "drug_id": variant.drug_id, + "strength": variant.strength, + "quantity": variant.quantity, + "unit": variant.unit, + "base_unit": variant.unit, + "low_stock_threshold": variant.low_stock_threshold, + "has_inventory_history": has_batch_history or has_dispense_history, + "packs": [serialize_variant_pack(pack) for pack in packs], + "batches": [], + } + + def serialize_batch_response(db: Session, batch: Batch) -> Dict[str, Any]: location = db.query(Location).filter(Location.id == batch.location_id).first() pack = None @@ -866,21 +929,112 @@ def admin_change_password(user_id: int, password_data: AdminPasswordChange, db: def read_root(): return {"message": "Drug Inventory API"} -@router.get("/drugs", response_model=List[DrugWithVariantsResponse]) +@router.get("/drugs", response_model=List[DrugSummaryResponse]) def list_drugs(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - """Get all drugs with their variants""" + """Get all drugs with lightweight variant summaries (no packs or batches).""" drugs = db.query(Drug).all() + if not drugs: + return [] + + drug_ids = [d.id for d in drugs] + + all_variants = ( + db.query(DrugVariant) + .filter(DrugVariant.drug_id.in_(drug_ids)) + .all() + ) + variants_by_drug: Dict[int, list] = {} + for v in all_variants: + variants_by_drug.setdefault(v.drug_id, []).append(v) + + variant_ids = [v.id for v in all_variants] + if not variant_ids: + return [ + { + "id": d.id, + "name": d.name, + "description": d.description, + "is_controlled": bool(d.is_controlled), + "locations": [], + "variants": [], + } + for d in drugs + ] + + # Variant IDs that have ever had a batch received + batch_history_ids = set( + row[0] + for row in db.query(Batch.drug_variant_id) + .filter(Batch.drug_variant_id.in_(variant_ids)) + .distinct() + .all() + ) + + # Variant IDs that have ever had a dispense + dispense_history_ids = set( + row[0] + for row in db.query(Dispensing.drug_variant_id) + .filter(Dispensing.drug_variant_id.in_(variant_ids)) + .distinct() + .all() + ) + + # Sum of expired active batch stock per variant + today = date.today() + expired_rows = ( + db.query(Batch.drug_variant_id, func.sum(Batch.quantity)) + .filter( + Batch.drug_variant_id.in_(variant_ids), + Batch.quantity > 0, + Batch.expiry_date < today, + ) + .group_by(Batch.drug_variant_id) + .all() + ) + expired_by_variant: Dict[int, float] = {row[0]: float(row[1]) for row in expired_rows} + + # Distinct location names per drug (from active batch stock) + location_rows = ( + db.query(DrugVariant.drug_id, Location.name) + .join(Batch, Batch.drug_variant_id == DrugVariant.id) + .join(Location, Location.id == Batch.location_id) + .filter( + DrugVariant.drug_id.in_(drug_ids), + Batch.quantity > 0, + ) + .distinct() + .all() + ) + locations_by_drug: Dict[int, list] = {} + for drug_id, loc_name in location_rows: + locations_by_drug.setdefault(drug_id, []).append(loc_name) + result = [] for drug in drugs: - variants = db.query(DrugVariant).filter(DrugVariant.drug_id == drug.id).all() - drug_dict = { - "id": drug.id, - "name": drug.name, - "description": drug.description, - "is_controlled": bool(drug.is_controlled), - "variants": [enrich_variant_with_batches(db, v) for v in variants], - } - result.append(drug_dict) + drug_variants = variants_by_drug.get(drug.id, []) + variant_summaries = [ + { + "id": v.id, + "drug_id": v.drug_id, + "strength": v.strength, + "quantity": v.quantity, + "unit": v.unit, + "low_stock_threshold": v.low_stock_threshold, + "has_inventory_history": v.id in batch_history_ids or v.id in dispense_history_ids, + "expired_quantity": expired_by_variant.get(v.id, 0.0), + } + for v in drug_variants + ] + result.append( + { + "id": drug.id, + "name": drug.name, + "description": drug.description, + "is_controlled": bool(drug.is_controlled), + "locations": locations_by_drug.get(drug.id, []), + "variants": variant_summaries, + } + ) return result @router.get("/drugs/low-stock", response_model=List[DrugWithVariantsResponse]) @@ -910,18 +1064,18 @@ def low_stock_drugs(db: Session = Depends(get_db), current_user: User = Depends( @router.get("/drugs/{drug_id}", response_model=DrugWithVariantsResponse) def get_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - """Get a specific drug with its variants""" + """Get a specific drug with its variants and packs (no batch detail — level-2 detail).""" drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") - + variants = db.query(DrugVariant).filter(DrugVariant.drug_id == drug.id).all() drug_dict = { "id": drug.id, "name": drug.name, "description": drug.description, "is_controlled": bool(drug.is_controlled), - "variants": [enrich_variant_with_batches(db, v) for v in variants], + "variants": [serialize_variant_with_packs(db, v) for v in variants], } return drug_dict diff --git a/frontend/app.js b/frontend/app.js index 27e25cb..3227e9c 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,5 +1,7 @@ const API_URL = '/api'; -let allDrugs = []; +let allDrugs = []; // level-1 summaries: no packs, no batches +let loadedDrugDetails = new Map(); // drugId → full drug with variant packs (level-2) +let loadedVariantBatches = new Map(); // variantId → Batch[] (level-3) let currentDrug = null; let showLowStockOnly = false; let selectedLocationFilter = ''; @@ -450,9 +452,21 @@ function setupEventListeners() { }); // User menu - if (userMenuBtn) userMenuBtn.addEventListener('click', () => { + if (userMenuBtn) userMenuBtn.addEventListener('click', (e) => { + e.stopPropagation(); const dropdown = document.getElementById('userDropdown'); - if (dropdown) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; + if (dropdown) { + const isHidden = getComputedStyle(dropdown).display === 'none'; + dropdown.style.display = isHidden ? 'block' : 'none'; + } + }); + + document.addEventListener('click', (e) => { + const dropdown = document.getElementById('userDropdown'); + const btn = document.getElementById('userMenuBtn'); + if (dropdown && btn && !btn.contains(e.target) && !dropdown.contains(e.target)) { + dropdown.style.display = 'none'; + } }); if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal); @@ -532,22 +546,24 @@ async function loadDrugs() { const newVariant = drug?.variants?.find(v => !restore._existingVariantIds.has(v.id)); if (newVariant) { restore.variantId = newVariant.id; - // If no pack snapshot, all packs are new — pick the first active one + // If no pack snapshot, all packs are new — pick the first active one from detail if (!restore._existingPackIds) { - const firstPack = getActivePacksForVariant(newVariant)?.[0]; + await ensureDrugDetailLoaded(restore.drugId); + const detailVariant = getVariantById(newVariant.id); + const firstPack = getActivePacksForVariant(detailVariant)?.[0]; if (firstPack) restore.packId = firstPack.id; } } } - // Resolve new pack by diffing (add-pack flow) + // Resolve new pack by diffing (add-pack flow) — need packs from loaded detail if (restore._existingPackIds && restore.drugId && restore.variantId) { - const drug = allDrugs.find(d => d.id === restore.drugId); - const variant = drug?.variants?.find(v => v.id === restore.variantId); + await ensureDrugDetailLoaded(restore.drugId); + const variant = getVariantById(restore.variantId); const newPack = getActivePacksForVariant(variant)?.find(p => !restore._existingPackIds.has(p.id)); if (newPack) restore.packId = newPack.id; } - _reinitGtinMappingModal(restore); + await _reinitGtinMappingModal(restore); } // After handleAddDrug's loadDrugs fires: find the newly created drug and set up @@ -959,10 +975,10 @@ function isBatchExpired(batch) { return expiryDate < today; } -function renderVariantInventoryDetails(variant) { +function renderVariantInventoryDetails(variant, batches) { const activePacks = getActivePacksForVariant(variant); const isReadOnly = currentUser?.role === 'readonly'; - const batches = [...(variant.batches || [])] + const sortedBatches = [...(batches || [])] .filter(batch => Number(batch.quantity) > 0) .sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date)); @@ -975,8 +991,8 @@ function renderVariantInventoryDetails(variant) { `).join('') : '
No active packs configured
'; - const batchesHtml = batches.length > 0 - ? batches.map(batch => { + const batchesHtml = sortedBatches.length > 0 + ? sortedBatches.map(batch => { const locationLabel = getBatchLocationLabel(batch); const expired = isBatchExpired(batch); const hasPackState = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_unit_name; @@ -1067,6 +1083,16 @@ async function handleDisposeBatch(e) { if (modal) { closeDisposeBatchModal(); } + // Find which variant this batch belongs to and invalidate its batch cache + const batchVariantId = (() => { + for (const [vid, batches] of loadedVariantBatches) { + if (batches.some(b => b.id === batchId)) return vid; + } + return null; + })(); + if (batchVariantId) { + loadedVariantBatches.delete(batchVariantId); + } await loadDrugs(); showToast('Expired batch marked as disposed.', 'success'); } catch (error) { @@ -1087,13 +1113,7 @@ function updateLocationFilterOptions() { const locations = new Set(); allDrugs.forEach(drug => { - drug.variants.forEach(variant => { - (variant.batches || []).forEach(batch => { - if (batch.quantity > 0) { - locations.add(getBatchLocationLabel(batch)); - } - }); - }); + (drug.locations || []).forEach(loc => locations.add(loc)); }); locationFilterSelect.innerHTML = ''; @@ -1388,7 +1408,10 @@ async function updateBatchInfo() { const variant = getVariantById(variantId); if (variant) { - populateDispensePackSelect(variant); + // Ensure drug detail (with packs) is loaded before populating pack select + const drugOfVariant = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId)); + if (drugOfVariant) await ensureDrugDetailLoaded(drugOfVariant.id); + populateDispensePackSelect(getVariantById(variantId)); } updateDispenseModeUi(); @@ -1621,14 +1644,10 @@ function renderDrugs() { ); } - // Apply location filter + // Apply location filter using the pre-computed locations list in the summary if (selectedLocationFilter) { drugsToShow = drugsToShow.filter(drug => - drug.variants.some(variant => - (variant.batches || []).some(batch => - batch.quantity > 0 && getBatchLocationLabel(batch) === selectedLocationFilter - ) - ) + (drug.locations || []).includes(selectedLocationFilter) ); } @@ -1650,43 +1669,57 @@ function renderDrugs() { const isExpanded = expandedDrugs.has(drug.id); const isReadOnly = currentUser.role === 'readonly'; const isControlled = drug.is_controlled; - - const variantsHtml = isExpanded ? ` - ${drug.variants.map(variant => { - const variantIsLowStock = variant.quantity <= variant.low_stock_threshold; - const variantExpanded = expandedVariants.has(variant.id); - const expiredQuantity = (variant.batches || []) - .filter(batch => Number(batch.quantity) > 0 && isBatchExpired(batch)) - .reduce((sum, batch) => sum + Number(batch.quantity || 0), 0); - const inDateQuantity = Math.max(0, Number(variant.quantity || 0) - expiredQuantity); - const quantityDisplay = expiredQuantity > 0 - ? `${formatDisplayNumber(inDateQuantity)} ${escapeHtml(variant.unit)} (${formatDisplayNumber(expiredQuantity)} expired)` - : `${formatDisplayNumber(variant.quantity)} ${escapeHtml(variant.unit)}`; - return ` -
-
-
-
${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}
-
${quantityDisplay}
+ const drugDetail = loadedDrugDetails.get(drug.id); + + let variantsHtml = ''; + if (isExpanded) { + if (!drugDetail) { + variantsHtml = '
Loading variants…
'; + } else { + variantsHtml = drug.variants.map(summaryVariant => { + // Use detail variant (has packs); fall back to summary if not found + const variant = (drugDetail.variants || []).find(v => v.id === summaryVariant.id) || summaryVariant; + const variantIsLowStock = summaryVariant.quantity <= summaryVariant.low_stock_threshold; + const variantExpanded = expandedVariants.has(summaryVariant.id); + // expiredQuantity is pre-computed in the summary + const expiredQuantity = summaryVariant.expired_quantity || 0; + const inDateQuantity = Math.max(0, Number(summaryVariant.quantity || 0) - expiredQuantity); + const quantityDisplay = expiredQuantity > 0 + ? `${formatDisplayNumber(inDateQuantity)} ${escapeHtml(summaryVariant.unit)} (${formatDisplayNumber(expiredQuantity)} expired)` + : `${formatDisplayNumber(summaryVariant.quantity)} ${escapeHtml(summaryVariant.unit)}`; + const batches = loadedVariantBatches.get(summaryVariant.id); + const batchesSection = (() => { + if (!variantExpanded) return ''; + if (!batches) return '
Loading batches…
'; + return renderVariantInventoryDetails(variant, batches); + })(); + return ` +
+
+
+
${escapeHtml(drug.name)} ${escapeHtml(summaryVariant.strength)}
+
${quantityDisplay}
+
+
+ + ${variantIsLowStock ? 'Low Stock' : 'OK'} + + Inventory ${variantExpanded ? '▼' : '▶'} +
-
- - ${variantIsLowStock ? 'Low Stock' : 'OK'} - - Inventory ${variantExpanded ? '▼' : '▶'} +
+ ${!isReadOnly ? ` + + + + ` : ''}
+ ${batchesSection}
-
- ${!isReadOnly ? ` - - - - ` : ''} -
- ${variantExpanded ? renderVariantInventoryDetails(variant) : ''} -
- `; - }).join('')}` : ''; + `; + }).join(''); + } + } return `
@@ -1934,6 +1967,7 @@ async function handleDispenseDrug(e) { document.getElementById('dispenseForm').reset(); resetDispensePrintFields(); closeModal(document.getElementById('dispenseModal')); + loadedVariantBatches.delete(variantId); // invalidate level-3 batch cache await loadDrugs(); showToast(successMessage, toastType, toastType === 'warning' ? 5000 : undefined); } catch (error) { @@ -1962,30 +1996,45 @@ function closeEditModal() { } // Show variants for a drug -function toggleDrugExpansion(drugId) { +async function toggleDrugExpansion(drugId) { if (expandedDrugs.has(drugId)) { expandedDrugs.delete(drugId); const collapsedDrug = allDrugs.find(drug => drug.id === drugId); if (collapsedDrug) { (collapsedDrug.variants || []).forEach(variant => expandedVariants.delete(variant.id)); } + renderDrugs(); } else { expandedDrugs.add(drugId); + renderDrugs(); // show loading state immediately + await ensureDrugDetailLoaded(drugId); + renderDrugs(); } - renderDrugs(); } -function toggleVariantExpansion(variantId, event) { +async function toggleVariantExpansion(variantId, event) { if (event) { event.stopPropagation(); } if (expandedVariants.has(variantId)) { expandedVariants.delete(variantId); + renderDrugs(); } else { expandedVariants.add(variantId); + if (!loadedVariantBatches.has(variantId)) { + renderDrugs(); // show loading state immediately + try { + const response = await apiCall(`/variants/${variantId}/batches`); + if (response.ok) { + loadedVariantBatches.set(variantId, await response.json()); + } + } catch (error) { + console.error(`Failed to load batches for variant ${variantId}:`, error); + } + } + renderDrugs(); } - renderDrugs(); } // Open add variant modal @@ -2256,6 +2305,7 @@ async function handleAddVariant(e) { document.getElementById('variantForm').reset(); closeModal(document.getElementById('addVariantModal')); + loadedDrugDetails.delete(drugId); // invalidate level-2 cache for this drug await loadDrugs(); if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) { refreshDeliveryVariantSelects(); @@ -2369,6 +2419,8 @@ async function handleEditVariant(e) { } closeModal(document.getElementById('editVariantModal')); + const variantDrugId = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId))?.id; + if (variantDrugId) loadedDrugDetails.delete(variantDrugId); // invalidate level-2 cache await loadDrugs(); renderDrugs(); const message = newPackPayloads.length > 0 @@ -2574,6 +2626,7 @@ async function handleEditDrug(e) { if (!response.ok) throw new Error('Failed to update drug'); closeEditModal(); + loadedDrugDetails.delete(drugId); // invalidate level-2 cache await loadDrugs(); showToast('Drug updated successfully!', 'success'); } catch (error) { @@ -2602,6 +2655,8 @@ async function deleteDrug(drugId) { throw new Error(error.detail || 'Failed to delete drug'); } + loadedDrugDetails.delete(drugId); // invalidate level-2 cache + loadedVariantBatches.clear(); // variants of this drug are gone await loadDrugs(); showToast('Drug deleted successfully!', 'success'); } catch (error) { @@ -3039,6 +3094,7 @@ async function handleBatchReceive(e) { document.getElementById('batchReceiveForm').reset(); closeModal(document.getElementById('batchReceiveModal')); + loadedVariantBatches.delete(variantId); // invalidate level-3 cache await loadDrugs(); showToast('Batch received successfully!', 'success'); } catch (error) { @@ -3052,6 +3108,12 @@ function getActiveDeliveryDrug() { } function getVariantById(variantId) { + // Prefer loaded drug details (have packs) + for (const detail of loadedDrugDetails.values()) { + const found = (detail.variants || []).find(v => v.id === variantId); + if (found) return found; + } + // Fall back to summary variant (no packs) for (const drug of allDrugs) { const found = (drug.variants || []).find(v => v.id === variantId); if (found) return found; @@ -3059,6 +3121,18 @@ function getVariantById(variantId) { return null; } +async function ensureDrugDetailLoaded(drugId) { + if (!drugId || loadedDrugDetails.has(drugId)) return; + try { + const response = await apiCall(`/drugs/${drugId}`); + if (response.ok) { + loadedDrugDetails.set(drugId, await response.json()); + } + } catch (error) { + console.error(`Failed to load drug detail for drug ${drugId}:`, error); + } +} + function buildDeliveryVariantOptions(drug, selectedVariantId = '') { if (!drug || !drug.variants || drug.variants.length === 0) { return ''; @@ -3116,8 +3190,9 @@ function wireDeliveryLineEvents(line) { const packCountInput = line.querySelector('.delivery-pack-count'); if (drugSelect && variantSelect) { - drugSelect.addEventListener('change', () => { + drugSelect.addEventListener('change', async () => { const drugId = parseInt(drugSelect.value || '', 10); + await ensureDrugDetailLoaded(drugId); const drug = allDrugs.find(d => d.id === drugId) || null; variantSelect.innerHTML = buildDeliveryVariantOptions(drug, ''); if (packSelect) packSelect.innerHTML = buildDeliveryPackOptions(null, ''); @@ -3466,7 +3541,7 @@ function _refreshGtinMappingSelects() { // Reinitialise the GTIN mapping modal dropdowns from fresh allDrugs data, // optionally pre-selecting specific drug/variant/pack IDs. -function _reinitGtinMappingModal(restore) { +async function _reinitGtinMappingModal(restore) { const drugSelect = document.getElementById('gtinMappingDrugSelect'); const variantSelect = document.getElementById('gtinMappingVariantSelect'); const packSelect = document.getElementById('gtinMappingPackSelect'); @@ -3488,6 +3563,7 @@ function _reinitGtinMappingModal(restore) { } drugSelect.value = String(drugId); + await ensureDrugDetailLoaded(drugId); const drug = allDrugs.find(d => d.id === drugId); // Rebuild variant list @@ -3504,7 +3580,7 @@ function _reinitGtinMappingModal(restore) { } variantSelect.value = String(variantId); - const variant = drug?.variants?.find(v => v.id === variantId); + const variant = getVariantById(variantId); // checks loadedDrugDetails first (has packs) // Rebuild pack list packSelect.innerHTML = ''; @@ -3540,9 +3616,9 @@ function gtinMappingAddPack() { const variantId = parseInt(document.getElementById('gtinMappingVariantSelect').value || '', 10); if (!variantId) { showToast('Select a variant first', 'warning'); return; } const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10); - const drug = allDrugs.find(d => d.id === drugId); - if (!drug) return; - const variant = drug.variants?.find(v => v.id === variantId); + if (!drugId) return; + // Use getVariantById which returns the detail variant (with packs) if loaded + const variant = getVariantById(variantId); const existingPackIds = new Set((getActivePacksForVariant(variant) || []).map(p => p.id)); _gtinMappingPendingRestore = { drugId, variantId, packId: null, _existingPackIds: existingPackIds }; _gtinMappingPendingRefresh = true; @@ -3571,15 +3647,17 @@ function openGtinMappingModal(gtin, expiryStr, lot) { openModal(document.getElementById('gtinMappingModal')); } -function onGtinMappingDrugChange() { +async function onGtinMappingDrugChange() { const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10); - const drug = allDrugs.find(d => d.id === drugId); const variantSelect = document.getElementById('gtinMappingVariantSelect'); const packSelect = document.getElementById('gtinMappingPackSelect'); variantSelect.innerHTML = ''; packSelect.innerHTML = ''; + if (!drugId) return; + await ensureDrugDetailLoaded(drugId); + const drug = allDrugs.find(d => d.id === drugId); if (!drug) return; variantSelect.innerHTML += drug.variants.map(v => `` @@ -3587,10 +3665,8 @@ function onGtinMappingDrugChange() { } function onGtinMappingVariantChange() { - const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10); const variantId = parseInt(document.getElementById('gtinMappingVariantSelect').value || '', 10); - const drug = allDrugs.find(d => d.id === drugId); - const variant = drug?.variants?.find(v => v.id === variantId); + const variant = getVariantById(variantId); // checks loadedDrugDetails first (has packs) const packSelect = document.getElementById('gtinMappingPackSelect'); packSelect.innerHTML = ''; @@ -3901,6 +3977,9 @@ async function handleAddPackSize(e) { } closeModal(document.getElementById('addPackSizeModal')); + // Invalidate drug detail cache so the new pack will be re-fetched on next expand + const packDrugId = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId))?.id; + if (packDrugId) loadedDrugDetails.delete(packDrugId); await loadDrugs(); // Refresh delivery line pack selects so the new pack is immediately available refreshDeliveryVariantSelects(); @@ -3986,6 +4065,8 @@ async function handleReceiveDelivery(e) { closeModal(document.getElementById('receiveDeliveryModal')); _detachDeliveryBarcodeListener(); + // Invalidate batch cache for all delivered variants + payloads.forEach(entry => loadedVariantBatches.delete(entry.variantId)); await loadDrugs(); showToast(`Delivery received successfully (${payloads.length} line${payloads.length === 1 ? '' : 's'})`, 'success'); } catch (error) {