"""Klant 360° — geaggregeerde data uit NAS, CRM, retail, trends.""" from __future__ import annotations from typing import Any from app.db import fetch_all, fetch_one from app.services import projects as projects_svc def _serialize_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 _serialize_rows(rows: list) -> list[dict]: return [_serialize_row(r) for r in rows if r] def get_client_360(client_id: int) -> dict[str, Any]: client = fetch_one("SELECT * FROM clients WHERE id = %s", (client_id,)) if not client: return {"ok": False, "error": "Client not found"} projs = projects_svc.list_projects(client_id=client_id, limit=50) links = fetch_all( """ SELECT dl.*, p.name AS project_name FROM document_links dl LEFT JOIN cockpit_projects p ON p.id = dl.project_id WHERE dl.client_id = %s ORDER BY dl.created_at DESC """, (client_id,), ) paths = [l["storage_path"] for l in links if l.get("storage_path")] docs: list[dict] = [] if paths: placeholders = ",".join(["%s"] * len(paths)) docs = fetch_all( f""" SELECT filename, storage_path, doc_type, word_count, sentiment_label, sentiment_compound, analyzed_at, user_labels FROM document_analytics WHERE storage_path IN ({placeholders}) OR storage_path LIKE ANY ( SELECT dl.storage_path || '/%%' FROM document_links dl WHERE dl.client_id = %s AND dl.is_folder = TRUE ) ORDER BY analyzed_at DESC NULLS LAST LIMIT 80 """, tuple(paths + [client_id]), ) sentiment_rows = fetch_all( """ SELECT sentiment_label, COUNT(*) AS n, AVG(sentiment_compound) AS avg_c FROM document_analytics da WHERE EXISTS ( SELECT 1 FROM document_links dl WHERE dl.client_id = %s AND (da.storage_path = dl.storage_path OR (dl.is_folder AND da.storage_path LIKE dl.storage_path || '/%%')) ) GROUP BY sentiment_label """, (client_id,), ) top_words = fetch_all( """ SELECT dwc.lemma, SUM(dwc.count) AS total FROM document_word_counts dwc JOIN document_analytics da ON da.id = dwc.document_id WHERE EXISTS ( SELECT 1 FROM document_links dl WHERE dl.client_id = %s AND (da.storage_path = dl.storage_path OR (dl.is_folder AND da.storage_path LIKE dl.storage_path || '/%%')) ) AND NOT dwc.is_stopword GROUP BY dwc.lemma ORDER BY total DESC LIMIT 25 """, (client_id,), ) deals = fetch_all( "SELECT id, title, value, stage, next_action, deadline FROM deals WHERE client_id = %s ORDER BY updated_at DESC LIMIT 15", (client_id,), ) stores = fetch_all( """ SELECT s.id, s.name, s.chain, s.city, s.partnership_status, s.halal_certified FROM client_supermarket_links l JOIN supermarkets s ON s.id = l.supermarket_id WHERE l.client_id = %s LIMIT 30 """, (client_id,), ) trends = fetch_all( """ SELECT DATE_TRUNC('month', da.analyzed_at) AS month, COUNT(*) AS docs, AVG(da.sentiment_compound) AS avg_sentiment, SUM(da.word_count) AS words FROM document_analytics da WHERE da.analyzed_at IS NOT NULL AND EXISTS ( SELECT 1 FROM document_links dl WHERE dl.client_id = %s AND (da.storage_path = dl.storage_path OR (dl.is_folder AND da.storage_path LIKE dl.storage_path || '/%%')) ) GROUP BY 1 ORDER BY 1 DESC LIMIT 12 """, (client_id,), ) for t in trends: if hasattr(t.get("month"), "isoformat"): t["month"] = t["month"].isoformat() return { "ok": True, "client": _serialize_row(client), "projects": projs, "links": _serialize_rows(links), "documents": _serialize_rows(docs), "sentiment_breakdown": _serialize_rows(sentiment_rows), "top_words": _serialize_rows(top_words), "deals": _serialize_rows(deals), "stores": _serialize_rows(stores), "monthly_trends": trends, "stats": { "linked_paths": len(links), "documents": len(docs), "projects": len(projs), "stores": len(stores), "deals": len(deals), }, }