Compare commits

...

7 Commits

Author SHA1 Message Date
511e8ebde4 Notes printing function 2026-02-18 11:57:37 -05:00
1a82776657 Add notes API only 2026-02-18 06:15:48 -05:00
fa192247fe Private network fixes 2026-02-11 15:35:27 -05:00
65f951dfcc MQTT broker add 2026-02-09 15:28:48 -05:00
62bb5cb60d Update frontend print logic 2026-02-07 09:54:52 -05:00
7fd2e0050b Printer realtime status 2026-02-07 09:38:36 -05:00
20d8cefe22 Tidy up 2026-02-07 08:46:08 -05:00
10 changed files with 504 additions and 44 deletions

View File

@@ -90,6 +90,23 @@ environment:
- DATABASE_URL=sqlite:///./drugs.db - DATABASE_URL=sqlite:///./drugs.db
``` ```
## MQTT
The system includes an MQTT broker (Mosquitto) with WebSocket support:
- **MQTT**: `localhost:1883`
- **WebSocket**: `localhost:9001` or `/mqtt` via nginx
To create a new MQTT user with a custom password:
```bash
docker run --rm -v $(pwd)/mosquitto/config:/temp eclipse-mosquitto mosquitto_passwd -b /temp/pwfile username password
```
Then restart the containers:
```bash
docker compose restart mosquitto
```
## Development ## Development
When you run `docker-compose up`, the backend automatically reloads when code changes (`--reload` flag). Just refresh your browser to see updates. When you run `docker-compose up`, the backend automatically reloads when code changes (`--reload` flag). Just refresh your browser to see updates.

View File

@@ -6,7 +6,7 @@ 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 .mqtt_service import publish_label_print_with_response
from pydantic import BaseModel from pydantic import BaseModel
# Create tables # Create tables
@@ -133,6 +133,17 @@ class LabelPrintResponse(BaseModel):
success: bool success: bool
message: str message: str
class NotesVariables(BaseModel):
animal_name: str
notes: str
class NotesPrintRequest(BaseModel):
variables: NotesVariables
class NotesPrintResponse(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)):
@@ -491,6 +502,25 @@ 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()
# Helper function to capitalize text for labels
def capitalize_label_text(text: str) -> str:
"""Capitalize the first letter of each sentence in the text"""
if not text:
return text
# Capitalize first letter of the entire string
result = text[0].upper() + text[1:] if len(text) > 1 else text.upper()
# Also capitalize after periods and common sentence breaks
for delimiter in ['. ', '! ', '? ']:
parts = result.split(delimiter)
result = delimiter.join([
part[0].upper() + part[1:] if part else part
for part in parts
])
return result
# Label printing endpoint # Label printing endpoint
@router.post("/labels/print", response_model=LabelPrintResponse) @router.post("/labels/print", response_model=LabelPrintResponse)
def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)): def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)):
@@ -507,27 +537,48 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g
label_size = os.getenv("LABEL_SIZE", "29x90") label_size = os.getenv("LABEL_SIZE", "29x90")
test_mode = os.getenv("LABEL_TEST", "false").lower() == "true" test_mode = os.getenv("LABEL_TEST", "false").lower() == "true"
# Capitalize all text fields for better presentation
variables = label_request.variables.dict()
variables["practice_name"] = capitalize_label_text(variables["practice_name"])
variables["animal_name"] = capitalize_label_text(variables["animal_name"])
variables["drug_name"] = capitalize_label_text(variables["drug_name"])
variables["dosage"] = capitalize_label_text(variables["dosage"])
variables["quantity"] = capitalize_label_text(variables["quantity"])
# expiry_date doesn't need capitalization
# Convert the request to the MQTT message format # Convert the request to the MQTT message format
mqtt_message = { mqtt_message = {
"template_id": template_id, "template_id": template_id,
"label_size": label_size, "label_size": label_size,
"variables": label_request.variables.dict(), "variables": variables,
"test": test_mode "test": test_mode
} }
# Publish to MQTT # Publish to MQTT and wait for response
success = publish_label_print(mqtt_message) success, response = publish_label_print_with_response(mqtt_message, timeout=10.0)
print(f"Label print result: success={success}, response={response}")
if success: if success:
return LabelPrintResponse( result = LabelPrintResponse(
success=True, success=True,
message="Label print request sent successfully" message=response.get("message", "Label printed successfully")
) )
print(f"Returning success response: {result}")
return result
else: else:
raise HTTPException( # Return error details from printer
status_code=500, # Check both 'message' and 'error' fields for error details
detail="Failed to send label print request to MQTT broker" if response:
error_msg = response.get("message") or response.get("error", "Unknown error")
else:
error_msg = "No response from printer"
result = LabelPrintResponse(
success=False,
message=f"Print failed: {error_msg}"
) )
print(f"Returning error response: {result}")
return result
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
@@ -535,5 +586,66 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g
detail=f"Error sending label print request: {str(e)}" detail=f"Error sending label print request: {str(e)}"
) )
# Notes printing endpoint
@router.post("/notes/print", response_model=NotesPrintResponse)
def print_notes(notes_request: NotesPrintRequest, current_user: User = Depends(get_current_user)):
"""
Print notes by publishing an MQTT message
This endpoint publishes a notes print request to the MQTT broker,
which will be picked up by the label printing service.
"""
try:
# Get notes template configuration from environment
import os
template_id = os.getenv("NOTES_TEMPLATE_ID", "notes_template")
label_size = os.getenv("LABEL_SIZE", "29x90")
test_mode = os.getenv("LABEL_TEST", "false").lower() == "true"
# Capitalize text fields for better presentation
variables = notes_request.variables.dict()
variables["animal_name"] = capitalize_label_text(variables["animal_name"])
variables["notes"] = capitalize_label_text(variables["notes"])
# Convert the request to the MQTT message format
mqtt_message = {
"template_id": template_id,
"label_size": label_size,
"variables": variables,
"test": test_mode
}
# Publish to MQTT and wait for response
success, response = publish_label_print_with_response(mqtt_message, timeout=10.0)
print(f"Notes print result: success={success}, response={response}")
if success:
result = NotesPrintResponse(
success=True,
message=response.get("message", "Notes printed successfully")
)
print(f"Returning success response: {result}")
return result
else:
# Return error details from printer
# Check both 'message' and 'error' fields for error details
if response:
error_msg = response.get("message") or response.get("error", "Unknown error")
else:
error_msg = "No response from printer"
result = NotesPrintResponse(
success=False,
message=f"Print failed: {error_msg}"
)
print(f"Returning error response: {result}")
return result
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error sending notes print request: {str(e)}"
)
# Include router with /api prefix # Include router with /api prefix
app.include_router(router) app.include_router(router)

