feat: complete Phase 1 MVP - add agent panel, email notifications, and PDF invoicing
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!
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user