864 lines
26 KiB
Python
864 lines
26 KiB
Python
"""
|
|
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, Form, Request
|
|
from sqlalchemy.orm import Session, joinedload
|
|
from sqlalchemy import or_, func, and_, desc, asc, text
|
|
from datetime import date, datetime
|
|
import os
|
|
import uuid
|
|
import shutil
|
|
|
|
from app.database.base import get_db
|
|
from app.models.qdro import QDRO
|
|
from app.models.files import File as FileModel
|
|
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
|
|
from app.core.logging import get_logger
|
|
from app.services.audit import audit_service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic schemas
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class QDROBase(BaseModel):
|
|
file_no: str
|
|
version: str = "01"
|
|
title: Optional[str] = None
|
|
content: Optional[str] = None
|
|
status: str = "DRAFT"
|
|
created_date: Optional[date] = None
|
|
approved_date: Optional[date] = None
|
|
filed_date: Optional[date] = None
|
|
participant_name: Optional[str] = None
|
|
spouse_name: Optional[str] = None
|
|
plan_name: Optional[str] = None
|
|
plan_administrator: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class QDROCreate(QDROBase):
|
|
pass
|
|
|
|
|
|
class QDROUpdate(BaseModel):
|
|
version: Optional[str] = None
|
|
title: Optional[str] = None
|
|
content: Optional[str] = None
|
|
status: Optional[str] = None
|
|
created_date: Optional[date] = None
|
|
approved_date: Optional[date] = None
|
|
filed_date: Optional[date] = None
|
|
participant_name: Optional[str] = None
|
|
spouse_name: Optional[str] = None
|
|
plan_name: Optional[str] = None
|
|
plan_administrator: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class QDROResponse(QDROBase):
|
|
id: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("/qdros/{file_no}", response_model=List[QDROResponse])
|
|
async def get_file_qdros(
|
|
file_no: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get QDROs for specific file"""
|
|
qdros = db.query(QDRO).filter(QDRO.file_no == file_no).order_by(QDRO.version).all()
|
|
return qdros
|
|
|
|
|
|
@router.get("/qdros/", response_model=List[QDROResponse])
|
|
async def list_qdros(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
status_filter: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List all QDROs with filtering"""
|
|
query = db.query(QDRO)
|
|
|
|
if status_filter:
|
|
query = query.filter(QDRO.status == status_filter)
|
|
|
|
if search:
|
|
query = query.filter(
|
|
or_(
|
|
QDRO.file_no.contains(search),
|
|
QDRO.title.contains(search),
|
|
QDRO.participant_name.contains(search),
|
|
QDRO.spouse_name.contains(search),
|
|
QDRO.plan_name.contains(search)
|
|
)
|
|
)
|
|
|
|
qdros = query.offset(skip).limit(limit).all()
|
|
return qdros
|
|
|
|
|
|
@router.post("/qdros/", response_model=QDROResponse)
|
|
async def create_qdro(
|
|
qdro_data: QDROCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create new QDRO"""
|
|
# Only accept fields that exist on the model and exclude None values
|
|
allowed_fields = {c.name for c in QDRO.__table__.columns}
|
|
payload = {
|
|
k: v
|
|
for k, v in qdro_data.model_dump(exclude_unset=True).items()
|
|
if v is not None and k in allowed_fields
|
|
}
|
|
qdro = QDRO(**payload)
|
|
|
|
# Backfill created_date if model supports it; otherwise rely on created_at
|
|
if hasattr(qdro, "created_date") and not getattr(qdro, "created_date"):
|
|
setattr(qdro, "created_date", date.today())
|
|
|
|
db.add(qdro)
|
|
db.commit()
|
|
db.refresh(qdro)
|
|
|
|
return qdro
|
|
|
|
|
|
@router.get("/qdros/{file_no}/{qdro_id}", response_model=QDROResponse)
|
|
async def get_qdro(
|
|
file_no: str,
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get specific QDRO"""
|
|
qdro = db.query(QDRO).filter(
|
|
QDRO.id == qdro_id,
|
|
QDRO.file_no == file_no
|
|
).first()
|
|
|
|
if not qdro:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="QDRO not found"
|
|
)
|
|
|
|
return qdro
|
|
|
|
|
|
@router.put("/qdros/{file_no}/{qdro_id}", response_model=QDROResponse)
|
|
async def update_qdro(
|
|
file_no: str,
|
|
qdro_id: int,
|
|
qdro_data: QDROUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Update QDRO"""
|
|
qdro = db.query(QDRO).filter(
|
|
QDRO.id == qdro_id,
|
|
QDRO.file_no == file_no
|
|
).first()
|
|
|
|
if not qdro:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="QDRO not found"
|
|
)
|
|
|
|
# Update fields present on the model only
|
|
allowed_fields = {c.name for c in QDRO.__table__.columns}
|
|
for field, value in qdro_data.model_dump(exclude_unset=True).items():
|
|
if field in allowed_fields:
|
|
setattr(qdro, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(qdro)
|
|
|
|
return qdro
|
|
|
|
|
|
@router.delete("/qdros/{file_no}/{qdro_id}")
|
|
async def delete_qdro(
|
|
file_no: str,
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete QDRO"""
|
|
qdro = db.query(QDRO).filter(
|
|
QDRO.id == qdro_id,
|
|
QDRO.file_no == file_no
|
|
).first()
|
|
|
|
if not qdro:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="QDRO not found"
|
|
)
|
|
|
|
db.delete(qdro)
|
|
db.commit()
|
|
|
|
return {"message": "QDRO deleted successfully"}
|
|
|
|
|
|
# Enhanced Document Management Endpoints
|
|
|
|
# Template Management Schemas
|
|
class TemplateBase(BaseModel):
|
|
"""Base template schema"""
|
|
form_id: str
|
|
form_name: str
|
|
category: str = "GENERAL"
|
|
content: str = ""
|
|
variables: Optional[Dict[str, str]] = None
|
|
|
|
class TemplateCreate(TemplateBase):
|
|
pass
|
|
|
|
class TemplateUpdate(BaseModel):
|
|
form_name: Optional[str] = None
|
|
category: Optional[str] = None
|
|
content: Optional[str] = None
|
|
variables: Optional[Dict[str, str]] = None
|
|
|
|
class TemplateResponse(TemplateBase):
|
|
active: bool = True
|
|
created_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
# Document Generation Schema
|
|
class DocumentGenerateRequest(BaseModel):
|
|
"""Request to generate document from template"""
|
|
template_id: str
|
|
file_no: str
|
|
output_format: str = "PDF" # PDF, DOCX, HTML
|
|
variables: Optional[Dict[str, Any]] = None
|
|
|
|
class DocumentResponse(BaseModel):
|
|
"""Generated document response"""
|
|
document_id: str
|
|
file_name: str
|
|
file_path: str
|
|
size: int
|
|
created_at: datetime
|
|
|
|
# Document Statistics
|
|
class DocumentStats(BaseModel):
|
|
"""Document system statistics"""
|
|
total_templates: int
|
|
total_qdros: int
|
|
templates_by_category: Dict[str, int]
|
|
recent_activity: List[Dict[str, Any]]
|
|
|
|
|
|
@router.get("/templates/", response_model=List[TemplateResponse])
|
|
async def list_templates(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
category: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
active_only: bool = Query(True),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List available document templates"""
|
|
query = db.query(FormIndex)
|
|
|
|
if active_only:
|
|
query = query.filter(FormIndex.active == True)
|
|
|
|
if category:
|
|
query = query.filter(FormIndex.category == category)
|
|
|
|
if search:
|
|
query = query.filter(
|
|
or_(
|
|
FormIndex.form_name.contains(search),
|
|
FormIndex.form_id.contains(search)
|
|
)
|
|
)
|
|
|
|
templates = query.offset(skip).limit(limit).all()
|
|
|
|
# Enhanced response with template content
|
|
results = []
|
|
for template in templates:
|
|
template_lines = db.query(FormList).filter(
|
|
FormList.form_id == template.form_id
|
|
).order_by(FormList.line_number).all()
|
|
|
|
content = "\n".join([line.content or "" for line in template_lines])
|
|
|
|
results.append({
|
|
"form_id": template.form_id,
|
|
"form_name": template.form_name,
|
|
"category": template.category,
|
|
"content": content,
|
|
"active": template.active,
|
|
"created_at": template.created_at,
|
|
"variables": _extract_variables_from_content(content)
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
@router.post("/templates/", response_model=TemplateResponse)
|
|
async def create_template(
|
|
template_data: TemplateCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create new document template"""
|
|
# Check if template already exists
|
|
existing = db.query(FormIndex).filter(FormIndex.form_id == template_data.form_id).first()
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Template with this ID already exists"
|
|
)
|
|
|
|
# Create form index entry
|
|
form_index = FormIndex(
|
|
form_id=template_data.form_id,
|
|
form_name=template_data.form_name,
|
|
category=template_data.category,
|
|
active=True
|
|
)
|
|
db.add(form_index)
|
|
|
|
# Create form content lines
|
|
content_lines = template_data.content.split('\n')
|
|
for i, line in enumerate(content_lines, 1):
|
|
form_line = FormList(
|
|
form_id=template_data.form_id,
|
|
line_number=i,
|
|
content=line
|
|
)
|
|
db.add(form_line)
|
|
|
|
db.commit()
|
|
db.refresh(form_index)
|
|
|
|
return {
|
|
"form_id": form_index.form_id,
|
|
"form_name": form_index.form_name,
|
|
"category": form_index.category,
|
|
"content": template_data.content,
|
|
"active": form_index.active,
|
|
"created_at": form_index.created_at,
|
|
"variables": template_data.variables or {}
|
|
}
|
|
|
|
|
|
@router.get("/templates/{template_id}", response_model=TemplateResponse)
|
|
async def get_template(
|
|
template_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get specific template with content"""
|
|
template = db.query(FormIndex).filter(FormIndex.form_id == template_id).first()
|
|
|
|
if not template:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Template not found"
|
|
)
|
|
|
|
# Get template content
|
|
template_lines = db.query(FormList).filter(
|
|
FormList.form_id == template_id
|
|
).order_by(FormList.line_number).all()
|
|
|
|
content = "\n".join([line.content or "" for line in template_lines])
|
|
|
|
return {
|
|
"form_id": template.form_id,
|
|
"form_name": template.form_name,
|
|
"category": template.category,
|
|
"content": content,
|
|
"active": template.active,
|
|
"created_at": template.created_at,
|
|
"variables": _extract_variables_from_content(content)
|
|
}
|
|
|
|
|
|
@router.put("/templates/{template_id}", response_model=TemplateResponse)
|
|
async def update_template(
|
|
template_id: str,
|
|
template_data: TemplateUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Update document template"""
|
|
template = db.query(FormIndex).filter(FormIndex.form_id == template_id).first()
|
|
|
|
if not template:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Template not found"
|
|
)
|
|
|
|
# Update form index
|
|
if template_data.form_name:
|
|
template.form_name = template_data.form_name
|
|
if template_data.category:
|
|
template.category = template_data.category
|
|
|
|
# Update content if provided
|
|
if template_data.content is not None:
|
|
# Delete existing content lines
|
|
db.query(FormList).filter(FormList.form_id == template_id).delete()
|
|
|
|
# Add new content lines
|
|
content_lines = template_data.content.split('\n')
|
|
for i, line in enumerate(content_lines, 1):
|
|
form_line = FormList(
|
|
form_id=template_id,
|
|
line_number=i,
|
|
content=line
|
|
)
|
|
db.add(form_line)
|
|
|
|
db.commit()
|
|
db.refresh(template)
|
|
|
|
# Get updated content
|
|
template_lines = db.query(FormList).filter(
|
|
FormList.form_id == template_id
|
|
).order_by(FormList.line_number).all()
|
|
|
|
content = "\n".join([line.content or "" for line in template_lines])
|
|
|
|
return {
|
|
"form_id": template.form_id,
|
|
"form_name": template.form_name,
|
|
"category": template.category,
|
|
"content": content,
|
|
"active": template.active,
|
|
"created_at": template.created_at,
|
|
"variables": _extract_variables_from_content(content)
|
|
}
|
|
|
|
|
|
@router.delete("/templates/{template_id}")
|
|
async def delete_template(
|
|
template_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete document template"""
|
|
template = db.query(FormIndex).filter(FormIndex.form_id == template_id).first()
|
|
|
|
if not template:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Template not found"
|
|
)
|
|
|
|
# Delete content lines
|
|
db.query(FormList).filter(FormList.form_id == template_id).delete()
|
|
|
|
# Delete template
|
|
db.delete(template)
|
|
db.commit()
|
|
|
|
return {"message": "Template deleted successfully"}
|
|
|
|
|
|
@router.post("/generate/{template_id}")
|
|
async def generate_document(
|
|
template_id: str,
|
|
request: DocumentGenerateRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Generate document from template"""
|
|
# Get template
|
|
template = db.query(FormIndex).filter(FormIndex.form_id == template_id).first()
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
|
|
|
# Get file information
|
|
file_obj = db.query(FileModel).options(
|
|
joinedload(FileModel.owner)
|
|
).filter(FileModel.file_no == request.file_no).first()
|
|
|
|
if not file_obj:
|
|
raise HTTPException(status_code=404, detail="File not found")
|
|
|
|
# Get template content
|
|
template_lines = db.query(FormList).filter(
|
|
FormList.form_id == template_id
|
|
).order_by(FormList.line_number).all()
|
|
|
|
template_content = "\n".join([line.content or "" for line in template_lines])
|
|
|
|
# Prepare merge variables
|
|
merge_vars = {
|
|
"FILE_NO": file_obj.file_no,
|
|
"CLIENT_FIRST": file_obj.owner.first if file_obj.owner else "",
|
|
"CLIENT_LAST": file_obj.owner.last if file_obj.owner else "",
|
|
"CLIENT_FULL": f"{file_obj.owner.first or ''} {file_obj.owner.last}".strip() if file_obj.owner else "",
|
|
"MATTER": file_obj.regarding or "",
|
|
"OPENED": file_obj.opened.strftime("%B %d, %Y") if file_obj.opened else "",
|
|
"ATTORNEY": file_obj.empl_num or "",
|
|
"TODAY": date.today().strftime("%B %d, %Y")
|
|
}
|
|
|
|
# Add any custom variables from the request
|
|
if request.variables:
|
|
merge_vars.update(request.variables)
|
|
|
|
# Perform variable substitution
|
|
merged_content = _merge_template_variables(template_content, merge_vars)
|
|
|
|
# Generate document file
|
|
document_id = str(uuid.uuid4())
|
|
file_name = f"{template.form_name}_{file_obj.file_no}_{date.today().isoformat()}"
|
|
|
|
exports_dir = "/app/exports"
|
|
try:
|
|
os.makedirs(exports_dir, exist_ok=True)
|
|
except Exception:
|
|
try:
|
|
os.makedirs("exports", exist_ok=True)
|
|
exports_dir = "exports"
|
|
except Exception:
|
|
exports_dir = "."
|
|
|
|
if request.output_format.upper() == "PDF":
|
|
file_path = f"{exports_dir}/{document_id}.pdf"
|
|
file_name += ".pdf"
|
|
# Here you would implement PDF generation
|
|
# For now, create a simple text file
|
|
with open(f"{exports_dir}/{document_id}.txt", "w") as f:
|
|
f.write(merged_content)
|
|
file_path = f"{exports_dir}/{document_id}.txt"
|
|
elif request.output_format.upper() == "DOCX":
|
|
file_path = f"{exports_dir}/{document_id}.docx"
|
|
file_name += ".docx"
|
|
# Implement DOCX generation
|
|
with open(f"{exports_dir}/{document_id}.txt", "w") as f:
|
|
f.write(merged_content)
|
|
file_path = f"{exports_dir}/{document_id}.txt"
|
|
else: # HTML
|
|
file_path = f"{exports_dir}/{document_id}.html"
|
|
file_name += ".html"
|
|
html_content = f"<html><body><pre>{merged_content}</pre></body></html>"
|
|
with open(file_path, "w") as f:
|
|
f.write(html_content)
|
|
|
|
file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
|
|
|
return {
|
|
"document_id": document_id,
|
|
"file_name": file_name,
|
|
"file_path": file_path,
|
|
"size": file_size,
|
|
"created_at": datetime.now()
|
|
}
|
|
|
|
|
|
@router.get("/categories/")
|
|
async def get_template_categories(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get available template categories"""
|
|
categories = db.query(FormIndex.category).distinct().all()
|
|
return [cat[0] for cat in categories if cat[0]]
|
|
|
|
|
|
@router.get("/stats/summary")
|
|
async def get_document_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get document system statistics"""
|
|
# Template statistics
|
|
total_templates = db.query(FormIndex).filter(FormIndex.active == True).count()
|
|
total_qdros = db.query(QDRO).count()
|
|
|
|
# Templates by category
|
|
category_stats = db.query(
|
|
FormIndex.category,
|
|
func.count(FormIndex.form_id)
|
|
).filter(FormIndex.active == True).group_by(FormIndex.category).all()
|
|
|
|
categories_dict = {cat[0] or "Uncategorized": cat[1] for cat in category_stats}
|
|
|
|
# Recent QDRO activity
|
|
recent_qdros = db.query(QDRO).order_by(desc(QDRO.updated_at)).limit(5).all()
|
|
recent_activity = [
|
|
{
|
|
"type": "QDRO",
|
|
"file_no": qdro.file_no,
|
|
"status": qdro.status,
|
|
"updated_at": qdro.updated_at.isoformat() if qdro.updated_at else None
|
|
}
|
|
for qdro in recent_qdros
|
|
]
|
|
|
|
return {
|
|
"total_templates": total_templates,
|
|
"total_qdros": total_qdros,
|
|
"templates_by_category": categories_dict,
|
|
"recent_activity": recent_activity
|
|
}
|
|
|
|
|
|
@router.get("/file/{file_no}/documents")
|
|
async def get_file_documents(
|
|
file_no: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get all documents associated with a specific file"""
|
|
# Get QDROs for this file
|
|
qdros = db.query(QDRO).filter(QDRO.file_no == file_no).order_by(desc(QDRO.updated_at)).all()
|
|
|
|
# Format response
|
|
documents = [
|
|
{
|
|
"id": qdro.id,
|
|
"type": "QDRO",
|
|
"title": f"QDRO v{qdro.version}",
|
|
"status": qdro.status,
|
|
"created_date": qdro.created_date.isoformat() if qdro.created_date else None,
|
|
"updated_at": qdro.updated_at.isoformat() if qdro.updated_at else None,
|
|
"file_no": qdro.file_no
|
|
}
|
|
for qdro in qdros
|
|
]
|
|
|
|
return {
|
|
"file_no": file_no,
|
|
"documents": documents,
|
|
"total_count": len(documents)
|
|
}
|
|
|
|
|
|
def _extract_variables_from_content(content: str) -> Dict[str, str]:
|
|
"""Extract variable placeholders from template content"""
|
|
import re
|
|
variables = {}
|
|
|
|
# Find variables in format {{VARIABLE_NAME}}
|
|
matches = re.findall(r'\{\{([^}]+)\}\}', content)
|
|
for match in matches:
|
|
var_name = match.strip()
|
|
variables[var_name] = f"Placeholder for {var_name}"
|
|
|
|
# Find variables in format ^VARIABLE
|
|
matches = re.findall(r'\^([A-Z_]+)', content)
|
|
for match in matches:
|
|
variables[match] = f"Placeholder for {match}"
|
|
|
|
return variables
|
|
|
|
|
|
def _merge_template_variables(content: str, variables: Dict[str, Any]) -> str:
|
|
"""Replace template variables with actual values"""
|
|
merged = content
|
|
|
|
# Replace {{VARIABLE}} format
|
|
for var_name, value in variables.items():
|
|
merged = merged.replace(f"{{{{{var_name}}}}}", str(value or ""))
|
|
merged = merged.replace(f"^{var_name}", str(value or ""))
|
|
|
|
return merged
|
|
|
|
|
|
# --- Client Error Logging (for Documents page) ---
|
|
class ClientErrorLog(BaseModel):
|
|
"""Payload for client-side error logging"""
|
|
message: str
|
|
action: Optional[str] = None
|
|
stack: Optional[str] = None
|
|
url: Optional[str] = None
|
|
line: Optional[int] = None
|
|
column: Optional[int] = None
|
|
user_agent: Optional[str] = None
|
|
extra: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@router.post("/client-error")
|
|
async def log_client_error(
|
|
payload: ClientErrorLog,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: Optional[User] = Depends(lambda: None)
|
|
):
|
|
"""Accept client-side error logs from the Documents page.
|
|
|
|
This endpoint is lightweight and safe to call; it records the error to the
|
|
application logs and best-effort to the audit log without interrupting the UI.
|
|
"""
|
|
logger = get_logger("client.documents")
|
|
client_ip = request.headers.get("x-forwarded-for")
|
|
if client_ip:
|
|
client_ip = client_ip.split(",")[0].strip()
|
|
else:
|
|
client_ip = request.client.host if request.client else None
|
|
|
|
logger.error(
|
|
"Client error reported",
|
|
action=payload.action,
|
|
message=payload.message,
|
|
stack=payload.stack,
|
|
page="/documents",
|
|
url=payload.url or str(request.url),
|
|
line=payload.line,
|
|
column=payload.column,
|
|
user=getattr(current_user, "username", None),
|
|
user_id=getattr(current_user, "id", None),
|
|
user_agent=payload.user_agent or request.headers.get("user-agent"),
|
|
client_ip=client_ip,
|
|
extra=payload.extra,
|
|
)
|
|
|
|
# Best-effort audit log; do not raise on failure
|
|
try:
|
|
audit_service.log_action(
|
|
db=db,
|
|
action="CLIENT_ERROR",
|
|
resource_type="DOCUMENTS",
|
|
user=current_user,
|
|
resource_id=None,
|
|
details={
|
|
"action": payload.action,
|
|
"message": payload.message,
|
|
"url": payload.url or str(request.url),
|
|
"line": payload.line,
|
|
"column": payload.column,
|
|
"extra": payload.extra,
|
|
},
|
|
request=request,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return {"status": "logged"}
|
|
|
|
|
|
@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()
|
|
# Treat zero-byte payloads as no file uploaded to provide a clearer client error
|
|
if len(content) == 0:
|
|
raise HTTPException(status_code=400, detail="No file uploaded")
|
|
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 |