"""Unified cockpit projects and cross-module assets.""" from __future__ import annotations import json from typing import Any, Optional from app.db import execute, fetch_all, fetch_one from app.services import nas_folders def _row(row: dict | None) -> dict | None: if not row: return None out = dict(row) for k, v in list(out.items()): if hasattr(v, "isoformat"): out[k] = v.isoformat() return out def list_projects(client_id: Optional[int] = None, limit: int = 50) -> list[dict[str, Any]]: clauses, params = [], [] if client_id: clauses.append("p.client_id = %s") params.append(client_id) where = (" WHERE " + " AND ".join(clauses)) if clauses else "" safe = max(1, min(limit, 200)) rows = fetch_all( f""" SELECT p.*, c.name AS client_name, (SELECT COUNT(*) FROM project_assets a WHERE a.project_id = p.id) AS asset_count FROM cockpit_projects p LEFT JOIN clients c ON c.id = p.client_id {where} ORDER BY p.updated_at DESC, p.id DESC LIMIT %s """, tuple(params + [safe]), ) return [_row(r) for r in rows] def get_project(project_id: int) -> dict[str, Any] | None: row = fetch_one( """ SELECT p.*, c.name AS client_name FROM cockpit_projects p LEFT JOIN clients c ON c.id = p.client_id WHERE p.id = %s """, (project_id,), ) if not row: return None out = _row(row) or {} assets = fetch_all( "SELECT * FROM project_assets WHERE project_id = %s ORDER BY created_at DESC", (project_id,), ) out["assets"] = [_row(a) for a in assets] return out def create_project( name: str, client_id: Optional[int] = None, description: str = "", created_by: str = "ceo", metadata: dict | None = None, project_type: str = "general", priority: str = "normal", ensure_nas: bool = True, ) -> dict[str, Any]: row = fetch_one( """ INSERT INTO cockpit_projects (name, client_id, description, created_by, metadata, project_type, priority, updated_at) VALUES (%s, %s, %s, %s, %s::jsonb, %s, %s, NOW()) RETURNING * """, (name.strip(), client_id, description or None, created_by, json.dumps(metadata or {}), project_type, priority), ) out = _row(row) or {} if ensure_nas and out.get("id"): client_name = None if client_id: c = fetch_one("SELECT name FROM clients WHERE id = %s", (client_id,)) client_name = c["name"] if c else None try: paths = nas_folders.ensure_project_folder( int(out["id"]), name, client_id, client_name, project_type ) out.update(paths) except Exception as exc: out["nas_error"] = str(exc) return out def update_project(project_id: int, **fields: Any) -> dict[str, Any] | None: allowed = ("name", "description", "status", "client_id", "project_type", "priority") sets, params = [], [] for k, v in fields.items(): if k in allowed and v is not None: sets.append(f"{k} = %s") params.append(v) if not sets: return get_project(project_id) params.append(project_id) execute(f"UPDATE cockpit_projects SET {', '.join(sets)}, updated_at = NOW() WHERE id = %s", tuple(params)) return get_project(project_id) def project_stats() -> dict[str, Any]: total = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects") or {"n": 0} by_type = fetch_all( "SELECT COALESCE(project_type, 'general') AS t, COUNT(*) AS n FROM cockpit_projects GROUP BY t" ) with_nas = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects WHERE nas_path IS NOT NULL") with_client = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects WHERE client_id IS NOT NULL") assets = fetch_one("SELECT COUNT(*) AS n FROM project_assets") return { "total": int(total.get("n") or 0), "with_nas": int((with_nas or {}).get("n") or 0), "with_client": int((with_client or {}).get("n") or 0), "assets": int((assets or {}).get("n") or 0), "by_type": {r["t"]: int(r["n"]) for r in by_type}, } def add_asset( project_id: int, asset_type: str, title: str, ref_id: str | None = None, file_path: str | None = None, payload: dict | None = None, created_by: str = "ceo", source_agent: str | None = None, ) -> dict[str, Any]: row = fetch_one( """ INSERT INTO project_assets (project_id, asset_type, ref_id, title, file_path, payload, created_by, source_agent) VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s, %s) RETURNING * """, ( project_id, asset_type, ref_id, title, file_path, json.dumps(payload or {}), created_by, source_agent, ), ) execute("UPDATE cockpit_projects SET updated_at = NOW() WHERE id = %s", (project_id,)) return _row(row) or {} def get_or_create_agent_project(agent_key: str, client_id: Optional[int] = None) -> int: key = (agent_key or "agent").strip().lower() row = fetch_one( """ SELECT id FROM cockpit_projects WHERE metadata->>'auto_agent' = %s AND status = 'active' ORDER BY updated_at DESC LIMIT 1 """, (key,), ) if row: return int(row["id"]) created = create_project( f"Agent ยท {key}", client_id=client_id, description=f"Automatisch project voor {key} output", created_by=key, metadata={"auto_agent": key}, ) return int(created["id"]) def register_agent_output( asset_type: str, title: str, ref_id: str | None = None, payload: dict | None = None, project_id: Optional[int] = None, source_agent: str | None = None, created_by: str = "ceo", file_path: str | None = None, ) -> dict[str, Any] | None: agent = (source_agent or created_by or "agent").strip().lower() pid = project_id or get_or_create_agent_project(agent) if ref_id: existing = fetch_one( """ SELECT id FROM project_assets WHERE project_id = %s AND asset_type = %s AND ref_id = %s LIMIT 1 """, (pid, asset_type, ref_id), ) if existing: return {"id": existing["id"], "project_id": pid, "dedup": True} return add_asset( pid, asset_type, title, ref_id=ref_id, file_path=file_path, payload=payload, created_by=created_by or agent, source_agent=agent, ) def link_photo_to_project(photo_id: int, project_id: int, created_by: str = "ceo") -> None: photo = fetch_one("SELECT id, filename, source FROM photo_imports WHERE id = %s", (photo_id,)) if not photo: raise ValueError("Photo not found") execute( "UPDATE photo_imports SET project_id = %s, created_by = COALESCE(created_by, %s) WHERE id = %s", (project_id, created_by, photo_id), ) add_asset( project_id, "photo", photo.get("filename") or f"Foto #{photo_id}", ref_id=str(photo_id), created_by=created_by, source_agent=photo.get("source"), )