""" File Management API endpoints """ from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session, joinedload from sqlalchemy import or_, func, and_, desc from datetime import date, datetime from app.database.base import get_db from app.models.files import File from app.models.rolodex import Rolodex from app.models.ledger import Ledger from app.models.lookups import Employee, FileType, FileStatus from app.models.user import User from app.auth.security import get_current_user router = APIRouter() # Pydantic schemas from pydantic import BaseModel class FileBase(BaseModel): file_no: str id: str # Rolodex ID (file owner) regarding: Optional[str] = None empl_num: str file_type: str opened: date closed: Optional[date] = None status: str footer_code: Optional[str] = None opposing: Optional[str] = None rate_per_hour: float memo: Optional[str] = None class FileCreate(FileBase): pass class FileUpdate(BaseModel): id: Optional[str] = None regarding: Optional[str] = None empl_num: Optional[str] = None file_type: Optional[str] = None opened: Optional[date] = None closed: Optional[date] = None status: Optional[str] = None footer_code: Optional[str] = None opposing: Optional[str] = None rate_per_hour: Optional[float] = None memo: Optional[str] = None class FileResponse(FileBase): # Financial balances trust_bal: float = 0.0 hours: float = 0.0 hourly_fees: float = 0.0 flat_fees: float = 0.0 disbursements: float = 0.0 credit_bal: float = 0.0 total_charges: float = 0.0 amount_owing: float = 0.0 transferable: float = 0.0 class Config: from_attributes = True @router.get("/", response_model=List[FileResponse]) async def list_files( skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), search: Optional[str] = Query(None), status_filter: Optional[str] = Query(None), employee_filter: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """List files with pagination and filtering""" query = db.query(File) if search: query = query.filter( or_( File.file_no.contains(search), File.id.contains(search), File.regarding.contains(search), File.file_type.contains(search) ) ) if status_filter: query = query.filter(File.status == status_filter) if employee_filter: query = query.filter(File.empl_num == employee_filter) files = query.offset(skip).limit(limit).all() return files @router.get("/{file_no}", response_model=FileResponse) async def get_file( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get specific file by file number""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) return file_obj @router.post("/", response_model=FileResponse) async def create_file( file_data: FileCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Create new file""" # Check if file number already exists existing = db.query(File).filter(File.file_no == file_data.file_no).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="File number already exists" ) file_obj = File(**file_data.model_dump()) db.add(file_obj) db.commit() db.refresh(file_obj) return file_obj @router.put("/{file_no}", response_model=FileResponse) async def update_file( file_no: str, file_data: FileUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Update fields for field, value in file_data.model_dump(exclude_unset=True).items(): setattr(file_obj, field, value) db.commit() db.refresh(file_obj) return file_obj @router.delete("/{file_no}") async def delete_file( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Delete file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) db.delete(file_obj) db.commit() return {"message": "File deleted successfully"} @router.get("/stats/summary") async def get_file_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get file statistics and summary""" total_files = db.query(File).count() active_files = db.query(File).filter(File.status == "ACTIVE").count() # Get status breakdown status_stats = db.query( File.status, func.count(File.file_no).label('count') ).group_by(File.status).all() # Get file type breakdown type_stats = db.query( File.file_type, func.count(File.file_no).label('count') ).group_by(File.file_type).all() # Get employee breakdown employee_stats = db.query( File.empl_num, func.count(File.file_no).label('count') ).group_by(File.empl_num).all() # Financial summary financial_summary = db.query( func.sum(File.total_charges).label('total_charges'), func.sum(File.amount_owing).label('total_owing'), func.sum(File.trust_bal).label('total_trust'), func.sum(File.hours).label('total_hours') ).first() return { "total_files": total_files, "active_files": active_files, "status_breakdown": [{"status": s[0], "count": s[1]} for s in status_stats], "type_breakdown": [{"type": t[0], "count": t[1]} for t in type_stats], "employee_breakdown": [{"employee": e[0], "count": e[1]} for e in employee_stats], "financial_summary": { "total_charges": float(financial_summary.total_charges or 0), "total_owing": float(financial_summary.total_owing or 0), "total_trust": float(financial_summary.total_trust or 0), "total_hours": float(financial_summary.total_hours or 0) } } @router.get("/lookups/file-types") async def get_file_types( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available file types""" file_types = db.query(FileType).filter(FileType.active == True).all() return [{"code": ft.type_code, "description": ft.description} for ft in file_types] @router.get("/lookups/file-statuses") async def get_file_statuses( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available file statuses""" statuses = db.query(FileStatus).filter(FileStatus.active == True).order_by(FileStatus.sort_order).all() return [{"code": s.status_code, "description": s.description} for s in statuses] @router.get("/lookups/employees") async def get_employees( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get active employees""" employees = db.query(Employee).filter(Employee.active == True).all() return [{"code": e.empl_num, "name": f"{e.first_name} {e.last_name}"} for e in employees] @router.get("/{file_no}/financial-summary") async def get_file_financial_summary( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get detailed financial summary for a file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Get recent ledger entries recent_entries = db.query(Ledger)\ .filter(Ledger.file_no == file_no)\ .order_by(desc(Ledger.date))\ .limit(10)\ .all() # Get unbilled entries unbilled_entries = db.query(Ledger)\ .filter(and_(Ledger.file_no == file_no, Ledger.billed == "N"))\ .all() unbilled_total = sum(entry.amount for entry in unbilled_entries) return { "file_no": file_no, "financial_data": { "trust_balance": file_obj.trust_bal, "hours_total": file_obj.hours, "hourly_fees": file_obj.hourly_fees, "flat_fees": file_obj.flat_fees, "disbursements": file_obj.disbursements, "total_charges": file_obj.total_charges, "amount_owing": file_obj.amount_owing, "credit_balance": file_obj.credit_bal, "transferable": file_obj.transferable, "unbilled_amount": unbilled_total }, "recent_entries": [ { "date": entry.date.isoformat() if entry.date else None, "amount": entry.amount, "description": entry.note, "billed": entry.billed == "Y" } for entry in recent_entries ], "unbilled_count": len(unbilled_entries) } @router.get("/{file_no}/client-info") async def get_file_client_info( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get client information for a file""" file_obj = db.query(File)\ .options(joinedload(File.owner))\ .filter(File.file_no == file_no)\ .first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) client = file_obj.owner if not client: return {"file_no": file_no, "client": None} return { "file_no": file_no, "client": { "id": client.id, "name": f"{client.first or ''} {client.last}".strip(), "email": client.email, "city": client.city, "state": client.abrev, "group": client.group } } @router.post("/{file_no}/close") async def close_file( file_no: str, close_date: Optional[date] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Close a file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) file_obj.closed = close_date or date.today() file_obj.status = "CLOSED" db.commit() db.refresh(file_obj) return {"message": f"File {file_no} closed successfully", "closed_date": file_obj.closed} @router.post("/{file_no}/reopen") async def reopen_file( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Reopen a closed file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) file_obj.closed = None file_obj.status = "ACTIVE" db.commit() db.refresh(file_obj) return {"message": f"File {file_no} reopened successfully"} @router.get("/search/advanced") async def advanced_file_search( file_no: Optional[str] = Query(None), client_name: Optional[str] = Query(None), regarding: Optional[str] = Query(None), file_type: Optional[str] = Query(None), status: Optional[str] = Query(None), employee: Optional[str] = Query(None), opened_after: Optional[date] = Query(None), opened_before: Optional[date] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Advanced file search with multiple criteria""" query = db.query(File).options(joinedload(File.owner)) if file_no: query = query.filter(File.file_no.contains(file_no)) if client_name: query = query.join(Rolodex).filter( or_( Rolodex.first.contains(client_name), Rolodex.last.contains(client_name), func.concat(Rolodex.first, ' ', Rolodex.last).contains(client_name) ) ) if regarding: query = query.filter(File.regarding.contains(regarding)) if file_type: query = query.filter(File.file_type == file_type) if status: query = query.filter(File.status == status) if employee: query = query.filter(File.empl_num == employee) if opened_after: query = query.filter(File.opened >= opened_after) if opened_before: query = query.filter(File.opened <= opened_before) # Get total count for pagination total = query.count() # Apply pagination files = query.offset(skip).limit(limit).all() # Format results with client names results = [] for file_obj in files: client = file_obj.owner client_name = f"{client.first or ''} {client.last}".strip() if client else "Unknown" results.append({ "file_no": file_obj.file_no, "client_id": file_obj.id, "client_name": client_name, "regarding": file_obj.regarding, "file_type": file_obj.file_type, "status": file_obj.status, "employee": file_obj.empl_num, "opened": file_obj.opened.isoformat() if file_obj.opened else None, "closed": file_obj.closed.isoformat() if file_obj.closed else None, "amount_owing": file_obj.amount_owing, "total_charges": file_obj.total_charges }) return { "files": results, "total": total, "skip": skip, "limit": limit }