This commit is contained in:
2026-03-29 10:39:03 -04:00
parent e00669ae2c
commit ad1bb59f98
6 changed files with 1615 additions and 51 deletions
+816 -8
View File
@@ -7,6 +7,10 @@ let searchTerm = '';
let expandedDrugs = 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) {
@@ -212,6 +216,7 @@ function setupEventListeners() {
const editModal = document.getElementById('editModal');
const printNotesModal = document.getElementById('printNotesModal');
const batchReceiveModal = document.getElementById('batchReceiveModal');
const receiveDeliveryModal = document.getElementById('receiveDeliveryModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const printNotesBtn = document.getElementById('printNotesBtn');
@@ -222,6 +227,15 @@ function setupEventListeners() {
const cancelPrescribeBtn = document.getElementById('cancelPrescribeBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const cancelBatchReceiveBtn = document.getElementById('cancelBatchReceiveBtn');
const cancelReceiveDeliveryBtn = document.getElementById('cancelReceiveDeliveryBtn');
const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn');
const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn');
const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn');
const variantUnitSelect = document.getElementById('variantUnit');
const variantStrengthInput = document.getElementById('variantStrength');
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');
@@ -246,11 +260,31 @@ function setupEventListeners() {
const batchReceiveForm = document.getElementById('batchReceiveForm');
if (batchReceiveForm) batchReceiveForm.addEventListener('submit', handleBatchReceive);
if (cancelBatchReceiveBtn) cancelBatchReceiveBtn.addEventListener('click', () => closeModal(batchReceiveModal));
const receiveDeliveryForm = document.getElementById('receiveDeliveryForm');
if (receiveDeliveryForm) receiveDeliveryForm.addEventListener('submit', handleReceiveDelivery);
if (cancelReceiveDeliveryBtn) cancelReceiveDeliveryBtn.addEventListener('click', () => closeModal(receiveDeliveryModal));
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow());
if (variantUnitSelect) {
variantUnitSelect.addEventListener('change', () => {
refreshVariantPackRowLabels();
});
}
if (variantStrengthInput && variantUnitSelect) {
variantStrengthInput.addEventListener('blur', () => {
variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value);
refreshVariantPackRowLabels();
});
}
if (dispenseModeSelect) dispenseModeSelect.addEventListener('change', updateDispenseModeUi);
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
updateDispenseDrugSelect();
updateDispenseModeUi();
openModal(dispenseModal);
});
@@ -272,6 +306,9 @@ 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);
@@ -333,6 +370,28 @@ function setupEventListeners() {
});
}
const dispenseQuantityInput = document.getElementById('dispenseQuantity');
if (dispenseQuantityInput) {
dispenseQuantityInput.addEventListener('input', () => {
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
if (mode !== 'subunit') {
return;
}
const packSelect = document.getElementById('dispensePackSelect');
const packCount = document.getElementById('dispensePackCount');
const packPreview = document.getElementById('dispensePackPreview');
const variantId = parseInt(document.getElementById('dispenseDrugSelect')?.value || '', 10);
const variant = getVariantById(variantId);
if (packSelect) packSelect.value = '';
if (packCount) packCount.value = '';
if (packPreview && variant) {
packPreview.textContent = `Enter direct quantity in ${variant.unit}.`;
}
});
}
// Close modal when clicking outside
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
@@ -398,6 +457,108 @@ function updateDispenseDrugSelect() {
select.appendChild(option);
});
});
const packSelect = document.getElementById('dispensePackSelect');
const packCount = document.getElementById('dispensePackCount');
const packPreview = document.getElementById('dispensePackPreview');
const modeSelect = document.getElementById('dispenseMode');
if (packSelect) {
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
}
if (packCount) {
packCount.value = '';
}
if (modeSelect) {
modeSelect.value = 'subunit';
}
if (packPreview) {
packPreview.textContent = 'Select a pack and whole-number count.';
}
updateDispenseModeUi();
}
function populateDispensePackSelect(variant) {
const packSelect = document.getElementById('dispensePackSelect');
const packCount = document.getElementById('dispensePackCount');
const packPreview = document.getElementById('dispensePackPreview');
if (!packSelect) return;
const activePacks = getActivePacksForVariant(variant);
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
activePacks.forEach(pack => {
const option = document.createElement('option');
option.value = String(pack.id);
option.textContent = `${pack.label} (${pack.pack_size_in_base_units} ${variant.unit})`;
packSelect.appendChild(option);
});
if (packCount) packCount.value = '';
if (packPreview) {
packPreview.textContent = activePacks.length > 0
? `Select a pack and whole-number count (${variant.unit} base unit).`
: `No active packs for this variant.`;
}
}
function updateDispenseModeUi() {
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
const quantityGroup = document.getElementById('dispenseQuantityGroup');
const packRow = document.getElementById('dispensePackRow');
const quantityInput = document.getElementById('dispenseQuantity');
const packSelect = document.getElementById('dispensePackSelect');
const packCount = document.getElementById('dispensePackCount');
if (quantityGroup) {
quantityGroup.style.display = mode === 'subunit' ? '' : 'none';
}
if (packRow) {
packRow.style.display = mode === 'pack' ? '' : 'none';
}
if (quantityInput) {
quantityInput.required = mode === 'subunit';
}
if (packSelect) {
packSelect.required = mode === 'pack';
}
if (packCount) {
packCount.required = mode === 'pack';
}
updateAllocationPreview();
}
function updateDispenseQuantityFromPack() {
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
if (mode !== 'pack') return;
const variantId = parseInt(document.getElementById('dispenseDrugSelect')?.value || '', 10);
const packId = parseInt(document.getElementById('dispensePackSelect')?.value || '', 10);
const packCount = parseFloat(document.getElementById('dispensePackCount')?.value || '');
const quantityInput = document.getElementById('dispenseQuantity');
const preview = document.getElementById('dispensePackPreview');
const variant = getVariantById(variantId);
if (!quantityInput || !preview || !variant) return;
const selectedPack = getActivePacksForVariant(variant).find(pack => pack.id === packId);
if (selectedPack && !Number.isNaN(packCount) && packCount > 0) {
if (Math.abs(packCount - Math.round(packCount)) > 1e-6) {
preview.textContent = 'Whole-pack mode requires a whole-number pack count.';
return;
}
const quantity = packCount * selectedPack.pack_size_in_base_units;
quantityInput.value = String(quantity);
preview.textContent = `${packCount} × ${selectedPack.pack_size_in_base_units} = ${quantity} ${variant.unit}`;
updateAllocationPreview();
return;
}
preview.textContent = selectedPack
? `1 ${selectedPack.pack_unit_name} = ${selectedPack.pack_size_in_base_units} ${variant.unit}`
: `Select a pack to calculate quantity.`;
}
function formatDisplayDate(value) {
@@ -455,6 +616,8 @@ function updateLocationFilterOptions() {
function populateDispenseBatchSelect(activeBatches) {
const batchSelect = document.getElementById('dispenseBatchSelect');
const selectedVariantId = parseInt(document.getElementById('dispenseDrugSelect').value || '', 10);
const unitLabel = getVariantById(selectedVariantId)?.unit || 'units';
const previousValue = batchSelect.value;
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
@@ -465,7 +628,7 @@ function populateDispenseBatchSelect(activeBatches) {
const locationLabel = getBatchLocationLabel(batch);
const fefoLabel = index === 0 ? ' [FEFO default]' : '';
option.value = batch.id;
option.textContent = `${batch.batch_number} | ${batch.quantity} units | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`;
option.textContent = `${batch.batch_number} | ${batch.quantity} ${unitLabel} | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`;
batchSelect.appendChild(option);
});
@@ -484,8 +647,16 @@ async function updateBatchInfo() {
if (!variantId) {
batchInfoSection.style.display = 'none';
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
const packSelect = document.getElementById('dispensePackSelect');
if (packSelect) packSelect.innerHTML = '<option value="">-- Select pack --</option>';
return;
}
const variant = getVariantById(variantId);
if (variant) {
populateDispensePackSelect(variant);
}
updateDispenseModeUi();
batchInfoSection.style.display = 'block';
batchInfoContent.innerHTML = '<p class="loading">Loading batches...</p>';
@@ -558,6 +729,7 @@ async function updateBatchInfo() {
// Update allocation preview based on quantity and allow_split flag
async function updateAllocationPreview() {
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
const unitLabel = getVariantById(variantId)?.unit || 'units';
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
const preferredBatchId = parseInt(document.getElementById('dispenseBatchSelect').value);
@@ -626,10 +798,10 @@ async function updateAllocationPreview() {
if (remainingQty > 0 && allowSplit) {
allocationPreviewContent.innerHTML = `
<p style="color: #d32f2f; margin: 0 0 10px 0;">✕ Warning: Only ${quantity - remainingQty} units available across all batches (${remainingQty} short)</p>
<p style="color: #d32f2f; margin: 0 0 10px 0;">✕ Warning: Only ${quantity - remainingQty} ${escapeHtml(unitLabel)} available across all batches (${remainingQty} short)</p>
<div>${allocations.map(a => `
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} ${escapeHtml(unitLabel)} (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
</div>
`).join('')}</div>
`;
@@ -638,7 +810,7 @@ async function updateAllocationPreview() {
const allocationHtml = allocations.map(a => `
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} ${escapeHtml(unitLabel)} (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
</div>
`).join('');
@@ -725,7 +897,7 @@ function renderDrugs() {
</div>
<div class="variant-actions">
${!isReadOnly ? `
<button class="btn btn-success btn-small" onclick="openBatchReceiveModal(${variant.id})">📦 Receive Batch</button>
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); openVariantPacksModal(${variant.id})">📦 Packs</button>
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
@@ -753,6 +925,7 @@ function renderDrugs() {
</div>
<div class="drug-actions">
${!isReadOnly ? `
<button class="btn btn-success btn-small" onclick="event.stopPropagation(); openReceiveDeliveryModal(${drug.id})">📦 Receive Delivery</button>
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); openAddVariantModal(${drug.id})"> Add</button>
` : ''}
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
@@ -825,13 +998,45 @@ async function handleDispenseDrug(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
let quantity = parseFloat(document.getElementById('dispenseQuantity').value);
const dispenseMode = (document.getElementById('dispenseMode').value || 'subunit').toLowerCase();
const preferredBatchIdValue = document.getElementById('dispenseBatchSelect').value;
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
const animalName = document.getElementById('dispenseAnimal').value;
const userName = document.getElementById('dispenseUser').value;
const notes = document.getElementById('dispenseNotes').value;
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
const variant = getVariantById(variantId);
const selectedPack = variant && selectedPackId
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
: null;
if (!['subunit', 'pack'].includes(dispenseMode)) {
showToast('Please select a valid dispense mode.', 'warning');
return;
}
if (dispenseMode === 'pack') {
if (!selectedPack) {
showToast('Please select a pack type for whole-pack dispensing.', 'warning');
return;
}
if (selectedPackCount == null || Number.isNaN(selectedPackCount) || selectedPackCount <= 0) {
showToast('Please enter a valid pack count greater than zero.', 'warning');
return;
}
if (Math.abs(selectedPackCount - Math.round(selectedPackCount)) > 1e-6) {
showToast('Whole-pack dispensing requires a whole-number pack count.', 'warning');
return;
}
quantity = selectedPackCount * selectedPack.pack_size_in_base_units;
}
if (!variantId || isNaN(quantity) || quantity <= 0 || !userName) {
showToast('Please fill in all required fields (Drug Variant, Quantity > 0, Dispensed by)', 'warning');
return;
@@ -840,7 +1045,10 @@ async function handleDispenseDrug(e) {
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
dispense_mode: dispenseMode,
batch_id: preferredBatchIdValue ? parseInt(preferredBatchIdValue) : null,
requested_pack_id: dispenseMode === 'pack' ? selectedPackId : null,
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
animal_name: animalName || null,
user_name: userName,
notes: notes || null,
@@ -903,19 +1111,143 @@ function openAddVariantModal(drugId) {
if (!drug) return;
currentDrug = drug;
const form = document.getElementById('variantForm');
if (form) form.reset();
document.getElementById('variantDrugId').value = drug.id;
initializeVariantPackRows();
document.getElementById('addVariantModal').classList.add('show');
}
function inferBaseUnitFromStrength(strength) {
const value = String(strength || '').toLowerCase();
if (value.includes('/ml') || value.includes('ml')) return 'ml';
if (value.includes('tablet')) return 'tablets';
if (value.includes('capsule')) return 'capsules';
return 'units';
}
function getVariantPackRowsContainer() {
return document.getElementById('variantPackRows');
}
function refreshVariantPackRowLabels() {
const container = getVariantPackRowsContainer();
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
if (!container) return;
container.querySelectorAll('.variant-pack-row').forEach(row => {
const packUnit = row.querySelector('.variant-pack-unit')?.value || 'pack';
const label = row.querySelector('.variant-pack-size-label');
if (!label) return;
const titleCasePack = packUnit.charAt(0).toUpperCase() + packUnit.slice(1);
label.textContent = `${titleCasePack} Size (${baseUnit}) *`;
});
}
function appendVariantPackRow(prefill = {}) {
const container = getVariantPackRowsContainer();
if (!container) return;
const row = document.createElement('div');
row.className = 'delivery-line variant-pack-row';
const selectedPackUnit = prefill.packUnit || 'bottle';
const selectedSize = prefill.packSize || '';
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
row.innerHTML = `
<div class="delivery-line-grid" style="grid-template-columns: 1.2fr 1.2fr auto;">
<div class="form-group">
<label>Pack Type *</label>
<select class="variant-pack-unit" required>
<option value="bottle" ${selectedPackUnit === 'bottle' ? 'selected' : ''}>Bottle</option>
<option value="box" ${selectedPackUnit === 'box' ? 'selected' : ''}>Box</option>
<option value="vial" ${selectedPackUnit === 'vial' ? 'selected' : ''}>Vial</option>
<option value="packet" ${selectedPackUnit === 'packet' ? 'selected' : ''}>Packet</option>
</select>
</div>
<div class="form-group">
<label class="variant-pack-size-label">Bottle Size (${baseUnit}) *</label>
<input type="number" class="variant-pack-size" min="0.0001" step="0.0001" value="${selectedSize}" required>
</div>
<button type="button" class="btn btn-danger btn-small variant-pack-remove-btn">Remove</button>
</div>
`;
const removeBtn = row.querySelector('.variant-pack-remove-btn');
const unitSelect = row.querySelector('.variant-pack-unit');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
if (container.querySelectorAll('.variant-pack-row').length <= 1) {
showToast('At least one pack size is required', 'warning');
return;
}
row.remove();
});
}
if (unitSelect) {
unitSelect.addEventListener('change', refreshVariantPackRowLabels);
}
container.appendChild(row);
refreshVariantPackRowLabels();
}
function initializeVariantPackRows() {
const container = getVariantPackRowsContainer();
if (!container) return;
container.innerHTML = '';
const strengthValue = document.getElementById('variantStrength')?.value || '';
const inferredBaseUnit = inferBaseUnitFromStrength(strengthValue);
const variantUnitSelect = document.getElementById('variantUnit');
if (variantUnitSelect) {
variantUnitSelect.value = inferredBaseUnit;
}
appendVariantPackRow({ packUnit: 'bottle' });
}
// Handle add variant form
async function handleAddVariant(e) {
e.preventDefault();
const drugId = parseInt(document.getElementById('variantDrugId').value);
const baseUnit = document.getElementById('variantUnit').value;
const rows = Array.from(document.querySelectorAll('#variantPackRows .variant-pack-row'));
if (rows.length === 0) {
showToast('Please add at least one pack size', 'warning');
return;
}
const packPayloads = [];
for (let i = 0; i < rows.length; i += 1) {
const row = rows[i];
const packUnit = row.querySelector('.variant-pack-unit')?.value;
const packSize = parseFloat(row.querySelector('.variant-pack-size')?.value || '');
if (!packUnit || Number.isNaN(packSize) || packSize <= 0) {
showToast(`Pack row ${i + 1} is incomplete`, 'warning');
return;
}
const normalizedPackUnit = packUnit.trim().toLowerCase();
const titleCasePack = normalizedPackUnit.charAt(0).toUpperCase() + normalizedPackUnit.slice(1);
packPayloads.push({
label: `${titleCasePack} ${packSize} ${baseUnit}`,
pack_unit_name: normalizedPackUnit,
pack_size_in_base_units: packSize,
is_active: true
});
}
const variantData = {
strength: document.getElementById('variantStrength').value,
quantity: parseFloat(document.getElementById('variantQuantity').value),
unit: document.getElementById('variantUnit').value,
quantity: 0,
unit: baseUnit,
base_unit: baseUnit,
low_stock_threshold: parseFloat(document.getElementById('variantThreshold').value)
};
@@ -927,9 +1259,41 @@ async function handleAddVariant(e) {
if (!response.ok) throw new Error('Failed to add variant');
const createdVariant = await response.json();
for (const packPayload of packPayloads) {
const packResponse = await apiCall(`/variants/${createdVariant.id}/packs`, {
method: 'POST',
body: JSON.stringify(packPayload)
});
if (!packResponse.ok) {
const packError = await packResponse.json();
throw new Error(packError.detail || 'Variant created but pack size creation failed');
}
}
// Archive the auto-created default 1:1 pack when custom pack sizes are configured.
const packsResponse = await apiCall(`/variants/${createdVariant.id}/packs`);
if (packsResponse.ok) {
const packs = await packsResponse.json();
const defaultPack = packs.find(
p => p.is_active && Number(p.pack_size_in_base_units) === 1 && (p.pack_unit_name || '').toLowerCase() === baseUnit.toLowerCase()
);
if (defaultPack && packs.filter(p => p.is_active).length > 1) {
await apiCall(`/variant-packs/${defaultPack.id}`, {
method: 'PUT',
body: JSON.stringify({ is_active: false })
});
}
}
document.getElementById('variantForm').reset();
closeModal(document.getElementById('addVariantModal'));
await loadDrugs();
if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) {
refreshDeliveryVariantSelects();
}
renderDrugs();
showToast('Variant added successfully!', 'success');
} catch (error) {
@@ -958,6 +1322,13 @@ function openEditVariantModal(variantId) {
document.getElementById('editVariantModal').classList.add('show');
}
function inferPackUnitName(baseUnit) {
const value = String(baseUnit || 'pack').trim().toLowerCase();
if (!value) return 'pack';
if (value.endsWith('s') && value.length > 1) return value.slice(0, -1);
return value;
}
// Handle edit variant form
async function handleEditVariant(e) {
e.preventDefault();
@@ -988,6 +1359,140 @@ async function handleEditVariant(e) {
}
}
async function openVariantPacksModal(variantId) {
const variant = getVariantById(variantId);
if (!variant) {
showToast('Variant not found', 'error');
return;
}
const parentDrug = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId));
activeVariantPacksVariantId = variantId;
const label = document.getElementById('variantPacksLabel');
if (label) {
const drugName = parentDrug ? parentDrug.name : 'Drug';
label.textContent = `${drugName} ${variant.strength} | Base Unit: ${variant.unit}`;
}
const form = document.getElementById('variantPacksForm');
if (form) form.reset();
document.getElementById('variantPacksVariantId').value = String(variantId);
document.getElementById('variantPacksNewUnit').value = inferPackUnitName(variant.unit);
await refreshVariantPacksList();
openModal(document.getElementById('variantPacksModal'));
}
async function refreshVariantPacksList() {
const variantId = parseInt(document.getElementById('variantPacksVariantId')?.value || '', 10);
const list = document.getElementById('variantPacksList');
if (!variantId || !list) return;
list.innerHTML = '<p class="loading">Loading packs...</p>';
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 = '<p class="empty">No pack presentations defined.</p>';
return;
}
list.innerHTML = `
<div class="locations-table">
${packs.map(pack => `
<div class="location-item" style="${pack.is_active ? '' : 'opacity: 0.6;'}">
<div style="flex: 1;">
<strong>${escapeHtml(pack.label)}</strong>
<div style="font-size: 0.88em; color: #666;">
${escapeHtml(pack.pack_unit_name)} | ${pack.pack_size_in_base_units} base units
${pack.is_active ? '' : ' | archived'}
</div>
</div>
<button class="btn ${pack.is_active ? 'btn-danger' : 'btn-secondary'} btn-small" onclick="toggleVariantPackActive(${pack.id}, ${pack.is_active ? 'false' : 'true'})">
${pack.is_active ? 'Archive' : 'Restore'}
</button>
</div>
`).join('')}
</div>
`;
} catch (error) {
console.error('Error loading variant packs:', error);
list.innerHTML = '<p class="empty">Failed to load pack presentations</p>';
}
}
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
@@ -1751,3 +2256,306 @@ async function handleBatchReceive(e) {
showToast('Failed to receive batch: ' + error.message, 'error');
}
}
function getActiveDeliveryDrug() {
return allDrugs.find(d => d.id === deliveryDrugId);
}
function getVariantById(variantId) {
for (const drug of allDrugs) {
const found = (drug.variants || []).find(v => v.id === variantId);
if (found) return found;
}
return null;
}
function buildDeliveryVariantOptions(drug, selectedVariantId = '') {
if (!drug || !drug.variants || drug.variants.length === 0) {
return '<option value="">-- No variants available --</option>';
}
return [`<option value="">-- Select variant --</option>`, ...drug.variants.map(v => {
const selected = String(v.id) === String(selectedVariantId) ? ' selected' : '';
return `<option value="${v.id}"${selected}>${escapeHtml(v.strength)} (${escapeHtml(v.unit)})</option>`;
})].join('');
}
function getActivePacksForVariant(variant) {
if (!variant || !Array.isArray(variant.packs)) return [];
return variant.packs.filter(pack => pack.is_active);
}
function buildDeliveryPackOptions(variant, selectedPackId = '') {
const packs = getActivePacksForVariant(variant);
if (packs.length === 0) {
return '<option value="">-- No active packs --</option>';
}
return [`<option value="">-- Select pack --</option>`, ...packs.map(pack => {
const selected = String(pack.id) === String(selectedPackId) ? ' selected' : '';
const label = `${pack.label} (${pack.pack_size_in_base_units} ${variant.unit})`;
return `<option value="${pack.id}"${selected}>${escapeHtml(label)}</option>`;
})].join('');
}
function buildDeliveryLocationOptions(selectedLocationId = '') {
return [`<option value="">-- Select location --</option>`, ...deliveryLocations.map(location => {
const selected = String(location.id) === String(selectedLocationId) ? ' selected' : '';
return `<option value="${location.id}"${selected}>${escapeHtml(location.name)}</option>`;
})].join('');
}
function updateDeliveryLineQuantityDisplay(line) {
const variantId = parseInt(line.querySelector('.delivery-variant-select')?.value || '', 10);
const packSelect = line.querySelector('.delivery-pack-select');
const variant = getVariantById(variantId);
if (!variant || !packSelect) {
return;
}
const currentPackId = packSelect.value;
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
}
function wireDeliveryLineEvents(line) {
const variantSelect = line.querySelector('.delivery-variant-select');
const packSelect = line.querySelector('.delivery-pack-select');
const packCountInput = line.querySelector('.delivery-pack-count');
if (variantSelect && packSelect) {
variantSelect.addEventListener('change', () => {
const variantId = parseInt(variantSelect.value || '', 10);
const variant = getVariantById(variantId);
packSelect.innerHTML = buildDeliveryPackOptions(variant, '');
if (packCountInput) packCountInput.value = '';
updateDeliveryLineQuantityDisplay(line);
});
}
if (packSelect) {
packSelect.addEventListener('change', () => {
updateDeliveryLineQuantityDisplay(line);
});
}
if (packCountInput) {
packCountInput.addEventListener('input', () => {
updateDeliveryLineQuantityDisplay(line);
});
}
}
function appendDeliveryLine(prefill = {}) {
const container = document.getElementById('deliveryLinesContainer');
const drug = getActiveDeliveryDrug();
if (!container || !drug) return;
deliveryLineCounter += 1;
const lineId = `delivery-line-${deliveryLineCounter}`;
const line = document.createElement('div');
line.className = 'delivery-line';
line.dataset.lineId = lineId;
const initialVariant = drug.variants.find(v => String(v.id) === String(prefill.variantId)) || drug.variants[0] || null;
const initialVariantId = prefill.variantId || (initialVariant ? initialVariant.id : '');
const initialPackId = prefill.packId || '';
const initialPackCount = prefill.packCount || '';
line.innerHTML = `
<div class="delivery-line-grid">
<div class="form-group">
<label>Variant</label>
<select class="delivery-variant-select" required>
${buildDeliveryVariantOptions(drug, initialVariantId)}
</select>
</div>
<div class="form-group">
<label>Pack Type</label>
<select class="delivery-pack-select" required>
${buildDeliveryPackOptions(initialVariant, initialPackId)}
</select>
</div>
<div class="form-group">
<label>Pack Count</label>
<input type="number" class="delivery-pack-count" min="0.0001" step="0.0001" value="${initialPackCount}" required>
</div>
<div class="form-group">
<label>Batch Number</label>
<input type="text" class="delivery-batch-number" value="${prefill.batchNumber || ''}" placeholder="e.g. ABC123" required>
</div>
<div class="form-group">
<label>Expiry</label>
<input type="date" class="delivery-expiry-date" value="${prefill.expiryDate || ''}" required>
</div>
<div class="form-group">
<label>Location</label>
<select class="delivery-location-select" required>
${buildDeliveryLocationOptions(prefill.locationId || '')}
</select>
</div>
<button type="button" class="btn btn-danger btn-small delivery-remove-btn">Remove</button>
</div>
`;
const removeBtn = line.querySelector('.delivery-remove-btn');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
if (container.children.length <= 1) {
showToast('At least one delivery line is required', 'warning');
return;
}
line.remove();
});
}
wireDeliveryLineEvents(line);
updateDeliveryLineQuantityDisplay(line);
container.appendChild(line);
}
function refreshDeliveryVariantSelects() {
const drug = getActiveDeliveryDrug();
const container = document.getElementById('deliveryLinesContainer');
if (!drug || !container) return;
container.querySelectorAll('.delivery-line').forEach(line => {
const select = line.querySelector('.delivery-variant-select');
const packSelect = line.querySelector('.delivery-pack-select');
if (!select) return;
const currentVariantId = select.value;
select.innerHTML = buildDeliveryVariantOptions(drug, currentVariantId);
const variant = getVariantById(parseInt(select.value || '', 10));
if (packSelect) {
const currentPackId = packSelect.value;
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
}
updateDeliveryLineQuantityDisplay(line);
});
}
async function initializeDeliveryLocations() {
try {
const response = await apiCall('/locations');
if (!response.ok) throw new Error('Failed to load locations');
const locations = await response.json();
deliveryLocations = locations.filter(location => location.is_active);
} catch (error) {
console.error('Error loading delivery locations:', error);
showToast('Failed to load storage locations', 'error');
deliveryLocations = [];
}
}
async function openReceiveDeliveryModal(drugId) {
deliveryDrugId = drugId;
const drug = getActiveDeliveryDrug();
if (!drug) {
showToast('Drug not found', 'error');
return;
}
const form = document.getElementById('receiveDeliveryForm');
const container = document.getElementById('deliveryLinesContainer');
const label = document.getElementById('receiveDeliveryDrugLabel');
if (form) form.reset();
if (container) container.innerHTML = '';
if (label) label.textContent = `Drug: ${drug.name}`;
await initializeDeliveryLocations();
appendDeliveryLine();
openModal(document.getElementById('receiveDeliveryModal'));
}
function handleAddVariantFromDelivery() {
if (!deliveryDrugId) {
showToast('Select a drug first', 'warning');
return;
}
openAddVariantModal(deliveryDrugId);
}
async function handleReceiveDelivery(e) {
e.preventDefault();
const drug = getActiveDeliveryDrug();
const container = document.getElementById('deliveryLinesContainer');
if (!drug || !container) {
showToast('Delivery context unavailable', 'error');
return;
}
const lines = Array.from(container.querySelectorAll('.delivery-line'));
if (lines.length === 0) {
showToast('Add at least one delivery line', 'warning');
return;
}
const payloads = [];
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const variantId = parseInt(line.querySelector('.delivery-variant-select')?.value || '', 10);
const packIdRaw = line.querySelector('.delivery-pack-select')?.value || '';
const packId = packIdRaw ? parseInt(packIdRaw, 10) : null;
const packCountRaw = line.querySelector('.delivery-pack-count')?.value || '';
const packCount = packCountRaw ? parseFloat(packCountRaw) : null;
const batchNumber = (line.querySelector('.delivery-batch-number')?.value || '').trim();
const expiryDate = line.querySelector('.delivery-expiry-date')?.value || '';
const locationId = parseInt(line.querySelector('.delivery-location-select')?.value || '', 10);
if (!variantId || !packId || packCount === null || Number.isNaN(packCount) || packCount <= 0 || !batchNumber || !expiryDate || !locationId) {
showToast(`Delivery line ${i + 1} is incomplete`, 'warning');
return;
}
const variant = drug.variants.find(v => v.id === variantId);
const selectedPack = variant ? getActivePacksForVariant(variant).find(pack => pack.id === packId) : null;
if (!selectedPack) {
showToast(`Delivery line ${i + 1} has an invalid pack selection`, 'warning');
return;
}
const computedQuantity = packCount * selectedPack.pack_size_in_base_units;
payloads.push({
variantId,
payload: {
batch_number: batchNumber,
received_pack_id: packId,
received_pack_count: packCount,
expiry_date: expiryDate,
location_id: locationId,
notes: `Received ${packCount} ${selectedPack.pack_unit_name}(s), total ${computedQuantity} ${variant ? variant.unit : 'units'}`
}
});
}
try {
for (let i = 0; i < payloads.length; i += 1) {
const entry = payloads[i];
const response = await apiCall(`/variants/${entry.variantId}/batches`, {
method: 'POST',
body: JSON.stringify(entry.payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Line ${i + 1}: ${error.detail || 'Failed to receive delivery line'}`);
}
}
closeModal(document.getElementById('receiveDeliveryModal'));
await loadDrugs();
showToast(`Delivery received successfully (${payloads.length} line${payloads.length === 1 ? '' : 's'})`, 'success');
} catch (error) {
console.error('Error receiving delivery:', error);
showToast('Failed to receive delivery: ' + error.message, 'error');
}
}