diff --git a/backend/app/migrate_compliance.py b/backend/app/migrate_compliance.py index 2643519..0bdbf3c 100644 --- a/backend/app/migrate_compliance.py +++ b/backend/app/migrate_compliance.py @@ -230,6 +230,10 @@ def migrate_compliance_schema() -> None: ) print("Backfilled dispensing mode where missing") + if _table_exists(cursor, "dispensings") and not _column_exists(cursor, "dispensings", "prescribing_vet"): + cursor.execute("ALTER TABLE dispensings ADD COLUMN prescribing_vet VARCHAR") + print("Added dispensings.prescribing_vet") + # Seed default locations once table exists (created via SQLAlchemy create_all). if _table_exists(cursor, "locations"): cursor.execute("INSERT OR IGNORE INTO locations(name, is_active) VALUES ('Cupboard', 1)") diff --git a/backend/app/models.py b/backend/app/models.py index 9a0f08d..2948fb0 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -60,6 +60,7 @@ class Dispensing(Base): requested_pack_id = Column(Integer, ForeignKey("variant_packs.id"), nullable=True) requested_pack_count = Column(Float, nullable=True) animal_name = Column(String, nullable=True) # Name/ID of the animal (optional) + prescribing_vet = Column(String, nullable=True) # Prescribing vet's name (required for controlled drugs) user_name = Column(String, nullable=False) # User who dispensed dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True) notes = Column(String, nullable=True) diff --git a/frontend/app.js b/frontend/app.js index 2b6292c..cc7790b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -318,6 +318,15 @@ function setupEventListeners() { }); }); + const vetInput = document.getElementById('dispenseVet'); + if (vetInput) { + vetInput.addEventListener('input', () => { + const pos = vetInput.selectionStart; + vetInput.value = toTitleCase(vetInput.value); + vetInput.setSelectionRange(pos, pos); + }); + } + if (variantForm) variantForm.addEventListener('submit', handleAddVariant); if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant); if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug); @@ -1432,6 +1441,20 @@ async function updateBatchInfo() { const drugOfVariant = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId)); if (drugOfVariant) await ensureDrugDetailLoaded(drugOfVariant.id); populateDispensePackSelect(getVariantById(variantId)); + + const vetLabel = document.getElementById('dispenseVetLabel'); + const vetInputEl = document.getElementById('dispenseVet'); + if (vetLabel && vetInputEl) { + const isControlled = drugOfVariant ? drugOfVariant.is_controlled : false; + vetLabel.textContent = isControlled ? 'Prescribing Vet *' : 'Prescribing Vet'; + vetLabel.style.color = isControlled ? '#d32f2f' : ''; + vetInputEl.placeholder = isControlled ? "Vet's name (required)" : "Vet's name"; + } + } else { + const vetLabel = document.getElementById('dispenseVetLabel'); + const vetInputEl = document.getElementById('dispenseVet'); + if (vetLabel) { vetLabel.textContent = 'Prescribing Vet'; vetLabel.style.color = ''; } + if (vetInputEl) vetInputEl.placeholder = "Vet's name"; } updateDispenseModeUi(); @@ -1817,6 +1840,7 @@ async function handleDispenseDrug(e) { const requestedPackIdValue = document.getElementById('dispensePackSelect').value; const requestedPackCountValue = document.getElementById('dispensePackCount').value; const animalName = document.getElementById('dispenseAnimal').value; + const vetName = document.getElementById('dispenseVet')?.value.trim() || ''; const notes = document.getElementById('dispenseNotes').value; const printEnabled = document.getElementById('dispensePrintEnabled')?.checked; const dosage = document.getElementById('dispenseDosage')?.value.trim() || ''; @@ -1825,6 +1849,7 @@ async function handleDispenseDrug(e) { const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null; const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null; const variant = getVariantById(variantId); + const drugForVariant = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId)); const legacyStockOnly = isLegacyDispenseSelected(); const selectedPack = variant && selectedPackId ? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId) @@ -1940,10 +1965,17 @@ async function handleDispenseDrug(e) { requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null, dispense_source: dispenseSource, animal_name: animalName || null, + prescribing_vet: vetName || null, notes: notes || null, allocations }; + if (drugForVariant && drugForVariant.is_controlled && !vetName) { + showToast('Prescribing vet name is required for controlled drugs.', 'warning'); + document.getElementById('dispenseVet')?.focus(); + return; + } + try { const response = await apiCall('/dispense', { method: 'POST', @@ -2608,6 +2640,12 @@ async function showDrugHistory(drugId) { User: ${escapeHtml(item.user_name)} + ${item.prescribing_vet ? ` +