Notifications and printing

This commit is contained in:
2026-02-07 07:01:31 -05:00
parent 3d1c007609
commit d277d5c07b
3 changed files with 307 additions and 27 deletions

View File

@@ -7,6 +7,37 @@ let expandedDrugs = new Set();
let currentUser = null; let currentUser = null;
let accessToken = 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 // Initialize on page load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
checkAuth(); checkAuth();
@@ -139,11 +170,13 @@ function setupEventListeners() {
const variantForm = document.getElementById('variantForm'); const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm'); const editVariantForm = document.getElementById('editVariantForm');
const dispenseForm = document.getElementById('dispenseForm'); const dispenseForm = document.getElementById('dispenseForm');
const prescribeForm = document.getElementById('prescribeForm');
const editForm = document.getElementById('editForm'); const editForm = document.getElementById('editForm');
const addModal = document.getElementById('addModal'); const addModal = document.getElementById('addModal');
const addVariantModal = document.getElementById('addVariantModal'); const addVariantModal = document.getElementById('addVariantModal');
const editVariantModal = document.getElementById('editVariantModal'); const editVariantModal = document.getElementById('editVariantModal');
const dispenseModal = document.getElementById('dispenseModal'); const dispenseModal = document.getElementById('dispenseModal');
const prescribeModal = document.getElementById('prescribeModal');
const editModal = document.getElementById('editModal'); const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn'); const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn'); const dispenseBtn = document.getElementById('dispenseBtn');
@@ -151,6 +184,7 @@ function setupEventListeners() {
const cancelVariantBtn = document.getElementById('cancelVariantBtn'); const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn'); const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
const cancelDispenseBtn = document.getElementById('cancelDispenseBtn'); const cancelDispenseBtn = document.getElementById('cancelDispenseBtn');
const cancelPrescribeBtn = document.getElementById('cancelPrescribeBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn'); const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn'); const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn'); const showLowStockBtn = document.getElementById('showLowStockBtn');
@@ -166,6 +200,7 @@ function setupEventListeners() {
if (variantForm) variantForm.addEventListener('submit', handleAddVariant); if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant); if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug); if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug); if (editForm) editForm.addEventListener('submit', handleEditDrug);
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal)); if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
@@ -178,6 +213,7 @@ function setupEventListeners() {
if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal)); if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal)); if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal)); if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal));
if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal); if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn'); const closeHistoryBtn = document.getElementById('closeHistoryBtn');
@@ -358,6 +394,7 @@ function renderDrugs() {
</div> </div>
</div> </div>
<div class="variant-actions"> <div class="variant-actions">
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}')">🏷️ Prescribe</button>
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</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-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button> <button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
@@ -435,10 +472,10 @@ async function handleAddDrug(e) {
document.getElementById('initialVariantThreshold').value = '10'; document.getElementById('initialVariantThreshold').value = '10';
closeModal(document.getElementById('addModal')); closeModal(document.getElementById('addModal'));
await loadDrugs(); await loadDrugs();
alert('Drug added successfully!'); showToast('Drug added successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error adding drug:', error); console.error('Error adding drug:', error);
alert('Failed to add drug. Check the console for details.'); showToast('Failed to add drug. Check the console for details.', 'error');
} }
} }
@@ -453,7 +490,7 @@ async function handleDispenseDrug(e) {
const notes = document.getElementById('dispenseNotes').value; const notes = document.getElementById('dispenseNotes').value;
if (!variantId || isNaN(quantity) || quantity <= 0 || !userName) { if (!variantId || isNaN(quantity) || quantity <= 0 || !userName) {
alert('Please fill in all required fields (Drug Variant, Quantity > 0, Dispensed by)'); showToast('Please fill in all required fields (Drug Variant, Quantity > 0, Dispensed by)', 'warning');
return; return;
} }
@@ -479,10 +516,10 @@ async function handleDispenseDrug(e) {
document.getElementById('dispenseForm').reset(); document.getElementById('dispenseForm').reset();
closeModal(document.getElementById('dispenseModal')); closeModal(document.getElementById('dispenseModal'));
await loadDrugs(); await loadDrugs();
alert('Drug dispensed successfully!'); showToast('Drug dispensed successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error dispensing drug:', error); console.error('Error dispensing drug:', error);
alert('Failed to dispense drug: ' + error.message); showToast('Failed to dispense drug: ' + error.message, 'error');
} }
} }
@@ -548,10 +585,10 @@ async function handleAddVariant(e) {
closeModal(document.getElementById('addVariantModal')); closeModal(document.getElementById('addVariantModal'));
await loadDrugs(); await loadDrugs();
renderDrugs(); renderDrugs();
alert('Variant added successfully!'); showToast('Variant added successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error adding variant:', error); console.error('Error adding variant:', error);
alert('Failed to add variant. Check the console for details.'); showToast('Failed to add variant. Check the console for details.', 'error');
} }
} }
@@ -598,10 +635,10 @@ async function handleEditVariant(e) {
closeModal(document.getElementById('editVariantModal')); closeModal(document.getElementById('editVariantModal'));
await loadDrugs(); await loadDrugs();
renderDrugs(); renderDrugs();
alert('Variant updated successfully!'); showToast('Variant updated successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error updating variant:', error); console.error('Error updating variant:', error);
alert('Failed to update variant. Check the console for details.'); showToast('Failed to update variant. Check the console for details.', 'error');
} }
} }
@@ -618,6 +655,105 @@ function dispenseVariant(variantId) {
openModal(document.getElementById('dispenseModal')); 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 {
showToast('Drug prescribed and label sent to printer!', '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');
}
}
// Delete variant // Delete variant
async function deleteVariant(variantId) { async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return; if (!confirm('Are you sure you want to delete this variant?')) return;
@@ -631,10 +767,10 @@ async function deleteVariant(variantId) {
await loadDrugs(); await loadDrugs();
renderDrugs(); renderDrugs();
alert('Variant deleted successfully!'); showToast('Variant deleted successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error deleting variant:', error); console.error('Error deleting variant:', error);
alert('Failed to delete variant. Check the console for details.'); showToast('Failed to delete variant. Check the console for details.', 'error');
} }
} }
@@ -730,10 +866,10 @@ async function handleEditDrug(e) {
closeEditModal(); closeEditModal();
await loadDrugs(); await loadDrugs();
alert('Drug updated successfully!'); showToast('Drug updated successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error updating drug:', error); console.error('Error updating drug:', error);
alert('Failed to update drug. Check the console for details.'); showToast('Failed to update drug. Check the console for details.', 'error');
} }
} }
@@ -749,10 +885,10 @@ async function deleteDrug(drugId) {
if (!response.ok) throw new Error('Failed to delete drug'); if (!response.ok) throw new Error('Failed to delete drug');
await loadDrugs(); await loadDrugs();
alert('Drug deleted successfully!'); showToast('Drug deleted successfully!', 'success');
} catch (error) { } catch (error) {
console.error('Error deleting drug:', error); console.error('Error deleting drug:', error);
alert('Failed to delete drug. Check the console for details.'); showToast('Failed to delete drug. Check the console for details.', 'error');
} }
} }
@@ -776,12 +912,12 @@ async function handleChangePassword(e) {
const confirmPassword = document.getElementById('confirmNewPassword').value; const confirmPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
alert('New passwords do not match!'); showToast('New passwords do not match!', 'warning');
return; return;
} }
if (newPassword.length < 1) { if (newPassword.length < 1) {
alert('New password cannot be empty!'); showToast('New password cannot be empty!', 'warning');
return; return;
} }
@@ -799,11 +935,11 @@ async function handleChangePassword(e) {
throw new Error(error.detail || 'Failed to change password'); throw new Error(error.detail || 'Failed to change password');
} }
alert('Password changed successfully!'); showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('changePasswordModal')); closeModal(document.getElementById('changePasswordModal'));
} catch (error) { } catch (error) {
console.error('Error changing password:', error); console.error('Error changing password:', error);
alert('Failed to change password: ' + error.message); showToast('Failed to change password: ' + error.message, 'error');
} }
} }
@@ -823,12 +959,12 @@ async function handleAdminChangePassword(e) {
const confirmPassword = document.getElementById('adminChangePasswordConfirm').value; const confirmPassword = document.getElementById('adminChangePasswordConfirm').value;
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
alert('Passwords do not match!'); showToast('Passwords do not match!', 'warning');
return; return;
} }
if (newPassword.length < 1) { if (newPassword.length < 1) {
alert('Password cannot be empty!'); showToast('Password cannot be empty!', 'warning');
return; return;
} }
@@ -845,12 +981,12 @@ async function handleAdminChangePassword(e) {
throw new Error(error.detail || 'Failed to change password'); throw new Error(error.detail || 'Failed to change password');
} }
alert('Password changed successfully!'); showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('adminChangePasswordModal')); closeModal(document.getElementById('adminChangePasswordModal'));
openUserManagement(); openUserManagement();
} catch (error) { } catch (error) {
console.error('Error changing password:', error); console.error('Error changing password:', error);
alert('Failed to change password: ' + error.message); showToast('Failed to change password: ' + error.message, 'error');
} }
} }
@@ -932,11 +1068,11 @@ async function createUser(e) {
document.getElementById('newUsername').value = ''; document.getElementById('newUsername').value = '';
document.getElementById('newUserPassword').value = ''; document.getElementById('newUserPassword').value = '';
alert('User created successfully!'); showToast('User created successfully!', 'success');
openUserManagement(); openUserManagement();
} catch (error) { } catch (error) {
console.error('Error creating user:', error); console.error('Error creating user:', error);
alert('Failed to create user: ' + error.message); showToast('Failed to create user: ' + error.message, 'error');
} }
} }
@@ -949,10 +1085,10 @@ async function deleteUser(userId) {
if (!response.ok) throw new Error('Failed to delete user'); if (!response.ok) throw new Error('Failed to delete user');
alert('User deleted successfully!'); showToast('User deleted successfully!', 'success');
openUserManagement(); openUserManagement();
} catch (error) { } catch (error) {
console.error('Error deleting user:', error); console.error('Error deleting user:', error);
alert('Failed to delete user: ' + error.message); showToast('Failed to delete user: ' + error.message, 'error');
} }
} }

