Files
mt-drugs/frontend/app.js
T
2026-03-28 15:10:11 -04:00

1754 lines
68 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 selectedLocationFilter = '';
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';
// Handle backward compatibility: convert old is_admin format to role
if (!currentUser.role && currentUser.is_admin !== undefined) {
currentUser.role = currentUser.is_admin ? 'admin' : 'user';
}
// Default to 'user' if role is still undefined
if (!currentUser.role) {
currentUser.role = 'user';
}
const userDisplay = document.getElementById('currentUser');
if (userDisplay) {
const roleLabel = currentUser.role.charAt(0).toUpperCase() + currentUser.role.slice(1);
userDisplay.textContent = `👤 ${currentUser.username} [${roleLabel}]`;
}
const adminBtn = document.getElementById('adminBtn');
if (adminBtn) {
adminBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
}
const locationsBtn = document.getElementById('locationsBtn');
if (locationsBtn) {
locationsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
}
const reportsBtn = document.getElementById('reportsBtn');
if (reportsBtn) {
reportsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
}
// Hide action buttons for read-only users
const isReadOnly = currentUser.role === 'readonly';
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const printNotesBtn = document.getElementById('printNotesBtn');
if (addDrugBtn) addDrugBtn.style.display = isReadOnly ? 'none' : 'block';
if (dispenseBtn) dispenseBtn.style.display = isReadOnly ? 'none' : 'block';
if (printNotesBtn) printNotesBtn.style.display = isReadOnly ? 'none' : 'block';
setupEventListeners();
loadDrugs();
}
// Handle login
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data = await response.json();
accessToken = data.access_token;
currentUser = data.user;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('currentUser', JSON.stringify(currentUser));
document.getElementById('loginForm').reset();
const errorDiv = document.getElementById('loginError');
if (errorDiv) errorDiv.style.display = 'none';
showMainApp();
} catch (error) {
const errorDiv = document.getElementById('loginError');
if (errorDiv) {
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
}
}
// Handle register
// Logout
function handleLogout() {
localStorage.removeItem('accessToken');
localStorage.removeItem('currentUser');
accessToken = null;
currentUser = null;
const loginForm = document.getElementById('loginForm');
if (loginForm) loginForm.reset();
const registerForm = document.getElementById('registerForm');
if (registerForm) {
registerForm.style.display = 'none';
}
const form = document.getElementById('loginForm');
if (form) form.style.display = 'block';
showLoginPage();
}
// API helper with authentication
async function apiCall(endpoint, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers
});
if (response.status === 401) {
handleLogout();
throw new Error('Authentication expired');
}
return response;
}
// Setup event listeners
function setupEventListeners() {
const drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
const dispenseForm = document.getElementById('dispenseForm');
const prescribeForm = document.getElementById('prescribeForm');
const editForm = document.getElementById('editForm');
const printNotesForm = document.getElementById('printNotesForm');
const addModal = document.getElementById('addModal');
const addVariantModal = document.getElementById('addVariantModal');
const editVariantModal = document.getElementById('editVariantModal');
const dispenseModal = document.getElementById('dispenseModal');
const prescribeModal = document.getElementById('prescribeModal');
const editModal = document.getElementById('editModal');
const printNotesModal = document.getElementById('printNotesModal');
const batchReceiveModal = document.getElementById('batchReceiveModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const printNotesBtn = document.getElementById('printNotesBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
const cancelDispenseBtn = document.getElementById('cancelDispenseBtn');
const cancelPrescribeBtn = document.getElementById('cancelPrescribeBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const cancelBatchReceiveBtn = document.getElementById('cancelBatchReceiveBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
const locationFilterSelect = document.getElementById('locationFilterSelect');
const userMenuBtn = document.getElementById('userMenuBtn');
const adminBtn = document.getElementById('adminBtn');
const locationsBtn = document.getElementById('locationsBtn');
const reportsBtn = document.getElementById('reportsBtn');
const logoutBtn = document.getElementById('logoutBtn');
const changePasswordBtn = document.getElementById('changePasswordBtn');
// Modal close buttons
const closeButtons = document.querySelectorAll('.close');
if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug);
if (printNotesForm) printNotesForm.addEventListener('submit', handlePrintNotes);
const batchReceiveForm = document.getElementById('batchReceiveForm');
if (batchReceiveForm) batchReceiveForm.addEventListener('submit', handleBatchReceive);
if (cancelBatchReceiveBtn) cancelBatchReceiveBtn.addEventListener('click', () => closeModal(batchReceiveModal));
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 closeLocationManagementBtn = document.getElementById('closeLocationManagementBtn');
if (closeLocationManagementBtn) closeLocationManagementBtn.addEventListener('click', () => closeModal(document.getElementById('locationManagementModal')));
const createLocationForm = document.getElementById('createLocationForm');
if (createLocationForm) createLocationForm.addEventListener('submit', createLocation);
const changePasswordForm = document.getElementById('changePasswordForm');
if (changePasswordForm) changePasswordForm.addEventListener('submit', handleChangePassword);
const cancelChangePasswordBtn = document.getElementById('cancelChangePasswordBtn');
if (cancelChangePasswordBtn) cancelChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('changePasswordModal')));
const adminChangePasswordForm = document.getElementById('adminChangePasswordForm');
if (adminChangePasswordForm) adminChangePasswordForm.addEventListener('submit', handleAdminChangePassword);
const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
closeModal(modal);
}));
if (showAllBtn) showAllBtn.addEventListener('click', () => {
showLowStockOnly = false;
updateFilterButtons();
renderDrugs();
});
if (showLowStockBtn) showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true;
updateFilterButtons();
renderDrugs();
});
if (locationFilterSelect) locationFilterSelect.addEventListener('change', (e) => {
selectedLocationFilter = e.target.value;
renderDrugs();
});
// User menu
if (userMenuBtn) userMenuBtn.addEventListener('click', () => {
const dropdown = document.getElementById('userDropdown');
if (dropdown) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
});
if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal);
if (adminBtn) adminBtn.addEventListener('click', openUserManagement);
if (locationsBtn) locationsBtn.addEventListener('click', openLocationManagement);
if (reportsBtn) reportsBtn.addEventListener('click', openReportsPage);
if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);
// Search functionality
const drugSearch = document.getElementById('drugSearch');
if (drugSearch) {
let searchTimeout;
drugSearch.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchTerm = e.target.value.toLowerCase().trim();
renderDrugs();
}, 150);
});
}
// Close modal when clicking outside
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
closeModal(e.target);
}
});
}
// Load drugs from API
async function loadDrugs() {
try {
const response = await apiCall('/drugs');
if (!response.ok) throw new Error('Failed to load drugs');
allDrugs = await response.json();
updateLocationFilterOptions();
renderDrugs();
updateDispenseDrugSelect();
} catch (error) {
console.error('Error loading drugs:', error);
document.getElementById('drugsList').innerHTML =
'<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);
});
});
}
function formatDisplayDate(value) {
if (!value) {
return 'Unknown';
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleDateString();
}
function getBatchLocationLabel(batch) {
return batch.location_name || batch.location?.name || `Location #${batch.location_id}`;
}
function updateLocationFilterOptions() {
const locationFilterSelect = document.getElementById('locationFilterSelect');
if (!locationFilterSelect) return;
const previousValue = selectedLocationFilter;
const locations = new Set();
allDrugs.forEach(drug => {
drug.variants.forEach(variant => {
(variant.batches || []).forEach(batch => {
if (batch.quantity > 0) {
locations.add(getBatchLocationLabel(batch));
}
});
});
});
locationFilterSelect.innerHTML = '<option value="">All Locations</option>';
Array.from(locations)
.sort((a, b) => a.localeCompare(b))
.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
locationFilterSelect.appendChild(option);
});
if (previousValue && locations.has(previousValue)) {
selectedLocationFilter = previousValue;
locationFilterSelect.value = previousValue;
} else {
selectedLocationFilter = '';
locationFilterSelect.value = '';
}
}
function populateDispenseBatchSelect(activeBatches) {
const batchSelect = document.getElementById('dispenseBatchSelect');
const previousValue = batchSelect.value;
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
activeBatches.forEach((batch, index) => {
const option = document.createElement('option');
const expiryLabel = formatDisplayDate(batch.expiry_date);
const locationLabel = getBatchLocationLabel(batch);
const fefoLabel = index === 0 ? ' [FEFO default]' : '';
option.value = batch.id;
option.textContent = `${batch.batch_number} | ${batch.quantity} units | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`;
batchSelect.appendChild(option);
});
if (previousValue && activeBatches.some(batch => String(batch.id) === previousValue)) {
batchSelect.value = previousValue;
}
}
// Update batch info display when variant is selected
async function updateBatchInfo() {
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
const batchInfoSection = document.getElementById('batchInfoSection');
const batchInfoContent = document.getElementById('batchInfoContent');
const batchSelect = document.getElementById('dispenseBatchSelect');
if (!variantId) {
batchInfoSection.style.display = 'none';
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
return;
}
batchInfoSection.style.display = 'block';
batchInfoContent.innerHTML = '<p class="loading">Loading batches...</p>';
try {
const response = await apiCall(`/variants/${variantId}/batches`);
if (!response.ok) throw new Error('Failed to load batches');
const batches = await response.json();
// Filter out empty batches
const activeBatches = batches.filter(b => b.quantity > 0);
if (activeBatches.length === 0) {
populateDispenseBatchSelect([]);
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">⚠️ No active batches available for this variant</p>';
return;
}
// Sort by expiry date (FEFO order)
activeBatches.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
populateDispenseBatchSelect(activeBatches);
const batchHtml = activeBatches.map((batch, index) => {
const expiryDate = new Date(batch.expiry_date);
const locationLabel = getBatchLocationLabel(batch);
const expiryLabel = formatDisplayDate(batch.expiry_date);
const today = new Date();
const isExpired = expiryDate < today;
const daysToExpiry = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24));
let expiryStatus = '✓ OK';
let statusColor = '#4caf50';
if (isExpired) {
expiryStatus = '✕ EXPIRED';
statusColor = '#d32f2f';
} else if (daysToExpiry <= 7) {
expiryStatus = `⚠️ ${daysToExpiry}d left`;
statusColor = '#ff9800';
}
const isFEFO = index === 0;
return `
<div style="padding: 8px; margin: 5px 0; background: white; border: 1px solid #e0e0e0; border-radius: 3px; ${isFEFO ? 'border-left: 3px solid #2196F3; background: #f0f8ff;' : ''}">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${batch.batch_number}</strong> ${isFEFO ? '<span style="background: #2196F3; color: white; padding: 2px 6px; border-radius: 2px; font-size: 0.8em; margin-left: 5px;">FIRST</span>' : ''}
<div style="font-size: 0.9em; color: #666; margin-top: 3px;">
Qty: <strong>${batch.quantity}</strong> |
Location: <strong>${escapeHtml(locationLabel)}</strong> |
Expiry: <strong>${expiryLabel}</strong> <span style="color: ${statusColor};">(${expiryStatus})</span>
</div>
</div>
</div>
</div>
`;
}).join('');
batchInfoContent.innerHTML = batchHtml;
} catch (error) {
console.error('Error loading batches:', error);
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">Error loading batches</p>';
}
// Update allocation preview when batches load
updateAllocationPreview();
}
// Update allocation preview based on quantity and allow_split flag
async function updateAllocationPreview() {
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
const preferredBatchId = parseInt(document.getElementById('dispenseBatchSelect').value);
const allocationPreviewSection = document.getElementById('allocationPreviewSection');
const allocationPreviewContent = document.getElementById('allocationPreviewContent');
if (!variantId || isNaN(quantity) || quantity <= 0) {
allocationPreviewSection.style.display = 'none';
return;
}
allocationPreviewSection.style.display = 'block';
allocationPreviewContent.innerHTML = '<p class="loading">Calculating allocation...</p>';
try {
const response = await apiCall(`/variants/${variantId}/batches`);
if (!response.ok) throw new Error('Failed to load batches');
const batches = await response.json();
let activeBatches = batches.filter(b => b.quantity > 0)
.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
if (activeBatches.length === 0) {
allocationPreviewContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">⚠️ No active batches available</p>';
return;
}
if (!Number.isNaN(preferredBatchId)) {
const preferredBatch = activeBatches.find(batch => batch.id === preferredBatchId);
if (!preferredBatch) {
allocationPreviewContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">✕ Selected preferred batch is no longer available.</p>';
return;
}
activeBatches = [preferredBatch, ...activeBatches.filter(batch => batch.id !== preferredBatchId)];
}
// Simulate FEFO allocation
const allocations = [];
let remainingQty = quantity;
for (const batch of activeBatches) {
if (remainingQty <= 0) break;
const allocQty = Math.min(remainingQty, batch.quantity);
allocations.push({
batchNumber: batch.batch_number,
batchId: batch.id,
quantity: allocQty,
location: getBatchLocationLabel(batch),
expiryDate: batch.expiry_date,
preferred: !Number.isNaN(preferredBatchId) && batch.id === preferredBatchId
});
remainingQty -= allocQty;
if (!allowSplit) break;
}
if (remainingQty > 0 && !allowSplit) {
const failureContext = !Number.isNaN(preferredBatchId)
? 'Preferred batch cannot fully satisfy this request. Enable split to fall through to FEFO batches.'
: 'Insufficient stock in first batch. Check "Allow Split" to use multiple batches.';
allocationPreviewContent.innerHTML = `<p style="color: #d32f2f; margin: 0;">✕ ${failureContext}</p>`;
return;
}
if (remainingQty > 0 && allowSplit) {
allocationPreviewContent.innerHTML = `
<p style="color: #d32f2f; margin: 0 0 10px 0;">✕ Warning: Only ${quantity - remainingQty} units available across all batches (${remainingQty} short)</p>
<div>${allocations.map(a => `
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
</div>
`).join('')}</div>
`;
return;
}
const allocationHtml = allocations.map(a => `
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
</div>
`).join('');
const pluralText = allocations.length === 1 ? 'batch' : 'batches';
const introText = !Number.isNaN(preferredBatchId)
? `✓ Will start from your preferred batch, then use FEFO for any remainder across <strong>${allocations.length} ${pluralText}</strong>:`
: `✓ Will dispense from <strong>${allocations.length} ${pluralText}</strong>:`;
allocationPreviewContent.innerHTML = `
<p style="margin: 0 0 8px 0; color: #333;">${introText}</p>
<div>${allocationHtml}</div>
`;
} catch (error) {
console.error('Error calculating allocation:', error);
allocationPreviewContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">Error calculating allocation</p>';
}
}
// Render drugs list
function renderDrugs() {
const drugsList = document.getElementById('drugsList');
let drugsToShow = allDrugs;
// Apply search filter
if (searchTerm) {
drugsToShow = drugsToShow.filter(drug =>
drug.name.toLowerCase().includes(searchTerm) ||
(drug.description && drug.description.toLowerCase().includes(searchTerm)) ||
drug.variants.some(variant => variant.strength.toLowerCase().includes(searchTerm))
);
}
// Apply stock filter
if (showLowStockOnly) {
drugsToShow = drugsToShow.filter(drug =>
drug.variants.some(variant => variant.quantity <= variant.low_stock_threshold)
);
}
// Apply location filter
if (selectedLocationFilter) {
drugsToShow = drugsToShow.filter(drug =>
drug.variants.some(variant =>
(variant.batches || []).some(batch =>
batch.quantity > 0 && getBatchLocationLabel(batch) === selectedLocationFilter
)
)
);
}
// Sort alphabetically by drug name
drugsToShow = drugsToShow.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
if (drugsToShow.length === 0) {
drugsList.innerHTML = '<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 isReadOnly = currentUser.role === 'readonly';
const isControlled = drug.is_controlled;
const variantsHtml = isExpanded ? `
${drug.variants.map(variant => {
const variantIsLowStock = variant.quantity <= variant.low_stock_threshold;
return `
<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">
${!isReadOnly ? `
<button class="btn btn-success btn-small" onclick="openBatchReceiveModal(${variant.id})">📦 Receive Batch</button>
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
<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)}
${isControlled ? '<span style="background: #d32f2f; color: white; padding: 2px 6px; border-radius: 2px; font-size: 0.75em; margin-left: 8px; display: inline-block;">⚠️ CONTROLLED</span>' : ''}
</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">
${!isReadOnly ? `
<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>
${!isReadOnly ? `
<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,
is_controlled: document.getElementById('drugIsControlled').checked
};
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 preferredBatchIdValue = document.getElementById('dispenseBatchSelect').value;
const animalName = document.getElementById('dispenseAnimal').value;
const userName = document.getElementById('dispenseUser').value;
const notes = document.getElementById('dispenseNotes').value;
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
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,
batch_id: preferredBatchIdValue ? parseInt(preferredBatchIdValue) : null,
animal_name: animalName || null,
user_name: userName,
notes: notes || null,
allow_split: allowSplit
};
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('editDrugIsControlled').checked = drug.is_controlled || false;
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;
// Update batch info for selected variant
updateBatchInfo();
// Open dispense modal
openModal(document.getElementById('dispenseModal'));
}
// Prescribe variant and print label
function prescribeVariant(variantId, drugName, variantStrength, unit) {
// Set hidden fields
document.getElementById('prescribeVariantId').value = variantId;
document.getElementById('prescribeDrugName').value = drugName;
document.getElementById('prescribeVariantStrength').value = variantStrength;
document.getElementById('prescribeUnit').value = unit || 'units';
// Pre-fill user name if available
if (currentUser) {
document.getElementById('prescribeUser').value = currentUser.username;
}
// Set default expiry date to 1 month from now
const defaultExpiry = new Date();
defaultExpiry.setMonth(defaultExpiry.getMonth() + 1);
document.getElementById('prescribeExpiry').value = defaultExpiry.toISOString().split('T')[0];
// Open prescribe modal
openModal(document.getElementById('prescribeModal'));
}
// Handle prescribe drug form submission
async function handlePrescribeDrug(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('prescribeVariantId').value);
const drugName = document.getElementById('prescribeDrugName').value;
const variantStrength = document.getElementById('prescribeVariantStrength').value;
const unit = document.getElementById('prescribeUnit').value;
const quantity = parseFloat(document.getElementById('prescribeQuantity').value);
const animalName = document.getElementById('prescribeAnimal').value;
const dosage = document.getElementById('prescribeDosage').value;
const expiryDate = document.getElementById('prescribeExpiry').value;
const userName = document.getElementById('prescribeUser').value;
const notes = document.getElementById('prescribeNotes').value;
if (!variantId || isNaN(quantity) || quantity <= 0 || !animalName || !dosage || !expiryDate || !userName) {
showToast('Please fill in all required fields', 'warning');
return;
}
// Convert expiry date to DD/MM/YYYY format
const expiryParts = expiryDate.split('-');
const formattedExpiry = `${expiryParts[2]}/${expiryParts[1]}/${expiryParts[0]}`;
try {
// First, print the label
const labelData = {
variables: {
practice_name: "Many Tears Animal Rescue",
animal_name: animalName,
drug_name: `${drugName} ${variantStrength}`,
dosage: dosage,
quantity: `${quantity} ${unit}`,
expiry_date: formattedExpiry
}
};
const labelResponse = await apiCall('/labels/print', {
method: 'POST',
body: JSON.stringify(labelData)
});
if (!labelResponse.ok) {
const error = await labelResponse.json();
throw new Error(error.detail || 'Label printing request failed');
}
const labelResult = await labelResponse.json();
console.log('Label print result:', labelResult);
if (!labelResult.success) {
// Label printing failed - don't dispense the drug
const isError = labelResult.message && (
labelResult.message.includes('not found') ||
labelResult.message.includes('error') ||
labelResult.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast('Cannot dispense: ' + labelResult.message, toastType, 5000);
return;
}
// Label printed successfully, now dispense the drug
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
animal_name: animalName,
user_name: userName,
notes: notes || null
};
const dispenseResponse = await apiCall('/dispense', {
method: 'POST',
body: JSON.stringify(dispensingData)
});
if (!dispenseResponse.ok) {
const error = await dispenseResponse.json();
throw new Error(error.detail || 'Failed to dispense drug');
}
// Both operations succeeded
showToast('Drug prescribed and label printed successfully!', 'success');
document.getElementById('prescribeForm').reset();
closeModal(document.getElementById('prescribeModal'));
await loadDrugs();
} catch (error) {
console.error('Error prescribing drug:', error);
showToast('Failed to prescribe drug: ' + error.message, 'error');
}
}
// Handle print notes form submission
async function handlePrintNotes(e) {
e.preventDefault();
const animalName = document.getElementById('notesAnimalName').value.trim();
const notes = document.getElementById('notesContent').value.trim();
if (!animalName || !notes) {
showToast('Please fill in all required fields', 'warning');
return;
}
try {
// Send notes to print endpoint
const notesData = {
variables: {
animal_name: animalName,
notes: notes
}
};
const response = await apiCall('/notes/print', {
method: 'POST',
body: JSON.stringify(notesData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Notes printing request failed');
}
const result = await response.json();
console.log('Notes print result:', result);
if (!result.success) {
// Printing failed
const isError = result.message && (
result.message.includes('not found') ||
result.message.includes('error') ||
result.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast(result.message, toastType, 5000);
return;
}
// Printing succeeded
showToast('Notes printed successfully!', 'success');
document.getElementById('printNotesForm').reset();
closeModal(document.getElementById('printNotesModal'));
} catch (error) {
console.error('Error printing notes:', error);
showToast('Failed to print notes: ' + error.message, 'error');
}
}
// Delete variant
async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
const response = await apiCall(`/variants/${variantId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete variant');
await loadDrugs();
renderDrugs();
showToast('Variant deleted successfully!', 'success');
} catch (error) {
console.error('Error deleting variant:', error);
showToast('Failed to delete variant. Check the console for details.', 'error');
}
}
// Show dispensing history for a drug
async function showDrugHistory(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
if (!drug) return;
const historyModal = document.getElementById('historyModal');
const historyContent = document.getElementById('historyContent');
document.getElementById('historyDrugName').textContent = drug.name;
historyContent.innerHTML = '<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,
is_controlled: document.getElementById('editDrugIsControlled').checked
};
try {
const response = await apiCall(`/drugs/${drugId}`, {
method: 'PUT',
body: JSON.stringify(drugData)
});
if (!response.ok) throw new Error('Failed to update drug');
closeEditModal();
await loadDrugs();
showToast('Drug updated successfully!', 'success');
} catch (error) {
console.error('Error updating drug:', error);
showToast('Failed to update drug. Check the console for details.', 'error');
}
}
// Delete drug
async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
const response = await apiCall(`/drugs/${drugId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete drug');
await loadDrugs();
showToast('Drug deleted successfully!', 'success');
} catch (error) {
console.error('Error deleting drug:', error);
showToast('Failed to delete drug. Check the console for details.', 'error');
}
}
// Password Management
function openChangePasswordModal() {
const modal = document.getElementById('changePasswordModal');
document.getElementById('changePasswordForm').reset();
// Close dropdown
const dropdown = document.getElementById('userDropdown');
if (dropdown) dropdown.style.display = 'none';
openModal(modal);
}
async function handleChangePassword(e) {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmPassword) {
showToast('New passwords do not match!', 'warning');
return;
}
if (newPassword.length < 1) {
showToast('New password cannot be empty!', 'warning');
return;
}
try {
const response = await apiCall('/auth/change-password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to change password');
}
showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('changePasswordModal'));
} catch (error) {
console.error('Error changing password:', error);
showToast('Failed to change password: ' + error.message, 'error');
}
}
async function openAdminChangePasswordModal(userId, username) {
const modal = document.getElementById('adminChangePasswordModal');
document.getElementById('adminChangePasswordForm').reset();
document.getElementById('adminChangePasswordUserId').value = userId;
document.getElementById('adminChangePasswordUsername').value = username;
openModal(modal);
}
async function handleAdminChangePassword(e) {
e.preventDefault();
const userId = document.getElementById('adminChangePasswordUserId').value;
const newPassword = document.getElementById('adminChangePasswordNewPassword').value;
const confirmPassword = document.getElementById('adminChangePasswordConfirm').value;
if (newPassword !== confirmPassword) {
showToast('Passwords do not match!', 'warning');
return;
}
if (newPassword.length < 1) {
showToast('Password cannot be empty!', 'warning');
return;
}
try {
const response = await apiCall(`/users/${userId}/change-password`, {
method: 'POST',
body: JSON.stringify({
new_password: newPassword
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to change password');
}
showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('adminChangePasswordModal'));
openUserManagement();
} catch (error) {
console.error('Error changing password:', error);
showToast('Failed to change password: ' + error.message, 'error');
}
}
// Update filter button states
function updateFilterButtons() {
document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly);
document.getElementById('showLowStockBtn').classList.toggle('active', showLowStockOnly);
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function openReportsPage() {
const dropdown = document.getElementById('userDropdown');
if (dropdown) dropdown.style.display = 'none';
window.location.href = 'reports.html';
}
// User Management
async function openUserManagement() {
const modal = document.getElementById('userManagementModal');
document.getElementById('newUsername').value = '';
document.getElementById('newUserPassword').value = '';
document.getElementById('newUserRole').value = '';
const usersList = document.getElementById('usersList');
usersList.innerHTML = '<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 => {
const roleLabel = user.role.charAt(0).toUpperCase() + user.role.slice(1);
const roleBadge = user.role === 'admin' ? '👑 Admin' :
user.role === 'readonly' ? '👁️ Read-Only' : '👤 Regular';
return `
<div class="user-item">
<span>${user.username}</span>
<span class="admin-badge">${roleBadge}</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;
const role = document.getElementById('newUserRole').value;
if (!role) {
showToast('Please select a role', 'warning');
return;
}
try {
const response = await apiCall('/users', {
method: 'POST',
body: JSON.stringify({ username, password, role })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create user');
}
document.getElementById('newUsername').value = '';
document.getElementById('newUserPassword').value = '';
document.getElementById('newUserRole').value = '';
showToast('User created successfully!', 'success');
openUserManagement();
} catch (error) {
console.error('Error creating user:', error);
showToast('Failed to create user: ' + error.message, 'error');
}
}
// Delete user
async function deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user?')) return;
try {
const response = await apiCall(`/users/${userId}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete user');
showToast('User deleted successfully!', 'success');
openUserManagement();
} catch (error) {
console.error('Error deleting user:', error);
showToast('Failed to delete user: ' + error.message, 'error');
}
}
// Location Management
async function openLocationManagement() {
const modal = document.getElementById('locationManagementModal');
document.getElementById('newLocationName').value = '';
const locationsList = document.getElementById('locationsList');
locationsList.innerHTML = '<h3>Active Locations</h3><p class="loading">Loading locations...</p>';
try {
const response = await apiCall('/locations');
if (!response.ok) throw new Error('Failed to load locations');
const locations = await response.json();
const activeLocations = locations.filter(loc => loc.is_active);
const inactiveLocations = locations.filter(loc => !loc.is_active);
let locationsHtml = '<h3>Active Locations</h3>';
if (activeLocations.length === 0) {
locationsHtml += '<p class="empty">No active locations</p>';
} else {
locationsHtml += `<div class="locations-table">
${activeLocations.map(location => `
<div class="location-item">
<div style="flex: 1;">
<strong>${location.name}</strong>
<div style="font-size: 0.85em; color: #666;">Created: ${new Date(location.created_at).toLocaleDateString()}</div>
</div>
<button class="btn btn-danger btn-small" onclick="archiveLocation(${location.id}, '${location.name.replace(/'/g, "\\'")}')">Archive</button>
</div>
`).join('')}
</div>`;
}
if (inactiveLocations.length > 0) {
locationsHtml += `
<h3 style="margin-top: 20px;">Archived Locations</h3>
<div class="locations-table">
${inactiveLocations.map(location => `
<div class="location-item" style="opacity: 0.6;">
<div style="flex: 1;">
<strong>${location.name}</strong> <span style="color: #999;">(archived)</span>
</div>
<button class="btn btn-secondary btn-small" onclick="restoreLocation(${location.id}, '${location.name.replace(/'/g, "\\'")}')">Restore</button>
</div>
`).join('')}
</div>
`;
}
locationsList.innerHTML = locationsHtml;
} catch (error) {
console.error('Error loading locations:', error);
locationsList.innerHTML = '<h3>Active Locations</h3><p class="empty">Error loading locations</p>';
}
openModal(modal);
}
// Create location
async function createLocation(e) {
e.preventDefault();
const name = document.getElementById('newLocationName').value.trim();
if (!name) {
showToast('Please enter a location name', 'warning');
return;
}
try {
const response = await apiCall('/locations', {
method: 'POST',
body: JSON.stringify({ name })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create location');
}
document.getElementById('newLocationName').value = '';
showToast('Location created successfully!', 'success');
openLocationManagement();
} catch (error) {
console.error('Error creating location:', error);
showToast('Failed to create location: ' + error.message, 'error');
}
}
// Archive location
async function archiveLocation(locationId, locationName) {
if (!confirm(`Archive location "${locationName}"?\n\nYou can restore it later if needed.`)) return;
try {
const response = await apiCall(`/locations/${locationId}`, {
method: 'PUT',
body: JSON.stringify({ is_active: false })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to archive location');
}
showToast('Location archived successfully!', 'success');
openLocationManagement();
} catch (error) {
console.error('Error archiving location:', error);
showToast('Failed to archive location: ' + error.message, 'error');
}
}
// Restore location
async function restoreLocation(locationId, locationName) {
if (!confirm(`Restore location "${locationName}"?`)) return;
try {
const response = await apiCall(`/locations/${locationId}`, {
method: 'PUT',
body: JSON.stringify({ is_active: true })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to restore location');
}
showToast('Location restored successfully!', 'success');
openLocationManagement();
} catch (error) {
console.error('Error restoring location:', error);
showToast('Failed to restore location: ' + error.message, 'error');
}
}
// Batch Management
async function openBatchReceiveModal(variantId) {
const batchReceiveModal = document.getElementById('batchReceiveModal');
document.getElementById('batchReceiveForm').reset();
document.getElementById('batchVariantId').value = variantId;
// Initialize locations
await initializeBatchLocations();
openModal(batchReceiveModal);
}
async function initializeBatchLocations() {
const locationSelect = document.getElementById('batchLocation');
try {
const response = await apiCall('/locations');
if (!response.ok) throw new Error('Failed to load locations');
const locations = await response.json();
locationSelect.innerHTML = '<option value="">-- Select location --</option>';
locations.forEach(location => {
if (location.is_active) {
const option = document.createElement('option');
option.value = location.id;
option.textContent = location.name;
locationSelect.appendChild(option);
}
});
} catch (error) {
console.error('Error loading locations:', error);
showToast('Failed to load storage locations', 'error');
}
}
async function handleBatchReceive(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('batchVariantId').value);
const batchNumber = document.getElementById('batchNumber').value.trim();
const quantity = parseFloat(document.getElementById('batchQuantity').value);
const expiryDate = document.getElementById('batchExpiryDate').value;
const locationId = parseInt(document.getElementById('batchLocation').value);
const notes = document.getElementById('batchNotes').value.trim();
if (!batchNumber || isNaN(quantity) || quantity <= 0 || !expiryDate || !locationId) {
showToast('Please fill in all required fields', 'warning');
return;
}
const batchData = {
batch_number: batchNumber,
quantity: quantity,
expiry_date: expiryDate,
location_id: locationId,
notes: notes || null
};
try {
const response = await apiCall(`/variants/${variantId}/batches`, {
method: 'POST',
body: JSON.stringify(batchData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to receive batch');
}
document.getElementById('batchReceiveForm').reset();
closeModal(document.getElementById('batchReceiveModal'));
await loadDrugs();
showToast('Batch received successfully!', 'success');
} catch (error) {
console.error('Error receiving batch:', error);
showToast('Failed to receive batch: ' + error.message, 'error');
}
}