Dispensing Vet
This commit is contained in:
@@ -230,6 +230,10 @@ def migrate_compliance_schema() -> None:
|
|||||||
)
|
)
|
||||||
print("Backfilled dispensing mode where missing")
|
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).
|
# Seed default locations once table exists (created via SQLAlchemy create_all).
|
||||||
if _table_exists(cursor, "locations"):
|
if _table_exists(cursor, "locations"):
|
||||||
cursor.execute("INSERT OR IGNORE INTO locations(name, is_active) VALUES ('Cupboard', 1)")
|
cursor.execute("INSERT OR IGNORE INTO locations(name, is_active) VALUES ('Cupboard', 1)")
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Dispensing(Base):
|
|||||||
requested_pack_id = Column(Integer, ForeignKey("variant_packs.id"), nullable=True)
|
requested_pack_id = Column(Integer, ForeignKey("variant_packs.id"), nullable=True)
|
||||||
requested_pack_count = Column(Float, nullable=True)
|
requested_pack_count = Column(Float, nullable=True)
|
||||||
animal_name = Column(String, nullable=True) # Name/ID of the animal (optional)
|
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
|
user_name = Column(String, nullable=False) # User who dispensed
|
||||||
dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||||
notes = Column(String, nullable=True)
|
notes = Column(String, nullable=True)
|
||||||
|
|||||||
+50
-5
@@ -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 (variantForm) variantForm.addEventListener('submit', handleAddVariant);
|
||||||
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
|
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
|
||||||
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
|
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));
|
const drugOfVariant = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId));
|
||||||
if (drugOfVariant) await ensureDrugDetailLoaded(drugOfVariant.id);
|
if (drugOfVariant) await ensureDrugDetailLoaded(drugOfVariant.id);
|
||||||
populateDispensePackSelect(getVariantById(variantId));
|
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();
|
updateDispenseModeUi();
|
||||||
|
|
||||||
@@ -1817,6 +1840,7 @@ async function handleDispenseDrug(e) {
|
|||||||
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
|
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
|
||||||
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
|
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
|
||||||
const animalName = document.getElementById('dispenseAnimal').value;
|
const animalName = document.getElementById('dispenseAnimal').value;
|
||||||
|
const vetName = document.getElementById('dispenseVet')?.value.trim() || '';
|
||||||
const notes = document.getElementById('dispenseNotes').value;
|
const notes = document.getElementById('dispenseNotes').value;
|
||||||
const printEnabled = document.getElementById('dispensePrintEnabled')?.checked;
|
const printEnabled = document.getElementById('dispensePrintEnabled')?.checked;
|
||||||
const dosage = document.getElementById('dispenseDosage')?.value.trim() || '';
|
const dosage = document.getElementById('dispenseDosage')?.value.trim() || '';
|
||||||
@@ -1825,6 +1849,7 @@ async function handleDispenseDrug(e) {
|
|||||||
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
|
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
|
||||||
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
|
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
|
||||||
const variant = getVariantById(variantId);
|
const variant = getVariantById(variantId);
|
||||||
|
const drugForVariant = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId));
|
||||||
const legacyStockOnly = isLegacyDispenseSelected();
|
const legacyStockOnly = isLegacyDispenseSelected();
|
||||||
const selectedPack = variant && selectedPackId
|
const selectedPack = variant && selectedPackId
|
||||||
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
|
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
|
||||||
@@ -1940,10 +1965,17 @@ async function handleDispenseDrug(e) {
|
|||||||
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
|
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
|
||||||
dispense_source: dispenseSource,
|
dispense_source: dispenseSource,
|
||||||
animal_name: animalName || null,
|
animal_name: animalName || null,
|
||||||
|
prescribing_vet: vetName || null,
|
||||||
notes: notes || null,
|
notes: notes || null,
|
||||||
allocations
|
allocations
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (drugForVariant && drugForVariant.is_controlled && !vetName) {
|
||||||
|
showToast('Prescribing vet name is required for controlled drugs.', 'warning');
|
||||||
|
document.getElementById('dispenseVet')?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiCall('/dispense', {
|
const response = await apiCall('/dispense', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -2608,6 +2640,12 @@ async function showDrugHistory(drugId) {
|
|||||||
<span class="history-label">User:</span>
|
<span class="history-label">User:</span>
|
||||||
<span class="history-value">${escapeHtml(item.user_name)}</span>
|
<span class="history-value">${escapeHtml(item.user_name)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${item.prescribing_vet ? `
|
||||||
|
<div class="history-row">
|
||||||
|
<span class="history-label">Prescribing Vet:</span>
|
||||||
|
<span class="history-value">${escapeHtml(item.prescribing_vet)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
${item.notes ? `
|
${item.notes ? `
|
||||||
<div class="history-row">
|
<div class="history-row">
|
||||||
<span class="history-label">Notes:</span>
|
<span class="history-label">Notes:</span>
|
||||||
@@ -2796,6 +2834,10 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTitleCase(str) {
|
||||||
|
return str.replace(/\S+/g, word => word.charAt(0).toUpperCase() + word.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
async function openReportsPage() {
|
async function openReportsPage() {
|
||||||
const dropdown = document.getElementById('userDropdown');
|
const dropdown = document.getElementById('userDropdown');
|
||||||
if (dropdown) dropdown.style.display = 'none';
|
if (dropdown) dropdown.style.display = 'none';
|
||||||
@@ -3600,10 +3642,10 @@ async function handleBarcodeScan(raw) {
|
|||||||
return;
|
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');
|
const container = document.getElementById('deliveryLinesContainer');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -3637,17 +3679,17 @@ function _applyBarcodeScanToLines(mapping, lot, expiryStr) {
|
|||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
if (emptyLine) {
|
if (emptyLine) {
|
||||||
_populateDeliveryLine(emptyLine, mapping, lot, expiryStr);
|
await _populateDeliveryLine(emptyLine, mapping, lot, expiryStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Append a new line
|
// 3. Append a new line
|
||||||
appendDeliveryLine();
|
appendDeliveryLine();
|
||||||
const newLine = container.querySelector('.delivery-line:last-child');
|
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 drugSelect = line.querySelector('.delivery-drug-select');
|
||||||
const variantSelect = line.querySelector('.delivery-variant-select');
|
const variantSelect = line.querySelector('.delivery-variant-select');
|
||||||
const packSelect = line.querySelector('.delivery-pack-select');
|
const packSelect = line.querySelector('.delivery-pack-select');
|
||||||
@@ -3660,6 +3702,9 @@ function _populateDeliveryLine(line, mapping, lot, expiryStr) {
|
|||||||
drugSelect.value = String(mapping.drug_id);
|
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) {
|
if (variantSelect) {
|
||||||
const drug = allDrugs.find(d => d.id === mapping.drug_id) || null;
|
const drug = allDrugs.find(d => d.id === mapping.drug_id) || null;
|
||||||
variantSelect.innerHTML = buildDeliveryVariantOptions(drug, mapping.drug_variant_id);
|
variantSelect.innerHTML = buildDeliveryVariantOptions(drug, mapping.drug_variant_id);
|
||||||
|
|||||||
@@ -228,6 +228,11 @@
|
|||||||
<input type="text" id="dispenseAnimal">
|
<input type="text" id="dispenseAnimal">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dispenseVet" id="dispenseVetLabel">Prescribing Vet</label>
|
||||||
|
<input type="text" id="dispenseVet" placeholder="Vet's name">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="margin-top: 18px; padding: 12px; background: #f9fafb; border: 1px solid #d9e2ec; border-radius: 6px;">
|
<div class="form-group" style="margin-top: 18px; padding: 12px; background: #f9fafb; border: 1px solid #d9e2ec; border-radius: 6px;">
|
||||||
<label style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 0; font-weight: 600;">
|
<label style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 0; font-weight: 600;">
|
||||||
<input type="checkbox" id="dispensePrintEnabled">
|
<input type="checkbox" id="dispensePrintEnabled">
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ function renderDispensingTable(rows) {
|
|||||||
const info = getVariantInfoById(row.drug_variant_id);
|
const info = getVariantInfoById(row.drug_variant_id);
|
||||||
const quantityText = `${row.quantity} ${info.unit || 'units'}`;
|
const quantityText = `${row.quantity} ${info.unit || 'units'}`;
|
||||||
const animal = row.animal_name || '-';
|
const animal = row.animal_name || '-';
|
||||||
|
const vet = row.prescribing_vet || '-';
|
||||||
const notes = row.notes || '-';
|
const notes = row.notes || '-';
|
||||||
const allocations = formatDispenseAllocation(row);
|
const allocations = formatDispenseAllocation(row);
|
||||||
|
|
||||||
@@ -328,6 +329,7 @@ function renderDispensingTable(rows) {
|
|||||||
<td>${escapeHtml(info.strength || '-')}</td>
|
<td>${escapeHtml(info.strength || '-')}</td>
|
||||||
<td>${escapeHtml(quantityText)}</td>
|
<td>${escapeHtml(quantityText)}</td>
|
||||||
<td>${escapeHtml(animal)}</td>
|
<td>${escapeHtml(animal)}</td>
|
||||||
|
<td>${escapeHtml(vet)}</td>
|
||||||
<td>${escapeHtml(allocations)}</td>
|
<td>${escapeHtml(allocations)}</td>
|
||||||
<td>${escapeHtml(notes)}</td>
|
<td>${escapeHtml(notes)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -344,6 +346,7 @@ function renderDispensingTable(rows) {
|
|||||||
<th>Strength</th>
|
<th>Strength</th>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
<th>Animal</th>
|
<th>Animal</th>
|
||||||
|
<th>Prescribing Vet</th>
|
||||||
<th>Batch Allocation</th>
|
<th>Batch Allocation</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -530,6 +533,7 @@ function applyCurrentFilters() {
|
|||||||
info.drugName || '',
|
info.drugName || '',
|
||||||
info.strength || '',
|
info.strength || '',
|
||||||
row.animal_name || '',
|
row.animal_name || '',
|
||||||
|
row.prescribing_vet || '',
|
||||||
row.notes || '',
|
row.notes || '',
|
||||||
formatDispenseAllocation(row)
|
formatDispenseAllocation(row)
|
||||||
].join(' ').toLowerCase();
|
].join(' ').toLowerCase();
|
||||||
|
|||||||
@@ -710,8 +710,6 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#dispenseModal .form-actions {
|
#dispenseModal .form-actions {
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
|
|||||||
Reference in New Issue
Block a user