""" OPC Projects and Invoicing Router """ from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional from pydantic import BaseModel from db import opc_db from rbac import User, _inject_user from services import pdf_service from datetime import datetime router = APIRouter() class ProjectCreate(BaseModel): name: str description: Optional[str] = None client_id: Optional[int] = None class InvoiceGenerate(BaseModel): project_id: int client_id: int invoice_number: Optional[str] = None @router.get("/projects") async def list_projects( user: User = Depends(_inject_user) ): """List all projects""" pool = await opc_db.get_pool() async with pool.acquire() as conn: rows = await conn.fetch("SELECT * FROM projects ORDER BY created_at DESC") return {"items": [dict(row) for row in rows]} @router.post("/projects") async def create_project( project: ProjectCreate, user: User = Depends(_inject_user) ): """Create a new project""" pool = await opc_db.get_pool() async with pool.acquire() as conn: row = await conn.fetchrow(""" INSERT INTO projects (name, description, client_id) VALUES ($1, $2, $3) RETURNING * """, project.name, project.description, project.client_id) return dict(row) @router.get("/clients") async def list_clients( user: User = Depends(_inject_user) ): """List all clients""" pool = await opc_db.get_pool() async with pool.acquire() as conn: rows = await conn.fetch("SELECT * FROM clients ORDER BY name") return {"items": [dict(row) for row in rows]} @router.post("/clients") async def create_client( name: str, email: Optional[str] = None, company: Optional[str] = None, user: User = Depends(_inject_user) ): """Create a new client""" pool = await opc_db.get_pool() async with pool.acquire() as conn: row = await conn.fetchrow(""" INSERT INTO clients (name, email, company) VALUES ($1, $2, $3) RETURNING * """, name, email, company) return dict(row) @router.post("/invoices/generate") async def generate_invoice( invoice: InvoiceGenerate, user: User = Depends(_inject_user) ): """Generate PDF invoice for a project""" pool = await opc_db.get_pool() async with pool.acquire() as conn: # Get client client_row = await conn.fetchrow("SELECT * FROM clients WHERE id = $1", invoice.client_id) if not client_row: raise HTTPException(status_code=404, detail="Client not found") client = dict(client_row) # Get time entries for project time_rows = await conn.fetch(""" SELECT * FROM time_entries WHERE project_id = $1 AND billable = TRUE ORDER BY started_at """, invoice.project_id) if not time_rows: raise HTTPException(status_code=400, detail="No billable time entries found") time_entries = [dict(row) for row in time_rows] # Generate PDF pdf_bytes = pdf_service.generate_invoice_from_project( project_id=invoice.project_id, client=client, time_entries=time_entries, invoice_number=invoice.invoice_number ) # Return PDF filename = f"invoice-{invoice.invoice_number or datetime.now().strftime('%Y%m%d')}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": f"attachment; filename={filename}" } ) @router.get("/projects/{project_id}/time") async def get_project_time( project_id: int, user: User = Depends(_inject_user) ): """Get time entries for a project""" entries = await opc_db.get_time_entries(project_id=project_id) total_minutes = sum(e["duration_minutes"] for e in entries) billable_minutes = sum(e["duration_minutes"] for e in entries if e.get("billable")) return { "entries": entries, "total_minutes": total_minutes, "total_hours": round(total_minutes / 60, 2), "billable_minutes": billable_minutes, "billable_hours": round(billable_minutes / 60, 2) }