feat: Introduce admin dashboard product standard management
- Added new API endpoints for managing the product dashboard standard, including retrieval, update, and deletion functionalities. - Enhanced the DashboardConfigurePage to support admin mode for configuring the product dashboard standard. - Updated the admin navigation to include a link for the product dashboard standard configuration. - Refactored the dashboard layout logic to utilize the new product standard management features. - Bumped app_dashboard version to 1.10.0 to reflect these enhancements and changes.
This commit is contained in:
parent
ff95ef63c7
commit
365ce49c6a
|
|
@ -39,7 +39,7 @@ def lab_default_layout_dict() -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def product_default_layout_dict() -> dict[str, Any]:
|
def product_default_layout_dict() -> dict[str, Any]:
|
||||||
"""System-Standard für die Produkt-Übersicht (kein DB-Override)."""
|
"""Code-Fallback für die Produkt-Übersicht; live-Standard ggf. system_config (siehe get_product_default_base_dict)."""
|
||||||
on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
||||||
return {
|
return {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ def widgets_catalog_payload(profile_id: str, conn) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def widgets_catalog_admin_payload() -> dict[str, Any]:
|
||||||
|
"""Admin: alle Widgets als auswählbar (ohne Feature-Filter)."""
|
||||||
|
return {
|
||||||
|
"catalog_version": 1,
|
||||||
|
"widgets": [_public_row(e, allowed=True) for e in WIDGET_CATALOG],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]:
|
def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome).
|
Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome).
|
||||||
|
|
|
||||||
8
backend/migrations/040_system_config.sql
Normal file
8
backend/migrations/040_system_config.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Globale System-Konfiguration (Key/Value, JSONB)
|
||||||
|
CREATE TABLE IF NOT EXISTS system_config (
|
||||||
|
key VARCHAR(64) PRIMARY KEY,
|
||||||
|
value JSONB NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_system_config_updated_at ON system_config (updated_at);
|
||||||
|
|
@ -7,12 +7,21 @@ import os
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
|
||||||
from db import get_db, get_cursor, r2d
|
from db import get_db, get_cursor, r2d
|
||||||
from auth import require_admin, hash_pin
|
from auth import require_admin, hash_pin
|
||||||
from models import AdminProfileUpdate
|
from models import AdminProfileUpdate
|
||||||
|
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||||
|
from dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||||
|
from system_dashboard_product_default import (
|
||||||
|
delete_product_default_override,
|
||||||
|
get_product_default_base_dict,
|
||||||
|
get_stored_product_default_validated,
|
||||||
|
upsert_product_default_base,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||||
|
|
||||||
|
|
@ -155,3 +164,52 @@ def admin_test_email(data: dict, session: dict=Depends(require_admin)):
|
||||||
return {"ok": True, "message": f"Test-E-Mail an {email} gesendet"}
|
return {"ok": True, "message": f"Test-E-Mail an {email} gesendet"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"Fehler beim Senden: {str(e)}")
|
raise HTTPException(500, f"Fehler beim Senden: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/widgets/catalog-full")
|
||||||
|
def admin_widgets_catalog_full(session: dict = Depends(require_admin)):
|
||||||
|
"""Dashboard-Widget-Katalog ohne Feature-Filter (Konfiguration des Produkt-Standards)."""
|
||||||
|
_ = session
|
||||||
|
return widgets_catalog_admin_payload()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dashboard-product-default")
|
||||||
|
def admin_get_dashboard_product_default(session: dict = Depends(require_admin)):
|
||||||
|
"""Aktueller Produkt-Dashboard-Standard (DB oder Code)."""
|
||||||
|
_ = session
|
||||||
|
with get_db() as conn:
|
||||||
|
layout = get_product_default_base_dict(conn)
|
||||||
|
from_database = get_stored_product_default_validated(conn) is not None
|
||||||
|
code_ref = product_default_layout_dict()
|
||||||
|
return {
|
||||||
|
"from_database": from_database,
|
||||||
|
"layout": layout,
|
||||||
|
"code_reference": code_ref,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/dashboard-product-default")
|
||||||
|
def admin_put_dashboard_product_default(
|
||||||
|
body: dict[str, Any],
|
||||||
|
session: dict = Depends(require_admin),
|
||||||
|
):
|
||||||
|
"""System-Standard persistieren (JSON wie Nutzer-Layout v1)."""
|
||||||
|
_ = session
|
||||||
|
try:
|
||||||
|
payload = DashboardLayoutPayload.model_validate(body)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(422, str(e)) from e
|
||||||
|
stored = payload.to_stored_dict()
|
||||||
|
with get_db() as conn:
|
||||||
|
upsert_product_default_base(conn, stored)
|
||||||
|
return {"ok": True, "layout": stored, "from_database": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/dashboard-product-default")
|
||||||
|
def admin_delete_dashboard_product_default(session: dict = Depends(require_admin)):
|
||||||
|
"""DB-Override entfernen; App fällt auf Code-Standard zurück."""
|
||||||
|
_ = session
|
||||||
|
with get_db() as conn:
|
||||||
|
delete_product_default_override(conn)
|
||||||
|
layout = get_product_default_base_dict(conn)
|
||||||
|
return {"ok": True, "layout": layout, "from_database": False}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ from dashboard_layout_schema import (
|
||||||
DashboardLayoutPayload,
|
DashboardLayoutPayload,
|
||||||
coalesce_effective_layout,
|
coalesce_effective_layout,
|
||||||
lab_default_layout_dict,
|
lab_default_layout_dict,
|
||||||
product_default_layout_dict,
|
|
||||||
)
|
)
|
||||||
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
||||||
from db import get_cursor, get_db
|
from db import get_cursor, get_db
|
||||||
from routers.profiles import get_pid
|
from routers.profiles import get_pid
|
||||||
|
from system_dashboard_product_default import get_product_default_base_dict
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
|
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
|
||||||
|
|
||||||
|
|
@ -51,8 +51,11 @@ def get_dashboard_layout(
|
||||||
raw = row["dashboard_layout"] if row else None
|
raw = row["dashboard_layout"] if row else None
|
||||||
custom, effective = coalesce_effective_layout(raw)
|
custom, effective = coalesce_effective_layout(raw)
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
base_product = get_product_default_base_dict(conn)
|
||||||
|
if not custom:
|
||||||
|
effective = base_product
|
||||||
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
||||||
product_adj = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn)
|
||||||
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
||||||
return {
|
return {
|
||||||
"custom": custom,
|
"custom": custom,
|
||||||
|
|
@ -107,5 +110,6 @@ def reset_dashboard_layout(
|
||||||
)
|
)
|
||||||
if cur.rowcount == 0:
|
if cur.rowcount == 0:
|
||||||
raise HTTPException(404, "Profil nicht gefunden")
|
raise HTTPException(404, "Profil nicht gefunden")
|
||||||
cleared = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
base = get_product_default_base_dict(conn)
|
||||||
|
cleared = apply_entitlements_to_layout_dict(base, pid, conn)
|
||||||
return {"ok": True, "layout": cleared}
|
return {"ok": True, "layout": cleared}
|
||||||
|
|
|
||||||
77
backend/system_dashboard_product_default.py
Normal file
77
backend/system_dashboard_product_default.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""
|
||||||
|
Persistenter System-Standard für die Produkt-Übersicht (Dashboard).
|
||||||
|
|
||||||
|
Key in system_config: dashboard_product_default — gültiges DashboardLayoutPayload (JSON).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from psycopg2.extras import Json
|
||||||
|
|
||||||
|
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||||
|
from db import get_cursor
|
||||||
|
|
||||||
|
SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT = "dashboard_product_default"
|
||||||
|
|
||||||
|
|
||||||
|
def get_stored_product_default_validated(conn) -> dict[str, Any] | None:
|
||||||
|
"""Gültiges Layout aus DB oder None (fehlt/ungültig)."""
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT value FROM system_config WHERE key = %s",
|
||||||
|
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row or row.get("value") is None:
|
||||||
|
return None
|
||||||
|
raw = row["value"]
|
||||||
|
if isinstance(raw, str):
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
payload = DashboardLayoutPayload.model_validate(
|
||||||
|
{"version": raw.get("version", 1), "widgets": raw.get("widgets", [])}
|
||||||
|
)
|
||||||
|
return payload.to_stored_dict()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_default_base_dict(conn) -> dict[str, Any]:
|
||||||
|
"""Basis-Layout (ohne Entitlements): DB-Override oder Code-Standard."""
|
||||||
|
stored = get_stored_product_default_validated(conn)
|
||||||
|
if stored is not None:
|
||||||
|
return stored
|
||||||
|
return product_default_layout_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_product_default_base(conn, layout: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
payload = DashboardLayoutPayload.model_validate(layout)
|
||||||
|
stored = payload.to_stored_dict()
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO system_config (key, value, updated_at)
|
||||||
|
VALUES (%s, %s, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET
|
||||||
|
value = EXCLUDED.value,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT, Json(stored)),
|
||||||
|
)
|
||||||
|
return stored
|
||||||
|
|
||||||
|
|
||||||
|
def delete_product_default_override(conn) -> None:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM system_config WHERE key = %s",
|
||||||
|
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
|
||||||
|
)
|
||||||
45
backend/tests/test_system_dashboard_product_default.py
Normal file
45
backend/tests/test_system_dashboard_product_default.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||||
|
from dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_widgets_catalog_admin_all_allowed():
|
||||||
|
p = widgets_catalog_admin_payload()
|
||||||
|
assert p["catalog_version"] == 1
|
||||||
|
assert len(p["widgets"]) >= 1
|
||||||
|
assert all(w["allowed"] is True for w in p["widgets"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_product_default_base_uses_code_when_no_row(monkeypatch):
|
||||||
|
from system_dashboard_product_default import get_product_default_base_dict
|
||||||
|
|
||||||
|
class _Cur:
|
||||||
|
def execute(self, *a, **k):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
|
||||||
|
assert get_product_default_base_dict(object()) == product_default_layout_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_product_default_base_uses_db_when_valid(monkeypatch):
|
||||||
|
from system_dashboard_product_default import get_product_default_base_dict
|
||||||
|
|
||||||
|
from widget_catalog import ALLOWED_WIDGET_IDS
|
||||||
|
|
||||||
|
small = {
|
||||||
|
"version": 1,
|
||||||
|
"widgets": [{"id": wid, "enabled": wid == "welcome"} for wid in sorted(ALLOWED_WIDGET_IDS)],
|
||||||
|
}
|
||||||
|
DashboardLayoutPayload.model_validate(small)
|
||||||
|
|
||||||
|
class _Cur:
|
||||||
|
def execute(self, *a, **k):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
return {"value": small}
|
||||||
|
|
||||||
|
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
|
||||||
|
assert get_product_default_base_dict(object()) == small
|
||||||
|
|
@ -9,7 +9,7 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
||||||
|
|
||||||
APP_VERSION = "0.9n"
|
APP_VERSION = "0.9n"
|
||||||
BUILD_DATE = "2026-04-05"
|
BUILD_DATE = "2026-04-05"
|
||||||
DB_SCHEMA_VERSION = "20260406c" # Migration 039
|
DB_SCHEMA_VERSION = "20260406d" # Migration 040
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"auth": "1.2.0",
|
"auth": "1.2.0",
|
||||||
|
|
@ -24,20 +24,21 @@ MODULE_VERSIONS = {
|
||||||
"photos": "1.0.0",
|
"photos": "1.0.0",
|
||||||
"insights": "1.3.0",
|
"insights": "1.3.0",
|
||||||
"prompts": "1.1.0",
|
"prompts": "1.1.0",
|
||||||
"admin": "1.2.0",
|
"admin": "1.3.0", # Dashboard Produkt-Standard (system_config) + catalog-full
|
||||||
"stats": "1.0.1",
|
"stats": "1.0.1",
|
||||||
"exportdata": "1.1.0",
|
"exportdata": "1.1.0",
|
||||||
"importdata": "1.0.0",
|
"importdata": "1.0.0",
|
||||||
"membership": "2.1.0",
|
"membership": "2.1.0",
|
||||||
"workflow": "0.6.0", # Phase 4: End Node Template Engine
|
"workflow": "0.6.0", # Phase 4: End Node Template Engine
|
||||||
"app_dashboard": "1.9.0", # product vs lab layout defaults; user dashboard configure page API fields
|
"app_dashboard": "1.10.0", # Produkt-Standard aus system_config; Response-Form unverändert
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
{
|
{
|
||||||
"version": "0.9n",
|
"version": "0.9n",
|
||||||
"date": "2026-04-05",
|
"date": "2026-04-06",
|
||||||
"changes": [
|
"changes": [
|
||||||
|
"Admin: Produkt-Dashboard-Systemstandard (Migration 040 system_config, API, UI)",
|
||||||
"Phase 4: End Node Template Engine",
|
"Phase 4: End Node Template Engine",
|
||||||
"workflow_models.py: EndNodeOutputMode enum (AUTO, TEMPLATE)",
|
"workflow_models.py: EndNodeOutputMode enum (AUTO, TEMPLATE)",
|
||||||
"workflow_executor.py: execute_end_node() with Jinja2 template rendering",
|
"workflow_executor.py: execute_end_node() with Jinja2 template rendering",
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ function AppShell() {
|
||||||
<Route path="g/:groupId" element={<AdminGroupHubPage />} />
|
<Route path="g/:groupId" element={<AdminGroupHubPage />} />
|
||||||
<Route path="users" element={<AdminUsersPage />} />
|
<Route path="users" element={<AdminUsersPage />} />
|
||||||
<Route path="system" element={<AdminSystemPage />} />
|
<Route path="system" element={<AdminSystemPage />} />
|
||||||
|
<Route path="dashboard-product-default" element={<DashboardConfigurePage adminMode />} />
|
||||||
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/>
|
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/>
|
||||||
<Route path="features" element={<AdminFeaturesPage/>}/>
|
<Route path="features" element={<AdminFeaturesPage/>}/>
|
||||||
<Route path="tiers" element={<AdminTiersPage/>}/>
|
<Route path="tiers" element={<AdminTiersPage/>}/>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,11 @@ export const ADMIN_GROUPS = [
|
||||||
label: 'SMTP & Metadaten-Export',
|
label: 'SMTP & Metadaten-Export',
|
||||||
description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).',
|
description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/admin/dashboard-product-default',
|
||||||
|
label: 'Produkt-Dashboard (Standard)',
|
||||||
|
description: 'Globales Standard-Layout der Startseite (DB oder Code-Fallback).',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,14 @@ function catalogMetaById(catalog) {
|
||||||
return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
|
return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardConfigurePage() {
|
/**
|
||||||
|
* @param {{ adminMode?: boolean }} [props]
|
||||||
|
*/
|
||||||
|
export default function DashboardConfigurePage({ adminMode = false } = {}) {
|
||||||
ensurePilotLabWidgetsRegistered()
|
ensurePilotLabWidgetsRegistered()
|
||||||
|
|
||||||
const [bundle, setBundle] = useState(null)
|
const [bundle, setBundle] = useState(null)
|
||||||
|
const [adminFromDatabase, setAdminFromDatabase] = useState(null)
|
||||||
const [catalog, setCatalog] = useState(null)
|
const [catalog, setCatalog] = useState(null)
|
||||||
const [layout, setLayout] = useState(null)
|
const [layout, setLayout] = useState(null)
|
||||||
const [addPanelOpen, setAddPanelOpen] = useState(false)
|
const [addPanelOpen, setAddPanelOpen] = useState(false)
|
||||||
|
|
@ -103,16 +107,29 @@ export default function DashboardConfigurePage() {
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
|
if (adminMode) {
|
||||||
|
const [cat, d] = await Promise.all([
|
||||||
|
api.adminGetWidgetsCatalogFull(),
|
||||||
|
api.adminGetDashboardProductDefault(),
|
||||||
|
])
|
||||||
|
setCatalog(cat)
|
||||||
|
setBundle({ custom: false, product_default_layout: d.layout })
|
||||||
|
setAdminFromDatabase(!!d.from_database)
|
||||||
|
setChartDaysDraftByWidgetId({})
|
||||||
|
setLayout(normalizeLayoutForEditor(structuredClone(d.layout)))
|
||||||
|
} else {
|
||||||
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
|
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
|
||||||
setCatalog(cat)
|
setCatalog(cat)
|
||||||
setBundle(b)
|
setBundle(b)
|
||||||
|
setAdminFromDatabase(null)
|
||||||
setChartDaysDraftByWidgetId({})
|
setChartDaysDraftByWidgetId({})
|
||||||
const base = b.custom ? b.layout : structuredClone(b.product_default_layout)
|
const base = b.custom ? b.layout : structuredClone(b.product_default_layout)
|
||||||
setLayout(normalizeLayoutForEditor(base))
|
setLayout(normalizeLayoutForEditor(base))
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
setErr(formatFastApiDetail(null, e.message))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [adminMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
|
|
@ -138,8 +155,13 @@ export default function DashboardConfigurePage() {
|
||||||
setMsg(null)
|
setMsg(null)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
|
if (adminMode) {
|
||||||
|
await api.adminPutDashboardProductDefault(toSave)
|
||||||
|
setMsg('System-Standard für die Übersicht wurde gespeichert.')
|
||||||
|
} else {
|
||||||
await api.putAppDashboardLayout(toSave)
|
await api.putAppDashboardLayout(toSave)
|
||||||
setMsg('Dein Dashboard wurde gespeichert.')
|
setMsg('Dein Dashboard wurde gespeichert.')
|
||||||
|
}
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
setErr(formatFastApiDetail(null, e.message))
|
||||||
|
|
@ -149,15 +171,27 @@ export default function DashboardConfigurePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetToSystem = async () => {
|
const resetToSystem = async () => {
|
||||||
if (!window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')) return
|
const ok = adminMode
|
||||||
|
? window.confirm(
|
||||||
|
'Eintrag in der Datenbank löschen und Layout aus dem Code (widget_catalog) wiederherstellen?'
|
||||||
|
)
|
||||||
|
: window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')
|
||||||
|
if (!ok) return
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
setMsg(null)
|
setMsg(null)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
|
if (adminMode) {
|
||||||
|
const r = await api.adminDeleteDashboardProductDefault()
|
||||||
|
setChartDaysDraftByWidgetId({})
|
||||||
|
setLayout(normalizeLayoutForEditor(r.layout))
|
||||||
|
setMsg('Code-Standard wiederhergestellt (kein DB-Override mehr).')
|
||||||
|
} else {
|
||||||
const r = await api.resetAppDashboardLayout()
|
const r = await api.resetAppDashboardLayout()
|
||||||
setChartDaysDraftByWidgetId({})
|
setChartDaysDraftByWidgetId({})
|
||||||
setLayout(normalizeLayoutForEditor(r.layout))
|
setLayout(normalizeLayoutForEditor(r.layout))
|
||||||
setMsg('Auf System-Standard zurückgesetzt.')
|
setMsg('Auf System-Standard zurückgesetzt.')
|
||||||
|
}
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
setErr(formatFastApiDetail(null, e.message))
|
||||||
|
|
@ -253,21 +287,36 @@ export default function DashboardConfigurePage() {
|
||||||
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 720, margin: '0 auto' }}>
|
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 720, margin: '0 auto' }}>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to={adminMode ? '/admin/g/system' : '/settings'}
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
|
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
← Einstellungen
|
{adminMode ? '← Basiseinstellungen (Admin)' : '← Einstellungen'}
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<LayoutDashboard size={26} color="var(--accent)" />
|
<LayoutDashboard size={26} color="var(--accent)" />
|
||||||
Übersicht anpassen
|
{adminMode ? 'Produkt-Übersicht: Systemstandard' : 'Übersicht anpassen'}
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
||||||
Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ – mit Suche direkt
|
{adminMode
|
||||||
im eigenen Fenster, ohne langes Scrollen.
|
? 'Globales Standard-Dashboard für alle Nutzer ohne eigenes Layout. Gespeichert in der Datenbank; mit „Code-Standard wiederherstellen“ wird der Eintrag entfernt und der Fallback aus dem Code genutzt.'
|
||||||
|
: 'Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ – mit Suche direkt im eigenen Fenster, ohne langes Scrollen.'}
|
||||||
</p>
|
</p>
|
||||||
{!bundle?.custom && (
|
{adminMode && adminFromDatabase != null && (
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
|
||||||
|
{adminFromDatabase ? (
|
||||||
|
<>
|
||||||
|
Aktuell gilt ein <strong>gespeicherter Systemstandard</strong> (Datenbank).
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Es liegt <strong>kein DB-Override</strong> vor — es wird der Code-Standard aus dem Widget-Katalog
|
||||||
|
verwendet.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!adminMode && !bundle?.custom && (
|
||||||
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
|
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
|
||||||
Du bearbeitest gerade das <strong>System-Standardlayout</strong>. Mit „Speichern“ legst du deine persönliche
|
Du bearbeitest gerade das <strong>System-Standardlayout</strong>. Mit „Speichern“ legst du deine persönliche
|
||||||
Version ab.
|
Version ab.
|
||||||
|
|
@ -457,10 +506,14 @@ export default function DashboardConfigurePage() {
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={resetToSystem}>
|
<button type="button" className="btn btn-secondary" disabled={busy} onClick={resetToSystem}>
|
||||||
System-Standard wiederherstellen
|
{adminMode ? 'Code-Standard wiederherstellen' : 'System-Standard wiederherstellen'}
|
||||||
</button>
|
</button>
|
||||||
<Link to="/" className="btn btn-secondary" style={{ textDecoration: 'none', textAlign: 'center' }}>
|
<Link
|
||||||
Zur Übersicht
|
to={adminMode ? '/admin' : '/'}
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ textDecoration: 'none', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
{adminMode ? 'Admin-Übersicht' : 'Zur Übersicht'}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,13 @@ export const api = {
|
||||||
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
|
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
|
||||||
resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }),
|
resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }),
|
||||||
|
|
||||||
|
adminGetWidgetsCatalogFull: () => req('/admin/widgets/catalog-full'),
|
||||||
|
adminGetDashboardProductDefault: () => req('/admin/dashboard-product-default'),
|
||||||
|
adminPutDashboardProductDefault: (layout) =>
|
||||||
|
req('/admin/dashboard-product-default', jput(layout)),
|
||||||
|
adminDeleteDashboardProductDefault: () =>
|
||||||
|
req('/admin/dashboard-product-default', { method: 'DELETE' }),
|
||||||
|
|
||||||
// Persönliche Referenzwerte (Profil, historisch)
|
// Persönliche Referenzwerte (Profil, historisch)
|
||||||
listReferenceValueTypes: () => req('/reference-value-types'),
|
listReferenceValueTypes: () => req('/reference-value-types'),
|
||||||
listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'),
|
listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user