diff --git a/backend/app/main.py b/backend/app/main.py index 9cef6a9..a9d6ce1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -211,6 +211,7 @@ class DrugVariantResponse(BaseModel): unit: str base_unit: str low_stock_threshold: float + has_inventory_history: bool = False packs: List[VariantPackResponse] = [] batches: List[BatchResponse] = [] @@ -318,6 +319,19 @@ def write_audit_log( def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, Any]: """Return variant data with active batch details for API responses.""" + 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 + ) + variant_dict = { "id": variant.id, "drug_id": variant.drug_id, @@ -326,6 +340,7 @@ def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, "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 = ( db.query(VariantPack) @@ -861,6 +876,26 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User raise HTTPException(status_code=404, detail="Drug not found") variant_ids = [row[0] for row in db.query(DrugVariant.id).filter(DrugVariant.drug_id == drug_id).all()] + + if variant_ids: + has_batch_history = ( + db.query(Batch.id) + .filter(Batch.drug_variant_id.in_(variant_ids)) + .first() + is not None + ) + has_dispense_history = ( + db.query(Dispensing.id) + .filter(Dispensing.drug_variant_id.in_(variant_ids)) + .first() + is not None + ) + + if has_batch_history or has_dispense_history: + raise HTTPException( + status_code=400, + detail="Cannot delete drug with variants that have batch or dispensing history. Archive or manage records first.", + ) if variant_ids: batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id.in_(variant_ids)).all()] if batch_ids: @@ -972,6 +1007,39 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: payload["unit"] = cleaned_base_unit payload.pop("base_unit", None) + 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 + ) + is_locked = has_batch_history or has_dispense_history + + locked_field_changes = [] + if is_locked: + if "strength" in payload and payload["strength"] != variant.strength: + locked_field_changes.append("strength") + if "unit" in payload and payload["unit"] != variant.unit: + locked_field_changes.append("base_unit") + if "quantity" in payload and payload["quantity"] != variant.quantity: + locked_field_changes.append("quantity") + + if locked_field_changes: + raise HTTPException( + status_code=400, + detail=( + "Cannot change " + + ", ".join(locked_field_changes) + + " after batches or dispensing history exist for this variant" + ), + ) + for field, value in payload.items(): setattr(variant, field, value) @@ -995,6 +1063,25 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_ if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") + 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 + ) + + if has_batch_history or has_dispense_history: + raise HTTPException( + status_code=400, + detail="Cannot delete variant with batch or dispensing history. Archive or manage records first.", + ) + batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id == variant_id).all()] if batch_ids: db.query(DispensingAllocation).filter(DispensingAllocation.batch_id.in_(batch_ids)).delete(synchronize_session=False) diff --git a/frontend/app.js b/frontend/app.js index 6f458df..08010fa 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -5,12 +5,12 @@ let showLowStockOnly = false; let selectedLocationFilter = ''; let searchTerm = ''; let expandedDrugs = new Set(); +let expandedVariants = new Set(); let currentUser = null; let accessToken = null; let deliveryDrugId = null; let deliveryLineCounter = 0; let deliveryLocations = []; -let activeVariantPacksVariantId = null; // Toast notification system function showToast(message, type = 'info', duration = 3000) { @@ -231,11 +231,11 @@ function setupEventListeners() { const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn'); const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn'); const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn'); + const addEditVariantPackRowBtn = document.getElementById('addEditVariantPackRowBtn'); const variantUnitSelect = document.getElementById('variantUnit'); const variantStrengthInput = document.getElementById('variantStrength'); + const editVariantUnitSelect = document.getElementById('editVariantUnit'); const dispenseModeSelect = document.getElementById('dispenseMode'); - const variantPacksForm = document.getElementById('variantPacksForm'); - const closeVariantPacksBtn = document.getElementById('closeVariantPacksBtn'); const showAllBtn = document.getElementById('showAllBtn'); const showLowStockBtn = document.getElementById('showLowStockBtn'); const locationFilterSelect = document.getElementById('locationFilterSelect'); @@ -267,11 +267,17 @@ function setupEventListeners() { if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine()); if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery); if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow()); + if (addEditVariantPackRowBtn) addEditVariantPackRowBtn.addEventListener('click', () => appendEditVariantPackRow()); if (variantUnitSelect) { variantUnitSelect.addEventListener('change', () => { refreshVariantPackRowLabels(); }); } + if (editVariantUnitSelect) { + editVariantUnitSelect.addEventListener('change', () => { + refreshEditVariantPackRowLabels(); + }); + } if (variantStrengthInput && variantUnitSelect) { variantStrengthInput.addEventListener('blur', () => { variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value); @@ -307,9 +313,6 @@ function setupEventListeners() { const closeLocationManagementBtn = document.getElementById('closeLocationManagementBtn'); if (closeLocationManagementBtn) closeLocationManagementBtn.addEventListener('click', () => closeModal(document.getElementById('locationManagementModal'))); - if (variantPacksForm) variantPacksForm.addEventListener('submit', handleCreateVariantPack); - if (closeVariantPacksBtn) closeVariantPacksBtn.addEventListener('click', () => closeModal(document.getElementById('variantPacksModal'))); - const createLocationForm = document.getElementById('createLocationForm'); if (createLocationForm) createLocationForm.addEventListener('submit', createLocation); @@ -574,6 +577,63 @@ function formatDisplayDate(value) { return parsed.toLocaleDateString(); } +function formatDisplayNumber(value) { + const numeric = Number(value); + if (Number.isNaN(numeric)) return '0'; + return Number.isInteger(numeric) ? String(numeric) : String(Number(numeric.toFixed(3))); +} + +function renderVariantInventoryDetails(variant) { + const activePacks = getActivePacksForVariant(variant); + const batches = [...(variant.batches || [])] + .filter(batch => Number(batch.quantity) > 0) + .sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date)); + + const packsHtml = activePacks.length > 0 + ? activePacks.map(pack => ` +
Loading packs...
'; - - try { - const response = await apiCall(`/variants/${variantId}/packs`); - if (!response.ok) throw new Error('Failed to load pack presentations'); - const packs = await response.json(); - - if (!Array.isArray(packs) || packs.length === 0) { - list.innerHTML = 'No pack presentations defined.
'; - return; - } - - list.innerHTML = ` -Failed to load pack presentations
'; - } -} - -async function handleCreateVariantPack(e) { - e.preventDefault(); - - const variantId = parseInt(document.getElementById('variantPacksVariantId').value, 10); - const label = document.getElementById('variantPacksNewLabel').value.trim(); - const packUnitName = document.getElementById('variantPacksNewUnit').value.trim(); - const size = parseFloat(document.getElementById('variantPacksNewSize').value); - - if (!variantId || !label || !packUnitName || Number.isNaN(size) || size <= 0) { - showToast('Please complete all pack fields', 'warning'); - return; - } - - try { - const response = await apiCall(`/variants/${variantId}/packs`, { - method: 'POST', - body: JSON.stringify({ - label, - pack_unit_name: packUnitName, - pack_size_in_base_units: size, - is_active: true - }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.detail || 'Failed to add pack presentation'); - } - - document.getElementById('variantPacksForm').reset(); - const variant = getVariantById(variantId); - document.getElementById('variantPacksNewUnit').value = inferPackUnitName(variant?.unit || 'pack'); - await loadDrugs(); - await refreshVariantPacksList(); - if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) { - refreshDeliveryVariantSelects(); - } - showToast('Pack presentation added', 'success'); - } catch (error) { - console.error('Error creating pack presentation:', error); - showToast('Failed to add pack presentation: ' + error.message, 'error'); - } -} - -async function toggleVariantPackActive(packId, nextActiveState) { - try { - const response = await apiCall(`/variant-packs/${packId}`, { - method: 'PUT', - body: JSON.stringify({ is_active: Boolean(nextActiveState) }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.detail || 'Failed to update pack state'); - } - - await loadDrugs(); - await refreshVariantPacksList(); - if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) { - refreshDeliveryVariantSelects(); - } - showToast('Pack updated', 'success'); - } catch (error) { - console.error('Error updating pack state:', error); - showToast('Failed to update pack: ' + error.message, 'error'); - } -} - // Dispense from variant function dispenseVariant(variantId) { // Update the dropdown display with all variants @@ -1683,6 +1766,12 @@ async function handlePrintNotes(e) { // Delete variant async function deleteVariant(variantId) { + const variant = getVariantById(variantId); + if (variant && variant.has_inventory_history) { + showToast('Cannot delete variant with batch or dispensing history', 'warning'); + return; + } + if (!confirm('Are you sure you want to delete this variant?')) return; try { @@ -1690,14 +1779,17 @@ async function deleteVariant(variantId) { method: 'DELETE' }); - if (!response.ok) throw new Error('Failed to delete variant'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Failed to delete variant'); + } await loadDrugs(); renderDrugs(); showToast('Variant deleted successfully!', 'success'); } catch (error) { console.error('Error deleting variant:', error); - showToast('Failed to delete variant. Check the console for details.', 'error'); + showToast('Failed to delete variant: ' + error.message, 'error'); } } @@ -1803,6 +1895,12 @@ async function handleEditDrug(e) { // Delete drug async function deleteDrug(drugId) { + const drug = allDrugs.find(d => d.id === drugId); + if (drug && drug.variants.some(v => v.has_inventory_history)) { + showToast('Cannot delete drug with variants that have batch or dispensing history', 'warning'); + return; + } + if (!confirm('Are you sure you want to delete this drug?')) return; try { @@ -1810,13 +1908,16 @@ async function deleteDrug(drugId) { method: 'DELETE' }); - if (!response.ok) throw new Error('Failed to delete drug'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Failed to delete drug'); + } await loadDrugs(); showToast('Drug deleted successfully!', 'success'); } catch (error) { console.error('Error deleting drug:', error); - showToast('Failed to delete drug. Check the console for details.', 'error'); + showToast('Failed to delete drug: ' + error.message, 'error'); } } diff --git a/frontend/index.html b/frontend/index.html index 33563f5..ea85bd9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -366,6 +366,9 @@