View File

@@ -1,8 +1,12 @@
import os import os
import json import json
import logging import logging
from typing import Optional import uuid
import time
from typing import Optional, Dict, Any
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import threading
import atexit
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -12,10 +16,169 @@ MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883"))
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "") MQTT_USERNAME = os.getenv("MQTT_USERNAME", "")
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "") MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "")
MQTT_LABEL_TOPIC = os.getenv("MQTT_LABEL_TOPIC", "vet/labels/print") MQTT_LABEL_TOPIC = os.getenv("MQTT_LABEL_TOPIC", "vet/labels/print")
MQTT_STATUS_TOPIC = os.getenv("MQTT_STATUS_TOPIC", "vet/labels/status")
# Store responses by job_id
_response_store: Dict[str, Any] = {}
_response_lock = threading.Lock()
# Persistent MQTT client
_mqtt_client: Optional[mqtt.Client] = None
_client_connected = threading.Event()
_client_lock = threading.Lock()
_initialization_attempted = False
def on_connect(client, userdata, flags, rc):
"""Callback when connected to broker"""
if rc == 0:
print(f"MQTT: Connected to broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
# Subscribe to status topic
result = client.subscribe(MQTT_STATUS_TOPIC, qos=1)
print(f"MQTT: Subscribed to {MQTT_STATUS_TOPIC}, result={result}")
_client_connected.set()
else:
print(f"MQTT: Failed to connect, return code: {rc}")
_client_connected.clear()
def on_disconnect(client, userdata, rc):
"""Callback when disconnected from broker"""
print(f"MQTT: Disconnected, return code: {rc}")
_client_connected.clear()
def on_status_message(client, userdata, message):
"""Callback for status messages"""
print(f"MQTT: Received message on topic '{message.topic}': {message.payload.decode()[:200]}")
try:
payload = json.loads(message.payload.decode())
job_id = payload.get("job_id")
if job_id:
with _response_lock:
_response_store[job_id] = payload
print(f"MQTT: Stored response for job {job_id}: {payload.get('status')}")
else:
print(f"MQTT: Message has no job_id: {payload}")
except Exception as e:
print(f"MQTT: Error processing status message: {str(e)}")
def get_mqtt_client():
"""Get or initialize the persistent MQTT client"""
global _mqtt_client, _initialization_attempted
with _client_lock:
# If client exists and is connected, return it
if _mqtt_client is not None and _client_connected.is_set():
return _mqtt_client
# If we already tried and failed recently, don't retry immediately
if _initialization_attempted and _mqtt_client is None:
return None
_initialization_attempted = True
try:
print(f"MQTT: Initializing client connection to {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
# Clean up old client if exists
if _mqtt_client is not None:
try:
_mqtt_client.loop_stop()
_mqtt_client.disconnect()
except:
pass
# Create new MQTT client
_mqtt_client = mqtt.Client(client_id=f"drug-inventory-main")
# Set username and password if provided
if MQTT_USERNAME and MQTT_PASSWORD:
_mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
# Set up callbacks
_mqtt_client.on_connect = on_connect
_mqtt_client.on_disconnect = on_disconnect
_mqtt_client.on_message = on_status_message
# Connect to broker
_mqtt_client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)
# Start network loop in background
_mqtt_client.loop_start()
# Wait for connection (max 3 seconds)
if _client_connected.wait(timeout=3.0):
print("MQTT: Client initialized successfully")
return _mqtt_client
else:
print("MQTT: Connection timeout")
_mqtt_client.loop_stop()
_mqtt_client = None
return None
except Exception as e:
print(f"MQTT: Error initializing client: {str(e)}")
_mqtt_client = None
return None
def publish_label_print_with_response(label_data: dict, timeout: float = 5.0) -> tuple[bool, Optional[dict]]:
"""
Publish a label print request to MQTT broker and wait for response
Args:
label_data: Dictionary containing label print information
timeout: Maximum time to wait for response in seconds
Returns:
Tuple of (success, response_data)
"""
# Get or initialize MQTT client
client = get_mqtt_client()
if client is None:
print("MQTT: Client not available")
return False, {"status": "error", "message": "MQTT client not connected"}
job_id = str(uuid.uuid4())
label_data["job_id"] = job_id
try:
# Publish message
message = json.dumps(label_data)
result = client.publish(MQTT_LABEL_TOPIC, message, qos=1)
if result.rc != mqtt.MQTT_ERR_SUCCESS:
print(f"MQTT: Failed to publish, rc={result.rc}")
return False, {"status": "error", "message": "Failed to publish message"}
print(f"MQTT: Published job {job_id}")
# Wait for response
start_time = time.time()
while time.time() - start_time < timeout:
with _response_lock:
if job_id in _response_store:
response = _response_store.pop(job_id)
# Check if print was successful
status = response.get("status", "").lower()
success = status in ["success", "completed", "ok"]
print(f"MQTT: Job {job_id} completed with status: {status}")
return success, response
time.sleep(0.05) # Check every 50ms
# Timeout - no response received
print(f"MQTT: Timeout waiting for job {job_id}")
# Clean up in case response arrives late
with _response_lock:
_response_store.pop(job_id, None)
return False, {"status": "timeout", "message": "No response from printer"}
except Exception as e:
print(f"MQTT: Error in publish_label_print_with_response: {str(e)}")
return False, {"status": "error", "message": str(e)}
def publish_label_print(label_data: dict) -> bool: def publish_label_print(label_data: dict) -> bool:
""" """
Publish a label print request to MQTT broker Publish a label print request to MQTT broker (fire and forget)
Args: Args:
label_data: Dictionary containing label print information label_data: Dictionary containing label print information

View File

@@ -5,8 +5,6 @@ services:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
ports:
- "${BACKEND_PORT}:8000"
volumes: volumes:
- ./backend/app:/app/app - ./backend/app:/app/app
- ./data:/app/data - ./data:/app/data
@@ -14,21 +12,45 @@ 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_HOST=${MQTT_BROKER_HOST:-mosquitto}
- MQTT_BROKER_PORT=${MQTT_BROKER_PORT:-1883} - MQTT_BROKER_PORT=${MQTT_BROKER_PORT:-1883}
- MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print} - MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print}
- MQTT_STATUS_TOPIC=${MQTT_STATUS_TOPIC:-vet/labels/status}
- LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label} - LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label}
- LABEL_SIZE=${LABEL_SIZE:-29x90} - LABEL_SIZE=${LABEL_SIZE:-29x90}
- LABEL_TEST=${LABEL_TEST:-false} - LABEL_TEST=${LABEL_TEST:-false}
mosquitto:
image: eclipse-mosquitto:latest
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./mosquitto/config/pwfile:/mosquitto/config/pwfile:ro
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
environment:
- PUID=1001
- PGID=1001
frontend: frontend:
image: nginx:alpine image: nginx:alpine
ports: container_name: drugsprod
- "${FRONTEND_PORT}:80"
volumes: volumes:
- ./frontend:/usr/share/nginx/html:ro - ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:
- backend - backend
- mosquitto
networks:
- default
- webapps
volumes:
mosquitto_data:
mosquitto_logs:
networks:
default:
webapps:
external: true

