203 lines
7.4 KiB
Python
203 lines
7.4 KiB
Python
"""User UI preferences — dashboard layout, widgets, KPIs, visualization modes."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from app.db import fetch_one
|
|
|
|
DEFAULT_LAYOUT = [
|
|
"kpis",
|
|
"executive",
|
|
"briefing",
|
|
"rss",
|
|
"retail",
|
|
"analytics",
|
|
"approvals",
|
|
"feed",
|
|
]
|
|
DEFAULT_VIZ = "neo-bars"
|
|
|
|
DEFAULT_KPI_LAYOUT = [
|
|
"pipeline",
|
|
"clients_active",
|
|
"crm_partnerships",
|
|
"supermarkets",
|
|
"trending_food",
|
|
"pending_approvals",
|
|
]
|
|
|
|
WIDGET_CATALOG = [
|
|
{"id": "kpis", "label": "CEO KPI's", "icon": "📊"},
|
|
{"id": "executive", "label": "Alles op een rij", "icon": "📋"},
|
|
{"id": "briefing", "label": "Herman briefing", "icon": "📝"},
|
|
{"id": "rss", "label": "RSS live feed", "icon": "📰"},
|
|
{"id": "retail", "label": "Retail operatie", "icon": "🏪"},
|
|
{"id": "analytics", "label": "Data & analytics", "icon": "📈"},
|
|
{"id": "approvals", "label": "Agent goedkeuringen", "icon": "✓"},
|
|
{"id": "feed", "label": "Agent feed & projecten", "icon": "⚡"},
|
|
]
|
|
|
|
KPI_CATALOG = [
|
|
{"id": "pipeline", "label": "Pipeline", "sub": "actieve deals", "icon": "💰"},
|
|
{"id": "clients_active", "label": "Actieve klanten", "sub": "CRM · zaken mee", "icon": "🤝"},
|
|
{"id": "clients_total", "label": "Totaal klanten", "sub": "CRM database", "icon": "👥"},
|
|
{"id": "crm_partnerships", "label": "CRM partnerships", "sub": "actieve filialen", "icon": "🏪"},
|
|
{"id": "supermarkets", "label": "Supermarkten", "sub": "Retail 360 DB", "icon": "🛒"},
|
|
{"id": "wholesalers", "label": "Groothandels", "sub": "Retail 360 DB", "icon": "📦"},
|
|
{"id": "trending_food", "label": "Food trends", "sub": "RSS live", "icon": "📰"},
|
|
{"id": "rss_items", "label": "RSS items", "sub": "totaal in DB", "icon": "📡"},
|
|
{"id": "rss_bookmarks", "label": "RSS bookmarks", "sub": "opgeslagen", "icon": "★"},
|
|
{"id": "pending_approvals", "label": "Goedkeuringen", "sub": "wacht op OK", "icon": "✓"},
|
|
{"id": "deals", "label": "Deals", "sub": "totaal CRM", "icon": "💼"},
|
|
{"id": "products", "label": "Producten", "sub": "catalogus", "icon": "🥫"},
|
|
{"id": "suppliers", "label": "Leveranciers", "sub": "sourcing", "icon": "🚚"},
|
|
{"id": "nas_docs", "label": "NAS documenten", "sub": "geanalyseerd", "icon": "📊"},
|
|
{"id": "promo_campaigns", "label": "Promo campagnes", "sub": "actief", "icon": "📁"},
|
|
{"id": "market_best", "label": "Aandeel top", "sub": "retail markt", "icon": "📈"},
|
|
]
|
|
|
|
VIZ_MODES = [
|
|
{"id": "neo-bars", "label": "Neo staafdiagram", "icon": "📊"},
|
|
{"id": "neo-rings", "label": "Neo ringen", "icon": "🍩"},
|
|
{"id": "neo-equalizer", "label": "Neo equalizer", "icon": "🎚️"},
|
|
{"id": "neo-cards", "label": "Neo kaarten", "icon": "🃏"},
|
|
{"id": "neo-table", "label": "Neo tabel", "icon": "📋"},
|
|
]
|
|
|
|
_ALL_WIDGET_IDS = {w["id"] for w in WIDGET_CATALOG}
|
|
_ALL_KPI_IDS = {k["id"] for k in KPI_CATALOG}
|
|
|
|
|
|
def _default_widgets() -> dict[str, bool]:
|
|
return {w["id"]: True for w in WIDGET_CATALOG}
|
|
|
|
|
|
def _parse_json(val: Any, fallback: Any) -> Any:
|
|
if val is None:
|
|
return fallback
|
|
if isinstance(val, str):
|
|
try:
|
|
return json.loads(val)
|
|
except Exception:
|
|
return fallback
|
|
return val
|
|
|
|
|
|
def _normalize_layout(layout: list[str]) -> list[str]:
|
|
seen: set[str] = set()
|
|
out: list[str] = []
|
|
for wid in layout:
|
|
if wid in _ALL_WIDGET_IDS and wid not in seen:
|
|
out.append(wid)
|
|
seen.add(wid)
|
|
for w in WIDGET_CATALOG:
|
|
if w["id"] not in seen:
|
|
out.append(w["id"])
|
|
return out
|
|
|
|
|
|
def _normalize_widgets(widgets: dict[str, Any] | None) -> dict[str, bool]:
|
|
base = _default_widgets()
|
|
if not widgets:
|
|
return base
|
|
for wid in _ALL_WIDGET_IDS:
|
|
if wid in widgets:
|
|
base[wid] = bool(widgets[wid])
|
|
return base
|
|
|
|
|
|
def _normalize_kpis(kpis: list[str] | None) -> list[str]:
|
|
if not kpis:
|
|
return list(DEFAULT_KPI_LAYOUT)
|
|
seen: set[str] = set()
|
|
out: list[str] = []
|
|
for kid in kpis:
|
|
if kid in _ALL_KPI_IDS and kid not in seen:
|
|
out.append(kid)
|
|
seen.add(kid)
|
|
return out or list(DEFAULT_KPI_LAYOUT)
|
|
|
|
|
|
def get_preferences(user_key: str = "ceo") -> dict[str, Any]:
|
|
row = fetch_one("SELECT * FROM user_ui_preferences WHERE user_key = %s", (user_key,))
|
|
if not row:
|
|
return {
|
|
"user_key": user_key,
|
|
"dashboard_layout": list(DEFAULT_LAYOUT),
|
|
"dashboard_widgets": _default_widgets(),
|
|
"dashboard_kpis": list(DEFAULT_KPI_LAYOUT),
|
|
"viz_modes": {},
|
|
"global_viz_mode": DEFAULT_VIZ,
|
|
"locale": "nl",
|
|
"viz_options": VIZ_MODES,
|
|
"widget_catalog": WIDGET_CATALOG,
|
|
"kpi_catalog": KPI_CATALOG,
|
|
}
|
|
layout = _normalize_layout(_parse_json(row.get("dashboard_layout"), DEFAULT_LAYOUT))
|
|
widgets = _normalize_widgets(_parse_json(row.get("dashboard_widgets"), None))
|
|
kpis = _normalize_kpis(_parse_json(row.get("dashboard_kpis"), None))
|
|
viz_modes = _parse_json(row.get("viz_modes"), {})
|
|
return {
|
|
"user_key": user_key,
|
|
"dashboard_layout": layout,
|
|
"dashboard_widgets": widgets,
|
|
"dashboard_kpis": kpis,
|
|
"viz_modes": viz_modes if isinstance(viz_modes, dict) else {},
|
|
"global_viz_mode": row.get("global_viz_mode") or DEFAULT_VIZ,
|
|
"locale": row.get("locale") or "nl",
|
|
"viz_options": VIZ_MODES,
|
|
"widget_catalog": WIDGET_CATALOG,
|
|
"kpi_catalog": KPI_CATALOG,
|
|
}
|
|
|
|
|
|
def save_preferences(
|
|
user_key: str,
|
|
dashboard_layout: list[str] | None = None,
|
|
dashboard_widgets: dict[str, bool] | None = None,
|
|
dashboard_kpis: list[str] | None = None,
|
|
global_viz_mode: str | None = None,
|
|
viz_modes: dict[str, str] | None = None,
|
|
locale: str | None = None,
|
|
) -> dict[str, Any]:
|
|
current = get_preferences(user_key)
|
|
layout = _normalize_layout(dashboard_layout if dashboard_layout is not None else current["dashboard_layout"])
|
|
widgets = _normalize_widgets(
|
|
dashboard_widgets if dashboard_widgets is not None else current["dashboard_widgets"]
|
|
)
|
|
kpis = _normalize_kpis(dashboard_kpis if dashboard_kpis is not None else current["dashboard_kpis"])
|
|
gviz = global_viz_mode if global_viz_mode is not None else current["global_viz_mode"]
|
|
vmodes = viz_modes if viz_modes is not None else current["viz_modes"]
|
|
loc = locale if locale is not None else current.get("locale", "nl")
|
|
if loc not in ("nl", "en"):
|
|
loc = "nl"
|
|
fetch_one(
|
|
"""
|
|
INSERT INTO user_ui_preferences (
|
|
user_key, dashboard_layout, dashboard_widgets, dashboard_kpis,
|
|
global_viz_mode, viz_modes, locale, updated_at
|
|
)
|
|
VALUES (%s, %s::jsonb, %s::jsonb, %s::jsonb, %s, %s::jsonb, %s, NOW())
|
|
ON CONFLICT (user_key) DO UPDATE SET
|
|
dashboard_layout = EXCLUDED.dashboard_layout,
|
|
dashboard_widgets = EXCLUDED.dashboard_widgets,
|
|
dashboard_kpis = EXCLUDED.dashboard_kpis,
|
|
global_viz_mode = EXCLUDED.global_viz_mode,
|
|
viz_modes = EXCLUDED.viz_modes,
|
|
locale = EXCLUDED.locale,
|
|
updated_at = NOW()
|
|
RETURNING user_key
|
|
""",
|
|
(
|
|
user_key,
|
|
json.dumps(layout),
|
|
json.dumps(widgets),
|
|
json.dumps(kpis),
|
|
gviz,
|
|
json.dumps(vmodes),
|
|
loc,
|
|
),
|
|
)
|
|
return get_preferences(user_key)
|