View File

@@ -7,6 +7,9 @@
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container"></div>
<!-- Login Page --> <!-- Login Page -->
<div id="loginPage" class="login-page"> <div id="loginPage" class="login-page">
<div class="login-container"> <div class="login-container">
@@ -196,6 +199,55 @@
</div> </div>
</div> </div>
<!-- Prescribe Drug Modal -->
<div id="prescribeModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Prescribe Drug & Print Label</h2>
<form id="prescribeForm" novalidate>
<input type="hidden" id="prescribeVariantId">
<input type="hidden" id="prescribeDrugName">
<input type="hidden" id="prescribeVariantStrength">
<input type="hidden" id="prescribeUnit">
<div class="form-group">
<label for="prescribeQuantity">Quantity *</label>
<input type="number" id="prescribeQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="prescribeAnimal">Animal Name/ID *</label>
<input type="text" id="prescribeAnimal" required>
</div>
<div class="form-group">
<label for="prescribeDosage">Dosage Instructions *</label>
<input type="text" id="prescribeDosage" placeholder="e.g., 1 tablet twice daily with food" required>
</div>
<div class="form-group">
<label for="prescribeExpiry">Expiry Date *</label>
<input type="date" id="prescribeExpiry" required>
</div>
<div class="form-group">
<label for="prescribeUser">Prescribed by *</label>
<input type="text" id="prescribeUser" required>
</div>
<div class="form-group">
<label for="prescribeNotes">Notes</label>
<input type="text" id="prescribeNotes" placeholder="Optional">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Prescribe & Print Label</button>
<button type="button" class="btn btn-secondary" id="cancelPrescribeBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Variant Modal --> <!-- Add Variant Modal -->
<div id="addVariantModal" class="modal"> <div id="addVariantModal" class="modal">
<div class="modal-content"> <div class="modal-content">

View File

@@ -947,3 +947,95 @@ footer {
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.toast {
background: var(--white);
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 12px;
min-width: 300px;
max-width: 400px;
pointer-events: auto;
animation: slideIn 0.3s ease-out;
border-left: 4px solid var(--primary-color);
}
.toast.success {
border-left-color: var(--success-color);
}
.toast.error {
border-left-color: var(--danger-color);
}
.toast.warning {
border-left-color: var(--warning-color);
}
.toast.info {
border-left-color: var(--secondary-color);
}
.toast-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.toast-message {
flex: 1;
color: var(--text-dark);
font-size: 0.95em;
}
.toast.fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
@media (max-width: 768px) {
.toast-container {
top: 10px;
right: 10px;
left: 10px;
}
.toast {
min-width: auto;
width: 100%;
}
}