Compare commits

..

2 Commits

Author SHA1 Message Date
d277d5c07b Notifications and printing 2026-02-07 07:01:31 -05:00
3d1c007609 Label print API 2026-02-07 06:41:39 -05:00
7 changed files with 439 additions and 28 deletions

View File

@@ -1,11 +1,12 @@
from fastapi import FastAPI, Depends, HTTPException, APIRouter, status from fastapi import FastAPI, Depends, HTTPException, APIRouter, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .database import engine, get_db, Base from .database import engine, get_db, Base
from .models import Drug, DrugVariant, Dispensing, User from .models import Drug, DrugVariant, Dispensing, User
from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, ACCESS_TOKEN_EXPIRE_MINUTES from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, ACCESS_TOKEN_EXPIRE_MINUTES
from .mqtt_service import publish_label_print
from pydantic import BaseModel from pydantic import BaseModel
# Create tables # Create tables
@@ -117,6 +118,21 @@ class DispensingResponse(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class LabelVariables(BaseModel):
practice_name: str
animal_name: str
drug_name: str
dosage: str
quantity: str
expiry_date: str
class LabelPrintRequest(BaseModel):
variables: LabelVariables
class LabelPrintResponse(BaseModel):
success: bool
message: str
# Authentication Routes # Authentication Routes
@router.post("/auth/register", response_model=TokenResponse) @router.post("/auth/register", response_model=TokenResponse)
def register(user_data: UserCreate, db: Session = Depends(get_db)): def register(user_data: UserCreate, db: Session = Depends(get_db)):
@@ -475,5 +491,49 @@ def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db), curr
return db.query(Dispensing).filter(Dispensing.drug_variant_id == variant_id).order_by(Dispensing.dispensed_at.desc()).all() return db.query(Dispensing).filter(Dispensing.drug_variant_id == variant_id).order_by(Dispensing.dispensed_at.desc()).all()
# Label printing endpoint
@router.post("/labels/print", response_model=LabelPrintResponse)
def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)):
"""
Print a drug label by publishing an MQTT message
This endpoint publishes a label print request to the MQTT broker,
which will be picked up by the label printing service.
"""
try:
# Get label configuration from environment
import os
template_id = os.getenv("LABEL_TEMPLATE_ID", "vet_label")
label_size = os.getenv("LABEL_SIZE", "29x90")
test_mode = os.getenv("LABEL_TEST", "false").lower() == "true"
# Convert the request to the MQTT message format
mqtt_message = {
"template_id": template_id,
"label_size": label_size,
"variables": label_request.variables.dict(),
"test": test_mode
}
# Publish to MQTT
success = publish_label_print(mqtt_message)
if success:
return LabelPrintResponse(
success=True,
message="Label print request sent successfully"
)
else:
raise HTTPException(
status_code=500,
detail="Failed to send label print request to MQTT broker"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error sending label print request: {str(e)}"
)
# Include router with /api prefix # Include router with /api prefix
app.include_router(router) app.include_router(router)

View File

@@ -0,0 +1,57 @@
import os
import json
import logging
from typing import Optional
import paho.mqtt.client as mqtt
logger = logging.getLogger(__name__)
# MQTT Configuration from environment
MQTT_BROKER_HOST = os.getenv("MQTT_BROKER_HOST", "localhost")
MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883"))
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "")
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "")
MQTT_LABEL_TOPIC = os.getenv("MQTT_LABEL_TOPIC", "vet/labels/print")
def publish_label_print(label_data: dict) -> bool:
"""
Publish a label print request to MQTT broker
Args:
label_data: Dictionary containing label print information
Returns:
True if successful, False otherwise
"""
try:
# Create MQTT client
client = mqtt.Client()
# Set username and password if provided
if MQTT_USERNAME and MQTT_PASSWORD:
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
# Connect to broker
client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)
# Start network loop to process connection
client.loop_start()
# Publish message with QoS 0 (fire and forget)
message = json.dumps(label_data)
result = client.publish(MQTT_LABEL_TOPIC, message, qos=0)
# Stop loop and disconnect
client.loop_stop()
client.disconnect()
if result.rc == mqtt.MQTT_ERR_SUCCESS:
logger.info(f"Successfully published label print request to {MQTT_LABEL_TOPIC}")
return True
else:
logger.error(f"Failed to publish label print request: {result.rc}")
return False
except Exception as e:
logger.error(f"Error publishing MQTT message: {str(e)}")
return False

View File

@@ -5,3 +5,4 @@ pydantic==2.5.0
python-multipart==0.0.6 python-multipart==0.0.6
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.3.0
passlib[argon2]==1.7.4 passlib[argon2]==1.7.4
paho-mqtt==1.6.1

View File

@@ -14,6 +14,14 @@ services:
- DATABASE_URL=sqlite:///./data/drugs.db - DATABASE_URL=sqlite:///./data/drugs.db
- PUID=1001 - PUID=1001
- PGID=1001 - PGID=1001
- MQTT_BROKER_HOST=${MQTT_BROKER_HOST:-localhost}
- MQTT_BROKER_PORT=${MQTT_BROKER_PORT:-1883}
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print}
- LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label}
- LABEL_SIZE=${LABEL_SIZE:-29x90}
- LABEL_TEST=${LABEL_TEST:-false}
frontend: frontend:
image: nginx:alpine image: nginx:alpine

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');
@@ -324,6 +360,11 @@ function renderDrugs() {
); );
} }
// Sort alphabetically by drug name
drugsToShow = drugsToShow.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
if (drugsToShow.length === 0) { if (drugsToShow.length === 0) {
drugsList.innerHTML = '<p class="empty">No drugs found matching your criteria</p>'; drugsList.innerHTML = '<p class="empty">No drugs found matching your criteria</p>';
return; return;
@@ -353,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>
@@ -430,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');
} }
} }
@@ -448,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;
} }
@@ -474,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');
} }
} }
@@ -543,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');
} }
} }
@@ -593,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');
} }
} }
@@ -613,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;
@@ -626,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');
} }
} }
@@ -725,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');
} }
} }
@@ -744,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');
} }
} }
@@ -771,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;
} }
@@ -794,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');
} }
} }
@@ -818,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;
} }
@@ -840,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');
} }
} }
@@ -927,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');
} }
} }
@@ -944,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%;
}
}