1176 lines
44 KiB
JavaScript
1176 lines
44 KiB
JavaScript
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 = `
|
||
<span class="toast-icon">${icons[type] || icons.info}</span>
|
||
<span class="toast-message">${message}</span>
|
||
`;
|
||
|
||
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 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 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 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 (printNotesForm) printNotesForm.addEventListener('submit', handlePrintNotes);
|
||
|
||
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
|
||
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
|
||
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 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 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 =
|
||
'<p class="empty">Error loading drugs. Make sure the backend is running.</p>';
|
||
}
|
||
}
|
||
|
||
// 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 = '<option value="">-- Select a drug variant --</option>';
|
||
|
||
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 = '<p class="empty">No drugs found matching your criteria</p>';
|
||
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 `
|
||
<div class="variant-item ${variantIsLowStock ? 'low-stock' : ''}">
|
||
<div class="variant-info">
|
||
<div class="variant-details">
|
||
<div class="variant-name">${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}</div>
|
||
<div class="variant-quantity">${variant.quantity} ${escapeHtml(variant.unit)}</div>
|
||
</div>
|
||
<div class="variant-status">
|
||
<span class="variant-badge ${variantIsLowStock ? 'badge-low' : 'badge-normal'}">
|
||
${variantIsLowStock ? 'Low Stock' : 'OK'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="variant-actions">
|
||
<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>
|
||
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}` : '';
|
||
|
||
return `
|
||
<div class="drug-item ${isLowStock ? 'low-stock' : ''} ${isExpanded ? 'expanded' : ''}" onclick="toggleDrugExpansion(${drug.id})">
|
||
<div class="drug-info">
|
||
<div class="drug-name">${escapeHtml(drug.name)}</div>
|
||
<div class="drug-description">${drug.description ? escapeHtml(drug.description) : 'No description'}</div>
|
||
<div class="drug-quantity">${totalVariants} variant${totalVariants !== 1 ? 's' : ''} (${totalQuantity} total units)</div>
|
||
<div class="drug-status">
|
||
<span class="drug-badge ${isLowStock ? 'badge-low' : 'badge-normal'}">
|
||
${isLowStock ? `${lowStockVariants} low` : 'All OK'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="drug-actions">
|
||
<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>
|
||
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
|
||
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})">Delete</button>
|
||
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
|
||
</div>
|
||
</div>
|
||
<div class="drug-variants ${isExpanded ? 'expanded' : ''}" id="variants-${drug.id}">
|
||
${variantsHtml}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Handle add drug form
|
||
async function handleAddDrug(e) {
|
||
e.preventDefault();
|
||
|
||
const drugData = {
|
||
name: document.getElementById('drugName').value,
|
||
description: document.getElementById('drugDescription').value
|
||
};
|
||
|
||
try {
|
||
// Create the drug first
|
||
const drugResponse = await apiCall('/drugs', {
|
||
method: 'POST',
|
||
body: JSON.stringify(drugData)
|
||
});
|
||
|
||
if (!drugResponse.ok) throw new Error('Failed to add drug');
|
||
const createdDrug = await drugResponse.json();
|
||
|
||
// Check if initial variant data was provided
|
||
const variantStrength = document.getElementById('initialVariantStrength').value.trim();
|
||
if (variantStrength) {
|
||
const variantData = {
|
||
strength: variantStrength,
|
||
quantity: parseFloat(document.getElementById('initialVariantQuantity').value) || 0,
|
||
unit: document.getElementById('initialVariantUnit').value || 'units',
|
||
low_stock_threshold: parseFloat(document.getElementById('initialVariantThreshold').value) || 10
|
||
};
|
||
|
||
const variantResponse = await apiCall(`/drugs/${createdDrug.id}/variants`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(variantData)
|
||
});
|
||
|
||
if (!variantResponse.ok) throw new Error('Failed to add variant');
|
||
}
|
||
|
||
document.getElementById('drugForm').reset();
|
||
document.getElementById('initialVariantUnit').value = 'units';
|
||
document.getElementById('initialVariantThreshold').value = '10';
|
||
closeModal(document.getElementById('addModal'));
|
||
await loadDrugs();
|
||
showToast('Drug added successfully!', 'success');
|
||
} catch (error) {
|
||
console.error('Error adding drug:', error);
|
||
showToast('Failed to add drug. Check the console for details.', 'error');
|
||
}
|
||
}
|
||
|
||
// Handle dispense drug form
|
||
async function handleDispenseDrug(e) {
|
||
e.preventDefault();
|
||
|
||
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
||
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
||
const animalName = document.getElementById('dispenseAnimal').value;
|
||
const userName = document.getElementById('dispenseUser').value;
|
||
const notes = document.getElementById('dispenseNotes').value;
|
||
|
||
if (!variantId || isNaN(quantity) || quantity <= 0 || !userName) {
|
||
showToast('Please fill in all required fields (Drug Variant, Quantity > 0, Dispensed by)', 'warning');
|
||
return;
|
||
}
|
||
|
||
const dispensingData = {
|
||
drug_variant_id: variantId,
|
||
quantity: quantity,
|
||
animal_name: animalName || null,
|
||
user_name: userName,
|
||
notes: notes || null
|
||
};
|
||
|
||
try {
|
||
const response = await apiCall('/dispense', {
|
||
method: 'POST',
|
||
body: JSON.stringify(dispensingData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Failed to dispense drug');
|
||
}
|
||
|
||
document.getElementById('dispenseForm').reset();
|
||
closeModal(document.getElementById('dispenseModal'));
|
||
await loadDrugs();
|
||
showToast('Drug dispensed successfully!', 'success');
|
||
} catch (error) {
|
||
console.error('Error dispensing drug:', error);
|
||
showToast('Failed to dispense drug: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Open edit modal
|
||
function openEditModal(drugId) {
|
||
const drug = allDrugs.find(d => d.id === drugId);
|
||
if (!drug) return;
|
||
|
||
document.getElementById('editDrugId').value = drug.id;
|
||
document.getElementById('editDrugName').value = drug.name;
|
||
document.getElementById('editDrugDescription').value = drug.description || '';
|
||
|
||
document.getElementById('editModal').classList.add('show');
|
||
}
|
||
|
||
// Close edit modal
|
||
function closeEditModal() {
|
||
document.getElementById('editModal').classList.remove('show');
|
||
document.getElementById('editForm').reset();
|
||
}
|
||
|
||
// Show variants for a drug
|
||
function toggleDrugExpansion(drugId) {
|
||
if (expandedDrugs.has(drugId)) {
|
||
expandedDrugs.delete(drugId);
|
||
} else {
|
||
expandedDrugs.add(drugId);
|
||
}
|
||
renderDrugs();
|
||
}
|
||
|
||
// Open add variant modal
|
||
function openAddVariantModal(drugId) {
|
||
const drug = allDrugs.find(d => d.id === drugId);
|
||
if (!drug) return;
|
||
|
||
currentDrug = drug;
|
||
document.getElementById('variantDrugId').value = drug.id;
|
||
document.getElementById('addVariantModal').classList.add('show');
|
||
}
|
||
|
||
// Handle add variant form
|
||
async function handleAddVariant(e) {
|
||
e.preventDefault();
|
||
|
||
const drugId = parseInt(document.getElementById('variantDrugId').value);
|
||
const variantData = {
|
||
strength: document.getElementById('variantStrength').value,
|
||
quantity: parseFloat(document.getElementById('variantQuantity').value),
|
||
unit: document.getElementById('variantUnit').value,
|
||
low_stock_threshold: parseFloat(document.getElementById('variantThreshold').value)
|
||
};
|
||
|
||
try {
|
||
const response = await apiCall(`/drugs/${drugId}/variants`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(variantData)
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to add variant');
|
||
|
||
document.getElementById('variantForm').reset();
|
||
closeModal(document.getElementById('addVariantModal'));
|
||
await loadDrugs();
|
||
renderDrugs();
|
||
showToast('Variant added successfully!', 'success');
|
||
} catch (error) {
|
||
console.error('Error adding variant:', error);
|
||
showToast('Failed to add variant. Check the console for details.', 'error');
|
||
}
|
||
}
|
||
|
||
// Open edit variant modal
|
||
function openEditVariantModal(variantId) {
|
||
// Find the variant from all drugs
|
||
let variant = null;
|
||
for (const drug of allDrugs) {
|
||
variant = drug.variants.find(v => v.id === variantId);
|
||
if (variant) break;
|
||
}
|
||
|
||
if (!variant) return;
|
||
|
||
document.getElementById('editVariantId').value = variant.id;
|
||
document.getElementById('editVariantStrength').value = variant.strength;
|
||
document.getElementById('editVariantQuantity').value = variant.quantity;
|
||
document.getElementById('editVariantUnit').value = variant.unit;
|
||
document.getElementById('editVariantThreshold').value = variant.low_stock_threshold;
|
||
|
||
document.getElementById('editVariantModal').classList.add('show');
|
||
}
|
||
|
||
// Handle edit variant form
|
||
async function handleEditVariant(e) {
|
||
e.preventDefault();
|
||
|
||
const variantId = parseInt(document.getElementById('editVariantId').value);
|
||
const variantData = {
|
||
strength: document.getElementById('editVariantStrength').value,
|
||
quantity: parseFloat(document.getElementById('editVariantQuantity').value),
|
||
unit: document.getElementById('editVariantUnit').value,
|
||
low_stock_threshold: parseFloat(document.getElementById('editVariantThreshold').value)
|
||
};
|
||
|
||
try {
|
||
const response = await apiCall(`/variants/${variantId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(variantData)
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to update variant');
|
||
|
||
closeModal(document.getElementById('editVariantModal'));
|
||
await loadDrugs();
|
||
renderDrugs();
|
||
showToast('Variant updated successfully!', 'success');
|
||
} catch (error) {
|
||
console.error('Error updating variant:', error);
|
||
showToast('Failed to update variant. Check the console for details.', '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;
|
||
|
||
// 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 = '<p class="loading">Loading history...</p>';
|
||
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 = '<p class="empty">No dispensing history for this drug.</p>';
|
||
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 `
|
||
<div class="history-item">
|
||
<div class="history-header">
|
||
<div class="history-variant">${drug.name} ${variant.strength}</div>
|
||
<div class="history-datetime">${date} ${time}</div>
|
||
</div>
|
||
<div class="history-details">
|
||
<div class="history-row">
|
||
<span class="history-label">Quantity:</span>
|
||
<span class="history-value">${item.quantity} ${variant.unit}</span>
|
||
</div>
|
||
<div class="history-row">
|
||
<span class="history-label">Animal:</span>
|
||
<span class="history-value">${escapeHtml(item.animal_name)}</span>
|
||
</div>
|
||
<div class="history-row">
|
||
<span class="history-label">User:</span>
|
||
<span class="history-value">${escapeHtml(item.user_name)}</span>
|
||
</div>
|
||
${item.notes ? `
|
||
<div class="history-row">
|
||
<span class="history-label">Notes:</span>
|
||
<span class="history-value">${escapeHtml(item.notes)}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
historyContent.innerHTML = historyHtml;
|
||
} catch (error) {
|
||
console.error('Error fetching history:', error);
|
||
historyContent.innerHTML = '<p class="error">Failed to load history. Check the console for details.</p>';
|
||
}
|
||
}
|
||
|
||
// 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 = '<h3>Users</h3><p class="loading">Loading users...</p>';
|
||
|
||
try {
|
||
const response = await apiCall('/users');
|
||
if (!response.ok) throw new Error('Failed to load users');
|
||
|
||
const users = await response.json();
|
||
|
||
const usersHtml = `
|
||
<h3>Users</h3>
|
||
<div class="users-table">
|
||
${users.map(user => `
|
||
<div class="user-item">
|
||
<span>${user.username}</span>
|
||
<span class="admin-badge">${user.is_admin ? '👑 Admin' : 'User'}</span>
|
||
<button class="btn btn-secondary btn-small" onclick="openAdminChangePasswordModal(${user.id}, '${escapeHtml(user.username)}')">🔑 Password</button>
|
||
${user.id !== currentUser.id ? `
|
||
<button class="btn btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
|
||
` : ''}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
usersList.innerHTML = usersHtml;
|
||
} catch (error) {
|
||
console.error('Error loading users:', error);
|
||
usersList.innerHTML = '<h3>Users</h3><p class="empty">Error loading users</p>';
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|