feat: complete Phase 1 MVP - add agent panel, email notifications, and PDF invoicing
Deploy Dashboard (Dev) / deploy-dev (push) Failing after 24m36s
Run Tests / Backend Tests (push) Successful in 10m13s
Run Tests / Frontend Tests (push) Failing after 22s
Run Tests / Test Summary (push) Failing after 41s

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:
Gan, Jimmy
2026-03-31 14:52:42 +08:00
parent 1422cc9bc8
commit 252b94aece
7 changed files with 745 additions and 8 deletions
+144
View File
@@ -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)
}