Compare commits
2 Commits
dd5aa7c4bc
...
d277d5c07b
| Author | SHA1 | Date | |
|---|---|---|---|
| d277d5c07b | |||
| 3d1c007609 |
@@ -1,11 +1,12 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, APIRouter, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from .database import engine, get_db, Base
|
||||
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 .mqtt_service import publish_label_print
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Create tables
|
||||
@@ -117,6 +118,21 @@ class DispensingResponse(BaseModel):
|
||||
class Config:
|
||||
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
|
||||
@router.post("/auth/register", response_model=TokenResponse)
|
||||
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()
|
||||
|
||||
# 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
|
||||
app.include_router(router)
|
||||
|
||||
57
backend/app/mqtt_service.py
Normal file
57
backend/app/mqtt_service.py
Normal 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
|
||||
@@ -5,3 +5,4 @@ pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[argon2]==1.7.4
|
||||
paho-mqtt==1.6.1
|
||||
|
||||
@@ -14,6 +14,14 @@ services:
|
||||
- DATABASE_URL=sqlite:///./data/drugs.db
|
||||
- PUID=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:
|
||||
image: nginx:alpine
|
||||
|
||||
195
frontend/app.js
195
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 = `
|
||||
<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();
|
||||
@@ -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');
|
||||
@@ -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) {
|
||||
drugsList.innerHTML = '<p class="empty">No drugs found matching your criteria</p>';
|
||||
return;
|
||||
@@ -353,6 +394,7 @@ function renderDrugs() {
|
||||
</div>
|
||||
</div>
|
||||
<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-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</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';
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,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;
|
||||
}
|
||||
|
||||
@@ -474,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,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;
|
||||
@@ -626,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,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;
|
||||
}
|
||||
|
||||
@@ -794,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,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;
|
||||
}
|
||||
|
||||
@@ -840,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
|
||||
<!-- Login Page -->
|
||||
<div id="loginPage" class="login-page">
|
||||
<div class="login-container">
|
||||
@@ -196,6 +199,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prescribe Drug Modal -->
|
||||
<div id="prescribeModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</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 -->
|
||||
<div id="addVariantModal" class="modal">
|
||||
<div class="modal-content">
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user