""" 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 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 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""" qdro = QDRO(**qdro_data.model_dump()) if not qdro.created_date: 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 for field, value in qdro_data.model_dump(exclude_unset=True).items(): 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()}" if request.output_format.upper() == "PDF": file_path = f"/app/exports/{document_id}.pdf" file_name += ".pdf" # Here you would implement PDF generation # For now, create a simple text file with open(f"/app/exports/{document_id}.txt", "w") as f: f.write(merged_content) file_path = f"/app/exports/{document_id}.txt" elif request.output_format.upper() == "DOCX": file_path = f"/app/exports/{document_id}.docx" file_name += ".docx" # Implement DOCX generation with open(f"/app/exports/{document_id}.txt", "w") as f: f.write(merged_content) file_path = f"/app/exports/{document_id}.txt" else: # HTML file_path = f"/app/exports/{document_id}.html" file_name += ".html" html_content = f"
{merged_content}"
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