Files
mt-drugs/frontend/app.js
2026-01-16 12:48:44 -05:00

568 lines
22 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();
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
const dispenseForm = document.getElementById('dispenseForm');
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 editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
const cancelDispenseBtn = document.getElementById('cancelDispenseBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
// Modal close buttons
const closeButtons = document.querySelectorAll('.close');
drugForm.addEventListener('submit', handleAddDrug);
variantForm.addEventListener('submit', handleAddVariant);
editVariantForm.addEventListener('submit', handleEditVariant);
dispenseForm.addEventListener('submit', handleDispenseDrug);
editForm.addEventListener('submit', handleEditDrug);
addDrugBtn.addEventListener('click', () => openModal(addModal));
cancelAddBtn.addEventListener('click', () => closeModal(addModal));
cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
closeModal(modal);
}));
showAllBtn.addEventListener('click', () => {
showLowStockOnly = false;
updateFilterButtons();
renderDrugs();
});
showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true;
updateFilterButtons();
renderDrugs();
});
// 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);
}
});
loadDrugs();
});
// Load drugs from API
async function loadDrugs() {
try {
const response = await fetch(`${API_URL}/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 on http://localhost:8000</p>';
}
}
// Modal utility functions
function openModal(modal) {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModal(modal) {
modal.classList.remove('show');
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)
);
}
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-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 {
const response = await fetch(`${API_URL}/drugs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
if (!response.ok) throw new Error('Failed to add drug');
document.getElementById('drugForm').reset();
closeModal(document.getElementById('addModal'));
await loadDrugs();
alert('Drug added successfully! Now add variants for this drug.');
} catch (error) {
console.error('Error adding drug:', error);
alert('Failed to add drug. Check the console for details.');
}
}
// 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 || !quantity || !animalName || !userName) {
alert('Please fill in all required fields');
return;
}
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
animal_name: animalName,
user_name: userName,
notes: notes || null
};
try {
const response = await fetch(`${API_URL}/dispense`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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();
alert('Drug dispensed successfully!');
} catch (error) {
console.error('Error dispensing drug:', error);
alert('Failed to dispense drug: ' + error.message);
}
}
// 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 fetch(`${API_URL}/drugs/${drugId}/variants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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();
alert('Variant added successfully!');
} catch (error) {
console.error('Error adding variant:', error);
alert('Failed to add variant. Check the console for details.');
}
}
// Open edit variant modal
function openEditVariantModal(variantId) {
const variant = currentDrug.variants.find(v => v.id === variantId);
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 fetch(`${API_URL}/variants/${variantId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
if (!response.ok) throw new Error('Failed to update variant');
closeModal(document.getElementById('editVariantModal'));
await loadDrugs();
renderDrugs();
alert('Variant updated successfully!');
} catch (error) {
console.error('Error updating variant:', error);
alert('Failed to update variant. Check the console for details.');
}
}
// 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'));
}
// Delete variant
async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
const response = await fetch(`${API_URL}/variants/${variantId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete variant');
await loadDrugs();
renderDrugs();
alert('Variant deleted successfully!');
} catch (error) {
console.error('Error deleting variant:', error);
alert('Failed to delete variant. Check the console for details.');
}
}
// 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 fetch(`${API_URL}/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 fetch(`${API_URL}/drugs/${drugId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
if (!response.ok) throw new Error('Failed to update drug');
closeEditModal();
await loadDrugs();
alert('Drug updated successfully!');
} catch (error) {
console.error('Error updating drug:', error);
alert('Failed to update drug. Check the console for details.');
}
}
// Delete drug
async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete drug');
await loadDrugs();
alert('Drug deleted successfully!');
} catch (error) {
console.error('Error deleting drug:', error);
alert('Failed to delete drug. Check the console for details.');
}
}
// 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;
}