Files
foodlinkk-command-center/cockpit/app/services/ui_preferences.py
T
2026-06-09 10:59:45 +00:00

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)