"""Revenue Cockpit — page + API.""" from __future__ import annotations from pathlib import Path from typing import Any, Optional from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field from app.services import revenue_cockpit as svc BASE_DIR = Path(__file__).resolve().parent.parent.parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) router = APIRouter(tags=["revenue-cockpit"]) api = APIRouter(prefix="/api/revenue-cockpit", tags=["revenue-cockpit-api"]) class ProjectUpdateBody(BaseModel): name: Optional[str] = None category: Optional[str] = None margin_month: Optional[float] = None margin_year: Optional[float] = None target_revenue: Optional[float] = None next_steps: Optional[str] = None status: Optional[str] = None priority: Optional[str] = None row_style: Optional[str] = None class GoalsUpdateBody(BaseModel): vision_text: Optional[str] = None horizon_text: Optional[str] = None mid_text: Optional[str] = None tagline: Optional[str] = None class ObjectiveBody(BaseModel): title: str = Field(..., min_length=1) description: Optional[str] = None priority: str = "normal" due_date: Optional[str] = None class ObjectiveUpdateBody(BaseModel): title: Optional[str] = None description: Optional[str] = None status: Optional[str] = None priority: Optional[str] = None due_date: Optional[str] = None class AssignTaskBody(BaseModel): agent_name: str = Field(..., min_length=1) title: str = Field(..., min_length=1) description: Optional[str] = None objective_id: Optional[int] = None priority: str = "normal" delegate_herman: bool = True class DelegateBody(BaseModel): message: str = Field(..., min_length=1) class ImportBody(BaseModel): path: str = "Succes Sheet .xlsx" sheet: Optional[str] = "Projects next steps revenue" replace: bool = True @router.get("/revenue-cockpit", response_class=HTMLResponse) async def revenue_cockpit_page(request: Request): return templates.TemplateResponse( "revenue_cockpit.html", {"request": request, "page_title": "Revenue Cockpit"}, ) @api.get("/live") async def live_from_excel(path: str = "Succes Sheet .xlsx", sheet: Optional[str] = "Projects next steps revenue"): """Leading data source: fresh parse from NAS Excel.""" try: parsed = await svc.fetch_excel_parse_async(path, sheet) except Exception as exc: raise HTTPException(502, str(exc)) from exc projects = parsed.get("projects") or [] margin_month = sum(p.get("margin_month") or 0 for p in projects) margin_year = sum(p.get("margin_year") or 0 for p in projects) by_style: dict[str, int] = {} by_category: dict[str, int] = {} for p in projects: by_style[p.get("row_style") or "white"] = by_style.get(p.get("row_style") or "white", 0) + 1 by_category[p.get("category") or "deal"] = by_category.get(p.get("category") or "deal", 0) + 1 return { "ok": True, "source": "excel", **parsed, "aggregates": { "total_margin_month": margin_month, "total_margin_year": margin_year, "with_margin": sum(1 for p in projects if p.get("margin_month")), "by_style": by_style, "by_category": by_category, }, } @api.get("/dashboard") async def dashboard(): return {"ok": True, "stats": svc.dashboard_stats(), "projects": svc.list_projects()} @api.get("/projects") def list_projects(status: Optional[str] = None): return {"ok": True, "items": svc.list_projects(status)} @api.get("/projects/{project_id}") def get_project(project_id: int): p = svc.get_project(project_id) if not p: raise HTTPException(404, "Project not found") return {"ok": True, "project": p} @api.patch("/projects/{project_id}") def patch_project(project_id: int, body: ProjectUpdateBody): data = body.model_dump(exclude_unset=True) row_style = data.pop("row_style", None) if row_style is not None: svc.set_project_row_style(project_id, row_style) p = svc.update_project(project_id, data) if not p: raise HTTPException(404, "Project not found") svc.take_snapshot() return {"ok": True, "project": p} @api.patch("/goals") def patch_goals(body: GoalsUpdateBody): data = body.model_dump(exclude_unset=True) g = svc.update_goals(data) if not g: raise HTTPException(404, "Goals not found") return {"ok": True, "goals": g} @api.post("/projects/{project_id}/objectives") def add_objective(project_id: int, body: ObjectiveBody): try: obj = svc.create_objective(project_id, body.title, body.description, body.priority) except Exception as exc: raise HTTPException(400, str(exc)) from exc svc.take_snapshot() return {"ok": True, "objective": obj} @api.patch("/objectives/{objective_id}") def patch_objective(objective_id: int, body: ObjectiveUpdateBody): data = body.model_dump(exclude_unset=True) obj = svc.update_objective(objective_id, data) if not obj: raise HTTPException(404, "Objective not found") svc.take_snapshot() return {"ok": True, "objective": obj} @api.post("/projects/{project_id}/assign-task") def assign_task(project_id: int, body: AssignTaskBody): try: task = svc.assign_agent_task( project_id, body.agent_name, body.title, body.description, body.objective_id, body.priority, body.delegate_herman, ) except ValueError as exc: raise HTTPException(404, str(exc)) from exc svc.take_snapshot() return {"ok": True, "task": task} @api.post("/projects/{project_id}/delegate") async def delegate_project(project_id: int, body: DelegateBody): try: result = await svc.delegate_via_herman(project_id, body.message) except ValueError as exc: raise HTTPException(404, str(exc)) from exc return result @api.get("/tasks") def list_tasks(limit: int = 50): return {"ok": True, "items": svc.list_agent_tasks(limit)} @api.get("/snapshots") def snapshots(limit: int = 90): return {"ok": True, "items": svc.list_snapshots(limit)} @api.post("/snapshot") def create_snapshot(): snap = svc.take_snapshot() return {"ok": True, "snapshot": snap} @api.post("/import-from-excel") async def import_from_excel(body: ImportBody): try: parsed = await svc.fetch_excel_parse_async(body.path, body.sheet) result = svc.import_from_parsed(parsed, imported_by="ceo", replace=body.replace) return {"ok": True, **result, "preview": {"project_count": parsed.get("project_count"), "sheet": parsed.get("sheet_name")}} except Exception as exc: raise HTTPException(502, f"Import failed: {exc}") from exc @api.get("/preview-excel") async def preview_excel(path: str = "Succes Sheet .xlsx", sheet: Optional[str] = "Projects next steps revenue"): try: parsed = await svc.fetch_excel_parse_async(path, sheet) return {"ok": True, **parsed} except Exception as exc: raise HTTPException(502, str(exc)) from exc