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