Files
foodlinkk-command-center/cockpit/app/routes/projects_api.py
T
2026-06-09 10:41:13 +00:00

198 lines
6.3 KiB
Python

"""Projects + UI preferences API."""
from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from app.services import projects, ui_preferences, nas_folders
router = APIRouter(tags=["projects-api"])
class ProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
client_id: Optional[int] = None
description: str = ""
created_by: str = "ceo"
project_type: str = "general"
priority: str = "normal"
class ProjectUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
client_id: Optional[int] = None
project_type: Optional[str] = None
priority: Optional[str] = None
class AssetCreate(BaseModel):
asset_type: str
title: str
ref_id: Optional[str] = None
file_path: Optional[str] = None
payload: dict[str, Any] = Field(default_factory=dict)
created_by: str = "ceo"
source_agent: Optional[str] = None
class LinkPhotoBody(BaseModel):
project_id: int
created_by: str = "ceo"
class LinkNasFileBody(BaseModel):
title: str
file_path: str
asset_type: str = "document"
class PreferencesBody(BaseModel):
dashboard_layout: Optional[list[str]] = None
global_viz_mode: Optional[str] = None
viz_modes: Optional[dict[str, str]] = None
locale: Optional[str] = None
@router.get("/projects")
async def projects_page(request: Request):
from fastapi.templating import Jinja2Templates
from pathlib import Path
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent.parent / "templates"))
return templates.TemplateResponse("projects.html", {"request": request, "page_title": "Projecten"})
@router.get("/api/projects/types")
def api_project_types() -> dict[str, Any]:
return {"items": nas_folders.list_types()}
@router.get("/api/projects/stats")
def api_project_stats() -> dict[str, Any]:
return {"ok": True, "stats": projects.project_stats()}
@router.get("/api/projects")
def api_list_projects(client_id: Optional[int] = None, limit: int = 50) -> dict[str, Any]:
items = projects.list_projects(client_id=client_id, limit=limit)
return {"items": items, "count": len(items)}
@router.post("/api/projects")
def api_create_project(body: ProjectCreate) -> dict[str, Any]:
try:
row = projects.create_project(
body.name,
client_id=body.client_id,
description=body.description,
created_by=body.created_by,
project_type=body.project_type,
priority=body.priority,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {"ok": True, "project": row}
@router.get("/api/projects/{project_id}")
def api_get_project(project_id: int) -> dict[str, Any]:
row = projects.get_project(project_id)
if not row:
raise HTTPException(status_code=404, detail="Project not found")
return {"project": row}
@router.patch("/api/projects/{project_id}")
def api_update_project(project_id: int, body: ProjectUpdate) -> dict[str, Any]:
row = projects.update_project(project_id, **body.model_dump(exclude_none=True))
if not row:
raise HTTPException(status_code=404, detail="Project not found")
return {"ok": True, "project": row}
@router.post("/api/projects/{project_id}/ensure-nas")
def api_ensure_nas(project_id: int) -> dict[str, Any]:
row = projects.get_project(project_id)
if not row:
raise HTTPException(status_code=404, detail="Project not found")
client_name = row.get("client_name")
try:
paths = nas_folders.ensure_project_folder(
project_id,
row["name"],
row.get("client_id"),
client_name,
row.get("project_type") or "general",
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {"ok": True, **paths}
@router.post("/api/projects/{project_id}/assets")
def api_add_asset(project_id: int, body: AssetCreate) -> dict[str, Any]:
if not projects.get_project(project_id):
raise HTTPException(status_code=404, detail="Project not found")
try:
asset = projects.add_asset(
project_id,
body.asset_type,
body.title,
ref_id=body.ref_id,
file_path=body.file_path,
payload=body.payload,
created_by=body.created_by,
source_agent=body.source_agent,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {"ok": True, "asset": asset}
@router.post("/api/projects/{project_id}/link-nas-file")
def api_link_nas_file(project_id: int, body: LinkNasFileBody) -> dict[str, Any]:
if not projects.get_project(project_id):
raise HTTPException(status_code=404, detail="Project not found")
asset = projects.add_asset(
project_id,
body.asset_type,
body.title,
file_path=body.file_path,
created_by="ceo",
)
return {"ok": True, "asset": asset}
@router.post("/api/projects/photos/{photo_id}/link")
def api_link_photo(photo_id: int, body: LinkPhotoBody) -> dict[str, Any]:
try:
projects.link_photo_to_project(photo_id, body.project_id, body.created_by)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"ok": True}
@router.get("/api/preferences/ui")
def api_get_ui_preferences() -> dict[str, Any]:
return ui_preferences.get_preferences("ceo")
@router.put("/api/preferences/ui")
def api_save_ui_preferences(body: PreferencesBody) -> dict[str, Any]:
allowed = {m["id"] for m in ui_preferences.VIZ_MODES}
if body.global_viz_mode and body.global_viz_mode not in allowed:
raise HTTPException(status_code=400, detail="Invalid viz mode")
if body.locale and body.locale not in ("nl", "en"):
raise HTTPException(status_code=400, detail="Invalid locale (nl or en)")
prefs = ui_preferences.save_preferences(
"ceo",
dashboard_layout=body.dashboard_layout,
global_viz_mode=body.global_viz_mode,
viz_modes=body.viz_modes,
locale=body.locale,
)
return {"ok": True, "preferences": prefs}