Files
mt-drugs/frontend/app.js

1108 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 =
'<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</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, dispense the drug (decrement inventory)
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');
}
// Second, 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) {
console.error('Label printing failed, but drug was dispensed');
showToast('Drug prescribed successfully, but label printing failed', 'warning', 5000);
} else {
const labelResult = await labelResponse.json();
console.log('Label print result:', labelResult);
if (labelResult.success) {
showToast('Drug prescribed and label printed successfully!', 'success');
} else {
// Show as error toast if it contains specific error keywords
const isError = labelResult.message && (
labelResult.message.includes('not found') ||
labelResult.message.includes('error') ||
labelResult.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast('Drug prescribed but ' + labelResult.message, toastType, 5000);
}
}
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');
}
}
// 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');
}
}