252b94aece
Agent Panel UI: - Visual dashboard showing all agents with status (idle/working) - Agent cards with stats: total tasks, completed tasks - Capabilities display for each agent - Execution log showing recent agent activity - Real-time updates (refreshes every 10 seconds) - Click agent to filter execution log - Status indicators: pending, running, completed, failed, pending_approval Email Notifications: - SMTP email service with HTML templates - Task assignment notifications - Agent approval request emails with action details - Agent completion notifications - Configurable via environment variables (SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_TO) - Integrated with agent executor service PDF Invoice Generation: - Professional PDF invoices using ReportLab - Generate from time entries by project - Client information and company branding - Itemized time entries with hours, rates, amounts - Automatic totals calculation - Download as PDF attachment - API endpoints: /api/opc/invoices/generate - Projects and clients management endpoints Additional Features: - Projects CRUD API - Clients CRUD API - Project time tracking summary - Billable vs non-billable hours tracking Phase 1 MVP 100% Complete: ✅ PostgreSQL database with full schema ✅ Task CRUD API with automatic time tracking ✅ Kanban board with drag-and-drop ✅ Agent executor service with LLM integration ✅ WebSocket real-time updates ✅ 3 core agents (PM, CTO, COO) ✅ Agent panel UI with execution logs ✅ Email notifications (Telegram + Email) ✅ PDF invoice generation All 12 tasks completed!
145 lines
4.2 KiB
Python
145 lines
4.2 KiB
Python
"""
|
|
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)
|
|
}
|