Added auth

This commit is contained in:
2026-01-21 16:38:52 -05:00
parent 615c7caee8
commit cc5c7ff42d
7 changed files with 1303 additions and 277 deletions

93
backend/app/auth.py Normal file
View File

@@ -0,0 +1,93 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status, Header
from sqlalchemy.orm import Session
from .database import get_db
from .models import User
# Configuration
SECRET_KEY = "your-secret-key-change-in-production" # Change this in production!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 720
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def hash_password(password: str) -> str:
"""Hash a password using bcrypt"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""Create a JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def get_current_user(authorization: Optional[str] = Header(None), db: Session = Depends(get_db)) -> User:
"""Get the current authenticated user from JWT token"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authorization header",
headers={"WWW-Authenticate": "Bearer"},
)
try:
scheme, credentials = authorization.split()
if scheme.lower() != "bearer":
raise ValueError("Invalid auth scheme")
except (ValueError, AttributeError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authorization header format",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(credentials, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = db.query(User).filter(User.username == username).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
"""Get the current user and verify they are an admin"""
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions. Admin access required."
)
return current_user

View File

@@ -1,10 +1,11 @@
from fastapi import FastAPI, Depends, HTTPException, APIRouter
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 datetime import datetime
from datetime import datetime, timedelta
from .database import engine, get_db, Base
from .models import Drug, DrugVariant, Dispensing
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 pydantic import BaseModel
# Create tables
@@ -25,18 +26,42 @@ app.add_middleware(
router = APIRouter(prefix="/api")
# Pydantic schemas
class UserCreate(BaseModel):
username: str
password: str
class PasswordChange(BaseModel):
current_password: str
new_password: str
class AdminPasswordChange(BaseModel):
new_password: str
class UserResponse(BaseModel):
id: int
username: str
is_admin: bool
class Config:
from_attributes = True
class TokenResponse(BaseModel):
access_token: str
token_type: str
user: UserResponse
class DrugCreate(BaseModel):
name: str
description: str = None
description: Optional[str] = None
class DrugUpdate(BaseModel):
name: str = None
description: str = None
name: Optional[str] = None
description: Optional[str] = None
class DrugResponse(BaseModel):
id: int
name: str
description: str = None
description: Optional[str] = None
class Config:
from_attributes = True
@@ -67,7 +92,7 @@ class DrugVariantResponse(BaseModel):
class DrugWithVariantsResponse(BaseModel):
id: int
name: str
description: str = None
description: Optional[str] = None
variants: List[DrugVariantResponse] = []
class Config:
@@ -92,13 +117,153 @@ class DispensingResponse(BaseModel):
class Config:
from_attributes = True
# Authentication Routes
@router.post("/auth/register", response_model=TokenResponse)
def register(user_data: UserCreate, db: Session = Depends(get_db)):
"""Register the first admin user (only allowed if no users exist)"""
# Check if users already exist
user_count = db.query(User).count()
if user_count > 0:
raise HTTPException(
status_code=403,
detail="Registration is disabled. Contact an administrator to create an account."
)
# Check if user already exists
existing_user = db.query(User).filter(User.username == user_data.username).first()
if existing_user:
raise HTTPException(status_code=400, detail="Username already registered")
# First (and only allowed) user is admin
hashed_password = hash_password(user_data.password)
db_user = User(
username=user_data.username,
hashed_password=hashed_password,
is_admin=True
)
db.add(db_user)
db.commit()
db.refresh(db_user)
# Create access token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": db_user.username},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": db_user
}
@router.post("/auth/login", response_model=TokenResponse)
def login(user_data: UserCreate, db: Session = Depends(get_db)):
"""Login with username and password"""
user = db.query(User).filter(User.username == user_data.username).first()
if not user or not verify_password(user_data.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
# Create access token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": user
}
@router.get("/auth/me", response_model=UserResponse)
def get_current_user_info(current_user: User = Depends(get_current_user)):
"""Get current user info"""
return current_user
# User Management Routes (Admin only)
@router.get("/users", response_model=List[UserResponse])
def list_users(db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
"""List all users (admin only)"""
return db.query(User).all()
@router.post("/users", response_model=UserResponse)
def create_user(user_data: UserCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
"""Create a new user (admin only)"""
# Check if user already exists
existing_user = db.query(User).filter(User.username == user_data.username).first()
if existing_user:
raise HTTPException(status_code=400, detail="Username already exists")
hashed_password = hash_password(user_data.password)
db_user = User(
username=user_data.username,
hashed_password=hashed_password,
is_admin=False
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.delete("/users/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
"""Delete a user (admin only)"""
# Don't allow deleting yourself
if current_user.id == user_id:
raise HTTPException(status_code=400, detail="Cannot delete your own user account")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
return {"message": "User deleted successfully"}
@router.post("/auth/change-password")
def change_own_password(password_data: PasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Change current user's password"""
user = db.query(User).filter(User.id == current_user.id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Verify current password
if not verify_password(password_data.current_password, user.hashed_password):
raise HTTPException(status_code=401, detail="Current password is incorrect")
# Update password
user.hashed_password = hash_password(password_data.new_password)
db.commit()
return {"message": "Password changed successfully"}
@router.post("/users/{user_id}/change-password")
def admin_change_password(user_id: int, password_data: AdminPasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
"""Change a user's password (admin only)"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Don't allow changing yourself via this endpoint
if current_user.id == user_id:
raise HTTPException(status_code=400, detail="Use /auth/change-password to change your own password")
# Update password
user.hashed_password = hash_password(password_data.new_password)
db.commit()
return {"message": "Password changed successfully"}
# Routes
@router.get("/")
def read_root():
return {"message": "Drug Inventory API"}
@router.get("/drugs", response_model=List[DrugWithVariantsResponse])
def list_drugs(db: Session = Depends(get_db)):
def list_drugs(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get all drugs with their variants"""
drugs = db.query(Drug).all()
result = []
@@ -110,7 +275,7 @@ def list_drugs(db: Session = Depends(get_db)):
return result
@router.get("/drugs/low-stock", response_model=List[DrugWithVariantsResponse])
def low_stock_drugs(db: Session = Depends(get_db)):
def low_stock_drugs(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get drugs with low stock variants"""
# Get variants that are low on stock
low_stock_variants = db.query(DrugVariant).filter(
@@ -130,7 +295,7 @@ def low_stock_drugs(db: Session = Depends(get_db)):
return result
@router.get("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
def get_drug(drug_id: int, db: Session = Depends(get_db)):
def get_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get a specific drug with its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -142,7 +307,7 @@ def get_drug(drug_id: int, db: Session = Depends(get_db)):
return drug_dict
@router.post("/drugs", response_model=DrugWithVariantsResponse)
def create_drug(drug: DrugCreate, db: Session = Depends(get_db)):
def create_drug(drug: DrugCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Create a new drug"""
# Check if drug name already exists
existing = db.query(Drug).filter(Drug.name == drug.name).first()
@@ -160,7 +325,7 @@ def create_drug(drug: DrugCreate, db: Session = Depends(get_db)):
return drug_dict
@router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db)):
def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Update a drug"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -178,7 +343,7 @@ def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get
return drug_dict
@router.delete("/drugs/{drug_id}")
def delete_drug(drug_id: int, db: Session = Depends(get_db)):
def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Delete a drug and all its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -194,7 +359,7 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db)):
# Drug Variant endpoints
@router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse)
def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db)):
def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Create a new variant for a drug"""
# Check if drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first()
@@ -222,7 +387,7 @@ def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session =
return db_variant
@router.get("/variants/{variant_id}", response_model=DrugVariantResponse)
def get_drug_variant(variant_id: int, db: Session = Depends(get_db)):
def get_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get a specific drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -230,7 +395,7 @@ def get_drug_variant(variant_id: int, db: Session = Depends(get_db)):
return variant
@router.put("/variants/{variant_id}", response_model=DrugVariantResponse)
def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db)):
def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Update a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -244,7 +409,7 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
return variant
@router.delete("/variants/{variant_id}")
def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)):
def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Delete a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -257,7 +422,7 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)):
# Dispensing endpoints
@router.post("/dispense", response_model=DispensingResponse)
def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db)):
def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Record a drug dispensing and reduce inventory"""
# Check if drug variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first()
@@ -283,12 +448,12 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db)):
return db_dispensing
@router.get("/dispense/history", response_model=List[DispensingResponse])
def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing records (audit log)"""
return db.query(Dispensing).order_by(Dispensing.dispensed_at.desc()).offset(skip).limit(limit).all()
@router.get("/drugs/{drug_id}/dispense/history", response_model=List[DispensingResponse])
def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db)):
def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing history for a specific drug (all variants)"""
# Verify drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first()
@@ -301,7 +466,7 @@ def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db)):
return db.query(Dispensing).filter(Dispensing.drug_variant_id.in_(variant_ids)).order_by(Dispensing.dispensed_at.desc()).all()
@router.get("/variants/{variant_id}/dispense/history", response_model=List[DispensingResponse])
def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db)):
def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing history for a specific drug variant"""
# Verify variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()

View File

@@ -1,7 +1,17 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean
from sqlalchemy.sql import func
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class Drug(Base):
__tablename__ = "drugs"
@@ -31,7 +41,7 @@ class Dispensing(Base):
id = Column(Integer, primary_key=True, index=True)
drug_variant_id = Column(Integer, ForeignKey("drug_variants.id"), nullable=False)
quantity = Column(Float, nullable=False)
animal_name = Column(String, nullable=False) # Name/ID of the animal
animal_name = Column(String, nullable=True) # Name/ID of the animal (optional)
user_name = Column(String, nullable=False) # User who dispensed
dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
notes = Column(String, nullable=True)

View File

@@ -3,3 +3,5 @@ uvicorn==0.24.0
sqlalchemy==2.0.23
pydantic==2.5.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[argon2]==1.7.4

View File

@@ -4,9 +4,137 @@ let currentDrug = null;
let showLowStockOnly = false;
let searchTerm = '';
let expandedDrugs = new Set();
let currentUser = null;
let accessToken = null;
// Initialize
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
});
// Check if user is already logged in
function checkAuth() {
const token = localStorage.getItem('accessToken');
const user = localStorage.getItem('currentUser');
if (token && user) {
accessToken = token;
currentUser = JSON.parse(user);
showMainApp();
} else {
showLoginPage();
}
}
// Show login page
function showLoginPage() {
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
const loginForm = document.getElementById('loginForm');
if (loginForm) loginForm.addEventListener('submit', handleLogin);
}
// Show main app
function showMainApp() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('mainApp').style.display = 'block';
const userDisplay = document.getElementById('currentUser');
if (userDisplay) {
userDisplay.textContent = `👤 ${currentUser.username}`;
}
const adminBtn = document.getElementById('adminBtn');
if (adminBtn) {
adminBtn.style.display = currentUser.is_admin ? 'block' : 'none';
}
setupEventListeners();
loadDrugs();
}
// Handle login
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data = await response.json();
accessToken = data.access_token;
currentUser = data.user;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('currentUser', JSON.stringify(currentUser));
document.getElementById('loginForm').reset();
const errorDiv = document.getElementById('loginError');
if (errorDiv) errorDiv.style.display = 'none';
showMainApp();
} catch (error) {
const errorDiv = document.getElementById('loginError');
if (errorDiv) {
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
}
}
// Handle register
// Logout
function handleLogout() {
localStorage.removeItem('accessToken');
localStorage.removeItem('currentUser');
accessToken = null;
currentUser = null;
const loginForm = document.getElementById('loginForm');
if (loginForm) loginForm.reset();
const registerForm = document.getElementById('registerForm');
if (registerForm) {
registerForm.style.display = 'none';
}
const form = document.getElementById('loginForm');
if (form) form.style.display = 'block';
showLoginPage();
}
// API helper with authentication
async function apiCall(endpoint, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers
});
if (response.status === 401) {
handleLogout();
throw new Error('Authentication expired');
}
return response;
}
// Setup event listeners
function setupEventListeners() {
const drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
@@ -18,6 +146,7 @@ document.addEventListener('DOMContentLoaded', () => {
const dispenseModal = document.getElementById('dispenseModal');
const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
@@ -25,42 +154,76 @@ document.addEventListener('DOMContentLoaded', () => {
const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
const userMenuBtn = document.getElementById('userMenuBtn');
const adminBtn = document.getElementById('adminBtn');
const logoutBtn = document.getElementById('logoutBtn');
const changePasswordBtn = document.getElementById('changePasswordBtn');
// Modal close buttons
const closeButtons = document.querySelectorAll('.close');
drugForm.addEventListener('submit', handleAddDrug);
variantForm.addEventListener('submit', handleAddVariant);
editVariantForm.addEventListener('submit', handleEditVariant);
dispenseForm.addEventListener('submit', handleDispenseDrug);
editForm.addEventListener('submit', handleEditDrug);
if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug);
addDrugBtn.addEventListener('click', () => openModal(addModal));
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
updateDispenseDrugSelect();
openModal(dispenseModal);
});
if (cancelAddBtn) cancelAddBtn.addEventListener('click', () => closeModal(addModal));
if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
cancelAddBtn.addEventListener('click', () => closeModal(addModal));
cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
const closeUserManagementBtn = document.getElementById('closeUserManagementBtn');
if (closeUserManagementBtn) closeUserManagementBtn.addEventListener('click', () => closeModal(document.getElementById('userManagementModal')));
const changePasswordForm = document.getElementById('changePasswordForm');
if (changePasswordForm) changePasswordForm.addEventListener('submit', handleChangePassword);
const cancelChangePasswordBtn = document.getElementById('cancelChangePasswordBtn');
if (cancelChangePasswordBtn) cancelChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('changePasswordModal')));
const adminChangePasswordForm = document.getElementById('adminChangePasswordForm');
if (adminChangePasswordForm) adminChangePasswordForm.addEventListener('submit', handleAdminChangePassword);
const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
closeModal(modal);
}));
showAllBtn.addEventListener('click', () => {
if (showAllBtn) showAllBtn.addEventListener('click', () => {
showLowStockOnly = false;
updateFilterButtons();
renderDrugs();
});
showLowStockBtn.addEventListener('click', () => {
if (showLowStockBtn) showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true;
updateFilterButtons();
renderDrugs();
});
// User menu
if (userMenuBtn) userMenuBtn.addEventListener('click', () => {
const dropdown = document.getElementById('userDropdown');
if (dropdown) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
});
if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal);
if (adminBtn) adminBtn.addEventListener('click', openUserManagement);
if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);
// Search functionality
const drugSearch = document.getElementById('drugSearch');
if (drugSearch) {
@@ -80,14 +243,12 @@ document.addEventListener('DOMContentLoaded', () => {
closeModal(e.target);
}
});
loadDrugs();
});
}
// Load drugs from API
async function loadDrugs() {
try {
const response = await fetch(`${API_URL}/drugs`);
const response = await apiCall('/drugs');
if (!response.ok) throw new Error('Failed to load drugs');
allDrugs = await response.json();
@@ -96,18 +257,32 @@ async function loadDrugs() {
} catch (error) {
console.error('Error loading drugs:', error);
document.getElementById('drugsList').innerHTML =
'<p class="empty">Error loading drugs. Make sure the backend is running on http://localhost:8000</p>';
'<p class="empty">Error loading drugs. Make sure the backend is running.</p>';
}
}
// Modal utility functions
function openModal(modal) {
// Find the highest z-index among currently visible modals
const visibleModals = document.querySelectorAll('.modal.show');
let maxZIndex = 1000;
visibleModals.forEach(m => {
const zIndex = parseInt(window.getComputedStyle(m).zIndex, 10) || 1000;
if (zIndex > maxZIndex) {
maxZIndex = zIndex;
}
});
// Set the new modal's z-index higher than any existing modal
modal.style.zIndex = (maxZIndex + 100).toString();
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModal(modal) {
modal.classList.remove('show');
modal.style.zIndex = '1000';
document.body.style.overflow = 'auto';
}
@@ -224,9 +399,8 @@ async function handleAddDrug(e) {
try {
// Create the drug first
const drugResponse = await fetch(`${API_URL}/drugs`, {
const drugResponse = await apiCall('/drugs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
@@ -243,9 +417,8 @@ async function handleAddDrug(e) {
low_stock_threshold: parseFloat(document.getElementById('initialVariantThreshold').value) || 10
};
const variantResponse = await fetch(`${API_URL}/drugs/${createdDrug.id}/variants`, {
const variantResponse = await apiCall(`/drugs/${createdDrug.id}/variants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -282,15 +455,14 @@ async function handleDispenseDrug(e) {
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
animal_name: animalName,
animal_name: animalName || null,
user_name: userName,
notes: notes || null
};
try {
const response = await fetch(`${API_URL}/dispense`, {
const response = await apiCall('/dispense', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dispensingData)
});
@@ -360,9 +532,8 @@ async function handleAddVariant(e) {
};
try {
const response = await fetch(`${API_URL}/drugs/${drugId}/variants`, {
const response = await apiCall(`/drugs/${drugId}/variants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -412,9 +583,8 @@ async function handleEditVariant(e) {
};
try {
const response = await fetch(`${API_URL}/variants/${variantId}`, {
const response = await apiCall(`/variants/${variantId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -448,7 +618,7 @@ async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
const response = await fetch(`${API_URL}/variants/${variantId}`, {
const response = await apiCall(`/variants/${variantId}`, {
method: 'DELETE'
});
@@ -476,7 +646,7 @@ async function showDrugHistory(drugId) {
openModal(historyModal);
try {
const response = await fetch(`${API_URL}/dispense/history`);
const response = await apiCall(`/dispense/history`);
if (!response.ok) throw new Error('Failed to fetch history');
const allHistory = await response.json();
@@ -546,9 +716,8 @@ async function handleEditDrug(e) {
};
try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, {
const response = await apiCall(`/drugs/${drugId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
@@ -568,7 +737,7 @@ async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, {
const response = await apiCall(`/drugs/${drugId}`, {
method: 'DELETE'
});
@@ -582,6 +751,104 @@ async function deleteDrug(drugId) {
}
}
// Password Management
function openChangePasswordModal() {
const modal = document.getElementById('changePasswordModal');
document.getElementById('changePasswordForm').reset();
// Close dropdown
const dropdown = document.getElementById('userDropdown');
if (dropdown) dropdown.style.display = 'none';
openModal(modal);
}
async function handleChangePassword(e) {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmPassword) {
alert('New passwords do not match!');
return;
}
if (newPassword.length < 1) {
alert('New password cannot be empty!');
return;
}
try {
const response = await apiCall('/auth/change-password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to change password');
}
alert('Password changed successfully!');
closeModal(document.getElementById('changePasswordModal'));
} catch (error) {
console.error('Error changing password:', error);
alert('Failed to change password: ' + error.message);
}
}
async function openAdminChangePasswordModal(userId, username) {
const modal = document.getElementById('adminChangePasswordModal');
document.getElementById('adminChangePasswordForm').reset();
document.getElementById('adminChangePasswordUserId').value = userId;
document.getElementById('adminChangePasswordUsername').value = username;
openModal(modal);
}
async function handleAdminChangePassword(e) {
e.preventDefault();
const userId = document.getElementById('adminChangePasswordUserId').value;
const newPassword = document.getElementById('adminChangePasswordNewPassword').value;
const confirmPassword = document.getElementById('adminChangePasswordConfirm').value;
if (newPassword !== confirmPassword) {
alert('Passwords do not match!');
return;
}
if (newPassword.length < 1) {
alert('Password cannot be empty!');
return;
}
try {
const response = await apiCall(`/users/${userId}/change-password`, {
method: 'POST',
body: JSON.stringify({
new_password: newPassword
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to change password');
}
alert('Password changed successfully!');
closeModal(document.getElementById('adminChangePasswordModal'));
openUserManagement();
} catch (error) {
console.error('Error changing password:', error);
alert('Failed to change password: ' + error.message);
}
}
// Update filter button states
function updateFilterButtons() {
document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly);
@@ -594,3 +861,93 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
// User Management
async function openUserManagement() {
const modal = document.getElementById('userManagementModal');
document.getElementById('newUsername').value = '';
document.getElementById('newUserPassword').value = '';
const usersList = document.getElementById('usersList');
usersList.innerHTML = '<h3>Users</h3><p class="loading">Loading users...</p>';
try {
const response = await apiCall('/users');
if (!response.ok) throw new Error('Failed to load users');
const users = await response.json();
const usersHtml = `
<h3>Users</h3>
<div class="users-table">
${users.map(user => `
<div class="user-item">
<span>${user.username}</span>
<span class="admin-badge">${user.is_admin ? '👑 Admin' : 'User'}</span>
<button class="btn btn-secondary btn-small" onclick="openAdminChangePasswordModal(${user.id}, '${escapeHtml(user.username)}')">🔑 Password</button>
${user.id !== currentUser.id ? `
<button class="btn btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
` : ''}
</div>
`).join('')}
</div>
`;
usersList.innerHTML = usersHtml;
} catch (error) {
console.error('Error loading users:', error);
usersList.innerHTML = '<h3>Users</h3><p class="empty">Error loading users</p>';
}
const createUserForm = document.getElementById('createUserForm');
if (createUserForm) {
createUserForm.onsubmit = createUser;
}
openModal(modal);
}
// Create user
async function createUser(e) {
e.preventDefault();
const username = document.getElementById('newUsername').value;
const password = document.getElementById('newUserPassword').value;
try {
const response = await apiCall('/users', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create user');
}
document.getElementById('newUsername').value = '';
document.getElementById('newUserPassword').value = '';
alert('User created successfully!');
openUserManagement();
} catch (error) {
console.error('Error creating user:', error);
alert('Failed to create user: ' + error.message);
}
}
// Delete user
async function deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user?')) return;
try {
const response = await apiCall(`/users/${userId}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete user');
alert('User deleted successfully!');
openUserManagement();
} catch (error) {
console.error('Error deleting user:', error);
alert('Failed to delete user: ' + error.message);
}
}

View File

@@ -7,19 +7,53 @@
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<!-- Login Page -->
<div id="loginPage" class="login-page">
<div class="login-container">
<h1>🐶 MTAR Drug Inventory System 🐶</h1>
</header>
<form id="loginForm" class="login-form">
<h2>Login</h2>
<div class="form-group">
<label for="loginUsername">Username</label>
<input type="text" id="loginUsername" required>
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<input type="password" id="loginPassword" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
<p class="login-info">Contact an administrator to create an account</p>
</form>
<div id="loginError" class="error-message" style="display: none;"></div>
</div>
</div>
<main>
<!-- Drug List Section -->
<section id="listSection" class="list-section">
<div class="section-header">
<h2>Current Inventory</h2>
<div class="header-actions">
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button>
<!-- Main App (hidden until logged in) -->
<div id="mainApp" class="main-app" style="display: none;">
<div class="container">
<header>
<div class="header-top">
<h1>🐶 MTAR Drug Inventory System 🐶</h1>
<div class="user-menu">
<span id="currentUser">User</span>
<button id="userMenuBtn" class="btn btn-small"></button>
<div id="userDropdown" class="user-dropdown" style="display: none;">
<button id="changePasswordBtn" class="dropdown-item">🔑 Change Password</button>
<button id="adminBtn" class="dropdown-item" style="display: none;">👤 Admin</button>
<button id="logoutBtn" class="dropdown-item">🚪 Logout</button>
</div>
</div>
</div>
</header>
<main>
<!-- Drug List Section -->
<section id="listSection" class="list-section">
<div class="section-header">
<h2>Current Inventory</h2>
<div class="header-actions">
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button>
</div>
<div class="filters">
<button id="showAllBtn" class="filter-btn active">All</button>
<button id="showLowStockBtn" class="filter-btn">Low Stock Only</button>
@@ -40,219 +74,307 @@
<footer>
<p>Many Tears Confidential</p>
</footer>
</div>
<!-- Edit Drug Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Drug</h2>
<form id="editForm">
<input type="hidden" id="editDrugId">
<div class="form-group">
<label for="editDrugName">Drug Name *</label>
<input type="text" id="editDrugName" required>
</div>
<div class="form-group">
<label for="editDrugDescription">Description</label>
<input type="text" id="editDrugDescription">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Drug Modal -->
<div id="addModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add New Drug</h2>
<form id="drugForm">
<div class="form-group">
<label for="drugName">Drug Name *</label>
<input type="text" id="drugName" required>
</div>
<div class="form-group">
<label for="drugDescription">Description</label>
<input type="text" id="drugDescription">
</div>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ddd;">
<h3 style="margin-top: 0;">Initial Variant (Optional)</h3>
<div class="form-group">
<label for="initialVariantStrength">Strength</label>
<input type="text" id="initialVariantStrength" placeholder="e.g., 10mg, 5.4mg">
</div>
<div class="form-group">
<label for="initialVariantQuantity">Quantity</label>
<input type="number" id="initialVariantQuantity" placeholder="0" min="0" step="0.1">
</div>
<div class="form-group">
<label for="initialVariantUnit">Unit</label>
<input type="text" id="initialVariantUnit" placeholder="e.g., tablets, capsules, vials" value="units">
</div>
<div class="form-group">
<label for="initialVariantThreshold">Low Stock Threshold</label>
<input type="number" id="initialVariantThreshold" placeholder="0" min="0" step="0.1" value="10">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Drug</button>
<button type="button" class="btn btn-secondary" id="cancelAddBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispense Drug Modal -->
<div id="dispenseModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Dispense Drug</h2>
<form id="dispenseForm" novalidate>
<div class="form-group">
<label for="dispenseDrugSelect">Drug Variant *</label>
<select id="dispenseDrugSelect">
<option value="">-- Select a drug variant --</option>
</select>
</div>
<div class="form-group">
<label for="dispenseQuantity">Quantity *</label>
<input type="number" id="dispenseQuantity" step="0.1">
</div>
<div class="form-group">
<label for="dispenseAnimal">Animal Name/ID</label>
<input type="text" id="dispenseAnimal">
</div>
<div class="form-group">
<label for="dispenseUser">Dispensed by *</label>
<input type="text" id="dispenseUser">
</div>
<div class="form-group">
<label for="dispenseNotes">Notes</label>
<input type="text" id="dispenseNotes" placeholder="Optional">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Dispense</button>
<button type="button" class="btn btn-secondary" id="cancelDispenseBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Variant Modal -->
<div id="addVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add Variant</h2>
<form id="variantForm">
<input type="hidden" id="variantDrugId">
<div class="form-group">
<label for="variantStrength">Strength *</label>
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 10.8mg, 100ml" required>
</div>
<div class="form-row">
<!-- Edit Drug Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Drug</h2>
<form id="editForm">
<input type="hidden" id="editDrugId">
<div class="form-group">
<label for="variantQuantity">Quantity *</label>
<input type="number" id="variantQuantity" step="0.1" required>
<label for="editDrugName">Drug Name *</label>
<input type="text" id="editDrugName" required>
</div>
<div class="form-group">
<label for="variantUnit">Unit *</label>
<select id="variantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="ml">ml</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
</div>
<div class="form-group">
<label for="variantThreshold">Low Stock Threshold *</label>
<input type="number" id="variantThreshold" step="0.1" value="10" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Variant</button>
<button type="button" class="btn btn-secondary" id="cancelVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Edit Variant Modal -->
<div id="editVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Variant</h2>
<form id="editVariantForm">
<input type="hidden" id="editVariantId">
<div class="form-group">
<label for="editVariantStrength">Strength *</label>
<input type="text" id="editVariantStrength" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="editVariantQuantity">Quantity *</label>
<input type="number" id="editVariantQuantity" step="0.1" required>
<label for="editDrugDescription">Description</label>
<input type="text" id="editDrugDescription">
</div>
<div class="form-group">
<label for="editVariantUnit">Unit *</label>
<select id="editVariantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="ml">ml</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditBtn">Cancel</button>
</div>
</div>
<div class="form-group">
<label for="editVariantThreshold">Low Stock Threshold *</label>
<input type="number" id="editVariantThreshold" step="0.1" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispensing History Modal -->
<div id="historyModal" class="modal">
<div class="modal-content modal-large">
<span class="close">&times;</span>
<h2>Dispensing History - <span id="historyDrugName"></span></h2>
<div id="historyContent" class="history-content">
<p class="loading">Loading history...</p>
</form>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="closeHistoryBtn">Close</button>
</div>
<!-- Add Drug Modal -->
<div id="addModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add New Drug</h2>
<form id="drugForm">
<div class="form-group">
<label for="drugName">Drug Name *</label>
<input type="text" id="drugName" required>
</div>
<div class="form-group">
<label for="drugDescription">Description</label>
<input type="text" id="drugDescription">
</div>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ddd;">
<h3 style="margin-top: 0;">Initial Variant (Optional)</h3>
<div class="form-group">
<label for="initialVariantStrength">Strength</label>
<input type="text" id="initialVariantStrength" placeholder="e.g., 10mg, 5.4mg">
</div>
<div class="form-group">
<label for="initialVariantQuantity">Quantity</label>
<input type="number" id="initialVariantQuantity" placeholder="0" min="0" step="0.1">
</div>
<div class="form-group">
<label for="initialVariantUnit">Unit</label>
<select id="initialVariantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="boxes">Boxes</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
<div class="form-group">
<label for="initialVariantThreshold">Low Stock Threshold</label>
<input type="number" id="initialVariantThreshold" placeholder="0" min="0" step="0.1" value="10">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Drug</button>
<button type="button" class="btn btn-secondary" id="cancelAddBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispense Drug Modal -->
<div id="dispenseModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Dispense Drug</h2>
<form id="dispenseForm" novalidate>
<div class="form-group">
<label for="dispenseDrugSelect">Drug Variant *</label>
<select id="dispenseDrugSelect">
<option value="">-- Select a drug variant --</option>
</select>
</div>
<div class="form-group">
<label for="dispenseQuantity">Quantity *</label>
<input type="number" id="dispenseQuantity" step="0.1">
</div>
<div class="form-group">
<label for="dispenseAnimal">Animal Name/ID</label>
<input type="text" id="dispenseAnimal">
</div>
<div class="form-group">
<label for="dispenseUser">Dispensed by *</label>
<input type="text" id="dispenseUser">
</div>
<div class="form-group">
<label for="dispenseNotes">Notes</label>
<input type="text" id="dispenseNotes" placeholder="Optional">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Dispense</button>
<button type="button" class="btn btn-secondary" id="cancelDispenseBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Variant Modal -->
<div id="addVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add Variant</h2>
<form id="variantForm">
<input type="hidden" id="variantDrugId">
<div class="form-group">
<label for="variantStrength">Strength *</label>
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 10.8mg, 100ml" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="variantQuantity">Quantity *</label>
<input type="number" id="variantQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="variantUnit">Unit *</label>
<select id="variantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="boxes">boxes</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
</div>
<div class="form-group">
<label for="variantThreshold">Low Stock Threshold *</label>
<input type="number" id="variantThreshold" step="0.1" value="10" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Variant</button>
<button type="button" class="btn btn-secondary" id="cancelVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Edit Variant Modal -->
<div id="editVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Variant</h2>
<form id="editVariantForm">
<input type="hidden" id="editVariantId">
<div class="form-group">
<label for="editVariantStrength">Strength *</label>
<input type="text" id="editVariantStrength" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="editVariantQuantity">Quantity *</label>
<input type="number" id="editVariantQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="editVariantUnit">Unit *</label>
<select id="editVariantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="boxes">Boxes</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
</div>
<div class="form-group">
<label for="editVariantThreshold">Low Stock Threshold *</label>
<input type="number" id="editVariantThreshold" step="0.1" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispensing History Modal -->
<div id="historyModal" class="modal">
<div class="modal-content modal-large">
<span class="close">&times;</span>
<h2>Dispensing History - <span id="historyDrugName"></span></h2>
<div id="historyContent" class="history-content">
<p class="loading">Loading history...</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="closeHistoryBtn">Close</button>
</div>
</div>
</div>
<!-- Change Password Modal -->
<div id="changePasswordModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Change Password</h2>
<form id="changePasswordForm">
<div class="form-group">
<label for="currentPassword">Current Password *</label>
<input type="password" id="currentPassword" required>
</div>
<div class="form-group">
<label for="newPassword">New Password *</label>
<input type="password" id="newPassword" required>
</div>
<div class="form-group">
<label for="confirmNewPassword">Confirm New Password *</label>
<input type="password" id="confirmNewPassword" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Change Password</button>
<button type="button" class="btn btn-secondary" id="cancelChangePasswordBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Admin Change User Password Modal -->
<div id="adminChangePasswordModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Change User Password</h2>
<form id="adminChangePasswordForm">
<input type="hidden" id="adminChangePasswordUserId">
<div class="form-group">
<label for="adminChangePasswordUsername">Username</label>
<input type="text" id="adminChangePasswordUsername" disabled>
</div>
<div class="form-group">
<label for="adminChangePasswordNewPassword">New Password *</label>
<input type="password" id="adminChangePasswordNewPassword" required>
</div>
<div class="form-group">
<label for="adminChangePasswordConfirm">Confirm Password *</label>
<input type="password" id="adminChangePasswordConfirm" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Set Password</button>
<button type="button" class="btn btn-secondary" id="cancelAdminChangePasswordBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="modal">
<div class="modal-content modal-large">
<span class="close">&times;</span>
<h2>User Management</h2>
<div class="user-management-content">
<div class="form-group">
<h3>Create New User</h3>
<form id="createUserForm">
<div class="form-row">
<input type="text" id="newUsername" placeholder="Username" required>
<input type="password" id="newUserPassword" placeholder="Password" required>
</div>
<button type="submit" class="btn btn-primary btn-small">Create User</button>
</form>
</div>
<div id="usersList" class="users-list">
<h3>Users</h3>
<p class="loading">Loading users...</p>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="closeUserManagementBtn">Close</button>
</div>
</div>
</div>
</div>

View File

@@ -24,6 +24,109 @@ body {
line-height: 1.6;
}
/* Login Page Styles */
.login-page {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
}
.login-container {
background: var(--white);
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 400px;
width: 100%;
}
.login-container h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2em;
}
.login-form {
display: none;
}
.login-form.active,
.login-form:first-of-type {
display: block;
}
.login-form h2 {
margin-bottom: 20px;
color: var(--primary-color);
font-size: 1.5em;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-dark);
}
.login-form input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1em;
}
.login-form input:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
}
.login-toggle {
text-align: center;
margin-top: 15px;
font-size: 0.9em;
}
.login-toggle button {
background: none;
border: none;
color: var(--secondary-color);
cursor: pointer;
text-decoration: underline;
padding: 0;
font: inherit;
}
.login-info {
text-align: center;
margin-top: 15px;
font-size: 0.9em;
color: var(--text-light);
font-style: italic;
}
.error-message {
background-color: #ffebee;
color: var(--danger-color);
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid var(--danger-color);
}
/* Main App Styles */
.main-app {
display: none;
}
.container {
max-width: 1200px;
margin: 0 auto;
@@ -40,14 +143,80 @@ header {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
}
.header-top h1 {
flex: 1;
font-size: 2.5em;
}
.user-menu {
position: relative;
display: flex;
align-items: center;
gap: 10px;
}
.user-menu span {
white-space: nowrap;
}
#userMenuBtn {
background: rgba(255,255,255,0.2);
color: var(--white);
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 1.2em;
}
#userMenuBtn:hover {
background: rgba(255,255,255,0.3);
}
.user-dropdown {
position: absolute;
top: 100%;
right: 0;
background: var(--white);
border: 1px solid var(--border-color);
border-radius: 6px;
min-width: 150px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
}
.dropdown-item {
display: block;
width: 100%;
padding: 12px 20px;
border: none;
background: none;
text-align: left;
cursor: pointer;
font-size: 1em;
color: var(--text-dark);
border-bottom: 1px solid var(--light-bg);
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:hover {
background-color: var(--light-bg);
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
main {
@@ -82,6 +251,7 @@ label {
input[type="text"],
input[type="number"],
input[type="password"],
select {
width: 100%;
padding: 12px;
@@ -94,6 +264,7 @@ select {
input[type="text"]:focus,
input[type="number"]:focus,
input[type="password"]:focus,
select:focus {
outline: none;
border-color: var(--secondary-color);
@@ -191,6 +362,9 @@ select:focus {
.header-actions {
margin-bottom: 15px;
display: flex;
gap: 10px;
width: fit-content;
}
.filters {
@@ -670,3 +844,106 @@ footer {
opacity: 1;
}
}
/* User Management Styles */
.user-management-content {
margin: 20px 0;
}
.user-management-content h3 {
margin-top: 20px;
margin-bottom: 15px;
color: var(--primary-color);
}
.user-management-content .form-row {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.user-management-content input {
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.95em;
}
.user-management-content .btn {
margin-bottom: 20px;
}
.users-list {
margin-top: 20px;
}
.users-table {
display: flex;
flex-direction: column;
gap: 10px;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f8f9fa;
border: 1px solid var(--border-color);
border-radius: 6px;
gap: 15px;
}
.admin-badge {
padding: 4px 12px;
background: var(--warning-color);
color: var(--white);
border-radius: 20px;
font-size: 0.9em;
font-weight: 500;
}
/* Responsive Styles */
@media (max-width: 768px) {
header {
padding: 20px 10px;
}
.header-top {
flex-direction: column;
gap: 15px;
text-align: center;
}
.header-top h1 {
flex: none;
font-size: 1.8em;
}
.user-menu {
justify-content: center;
}
.modal-content {
width: 90%;
max-width: none;
}
.drug-item {
flex-direction: column;
}
.drug-actions {
flex-wrap: wrap;
}
.variant-item {
flex-direction: column;
}
.variant-actions {
flex-direction: row;
flex-wrap: wrap;
}
}