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 fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from typing import List, Optional
from datetime import datetime 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 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 from pydantic import BaseModel
# Create tables # Create tables
@@ -25,18 +26,42 @@ app.add_middleware(
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
# Pydantic schemas # 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): class DrugCreate(BaseModel):
name: str name: str
description: str = None description: Optional[str] = None
class DrugUpdate(BaseModel): class DrugUpdate(BaseModel):
name: str = None name: Optional[str] = None
description: str = None description: Optional[str] = None
class DrugResponse(BaseModel): class DrugResponse(BaseModel):
id: int id: int
name: str name: str
description: str = None description: Optional[str] = None
class Config: class Config:
from_attributes = True from_attributes = True
@@ -67,7 +92,7 @@ class DrugVariantResponse(BaseModel):
class DrugWithVariantsResponse(BaseModel): class DrugWithVariantsResponse(BaseModel):
id: int id: int
name: str name: str
description: str = None description: Optional[str] = None
variants: List[DrugVariantResponse] = [] variants: List[DrugVariantResponse] = []
class Config: class Config:
@@ -92,13 +117,153 @@ class DispensingResponse(BaseModel):
class Config: class Config:
from_attributes = True 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 # Routes
@router.get("/") @router.get("/")
def read_root(): def read_root():
return {"message": "Drug Inventory API"} return {"message": "Drug Inventory API"}
@router.get("/drugs", response_model=List[DrugWithVariantsResponse]) @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""" """Get all drugs with their variants"""
drugs = db.query(Drug).all() drugs = db.query(Drug).all()
result = [] result = []
@@ -110,7 +275,7 @@ def list_drugs(db: Session = Depends(get_db)):
return result return result
@router.get("/drugs/low-stock", response_model=List[DrugWithVariantsResponse]) @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 drugs with low stock variants"""
# Get variants that are low on stock # Get variants that are low on stock
low_stock_variants = db.query(DrugVariant).filter( low_stock_variants = db.query(DrugVariant).filter(
@@ -130,7 +295,7 @@ def low_stock_drugs(db: Session = Depends(get_db)):
return result return result
@router.get("/drugs/{drug_id}", response_model=DrugWithVariantsResponse) @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""" """Get a specific drug with its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first() drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug: if not drug:
@@ -142,7 +307,7 @@ def get_drug(drug_id: int, db: Session = Depends(get_db)):
return drug_dict return drug_dict
@router.post("/drugs", response_model=DrugWithVariantsResponse) @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""" """Create a new drug"""
# Check if drug name already exists # Check if drug name already exists
existing = db.query(Drug).filter(Drug.name == drug.name).first() 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 return drug_dict
@router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse) @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""" """Update a drug"""
drug = db.query(Drug).filter(Drug.id == drug_id).first() drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug: if not drug:
@@ -178,7 +343,7 @@ def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get
return drug_dict return drug_dict
@router.delete("/drugs/{drug_id}") @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""" """Delete a drug and all its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first() drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug: if not drug:
@@ -194,7 +359,7 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db)):
# Drug Variant endpoints # Drug Variant endpoints
@router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse) @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""" """Create a new variant for a drug"""
# Check if drug exists # Check if drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first() 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 return db_variant
@router.get("/variants/{variant_id}", response_model=DrugVariantResponse) @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""" """Get a specific drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant: if not variant:
@@ -230,7 +395,7 @@ def get_drug_variant(variant_id: int, db: Session = Depends(get_db)):
return variant return variant
@router.put("/variants/{variant_id}", response_model=DrugVariantResponse) @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""" """Update a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant: if not variant:
@@ -244,7 +409,7 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
return variant return variant
@router.delete("/variants/{variant_id}") @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""" """Delete a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant: if not variant:
@@ -257,7 +422,7 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)):
# Dispensing endpoints # Dispensing endpoints
@router.post("/dispense", response_model=DispensingResponse) @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""" """Record a drug dispensing and reduce inventory"""
# Check if drug variant exists # Check if drug variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first() 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 return db_dispensing
@router.get("/dispense/history", response_model=List[DispensingResponse]) @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)""" """Get dispensing records (audit log)"""
return db.query(Dispensing).order_by(Dispensing.dispensed_at.desc()).offset(skip).limit(limit).all() 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]) @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)""" """Get dispensing history for a specific drug (all variants)"""
# Verify drug exists # Verify drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first() 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() 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]) @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""" """Get dispensing history for a specific drug variant"""
# Verify variant exists # Verify variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() 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 sqlalchemy.sql import func
from .database import Base 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): class Drug(Base):
__tablename__ = "drugs" __tablename__ = "drugs"
@@ -31,7 +41,7 @@ class Dispensing(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
drug_variant_id = Column(Integer, ForeignKey("drug_variants.id"), nullable=False) drug_variant_id = Column(Integer, ForeignKey("drug_variants.id"), nullable=False)
quantity = Column(Float, 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 user_name = Column(String, nullable=False) # User who dispensed
dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True) dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
notes = Column(String, nullable=True) notes = Column(String, nullable=True)

View File

@@ -3,3 +3,5 @@ uvicorn==0.24.0
sqlalchemy==2.0.23 sqlalchemy==2.0.23
pydantic==2.5.0 pydantic==2.5.0
python-multipart==0.0.6 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 showLowStockOnly = false;
let searchTerm = ''; let searchTerm = '';
let expandedDrugs = new Set(); let expandedDrugs = new Set();
let currentUser = null;
let accessToken = null;
// Initialize // Initialize on page load
document.addEventListener('DOMContentLoaded', () => { 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 drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm'); const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm'); const editVariantForm = document.getElementById('editVariantForm');
@@ -18,6 +146,7 @@ document.addEventListener('DOMContentLoaded', () => {
const dispenseModal = document.getElementById('dispenseModal'); const dispenseModal = document.getElementById('dispenseModal');
const editModal = document.getElementById('editModal'); const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn'); const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const 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');
@@ -25,41 +154,75 @@ document.addEventListener('DOMContentLoaded', () => {
const cancelEditBtn = document.getElementById('cancelEditBtn'); const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn'); const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn'); const showLowStockBtn = document.getElementById('showLowStockBtn');
const userMenuBtn = document.getElementById('userMenuBtn');
const adminBtn = document.getElementById('adminBtn');
const logoutBtn = document.getElementById('logoutBtn');
const changePasswordBtn = document.getElementById('changePasswordBtn');
// Modal close buttons // Modal close buttons
const closeButtons = document.querySelectorAll('.close'); const closeButtons = document.querySelectorAll('.close');
drugForm.addEventListener('submit', handleAddDrug); if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
variantForm.addEventListener('submit', handleAddVariant); if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
editVariantForm.addEventListener('submit', handleEditVariant); if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
dispenseForm.addEventListener('submit', handleDispenseDrug); if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
editForm.addEventListener('submit', handleEditDrug); 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'); 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) => { closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal'); const modal = e.target.closest('.modal');
closeModal(modal); closeModal(modal);
})); }));
showAllBtn.addEventListener('click', () => { if (showAllBtn) showAllBtn.addEventListener('click', () => {
showLowStockOnly = false; showLowStockOnly = false;
updateFilterButtons(); updateFilterButtons();
renderDrugs(); renderDrugs();
}); });
showLowStockBtn.addEventListener('click', () => { if (showLowStockBtn) showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true; showLowStockOnly = true;
updateFilterButtons(); updateFilterButtons();
renderDrugs(); 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 // Search functionality
const drugSearch = document.getElementById('drugSearch'); const drugSearch = document.getElementById('drugSearch');
@@ -80,14 +243,12 @@ document.addEventListener('DOMContentLoaded', () => {
closeModal(e.target); closeModal(e.target);
} }
}); });
}
loadDrugs();
});
// Load drugs from API // Load drugs from API
async function loadDrugs() { async function loadDrugs() {
try { try {
const response = await fetch(`${API_URL}/drugs`); const response = await apiCall('/drugs');
if (!response.ok) throw new Error('Failed to load drugs'); if (!response.ok) throw new Error('Failed to load drugs');
allDrugs = await response.json(); allDrugs = await response.json();
@@ -96,18 +257,32 @@ async function loadDrugs() {
} catch (error) { } catch (error) {
console.error('Error loading drugs:', error); console.error('Error loading drugs:', error);
document.getElementById('drugsList').innerHTML = 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 // Modal utility functions
function openModal(modal) { 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'); modal.classList.add('show');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
function closeModal(modal) { function closeModal(modal) {
modal.classList.remove('show'); modal.classList.remove('show');
modal.style.zIndex = '1000';
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
} }
@@ -224,9 +399,8 @@ async function handleAddDrug(e) {
try { try {
// Create the drug first // Create the drug first
const drugResponse = await fetch(`${API_URL}/drugs`, { const drugResponse = await apiCall('/drugs', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData) body: JSON.stringify(drugData)
}); });
@@ -243,9 +417,8 @@ async function handleAddDrug(e) {
low_stock_threshold: parseFloat(document.getElementById('initialVariantThreshold').value) || 10 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', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData) body: JSON.stringify(variantData)
}); });
@@ -282,15 +455,14 @@ async function handleDispenseDrug(e) {
const dispensingData = { const dispensingData = {
drug_variant_id: variantId, drug_variant_id: variantId,
quantity: quantity, quantity: quantity,
animal_name: animalName, animal_name: animalName || null,
user_name: userName, user_name: userName,
notes: notes || null notes: notes || null
}; };
try { try {
const response = await fetch(`${API_URL}/dispense`, { const response = await apiCall('/dispense', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dispensingData) body: JSON.stringify(dispensingData)
}); });
@@ -360,9 +532,8 @@ async function handleAddVariant(e) {
}; };
try { try {
const response = await fetch(`${API_URL}/drugs/${drugId}/variants`, { const response = await apiCall(`/drugs/${drugId}/variants`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData) body: JSON.stringify(variantData)
}); });
@@ -412,9 +583,8 @@ async function handleEditVariant(e) {
}; };
try { try {
const response = await fetch(`${API_URL}/variants/${variantId}`, { const response = await apiCall(`/variants/${variantId}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData) body: JSON.stringify(variantData)
}); });
@@ -448,7 +618,7 @@ 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;
try { try {
const response = await fetch(`${API_URL}/variants/${variantId}`, { const response = await apiCall(`/variants/${variantId}`, {
method: 'DELETE' method: 'DELETE'
}); });
@@ -476,7 +646,7 @@ async function showDrugHistory(drugId) {
openModal(historyModal); openModal(historyModal);
try { 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'); if (!response.ok) throw new Error('Failed to fetch history');
const allHistory = await response.json(); const allHistory = await response.json();
@@ -546,9 +716,8 @@ async function handleEditDrug(e) {
}; };
try { try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, { const response = await apiCall(`/drugs/${drugId}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData) body: JSON.stringify(drugData)
}); });
@@ -568,7 +737,7 @@ async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return; if (!confirm('Are you sure you want to delete this drug?')) return;
try { try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, { const response = await apiCall(`/drugs/${drugId}`, {
method: 'DELETE' 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 // Update filter button states
function updateFilterButtons() { function updateFilterButtons() {
document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly); document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly);
@@ -594,3 +861,93 @@ function escapeHtml(text) {
div.textContent = text; div.textContent = text;
return div.innerHTML; 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"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div class="container"> <!-- Login Page -->
<header> <div id="loginPage" class="login-page">
<div class="login-container">
<h1>🐶 MTAR Drug Inventory System 🐶</h1> <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> <!-- Main App (hidden until logged in) -->
<!-- Drug List Section --> <div id="mainApp" class="main-app" style="display: none;">
<section id="listSection" class="list-section"> <div class="container">
<div class="section-header"> <header>
<h2>Current Inventory</h2> <div class="header-top">
<div class="header-actions"> <h1>🐶 MTAR Drug Inventory System 🐶</h1>
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button> <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>
</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"> <div class="filters">
<button id="showAllBtn" class="filter-btn active">All</button> <button id="showAllBtn" class="filter-btn active">All</button>
<button id="showLowStockBtn" class="filter-btn">Low Stock Only</button> <button id="showLowStockBtn" class="filter-btn">Low Stock Only</button>
@@ -40,219 +74,307 @@
<footer> <footer>
<p>Many Tears Confidential</p> <p>Many Tears Confidential</p>
</footer> </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>
</div>
<!-- Add Drug Modal --> <!-- Edit Drug Modal -->
<div id="addModal" class="modal"> <div id="editModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close">&times;</span> <span class="close">&times;</span>
<h2>Add New Drug</h2> <h2>Edit Drug</h2>
<form id="drugForm"> <form id="editForm">
<div class="form-group"> <input type="hidden" id="editDrugId">
<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">
<div class="form-group"> <div class="form-group">
<label for="variantQuantity">Quantity *</label> <label for="editDrugName">Drug Name *</label>
<input type="number" id="variantQuantity" step="0.1" required> <input type="text" id="editDrugName" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="variantUnit">Unit *</label> <label for="editDrugDescription">Description</label>
<select id="variantUnit"> <input type="text" id="editDrugDescription">
<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>
</div> </div>
<div class="form-group"> <div class="form-actions">
<label for="editVariantUnit">Unit *</label> <button type="submit" class="btn btn-primary">Save Changes</button>
<select id="editVariantUnit"> <button type="button" class="btn btn-secondary" id="cancelEditBtn">Cancel</button>
<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> </form>
<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>
<div class="form-actions"> </div>
<button type="button" class="btn btn-secondary" id="closeHistoryBtn">Close</button>
<!-- 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> </div>
</div> </div>

View File

@@ -24,6 +24,109 @@ body {
line-height: 1.6; 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 { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
@@ -40,14 +143,80 @@ header {
box-shadow: 0 2px 4px rgba(0,0,0,0.1); 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 { header h1 {
font-size: 2.5em; font-size: 2.5em;
margin-bottom: 10px; margin-bottom: 10px;
} }
.subtitle {
font-size: 1.1em;
opacity: 0.9;
} }
main { main {
@@ -82,6 +251,7 @@ label {
input[type="text"], input[type="text"],
input[type="number"], input[type="number"],
input[type="password"],
select { select {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
@@ -94,6 +264,7 @@ select {
input[type="text"]:focus, input[type="text"]:focus,
input[type="number"]:focus, input[type="number"]:focus,
input[type="password"]:focus,
select:focus { select:focus {
outline: none; outline: none;
border-color: var(--secondary-color); border-color: var(--secondary-color);
@@ -191,6 +362,9 @@ select:focus {
.header-actions { .header-actions {
margin-bottom: 15px; margin-bottom: 15px;
display: flex;
gap: 10px;
width: fit-content;
} }
.filters { .filters {
@@ -670,3 +844,106 @@ footer {
opacity: 1; 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;
}
}