From 05a093afd3e6743bdfe38b67ff4ee8779db04a1e Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sun, 26 Apr 2026 16:02:42 -0400 Subject: [PATCH] Dispensing Vet --- backend/app/migrate_compliance.py | 4 +++ backend/app/models.py | 1 + frontend/app.js | 55 ++++++++++++++++++++++++++++--- frontend/index.html | 5 +++ frontend/reports.js | 4 +++ frontend/styles.css | 2 -- 6 files changed, 64 insertions(+), 7 deletions(-) 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 ? ` +
+ Prescribing Vet: + ${escapeHtml(item.prescribing_vet)} +
+ ` : ''} ${item.notes ? `
Notes: @@ -2796,6 +2834,10 @@ function escapeHtml(text) { return div.innerHTML; } +function toTitleCase(str) { + return str.replace(/\S+/g, word => word.charAt(0).toUpperCase() + word.slice(1)); +} + async function openReportsPage() { const dropdown = document.getElementById('userDropdown'); if (dropdown) dropdown.style.display = 'none'; @@ -3600,10 +3642,10 @@ async function handleBarcodeScan(raw) { return; } - _applyBarcodeScanToLines(mapping, lot, expiryStr); + await _applyBarcodeScanToLines(mapping, lot, expiryStr); } -function _applyBarcodeScanToLines(mapping, lot, expiryStr) { +async function _applyBarcodeScanToLines(mapping, lot, expiryStr) { const container = document.getElementById('deliveryLinesContainer'); if (!container) return; @@ -3637,17 +3679,17 @@ function _applyBarcodeScanToLines(mapping, lot, expiryStr) { }) || null; if (emptyLine) { - _populateDeliveryLine(emptyLine, mapping, lot, expiryStr); + await _populateDeliveryLine(emptyLine, mapping, lot, expiryStr); return; } // 3. Append a new line appendDeliveryLine(); const newLine = container.querySelector('.delivery-line:last-child'); - if (newLine) _populateDeliveryLine(newLine, mapping, lot, expiryStr); + if (newLine) await _populateDeliveryLine(newLine, mapping, lot, expiryStr); } -function _populateDeliveryLine(line, mapping, lot, expiryStr) { +async function _populateDeliveryLine(line, mapping, lot, expiryStr) { const drugSelect = line.querySelector('.delivery-drug-select'); const variantSelect = line.querySelector('.delivery-variant-select'); const packSelect = line.querySelector('.delivery-pack-select'); @@ -3660,6 +3702,9 @@ function _populateDeliveryLine(line, mapping, lot, expiryStr) { drugSelect.value = String(mapping.drug_id); } + // Ensure drug detail (with packs) is loaded before trying to populate pack select + await ensureDrugDetailLoaded(mapping.drug_id); + if (variantSelect) { const drug = allDrugs.find(d => d.id === mapping.drug_id) || null; variantSelect.innerHTML = buildDeliveryVariantOptions(drug, mapping.drug_variant_id); diff --git a/frontend/index.html b/frontend/index.html index 95bbfb1..e4f0c3b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -228,6 +228,11 @@
+
+ + +
+