const API_URL = '/api'; let allDrugs = []; let currentDrug = null; let showLowStockOnly = false; let selectedLocationFilter = ''; 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) { const container = document.getElementById('toastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; toast.innerHTML = ` `; container.appendChild(toast); // Auto remove after duration setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => { container.removeChild(toast); }, 300); }, duration); } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { checkAuth(); }); // Check if user is already logged in function checkAuth() { const token = localStorage.getItem('accessToken'); const user = localStorage.getItem('currentUser'); if (token && user) { accessToken = token; currentUser = JSON.parse(user); showMainApp(); } else { showLoginPage(); } } // Show login page function showLoginPage() { document.getElementById('loginPage').style.display = 'flex'; document.getElementById('mainApp').style.display = 'none'; const loginForm = document.getElementById('loginForm'); if (loginForm) loginForm.addEventListener('submit', handleLogin); } // Show main app function showMainApp() { document.getElementById('loginPage').style.display = 'none'; document.getElementById('mainApp').style.display = 'block'; // Handle backward compatibility: convert old is_admin format to role if (!currentUser.role && currentUser.is_admin !== undefined) { currentUser.role = currentUser.is_admin ? 'admin' : 'user'; } // Default to 'user' if role is still undefined if (!currentUser.role) { currentUser.role = 'user'; } const userDisplay = document.getElementById('currentUser'); if (userDisplay) { const roleLabel = currentUser.role.charAt(0).toUpperCase() + currentUser.role.slice(1); userDisplay.textContent = `👤 ${currentUser.username} [${roleLabel}]`; } const adminBtn = document.getElementById('adminBtn'); if (adminBtn) { adminBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none'; } const locationsBtn = document.getElementById('locationsBtn'); if (locationsBtn) { locationsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none'; } const reportsBtn = document.getElementById('reportsBtn'); if (reportsBtn) { reportsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none'; } // Hide action buttons for read-only users const isReadOnly = currentUser.role === 'readonly'; const addDrugBtn = document.getElementById('addDrugBtn'); const dispenseBtn = document.getElementById('dispenseBtn'); const printNotesBtn = document.getElementById('printNotesBtn'); if (addDrugBtn) addDrugBtn.style.display = isReadOnly ? 'none' : 'block'; if (dispenseBtn) dispenseBtn.style.display = isReadOnly ? 'none' : 'block'; if (printNotesBtn) printNotesBtn.style.display = isReadOnly ? 'none' : 'block'; setupEventListeners(); loadDrugs(); } // Handle login async function handleLogin(e) { e.preventDefault(); const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; try { const response = await fetch(`${API_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (!response.ok) { throw new Error('Invalid credentials'); } const data = await response.json(); accessToken = data.access_token; currentUser = data.user; localStorage.setItem('accessToken', accessToken); localStorage.setItem('currentUser', JSON.stringify(currentUser)); document.getElementById('loginForm').reset(); const errorDiv = document.getElementById('loginError'); if (errorDiv) errorDiv.style.display = 'none'; showMainApp(); } catch (error) { const errorDiv = document.getElementById('loginError'); if (errorDiv) { errorDiv.textContent = error.message; errorDiv.style.display = 'block'; } } } // Handle register // Logout function handleLogout() { localStorage.removeItem('accessToken'); localStorage.removeItem('currentUser'); accessToken = null; currentUser = null; const loginForm = document.getElementById('loginForm'); if (loginForm) loginForm.reset(); const registerForm = document.getElementById('registerForm'); if (registerForm) { registerForm.style.display = 'none'; } const form = document.getElementById('loginForm'); if (form) form.style.display = 'block'; showLoginPage(); } // API helper with authentication async function apiCall(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (accessToken) { headers['Authorization'] = `Bearer ${accessToken}`; } const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers }); if (response.status === 401) { handleLogout(); throw new Error('Authentication expired'); } return response; } // Setup event listeners function setupEventListeners() { const drugForm = document.getElementById('drugForm'); const variantForm = document.getElementById('variantForm'); const editVariantForm = document.getElementById('editVariantForm'); const dispenseForm = document.getElementById('dispenseForm'); const prescribeForm = document.getElementById('prescribeForm'); const editForm = document.getElementById('editForm'); const printNotesForm = document.getElementById('printNotesForm'); const addModal = document.getElementById('addModal'); const addVariantModal = document.getElementById('addVariantModal'); const editVariantModal = document.getElementById('editVariantModal'); const dispenseModal = document.getElementById('dispenseModal'); const prescribeModal = document.getElementById('prescribeModal'); 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'); const cancelAddBtn = document.getElementById('cancelAddBtn'); const cancelVariantBtn = document.getElementById('cancelVariantBtn'); const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn'); const cancelDispenseBtn = document.getElementById('cancelDispenseBtn'); 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'); const userMenuBtn = document.getElementById('userMenuBtn'); const adminBtn = document.getElementById('adminBtn'); const locationsBtn = document.getElementById('locationsBtn'); const reportsBtn = document.getElementById('reportsBtn'); const logoutBtn = document.getElementById('logoutBtn'); const changePasswordBtn = document.getElementById('changePasswordBtn'); // Modal close buttons const closeButtons = document.querySelectorAll('.close'); if (drugForm) drugForm.addEventListener('submit', handleAddDrug); if (variantForm) variantForm.addEventListener('submit', handleAddVariant); if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant); if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug); if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug); if (editForm) editForm.addEventListener('submit', handleEditDrug); if (printNotesForm) printNotesForm.addEventListener('submit', handlePrintNotes); 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); }); if (cancelAddBtn) cancelAddBtn.addEventListener('click', () => closeModal(addModal)); if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal)); if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal)); if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal)); if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal)); if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal); const cancelPrintNotesBtn = document.getElementById('cancelPrintNotesBtn'); if (cancelPrintNotesBtn) cancelPrintNotesBtn.addEventListener('click', () => closeModal(printNotesModal)); const closeHistoryBtn = document.getElementById('closeHistoryBtn'); if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal'))); const closeUserManagementBtn = document.getElementById('closeUserManagementBtn'); if (closeUserManagementBtn) closeUserManagementBtn.addEventListener('click', () => closeModal(document.getElementById('userManagementModal'))); 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); const changePasswordForm = document.getElementById('changePasswordForm'); if (changePasswordForm) changePasswordForm.addEventListener('submit', handleChangePassword); const cancelChangePasswordBtn = document.getElementById('cancelChangePasswordBtn'); if (cancelChangePasswordBtn) cancelChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('changePasswordModal'))); const adminChangePasswordForm = document.getElementById('adminChangePasswordForm'); if (adminChangePasswordForm) adminChangePasswordForm.addEventListener('submit', handleAdminChangePassword); const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn'); if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal'))); closeButtons.forEach(btn => btn.addEventListener('click', (e) => { const modal = e.target.closest('.modal'); closeModal(modal); })); if (showAllBtn) showAllBtn.addEventListener('click', () => { showLowStockOnly = false; updateFilterButtons(); renderDrugs(); }); if (showLowStockBtn) showLowStockBtn.addEventListener('click', () => { showLowStockOnly = true; updateFilterButtons(); renderDrugs(); }); if (locationFilterSelect) locationFilterSelect.addEventListener('change', (e) => { selectedLocationFilter = e.target.value; renderDrugs(); }); // User menu if (userMenuBtn) userMenuBtn.addEventListener('click', () => { const dropdown = document.getElementById('userDropdown'); if (dropdown) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }); if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal); if (adminBtn) adminBtn.addEventListener('click', openUserManagement); if (locationsBtn) locationsBtn.addEventListener('click', openLocationManagement); if (reportsBtn) reportsBtn.addEventListener('click', openReportsPage); if (logoutBtn) logoutBtn.addEventListener('click', handleLogout); // Search functionality const drugSearch = document.getElementById('drugSearch'); if (drugSearch) { let searchTimeout; drugSearch.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { searchTerm = e.target.value.toLowerCase().trim(); renderDrugs(); }, 150); }); } 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')) { closeModal(e.target); } }); } // Load drugs from API async function loadDrugs() { try { const response = await apiCall('/drugs'); if (!response.ok) throw new Error('Failed to load drugs'); allDrugs = await response.json(); updateLocationFilterOptions(); renderDrugs(); updateDispenseDrugSelect(); } catch (error) { console.error('Error loading drugs:', error); document.getElementById('drugsList').innerHTML = '
Error loading drugs. Make sure the backend is running.
'; } } // Modal utility functions function openModal(modal) { // Find the highest z-index among currently visible modals const visibleModals = document.querySelectorAll('.modal.show'); let maxZIndex = 1000; visibleModals.forEach(m => { const zIndex = parseInt(window.getComputedStyle(m).zIndex, 10) || 1000; if (zIndex > maxZIndex) { maxZIndex = zIndex; } }); // Set the new modal's z-index higher than any existing modal modal.style.zIndex = (maxZIndex + 100).toString(); modal.classList.add('show'); document.body.style.overflow = 'hidden'; } function closeModal(modal) { modal.classList.remove('show'); modal.style.zIndex = '1000'; document.body.style.overflow = 'auto'; } function closeEditModal() { closeModal(document.getElementById('editModal')); } function updateDispenseDrugSelect() { const select = document.getElementById('dispenseDrugSelect'); select.innerHTML = ''; allDrugs.forEach(drug => { drug.variants.forEach(variant => { const option = document.createElement('option'); option.value = variant.id; option.textContent = `${drug.name} ${variant.strength} (${variant.quantity} ${variant.unit})`; 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 = ''; } 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 = ''; 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) { if (!value) { return 'Unknown'; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return value; } return parsed.toLocaleDateString(); } function getBatchLocationLabel(batch) { return batch.location_name || batch.location?.name || `Location #${batch.location_id}`; } function updateLocationFilterOptions() { const locationFilterSelect = document.getElementById('locationFilterSelect'); if (!locationFilterSelect) return; const previousValue = selectedLocationFilter; const locations = new Set(); allDrugs.forEach(drug => { drug.variants.forEach(variant => { (variant.batches || []).forEach(batch => { if (batch.quantity > 0) { locations.add(getBatchLocationLabel(batch)); } }); }); }); locationFilterSelect.innerHTML = ''; Array.from(locations) .sort((a, b) => a.localeCompare(b)) .forEach(location => { const option = document.createElement('option'); option.value = location; option.textContent = location; locationFilterSelect.appendChild(option); }); if (previousValue && locations.has(previousValue)) { selectedLocationFilter = previousValue; locationFilterSelect.value = previousValue; } else { selectedLocationFilter = ''; locationFilterSelect.value = ''; } } 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 = ''; activeBatches.forEach((batch, index) => { const option = document.createElement('option'); const expiryLabel = formatDisplayDate(batch.expiry_date); const locationLabel = getBatchLocationLabel(batch); const fefoLabel = index === 0 ? ' [FEFO default]' : ''; option.value = batch.id; option.textContent = `${batch.batch_number} | ${batch.quantity} ${unitLabel} | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`; batchSelect.appendChild(option); }); if (previousValue && activeBatches.some(batch => String(batch.id) === previousValue)) { batchSelect.value = previousValue; } } // Update batch info display when variant is selected async function updateBatchInfo() { const variantId = parseInt(document.getElementById('dispenseDrugSelect').value); const batchInfoSection = document.getElementById('batchInfoSection'); const batchInfoContent = document.getElementById('batchInfoContent'); const batchSelect = document.getElementById('dispenseBatchSelect'); if (!variantId) { batchInfoSection.style.display = 'none'; batchSelect.innerHTML = ''; const packSelect = document.getElementById('dispensePackSelect'); if (packSelect) packSelect.innerHTML = ''; return; } const variant = getVariantById(variantId); if (variant) { populateDispensePackSelect(variant); } updateDispenseModeUi(); batchInfoSection.style.display = 'block'; batchInfoContent.innerHTML = 'Loading batches...
'; try { const response = await apiCall(`/variants/${variantId}/batches`); if (!response.ok) throw new Error('Failed to load batches'); const batches = await response.json(); // Filter out empty batches const activeBatches = batches.filter(b => b.quantity > 0); if (activeBatches.length === 0) { populateDispenseBatchSelect([]); batchInfoContent.innerHTML = '⚠️ No active batches available for this variant
'; return; } // Sort by expiry date (FEFO order) activeBatches.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date)); populateDispenseBatchSelect(activeBatches); const batchHtml = activeBatches.map((batch, index) => { const expiryDate = new Date(batch.expiry_date); const locationLabel = getBatchLocationLabel(batch); const expiryLabel = formatDisplayDate(batch.expiry_date); const today = new Date(); const isExpired = expiryDate < today; const daysToExpiry = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24)); let expiryStatus = '✓ OK'; let statusColor = '#4caf50'; if (isExpired) { expiryStatus = '✕ EXPIRED'; statusColor = '#d32f2f'; } else if (daysToExpiry <= 7) { expiryStatus = `⚠️ ${daysToExpiry}d left`; statusColor = '#ff9800'; } const isFEFO = index === 0; return `Error loading batches
'; } // Update allocation preview when batches load updateAllocationPreview(); } // 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); const allocationPreviewSection = document.getElementById('allocationPreviewSection'); const allocationPreviewContent = document.getElementById('allocationPreviewContent'); if (!variantId || isNaN(quantity) || quantity <= 0) { allocationPreviewSection.style.display = 'none'; return; } allocationPreviewSection.style.display = 'block'; allocationPreviewContent.innerHTML = 'Calculating allocation...
'; try { const response = await apiCall(`/variants/${variantId}/batches`); if (!response.ok) throw new Error('Failed to load batches'); const batches = await response.json(); let activeBatches = batches.filter(b => b.quantity > 0) .sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date)); if (activeBatches.length === 0) { allocationPreviewContent.innerHTML = '⚠️ No active batches available
'; return; } if (!Number.isNaN(preferredBatchId)) { const preferredBatch = activeBatches.find(batch => batch.id === preferredBatchId); if (!preferredBatch) { allocationPreviewContent.innerHTML = '✕ Selected preferred batch is no longer available.
'; return; } activeBatches = [preferredBatch, ...activeBatches.filter(batch => batch.id !== preferredBatchId)]; } // Simulate FEFO allocation const allocations = []; let remainingQty = quantity; for (const batch of activeBatches) { if (remainingQty <= 0) break; const allocQty = Math.min(remainingQty, batch.quantity); allocations.push({ batchNumber: batch.batch_number, batchId: batch.id, quantity: allocQty, location: getBatchLocationLabel(batch), expiryDate: batch.expiry_date, preferred: !Number.isNaN(preferredBatchId) && batch.id === preferredBatchId }); remainingQty -= allocQty; if (!allowSplit) break; } if (remainingQty > 0 && !allowSplit) { const failureContext = !Number.isNaN(preferredBatchId) ? 'Preferred batch cannot fully satisfy this request. Enable split to fall through to FEFO batches.' : 'Insufficient stock in first batch. Check "Allow Split" to use multiple batches.'; allocationPreviewContent.innerHTML = `✕ ${failureContext}
`; return; } if (remainingQty > 0 && allowSplit) { allocationPreviewContent.innerHTML = `✕ Warning: Only ${quantity - remainingQty} ${escapeHtml(unitLabel)} available across all batches (${remainingQty} short)
${introText}
Error calculating allocation
'; } } // Render drugs list function renderDrugs() { const drugsList = document.getElementById('drugsList'); let drugsToShow = allDrugs; // Apply search filter if (searchTerm) { drugsToShow = drugsToShow.filter(drug => drug.name.toLowerCase().includes(searchTerm) || (drug.description && drug.description.toLowerCase().includes(searchTerm)) || drug.variants.some(variant => variant.strength.toLowerCase().includes(searchTerm)) ); } // Apply stock filter if (showLowStockOnly) { drugsToShow = drugsToShow.filter(drug => drug.variants.some(variant => variant.quantity <= variant.low_stock_threshold) ); } // Apply location filter if (selectedLocationFilter) { drugsToShow = drugsToShow.filter(drug => drug.variants.some(variant => (variant.batches || []).some(batch => batch.quantity > 0 && getBatchLocationLabel(batch) === selectedLocationFilter ) ) ); } // Sort alphabetically by drug name drugsToShow = drugsToShow.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()) ); if (drugsToShow.length === 0) { drugsList.innerHTML = 'No drugs found matching your criteria
'; return; } drugsList.innerHTML = drugsToShow.map(drug => { const totalVariants = drug.variants.length; const lowStockVariants = drug.variants.filter(v => v.quantity <= v.low_stock_threshold).length; const totalQuantity = drug.variants.reduce((sum, v) => sum + v.quantity, 0); const isLowStock = lowStockVariants > 0; const isExpanded = expandedDrugs.has(drug.id); const isReadOnly = currentUser.role === 'readonly'; const isControlled = drug.is_controlled; const variantsHtml = isExpanded ? ` ${drug.variants.map(variant => { const variantIsLowStock = variant.quantity <= variant.low_stock_threshold; return `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 updateDispenseDrugSelect(); // Pre-select the variant in the dispense modal const drugSelect = document.getElementById('dispenseDrugSelect'); drugSelect.value = variantId; // Update batch info for selected variant updateBatchInfo(); // Open dispense modal openModal(document.getElementById('dispenseModal')); } // Prescribe variant and print label function prescribeVariant(variantId, drugName, variantStrength, unit) { // Set hidden fields document.getElementById('prescribeVariantId').value = variantId; document.getElementById('prescribeDrugName').value = drugName; document.getElementById('prescribeVariantStrength').value = variantStrength; document.getElementById('prescribeUnit').value = unit || 'units'; // Pre-fill user name if available if (currentUser) { document.getElementById('prescribeUser').value = currentUser.username; } // Set default expiry date to 1 month from now const defaultExpiry = new Date(); defaultExpiry.setMonth(defaultExpiry.getMonth() + 1); document.getElementById('prescribeExpiry').value = defaultExpiry.toISOString().split('T')[0]; // Open prescribe modal openModal(document.getElementById('prescribeModal')); } // Handle prescribe drug form submission async function handlePrescribeDrug(e) { e.preventDefault(); const variantId = parseInt(document.getElementById('prescribeVariantId').value); const drugName = document.getElementById('prescribeDrugName').value; const variantStrength = document.getElementById('prescribeVariantStrength').value; const unit = document.getElementById('prescribeUnit').value; const quantity = parseFloat(document.getElementById('prescribeQuantity').value); const animalName = document.getElementById('prescribeAnimal').value; const dosage = document.getElementById('prescribeDosage').value; const expiryDate = document.getElementById('prescribeExpiry').value; const userName = document.getElementById('prescribeUser').value; const notes = document.getElementById('prescribeNotes').value; if (!variantId || isNaN(quantity) || quantity <= 0 || !animalName || !dosage || !expiryDate || !userName) { showToast('Please fill in all required fields', 'warning'); return; } // Convert expiry date to DD/MM/YYYY format const expiryParts = expiryDate.split('-'); const formattedExpiry = `${expiryParts[2]}/${expiryParts[1]}/${expiryParts[0]}`; try { // First, print the label const labelData = { variables: { practice_name: "Many Tears Animal Rescue", animal_name: animalName, drug_name: `${drugName} ${variantStrength}`, dosage: dosage, quantity: `${quantity} ${unit}`, expiry_date: formattedExpiry } }; const labelResponse = await apiCall('/labels/print', { method: 'POST', body: JSON.stringify(labelData) }); if (!labelResponse.ok) { const error = await labelResponse.json(); throw new Error(error.detail || 'Label printing request failed'); } const labelResult = await labelResponse.json(); console.log('Label print result:', labelResult); if (!labelResult.success) { // Label printing failed - don't dispense the drug const isError = labelResult.message && ( labelResult.message.includes('not found') || labelResult.message.includes('error') || labelResult.message.includes('failed') ); const toastType = isError ? 'error' : 'warning'; showToast('Cannot dispense: ' + labelResult.message, toastType, 5000); return; } // Label printed successfully, now dispense the drug const dispensingData = { drug_variant_id: variantId, quantity: quantity, animal_name: animalName, user_name: userName, notes: notes || null }; const dispenseResponse = await apiCall('/dispense', { method: 'POST', body: JSON.stringify(dispensingData) }); if (!dispenseResponse.ok) { const error = await dispenseResponse.json(); throw new Error(error.detail || 'Failed to dispense drug'); } // Both operations succeeded showToast('Drug prescribed and label printed successfully!', 'success'); document.getElementById('prescribeForm').reset(); closeModal(document.getElementById('prescribeModal')); await loadDrugs(); } catch (error) { console.error('Error prescribing drug:', error); showToast('Failed to prescribe drug: ' + error.message, 'error'); } } // Handle print notes form submission async function handlePrintNotes(e) { e.preventDefault(); const animalName = document.getElementById('notesAnimalName').value.trim(); const notes = document.getElementById('notesContent').value.trim(); if (!animalName || !notes) { showToast('Please fill in all required fields', 'warning'); return; } try { // Send notes to print endpoint const notesData = { variables: { animal_name: animalName, notes: notes } }; const response = await apiCall('/notes/print', { method: 'POST', body: JSON.stringify(notesData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Notes printing request failed'); } const result = await response.json(); console.log('Notes print result:', result); if (!result.success) { // Printing failed const isError = result.message && ( result.message.includes('not found') || result.message.includes('error') || result.message.includes('failed') ); const toastType = isError ? 'error' : 'warning'; showToast(result.message, toastType, 5000); return; } // Printing succeeded showToast('Notes printed successfully!', 'success'); document.getElementById('printNotesForm').reset(); closeModal(document.getElementById('printNotesModal')); } catch (error) { console.error('Error printing notes:', error); showToast('Failed to print notes: ' + error.message, 'error'); } } // Delete variant async function deleteVariant(variantId) { if (!confirm('Are you sure you want to delete this variant?')) return; try { const response = await apiCall(`/variants/${variantId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('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'); } } // Show dispensing history for a drug async function showDrugHistory(drugId) { const drug = allDrugs.find(d => d.id === drugId); if (!drug) return; const historyModal = document.getElementById('historyModal'); const historyContent = document.getElementById('historyContent'); document.getElementById('historyDrugName').textContent = drug.name; historyContent.innerHTML = 'Loading history...
'; openModal(historyModal); try { const response = await apiCall(`/dispense/history`); if (!response.ok) throw new Error('Failed to fetch history'); const allHistory = await response.json(); // Filter history for this drug's variants const variantIds = drug.variants.map(v => v.id); const drugHistory = allHistory.filter(item => variantIds.includes(item.drug_variant_id)); if (drugHistory.length === 0) { historyContent.innerHTML = 'No dispensing history for this drug.
'; return; } // Sort by dispensed_at descending (most recent first) drugHistory.sort((a, b) => new Date(b.dispensed_at) - new Date(a.dispensed_at)); const historyHtml = drugHistory.map(item => { const variant = drug.variants.find(v => v.id === item.drug_variant_id); const date = new Date(item.dispensed_at).toLocaleDateString(); const time = new Date(item.dispensed_at).toLocaleTimeString(); return `Failed to load history. Check the console for details.
'; } } // Handle edit drug form async function handleEditDrug(e) { e.preventDefault(); const drugId = parseInt(document.getElementById('editDrugId').value); const drugData = { name: document.getElementById('editDrugName').value, description: document.getElementById('editDrugDescription').value, is_controlled: document.getElementById('editDrugIsControlled').checked }; try { const response = await apiCall(`/drugs/${drugId}`, { method: 'PUT', body: JSON.stringify(drugData) }); if (!response.ok) throw new Error('Failed to update drug'); closeEditModal(); await loadDrugs(); showToast('Drug updated successfully!', 'success'); } catch (error) { console.error('Error updating drug:', error); showToast('Failed to update drug. Check the console for details.', 'error'); } } // Delete drug async function deleteDrug(drugId) { if (!confirm('Are you sure you want to delete this drug?')) return; try { const response = await apiCall(`/drugs/${drugId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('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'); } } // Password Management function openChangePasswordModal() { const modal = document.getElementById('changePasswordModal'); document.getElementById('changePasswordForm').reset(); // Close dropdown const dropdown = document.getElementById('userDropdown'); if (dropdown) dropdown.style.display = 'none'; openModal(modal); } async function handleChangePassword(e) { e.preventDefault(); const currentPassword = document.getElementById('currentPassword').value; const newPassword = document.getElementById('newPassword').value; const confirmPassword = document.getElementById('confirmNewPassword').value; if (newPassword !== confirmPassword) { showToast('New passwords do not match!', 'warning'); return; } if (newPassword.length < 1) { showToast('New password cannot be empty!', 'warning'); return; } try { const response = await apiCall('/auth/change-password', { method: 'POST', body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to change password'); } showToast('Password changed successfully!', 'success'); closeModal(document.getElementById('changePasswordModal')); } catch (error) { console.error('Error changing password:', error); showToast('Failed to change password: ' + error.message, 'error'); } } async function openAdminChangePasswordModal(userId, username) { const modal = document.getElementById('adminChangePasswordModal'); document.getElementById('adminChangePasswordForm').reset(); document.getElementById('adminChangePasswordUserId').value = userId; document.getElementById('adminChangePasswordUsername').value = username; openModal(modal); } async function handleAdminChangePassword(e) { e.preventDefault(); const userId = document.getElementById('adminChangePasswordUserId').value; const newPassword = document.getElementById('adminChangePasswordNewPassword').value; const confirmPassword = document.getElementById('adminChangePasswordConfirm').value; if (newPassword !== confirmPassword) { showToast('Passwords do not match!', 'warning'); return; } if (newPassword.length < 1) { showToast('Password cannot be empty!', 'warning'); return; } try { const response = await apiCall(`/users/${userId}/change-password`, { method: 'POST', body: JSON.stringify({ new_password: newPassword }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to change password'); } showToast('Password changed successfully!', 'success'); closeModal(document.getElementById('adminChangePasswordModal')); openUserManagement(); } catch (error) { console.error('Error changing password:', error); showToast('Failed to change password: ' + error.message, 'error'); } } // Update filter button states function updateFilterButtons() { document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly); document.getElementById('showLowStockBtn').classList.toggle('active', showLowStockOnly); } // Escape HTML to prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function openReportsPage() { const dropdown = document.getElementById('userDropdown'); if (dropdown) dropdown.style.display = 'none'; window.location.href = 'reports.html'; } // User Management async function openUserManagement() { const modal = document.getElementById('userManagementModal'); document.getElementById('newUsername').value = ''; document.getElementById('newUserPassword').value = ''; document.getElementById('newUserRole').value = ''; const usersList = document.getElementById('usersList'); usersList.innerHTML = 'Loading users...
'; try { const response = await apiCall('/users'); if (!response.ok) throw new Error('Failed to load users'); const users = await response.json(); const usersHtml = `Error loading users
'; } const createUserForm = document.getElementById('createUserForm'); if (createUserForm) { createUserForm.onsubmit = createUser; } openModal(modal); } // Create user async function createUser(e) { e.preventDefault(); const username = document.getElementById('newUsername').value; const password = document.getElementById('newUserPassword').value; const role = document.getElementById('newUserRole').value; if (!role) { showToast('Please select a role', 'warning'); return; } try { const response = await apiCall('/users', { method: 'POST', body: JSON.stringify({ username, password, role }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to create user'); } document.getElementById('newUsername').value = ''; document.getElementById('newUserPassword').value = ''; document.getElementById('newUserRole').value = ''; showToast('User created successfully!', 'success'); openUserManagement(); } catch (error) { console.error('Error creating user:', error); showToast('Failed to create user: ' + error.message, 'error'); } } // Delete user async function deleteUser(userId) { if (!confirm('Are you sure you want to delete this user?')) return; try { const response = await apiCall(`/users/${userId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete user'); showToast('User deleted successfully!', 'success'); openUserManagement(); } catch (error) { console.error('Error deleting user:', error); showToast('Failed to delete user: ' + error.message, 'error'); } } // Location Management async function openLocationManagement() { const modal = document.getElementById('locationManagementModal'); document.getElementById('newLocationName').value = ''; const locationsList = document.getElementById('locationsList'); locationsList.innerHTML = 'Loading locations...
'; try { const response = await apiCall('/locations'); if (!response.ok) throw new Error('Failed to load locations'); const locations = await response.json(); const activeLocations = locations.filter(loc => loc.is_active); const inactiveLocations = locations.filter(loc => !loc.is_active); let locationsHtml = 'No active locations
'; } else { locationsHtml += `Error loading locations
'; } openModal(modal); } // Create location async function createLocation(e) { e.preventDefault(); const name = document.getElementById('newLocationName').value.trim(); if (!name) { showToast('Please enter a location name', 'warning'); return; } try { const response = await apiCall('/locations', { method: 'POST', body: JSON.stringify({ name }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to create location'); } document.getElementById('newLocationName').value = ''; showToast('Location created successfully!', 'success'); openLocationManagement(); } catch (error) { console.error('Error creating location:', error); showToast('Failed to create location: ' + error.message, 'error'); } } // Archive location async function archiveLocation(locationId, locationName) { if (!confirm(`Archive location "${locationName}"?\n\nYou can restore it later if needed.`)) return; try { const response = await apiCall(`/locations/${locationId}`, { method: 'PUT', body: JSON.stringify({ is_active: false }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to archive location'); } showToast('Location archived successfully!', 'success'); openLocationManagement(); } catch (error) { console.error('Error archiving location:', error); showToast('Failed to archive location: ' + error.message, 'error'); } } // Restore location async function restoreLocation(locationId, locationName) { if (!confirm(`Restore location "${locationName}"?`)) return; try { const response = await apiCall(`/locations/${locationId}`, { method: 'PUT', body: JSON.stringify({ is_active: true }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to restore location'); } showToast('Location restored successfully!', 'success'); openLocationManagement(); } catch (error) { console.error('Error restoring location:', error); showToast('Failed to restore location: ' + error.message, 'error'); } } // Batch Management async function openBatchReceiveModal(variantId) { const batchReceiveModal = document.getElementById('batchReceiveModal'); document.getElementById('batchReceiveForm').reset(); document.getElementById('batchVariantId').value = variantId; // Initialize locations await initializeBatchLocations(); openModal(batchReceiveModal); } async function initializeBatchLocations() { const locationSelect = document.getElementById('batchLocation'); try { const response = await apiCall('/locations'); if (!response.ok) throw new Error('Failed to load locations'); const locations = await response.json(); locationSelect.innerHTML = ''; locations.forEach(location => { if (location.is_active) { const option = document.createElement('option'); option.value = location.id; option.textContent = location.name; locationSelect.appendChild(option); } }); } catch (error) { console.error('Error loading locations:', error); showToast('Failed to load storage locations', 'error'); } } async function handleBatchReceive(e) { e.preventDefault(); const variantId = parseInt(document.getElementById('batchVariantId').value); const batchNumber = document.getElementById('batchNumber').value.trim(); const quantity = parseFloat(document.getElementById('batchQuantity').value); const expiryDate = document.getElementById('batchExpiryDate').value; const locationId = parseInt(document.getElementById('batchLocation').value); const notes = document.getElementById('batchNotes').value.trim(); if (!batchNumber || isNaN(quantity) || quantity <= 0 || !expiryDate || !locationId) { showToast('Please fill in all required fields', 'warning'); return; } const batchData = { batch_number: batchNumber, quantity: quantity, expiry_date: expiryDate, location_id: locationId, notes: notes || null }; try { const response = await apiCall(`/variants/${variantId}/batches`, { method: 'POST', body: JSON.stringify(batchData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to receive batch'); } document.getElementById('batchReceiveForm').reset(); closeModal(document.getElementById('batchReceiveModal')); await loadDrugs(); showToast('Batch received successfully!', 'success'); } catch (error) { console.error('Error receiving batch:', error); 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 ''; } return [``, ...drug.variants.map(v => { const selected = String(v.id) === String(selectedVariantId) ? ' selected' : ''; return ``; })].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 ''; } return [``, ...packs.map(pack => { const selected = String(pack.id) === String(selectedPackId) ? ' selected' : ''; const label = `${pack.label} (${pack.pack_size_in_base_units} ${variant.unit})`; return ``; })].join(''); } function buildDeliveryLocationOptions(selectedLocationId = '') { return [``, ...deliveryLocations.map(location => { const selected = String(location.id) === String(selectedLocationId) ? ' selected' : ''; return ``; })].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 = `