198 lines
6.3 KiB
Python
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}
|