"""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}