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) {