progress
This commit is contained in:
@@ -20,7 +20,8 @@ from app.auth.schemas import (
|
||||
Token,
|
||||
UserCreate,
|
||||
UserResponse,
|
||||
LoginRequest
|
||||
LoginRequest,
|
||||
ThemePreferenceUpdate
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@@ -116,4 +117,23 @@ async def list_users(
|
||||
):
|
||||
"""List all users (admin only)"""
|
||||
users = db.query(User).all()
|
||||
return users
|
||||
return users
|
||||
|
||||
|
||||
@router.post("/theme-preference")
|
||||
async def update_theme_preference(
|
||||
theme_data: ThemePreferenceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update user's theme preference"""
|
||||
if theme_data.theme_preference not in ['light', 'dark']:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Theme preference must be 'light' or 'dark'"
|
||||
)
|
||||
|
||||
current_user.theme_preference = theme_data.theme_preference
|
||||
db.commit()
|
||||
|
||||
return {"message": "Theme preference updated successfully", "theme": theme_data.theme_preference}
|
||||
@@ -2,7 +2,7 @@
|
||||
Document Management API endpoints - QDROs, Templates, and General Documents
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import or_, func, and_, desc, asc, text
|
||||
from datetime import date, datetime
|
||||
@@ -17,6 +17,7 @@ from app.models.rolodex import Rolodex
|
||||
from app.models.lookups import FormIndex, FormList, Footer, Employee
|
||||
from app.models.user import User
|
||||
from app.auth.security import get_current_user
|
||||
from app.models.additional import Document
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -662,4 +663,105 @@ def _merge_template_variables(content: str, variables: Dict[str, Any]) -> str:
|
||||
merged = merged.replace(f"{{{{{var_name}}}}}", str(value or ""))
|
||||
merged = merged.replace(f"^{var_name}", str(value or ""))
|
||||
|
||||
return merged
|
||||
return merged
|
||||
|
||||
|
||||
@router.post("/upload/{file_no}")
|
||||
async def upload_document(
|
||||
file_no: str,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Upload a document to a file"""
|
||||
file_obj = db.query(FileModel).filter(FileModel.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file uploaded")
|
||||
|
||||
allowed_types = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"image/jpeg",
|
||||
"image/png"
|
||||
]
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||
|
||||
max_size = 10 * 1024 * 1024 # 10MB
|
||||
content = await file.read()
|
||||
if len(content) > max_size:
|
||||
raise HTTPException(status_code=400, detail="File too large")
|
||||
|
||||
upload_dir = f"uploads/{file_no}"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
ext = file.filename.split(".")[-1]
|
||||
unique_name = f"{uuid.uuid4()}.{ext}"
|
||||
path = f"{upload_dir}/{unique_name}"
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
doc = Document(
|
||||
file_no=file_no,
|
||||
filename=file.filename,
|
||||
path=path,
|
||||
description=description,
|
||||
type=file.content_type,
|
||||
size=len(content),
|
||||
uploaded_by=current_user.username
|
||||
)
|
||||
db.add(doc)
|
||||
db.commit()
|
||||
db.refresh(doc)
|
||||
return doc
|
||||
|
||||
@router.get("/{file_no}/uploaded")
|
||||
async def list_uploaded_documents(
|
||||
file_no: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List uploaded documents for a file"""
|
||||
docs = db.query(Document).filter(Document.file_no == file_no).all()
|
||||
return docs
|
||||
|
||||
@router.delete("/uploaded/{doc_id}")
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete an uploaded document"""
|
||||
doc = db.query(Document).filter(Document.id == doc_id).first()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
if os.path.exists(doc.path):
|
||||
os.remove(doc.path)
|
||||
|
||||
db.delete(doc)
|
||||
db.commit()
|
||||
return {"message": "Document deleted successfully"}
|
||||
|
||||
@router.put("/uploaded/{doc_id}")
|
||||
async def update_document(
|
||||
doc_id: int,
|
||||
description: str = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update document description"""
|
||||
doc = db.query(Document).filter(Document.id == doc_id).first()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
doc.description = description
|
||||
db.commit()
|
||||
db.refresh(doc)
|
||||
return doc
|
||||
37
app/api/settings.py
Normal file
37
app/api/settings.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Public (authenticated) settings endpoints for client configuration
|
||||
"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database.base import get_db
|
||||
from app.auth.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.lookups import SystemSetup
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/inactivity_warning_minutes")
|
||||
async def get_inactivity_warning_minutes(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Returns the inactivity warning threshold in minutes (default 240)."""
|
||||
default_minutes = 240
|
||||
setting = (
|
||||
db.query(SystemSetup)
|
||||
.filter(SystemSetup.setting_key == "inactivity_warning_minutes")
|
||||
.first()
|
||||
)
|
||||
if not setting:
|
||||
return {"minutes": default_minutes}
|
||||
|
||||
try:
|
||||
minutes = int(setting.setting_value)
|
||||
except Exception:
|
||||
minutes = default_minutes
|
||||
return {"minutes": minutes}
|
||||
|
||||
|
||||
@@ -30,11 +30,17 @@ class UserResponse(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
theme_preference: Optional[str] = "light"
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ThemePreferenceUpdate(BaseModel):
|
||||
"""Theme preference update schema"""
|
||||
theme_preference: str
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
"""Token response schema"""
|
||||
access_token: str
|
||||
|
||||
@@ -34,6 +34,7 @@ app.add_middleware(
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
|
||||
# Templates
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
@@ -48,6 +49,7 @@ from app.api.search import router as search_router
|
||||
from app.api.admin import router as admin_router
|
||||
from app.api.import_data import router as import_router
|
||||
from app.api.support import router as support_router
|
||||
from app.api.settings import router as settings_router
|
||||
|
||||
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
|
||||
app.include_router(customers_router, prefix="/api/customers", tags=["customers"])
|
||||
@@ -58,6 +60,7 @@ app.include_router(search_router, prefix="/api/search", tags=["search"])
|
||||
app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
|
||||
app.include_router(import_router, prefix="/api/import", tags=["import"])
|
||||
app.include_router(support_router, prefix="/api/support", tags=["support"])
|
||||
app.include_router(settings_router, prefix="/api/settings", tags=["settings"])
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
|
||||
@@ -8,7 +8,7 @@ from .files import File
|
||||
from .ledger import Ledger
|
||||
from .qdro import QDRO
|
||||
from .audit import AuditLog, LoginAttempt
|
||||
from .additional import Deposit, Payment, FileNote, FormVariable, ReportVariable
|
||||
from .additional import Deposit, Payment, FileNote, FormVariable, ReportVariable, Document
|
||||
from .support import SupportTicket, TicketResponse, TicketStatus, TicketPriority, TicketCategory
|
||||
from .pensions import (
|
||||
Pension, PensionSchedule, MarriageHistory, DeathBenefit,
|
||||
@@ -23,7 +23,7 @@ from .lookups import (
|
||||
__all__ = [
|
||||
"BaseModel", "User", "Rolodex", "Phone", "File", "Ledger", "QDRO",
|
||||
"AuditLog", "LoginAttempt",
|
||||
"Deposit", "Payment", "FileNote", "FormVariable", "ReportVariable",
|
||||
"Deposit", "Payment", "FileNote", "FormVariable", "ReportVariable", "Document",
|
||||
"SupportTicket", "TicketResponse", "TicketStatus", "TicketPriority", "TicketCategory",
|
||||
"Pension", "PensionSchedule", "MarriageHistory", "DeathBenefit",
|
||||
"SeparationAgreement", "LifeTable", "NumberTable",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Additional models for complete legacy system coverage
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Date, Float, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, Text, Date, Float, ForeignKey, func, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.base import BaseModel
|
||||
|
||||
@@ -95,4 +95,25 @@ class ReportVariable(BaseModel):
|
||||
active = Column(Integer, default=1) # Legacy system uses integer for boolean
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ReportVariable(identifier='{self.identifier}')>"
|
||||
return f"<ReportVariable(identifier='{self.identifier}')>"
|
||||
|
||||
|
||||
class Document(BaseModel):
|
||||
__tablename__ = "documents"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
filename = Column(String(255), nullable=False)
|
||||
path = Column(String(512), nullable=False)
|
||||
description = Column(Text)
|
||||
type = Column(String(50))
|
||||
size = Column(Integer)
|
||||
uploaded_by = Column(String, ForeignKey("users.username"))
|
||||
upload_date = Column(DateTime, default=func.now())
|
||||
|
||||
# Relationships
|
||||
file = relationship("File", back_populates="documents")
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Document(id={self.id}, filename='{self.filename}', file_no='{self.file_no}')>"
|
||||
@@ -64,4 +64,5 @@ class File(BaseModel):
|
||||
death_benefits = relationship("DeathBenefit", back_populates="file", cascade="all, delete-orphan")
|
||||
separation_agreements = relationship("SeparationAgreement", back_populates="file", cascade="all, delete-orphan")
|
||||
payments = relationship("Payment", back_populates="file", cascade="all, delete-orphan")
|
||||
notes = relationship("FileNote", back_populates="file", cascade="all, delete-orphan")
|
||||
notes = relationship("FileNote", back_populates="file", cascade="all, delete-orphan")
|
||||
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")
|
||||
@@ -25,6 +25,9 @@ class User(BaseModel):
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
|
||||
# User Preferences
|
||||
theme_preference = Column(String(10), default='light') # 'light', 'dark'
|
||||
|
||||
# Activity tracking
|
||||
last_login = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
Reference in New Issue
Block a user