View File

@@ -172,14 +172,17 @@ function setupEventListeners() {
const dispenseForm = document.getElementById('dispenseForm'); const dispenseForm = document.getElementById('dispenseForm');
const prescribeForm = document.getElementById('prescribeForm'); const prescribeForm = document.getElementById('prescribeForm');
const editForm = document.getElementById('editForm'); const editForm = document.getElementById('editForm');
const printNotesForm = document.getElementById('printNotesForm');
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 prescribeModal = document.getElementById('prescribeModal');
const editModal = document.getElementById('editModal'); const editModal = document.getElementById('editModal');
const printNotesModal = document.getElementById('printNotesModal');
const addDrugBtn = document.getElementById('addDrugBtn'); const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn'); const dispenseBtn = document.getElementById('dispenseBtn');
const printNotesBtn = document.getElementById('printNotesBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn'); const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn'); const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn'); const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
@@ -202,8 +205,10 @@ function setupEventListeners() {
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug); if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug); if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug); if (editForm) editForm.addEventListener('submit', handleEditDrug);
if (printNotesForm) printNotesForm.addEventListener('submit', handlePrintNotes);
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal)); if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
if (dispenseBtn) dispenseBtn.addEventListener('click', () => { if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
updateDispenseDrugSelect(); updateDispenseDrugSelect();
openModal(dispenseModal); openModal(dispenseModal);
@@ -216,6 +221,9 @@ function setupEventListeners() {
if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal)); if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal));
if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal); if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
const cancelPrintNotesBtn = document.getElementById('cancelPrintNotesBtn');
if (cancelPrintNotesBtn) cancelPrintNotesBtn.addEventListener('click', () => closeModal(printNotesModal));
const closeHistoryBtn = document.getElementById('closeHistoryBtn'); const closeHistoryBtn = document.getElementById('closeHistoryBtn');
if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal'))); if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
@@ -394,7 +402,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-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</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>
@@ -702,7 +710,44 @@ async function handlePrescribeDrug(e) {
const formattedExpiry = `${expiryParts[2]}/${expiryParts[1]}/${expiryParts[0]}`; const formattedExpiry = `${expiryParts[2]}/${expiryParts[1]}/${expiryParts[0]}`;
try { try {
// First, dispense the drug (decrement inventory) // First, 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) {
const error = await labelResponse.json();
throw new Error(error.detail || 'Label printing request failed');
}
const labelResult = await labelResponse.json();
console.log('Label print result:', labelResult);
if (!labelResult.success) {
// Label printing failed - don't dispense the drug
const isError = labelResult.message && (
labelResult.message.includes('not found') ||
labelResult.message.includes('error') ||
labelResult.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast('Cannot dispense: ' + labelResult.message, toastType, 5000);
return;
}
// Label printed successfully, now dispense the drug
const dispensingData = { const dispensingData = {
drug_variant_id: variantId, drug_variant_id: variantId,
quantity: quantity, quantity: quantity,
@@ -721,29 +766,8 @@ async function handlePrescribeDrug(e) {
throw new Error(error.detail || 'Failed to dispense drug'); throw new Error(error.detail || 'Failed to dispense drug');
} }
// Second, print the label // Both operations succeeded
const labelData = { showToast('Drug prescribed and label printed successfully!', 'success');
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(); document.getElementById('prescribeForm').reset();
closeModal(document.getElementById('prescribeModal')); closeModal(document.getElementById('prescribeModal'));
@@ -754,6 +778,63 @@ async function handlePrescribeDrug(e) {
} }
} }
// Handle print notes form submission
async function handlePrintNotes(e) {
e.preventDefault();
const animalName = document.getElementById('notesAnimalName').value.trim();
const notes = document.getElementById('notesContent').value.trim();
if (!animalName || !notes) {
showToast('Please fill in all required fields', 'warning');
return;
}
try {
// Send notes to print endpoint
const notesData = {
variables: {
animal_name: animalName,
notes: notes
}
};
const response = await apiCall('/notes/print', {
method: 'POST',
body: JSON.stringify(notesData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Notes printing request failed');
}
const result = await response.json();
console.log('Notes print result:', result);
if (!result.success) {
// Printing failed
const isError = result.message && (
result.message.includes('not found') ||
result.message.includes('error') ||
result.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast(result.message, toastType, 5000);
return;
}
// Printing succeeded
showToast('Notes printed successfully!', 'success');
document.getElementById('printNotesForm').reset();
closeModal(document.getElementById('printNotesModal'));
} catch (error) {
console.error('Error printing notes:', error);
showToast('Failed to print notes: ' + 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;

View File

@@ -55,6 +55,7 @@
<div class="section-header"> <div class="section-header">
<h2>Current Inventory</h2> <h2>Current Inventory</h2>
<div class="header-actions"> <div class="header-actions">
<button id="printNotesBtn" class="btn btn-primary btn-small">📝 Print Notes</button>
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button> <button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button>
</div> </div>
<div class="filters"> <div class="filters">
@@ -429,6 +430,30 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Print Notes Modal -->
<div id="printNotesModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Print Notes</h2>
<form id="printNotesForm" novalidate>
<div class="form-group">
<label for="notesAnimalName">Animal Name/ID *</label>
<input type="text" id="notesAnimalName" required>
</div>
<div class="form-group">
<label for="notesContent">Notes *</label>
<textarea id="notesContent" rows="6" placeholder="Enter notes to print..." required></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Print Notes</button>
<button type="button" class="btn btn-secondary" id="cancelPrintNotesBtn">Cancel</button>
</div>
</form>
</div>
</div>
</div> </div>
<script src="app.js"></script> <script src="app.js"></script>

View File

@@ -252,7 +252,9 @@ label {
input[type="text"], input[type="text"],
input[type="number"], input[type="number"],
input[type="password"], input[type="password"],
select { input[type="date"],
select,
textarea {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
@@ -262,10 +264,17 @@ select {
transition: border-color 0.2s; transition: border-color 0.2s;
} }
textarea {
resize: vertical;
min-height: 100px;
}
input[type="text"]:focus, input[type="text"]:focus,
input[type="number"]:focus, input[type="number"]:focus,
input[type="password"]:focus, input[type="password"]:focus,
select:focus { input[type="date"]:focus,
select:focus,
textarea:focus {
outline: none; outline: none;
border-color: var(--secondary-color); border-color: var(--secondary-color);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3); box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
@@ -291,7 +300,6 @@ select:focus {
.btn-primary { .btn-primary {
background-color: var(--secondary-color); background-color: var(--secondary-color);
color: var(--white); color: var(--white);
width: 100%;
} }
.btn-primary:hover { .btn-primary:hover {

View File

@@ -0,0 +1,20 @@
listener 1883
listener 9001
protocol websockets
persistence true
persistence_location /mosquitto/data/
# Authentication
allow_anonymous false
password_file /mosquitto/config/pwfile
# Logging
log_dest stdout
log_timestamp true
#log_type all
# Performance
max_connections -1
max_queued_messages 1000

1
mosquitto/config/pwfile Normal file
View File

@@ -0,0 +1 @@
mqtt:$7$1000$x5foCeSdbDKyb+S/CkGI37HWJEIAQil+PfMwREfPHHbRGsUwTWKcoHXcOC4l15mj1HMH8GovYvGGOVDHkOrcBA==$kXYD9LRqSkThHyWTivw8l8/NBdXvpZ9d8qwmIJcGabyrohRdkfXcEgRbEJP7sJ43r6nPX7+p1lb+nF0Actt4ww==

View File

@@ -16,5 +16,16 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location /mqtt {
proxy_pass http://mosquitto:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} }