diff --git a/frontend/app.js b/frontend/app.js
index cbcfab4..6b0e00e 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -7,6 +7,37 @@ 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 = `
+ ${icons[type] || icons.info}
+ ${message}
+ `;
+
+ 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();
@@ -139,11 +170,13 @@ function setupEventListeners() {
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
const dispenseForm = document.getElementById('dispenseForm');
+ const prescribeForm = document.getElementById('prescribeForm');
const editForm = document.getElementById('editForm');
const addModal = document.getElementById('addModal');
const addVariantModal = document.getElementById('addVariantModal');
const editVariantModal = document.getElementById('editVariantModal');
const dispenseModal = document.getElementById('dispenseModal');
+ const prescribeModal = document.getElementById('prescribeModal');
const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
@@ -151,6 +184,7 @@ function setupEventListeners() {
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
const cancelDispenseBtn = document.getElementById('cancelDispenseBtn');
+ const cancelPrescribeBtn = document.getElementById('cancelPrescribeBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
@@ -166,6 +200,7 @@ function setupEventListeners() {
if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
+ if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug);
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
@@ -178,6 +213,7 @@ function setupEventListeners() {
if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
+ if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal));
if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
@@ -358,6 +394,7 @@ function renderDrugs() {
+
@@ -435,10 +472,10 @@ async function handleAddDrug(e) {
document.getElementById('initialVariantThreshold').value = '10';
closeModal(document.getElementById('addModal'));
await loadDrugs();
- alert('Drug added successfully!');
+ showToast('Drug added successfully!', 'success');
} catch (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;
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;
}
@@ -479,10 +516,10 @@ async function handleDispenseDrug(e) {
document.getElementById('dispenseForm').reset();
closeModal(document.getElementById('dispenseModal'));
await loadDrugs();
- alert('Drug dispensed successfully!');
+ showToast('Drug dispensed successfully!', 'success');
} catch (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'));
await loadDrugs();
renderDrugs();
- alert('Variant added successfully!');
+ showToast('Variant added successfully!', 'success');
} catch (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'));
await loadDrugs();
renderDrugs();
- alert('Variant updated successfully!');
+ showToast('Variant updated successfully!', 'success');
} catch (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'));
}
+// 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
async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
@@ -631,10 +767,10 @@ async function deleteVariant(variantId) {
await loadDrugs();
renderDrugs();
- alert('Variant deleted successfully!');
+ showToast('Variant deleted successfully!', 'success');
} catch (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();
await loadDrugs();
- alert('Drug updated successfully!');
+ showToast('Drug updated successfully!', 'success');
} catch (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');
await loadDrugs();
- alert('Drug deleted successfully!');
+ showToast('Drug deleted successfully!', 'success');
} catch (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;
if (newPassword !== confirmPassword) {
- alert('New passwords do not match!');
+ showToast('New passwords do not match!', 'warning');
return;
}
if (newPassword.length < 1) {
- alert('New password cannot be empty!');
+ showToast('New password cannot be empty!', 'warning');
return;
}
@@ -799,11 +935,11 @@ async function handleChangePassword(e) {
throw new Error(error.detail || 'Failed to change password');
}
- alert('Password changed successfully!');
+ showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('changePasswordModal'));
} catch (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;
if (newPassword !== confirmPassword) {
- alert('Passwords do not match!');
+ showToast('Passwords do not match!', 'warning');
return;
}
if (newPassword.length < 1) {
- alert('Password cannot be empty!');
+ showToast('Password cannot be empty!', 'warning');
return;
}
@@ -845,12 +981,12 @@ async function handleAdminChangePassword(e) {
throw new Error(error.detail || 'Failed to change password');
}
- alert('Password changed successfully!');
+ showToast('Password changed successfully!', 'success');
closeModal(document.getElementById('adminChangePasswordModal'));
openUserManagement();
} catch (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('newUserPassword').value = '';
- alert('User created successfully!');
+ showToast('User created successfully!', 'success');
openUserManagement();
} catch (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');
- alert('User deleted successfully!');
+ showToast('User deleted successfully!', 'success');
openUserManagement();
} catch (error) {
console.error('Error deleting user:', error);
- alert('Failed to delete user: ' + error.message);
+ showToast('Failed to delete user: ' + error.message, 'error');
}
}
diff --git a/frontend/index.html b/frontend/index.html
index 32da6c1..7505912 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7,6 +7,9 @@
+
+
+
+
+
+
+
×
+
Prescribe Drug & Print Label
+
+
+
+
diff --git a/frontend/styles.css b/frontend/styles.css
index 5ce66db..9fa7ff4 100644
--- a/frontend/styles.css
+++ b/frontend/styles.css
@@ -947,3 +947,95 @@ footer {
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%;
+ }
+}
\ No newline at end of file