const API_URL = '/api'; let allDrugs = []; let currentDrug = null; let showLowStockOnly = false; let searchTerm = ''; let expandedDrugs = new Set(); let currentUser = null; let accessToken = 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'; const userDisplay = document.getElementById('currentUser'); if (userDisplay) { userDisplay.textContent = `👤 ${currentUser.username}`; } const adminBtn = document.getElementById('adminBtn'); if (adminBtn) { adminBtn.style.display = currentUser.is_admin ? 'block' : 'none'; } 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 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 addDrugBtn = document.getElementById('addDrugBtn'); const dispenseBtn = document.getElementById('dispenseBtn'); 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 showAllBtn = document.getElementById('showAllBtn'); const showLowStockBtn = document.getElementById('showLowStockBtn'); const userMenuBtn = document.getElementById('userMenuBtn'); const adminBtn = document.getElementById('adminBtn'); 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 (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal)); if (dispenseBtn) dispenseBtn.addEventListener('click', () => { updateDispenseDrugSelect(); 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 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 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(); }); // 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 (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); }); } // 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(); 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); }); }); } // 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) ); } // 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 variantsHtml = isExpanded ? ` ${drug.variants.map(variant => { const variantIsLowStock = variant.quantity <= variant.low_stock_threshold; return `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 }; 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; } // User Management async function openUserManagement() { const modal = document.getElementById('userManagementModal'); document.getElementById('newUsername').value = ''; document.getElementById('newUserPassword').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; try { const response = await apiCall('/users', { method: 'POST', body: JSON.stringify({ username, password }) }); 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 = ''; 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'); } }